Skip to content

fix(backend): use full token chain for all sigstore attestation calls#9307

Merged
jdx merged 6 commits intojdx:mainfrom
cameronbrill:fix/unified-sigstore-auth
Apr 22, 2026
Merged

fix(backend): use full token chain for all sigstore attestation calls#9307
jdx merged 6 commits intojdx:mainfrom
cameronbrill:fix/unified-sigstore-auth

Conversation

@cameronbrill
Copy link
Copy Markdown
Contributor

@cameronbrill cameronbrill commented Apr 22, 2026

Summary

mise lock runs GitHub attestation verification unauthenticated because three of four sigstore call sites — aqua backend (src/backend/aqua.rs:939), python core plugin (src/plugins/core/python.rs:628), ruby core plugin (src/plugins/core/ruby.rs:805) — bypass mise's full token resolution chain by passing env::GITHUB_TOKEN.as_deref(), which only reads the MISE_GITHUB_TOKEN / GITHUB_API_TOKEN / GITHUB_TOKEN env vars. The fourth call path (github backend) already uses the full chain via github::resolve_token_for_api_url. On a repo with many GitHub-sourced tools, the unauthenticated calls exhaust the 60/hour IP rate limit on the second mise lock.

This PR unifies all four call sites through a new wrapper whose signatures structurally prevent re-introducing the bug.

Approach

Introduce a crate-internal wrapper at src/github/sigstore.rs that owns every sigstore_verification import (verified by rg -l 'use sigstore_verification\b|sigstore_verification::' src/ returning exactly that one file). The wrapper's functions (verify_attestation, detect_attestations, verify_slsa_provenance, verify_cosign_signature, verify_cosign_signature_with_key) resolve the token internally via github::resolve_token_for_api_url, using github::API_URL as the default. Callers can't pass a token at all. env::GITHUB_TOKEN stays — it's still needed for subprocess env-var plumbing in cargo.rs and asdf_plugin.rs — and gains a doc comment steering future authors away from HTTP use.

A local DetectError enum in the wrapper preserves the original "source creation" vs "API fetch" warning distinction that pre-wrapper code emitted. Its Error::source() implementation lets callers inspect the wrapped AttestationError.

Test plan

  • 4 passing unit tests in src/github/sigstore.rs cover env-var, explicit and enterprise api_url, and the non-env-var github_tokens.toml source (via a new #[cfg(test)] test_support::TOKENS_FILE_OVERRIDE hook).
  • Pre-existing is_slsa_format_issue tests still pass after the AttestationError signature switch to the re-export.
  • mise run lint passes locally (runs the clippy check that was failing in CI).
  • mise run ci passes locally (runs format, build, and the full test suite).
  • Manual: mise lock on a repo with many github/aqua-backed tools should authenticate all attestation requests when a token source is configured (env var, github_tokens.toml, or credential_command).

🤖 Generated with Claude Code

cameronbrill and others added 3 commits April 22, 2026 11:31
…ken chain

Lock-time provenance verification hit GitHub's 60/hour unauthenticated rate
limit because three sigstore call sites (aqua, python, ruby) passed
env::GITHUB_TOKEN — env vars only — while the github backend used the full
resolution chain via github::resolve_token_for_api_url. Users with gh auth
or github_tokens.toml but no GITHUB_TOKEN env var saw lock-time requests go
out unauthenticated.

Concentrate the sigstore_verification surface in a new wrapper at
src/github/sigstore.rs. The wrapper resolves tokens internally and exposes
token-free signatures, making the asymmetry structurally impossible to
reintroduce. All call sites (github, aqua, python, ruby) migrate to the
wrapper, and env::GITHUB_TOKEN gets a doc comment steering future authors
to the wrapper for HTTP or sigstore use.

Unit tests prove the default api.github.com URL, an explicit api.github.com
URL, and a GitHub Enterprise base URL each resolve the expected token
without hitting the network.

Does not address the separate root cause where gh CLI stores OAuth tokens
in the macOS Keychain (leaving hosts.yml without oauth_token); that is a
stacked follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… wrapper pub(crate) (auto-fix from review)

Three auto-fix findings from review:

F-2 (P2): test lock was duplicated in github.rs and github/sigstore.rs
tests, so parallel test runs could race on the same env vars. Hoist the
lock to a pub(crate) static under #[cfg(test)] so both modules share it.

F-3 (P2): sigstore test teardown was manual — a panicking assertion
skipped restore_token_env_vars and leaked state into other tests. Replace
with a RAII TokenEnvGuard whose Drop restores the snapshot unconditionally.

F-5 (P3): pub mod sigstore; made an internal wrapper externally visible
for no reason. Tighten to pub(crate) mod sigstore;.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…var token source (review triage)

Addresses two human-triaged review findings.

F-1 (P1) — Tests only covered env-var token paths; R3 asked for coverage of
non-env-var sources too. Add a `#[cfg(test)]` override hook on `resolve_token`
that lets the wrapper's test suite seed the `github_tokens.toml` path, and a
new test `test_resolve_token_wrapper_uses_github_tokens_toml_source` that
asserts the wrapper walks past env vars and reaches the tokens-file source.

Also add an `#[ignore]`d regression marker
`test_wrapper_resolves_gh_cli_token_from_macos_keychain_after_fix` that
documents the gh-CLI-Keychain gap on macOS (the stacked follow-up's scope).
Running `cargo test -- --ignored` surfaces it; removing `#[ignore]` and
filling in the body becomes part of that follow-up PR.

F-4 (P2) — `detect_attestations` had collapsed two distinct error paths
(`GitHubSource::with_base_url` failure vs. `fetch_attestations` failure) into
a single `AttestationError`, losing the "Failed to create GitHub attestation
source" warning. Introduce a local `DetectError` enum with
`SourceCreation` / `Fetch` variants so the caller at
`src/backend/github.rs:477` can restore both warning messages verbatim.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR fixes a bug where three sigstore attestation call sites (aqua.rs, python.rs, ruby.rs) passed only env::GITHUB_TOKEN (env-var only) while the GitHub backend already used github::resolve_token_for_api_url (full token chain). The fix introduces a src/github/sigstore.rs wrapper module that resolves tokens internally from the full chain and omits the token parameter from its public API, making the asymmetry structurally impossible to re-introduce. The approach is clean, well-documented, and covered by targeted unit tests.

Confidence Score: 5/5

Safe to merge — the fix is correct, well-tested, and the wrapper pattern makes the bug structurally impossible to reintroduce.

All sigstore call sites consistently use the new wrapper which resolves tokens via the full chain. No P0 or P1 issues found. The only minor observation is that the PR description references an #[ignore]d test (test_wrapper_resolves_gh_cli_token_from_macos_keychain_after_fix) that is not present in the reviewed file, but this is a documentation discrepancy, not a code defect.

No files require special attention.

Important Files Changed

Filename Overview
src/github/sigstore.rs New wrapper module centralizing all sigstore_verification calls; resolves tokens via the full chain internally and exposes no token parameter to callers. Well-structured with good doc comments and a clean DetectError enum preserving original warning distinction.
src/github.rs Adds the sigstore submodule declaration, promotes TEST_ENV_LOCK to crate-level pub(crate), and injects a cfg(test)-only TOKENS_FILE_OVERRIDE hook at source #4 in resolve_token. The hook is correctly positioned after credential_command and before the real MISE_GITHUB_TOKENS read.
src/backend/aqua.rs All four sigstore call sites updated to the new wrapper; env import removed. Aqua correctly passes api_url: None (public GitHub only).
src/backend/github.rs Migrated detect_attestations and two verify_attestation call sites to the wrapper; inline source-creation vs. fetch warning distinction preserved via DetectError variants. AttestationError re-export used cleanly in test helpers.
src/env.rs GITHUB_TOKEN gains a clear doc comment steering future authors away from HTTP/sigstore use. No logic changes.
src/plugins/core/python.rs Switched to wrapper; env import removed. Correct — astral-sh/python-build-standalone is always public GitHub.
src/plugins/core/ruby.rs Switched to wrapper; env import removed. Same pattern as python.rs.
src/main.rs Minor cleanup: replaces std::io::Error::new(ErrorKind::Other, ...) with std::io::Error::other(...). Unrelated to the main fix.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[aqua.rs / python.rs / ruby.rs / github.rs] -->|calls| B[github::sigstore wrapper]
    B --> C[resolve_token_for_wrapper]
    C --> D[github::resolve_token_for_api_url]
    D --> E[1. env vars: MISE_GITHUB_TOKEN / GITHUB_TOKEN]
    D --> F[2. credential_command]
    D --> G[3. github_tokens.toml]
    D --> H[4. gh CLI hosts.yml]
    D --> I[5. git credential fill]
    E & F & G & H & I --> J[token resolved]
    J --> K{api_url Some or None}
    K -->|Some base_url| L[verify_github_attestation_with_base_url]
    K -->|None| M[verify_github_attestation]
    L & M --> N[sigstore_verification crate]

    style B fill:#d4edda,stroke:#28a745
    style D fill:#cce5ff,stroke:#004085
Loading

Reviews (2): Last reviewed commit: "chore(github): drop macOS Keychain regre..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a centralized sigstore wrapper module to handle GitHub attestation verification, ensuring consistent token resolution across all call sites and resolving a rate-limiting issue. The aqua, python, and ruby backends have been refactored to use this wrapper, and test infrastructure was updated with a shared mutex to prevent environment variable race conditions. Feedback was provided to implement the source() method for the DetectError type to enhance error diagnostics.

Comment thread src/github/sigstore.rs Outdated
cameronbrill and others added 3 commits April 22, 2026 13:44
The `cargo clippy --all-features --all-targets -- -D warnings` CI job was
failing on `src/main.rs:255-258`:

    std::io::Error::new(std::io::ErrorKind::Other, "user cancelled")

Clippy 1.94+ flags this as `clippy::io_other_error`, preferring
`std::io::Error::other("user cancelled")`. The local clippy invocation
during PR jdx#9307's review phase used `--bin mise`, which does not compile
the test target and therefore did not surface this. The failure is not
introduced by this branch (the offending code came in via jdx#9294), but
the branch can't merge without fixing it.

The sibling `Error::new(ErrorKind::Interrupted, ...)` on the line above
is intentionally left alone — clippy does not flag it (and should not;
`Error::other` is specific to `ErrorKind::Other`).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Addresses jdx#9307 (comment):
`DetectError` wraps `AttestationError` but the `std::error::Error` impl
was empty, so callers that walk the error chain (`e.source()`) couldn't
reach the inner error.

Return the wrapped variant via `source()` per the standard idiom for
error-wrapping types.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The `#[ignore]`d test `test_wrapper_resolves_gh_cli_token_from_macos_keychain_after_fix`
was added during PR jdx#9307 refinement as a placeholder for a follow-up
branch that would add `gh auth token` / macOS Keychain support.

The project decision is to not pursue that follow-up. Users whose gh CLI
tokens live only in the macOS Keychain have two working paths today:

    [settings.github]
    credential_command = 'gh auth token --hostname "$1"'

or:

    [settings.github]
    use_git_credentials = true

Drop the placeholder test and its explanatory doc block; a regression
marker for work that won't happen is noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@cameronbrill cameronbrill marked this pull request as ready for review April 22, 2026 21:10
@jdx jdx merged commit 7600daa into jdx:main Apr 22, 2026
35 checks passed
jdx added a commit that referenced this pull request Apr 23, 2026
Replace the external `sigstore-verification` dependency with a local
`mise-sigstore` adapter built on `sigstore-verify` 0.6.5 from sigstore-rust.
The wrapper at `src/github/sigstore.rs` and the `vfox` crate are rerouted to
the new adapter; mise's call sites are unchanged because the wrapper from
PR #9307 already abstracts the underlying verifier.

Why a local adapter and not just bumping `sigstore-verification`: the upstream
`sigstore-verify` 0.6.x exposes a different (and richer) API surface. The
adapter keeps mise's existing helper-style API (`verify_github_attestation`,
`verify_cosign_signature`, `verify_slsa_provenance`, `detect_attestations`)
so call sites do not need to change.

Why we ship a custom TUF root and bypass `TrustedRoot::production()`: the
`sigstore-trust-root` crate embeds TUF root v1 (from 2021), which fails
verification under `tough` because the `expires` field uses a timezone-offset
format that `chrono` normalizes to UTC `Z` on re-serialization, breaking
the canonical-JSON byte sequence that the original signature was computed
over (0 of 5 signatures verify, threshold is 3). We embed root v12 (the
same workaround the original `sigstore` 0.13 crate uses) and pass it via
`TufConfig::custom`, so `tough` walks v12 -> v13 -> v14 successfully.
The TUF root file is added to `.prettierignore` because reformatting would
invalidate its signatures.

Verified end-to-end:
- cargo check, cargo clippy --all-targets, cargo fmt --all -- --check, taplo fmt
- cargo test --bin mise (747 tests pass)
- cargo test -p mise-sigstore (4 tests pass)
- mise run test:e2e -- test_aqua_github_attestations passes against
  goreleaser 2.14.1 (live download + TUF fetch + bundle verification)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
jdx added a commit that referenced this pull request Apr 29, 2026
Replace the external `sigstore-verification` dependency with a local
`mise-sigstore` adapter built on `sigstore-verify` 0.6.5 from sigstore-rust.
The wrapper at `src/github/sigstore.rs` and the `vfox` crate are rerouted to
the new adapter; mise's call sites are unchanged because the wrapper from
PR #9307 already abstracts the underlying verifier.

Why a local adapter and not just bumping `sigstore-verification`: the upstream
`sigstore-verify` 0.6.x exposes a different (and richer) API surface. The
adapter keeps mise's existing helper-style API (`verify_github_attestation`,
`verify_cosign_signature`, `verify_slsa_provenance`, `detect_attestations`)
so call sites do not need to change.

Why we ship a custom TUF root and bypass `TrustedRoot::production()`: the
`sigstore-trust-root` crate embeds TUF root v1 (from 2021), which fails
verification under `tough` because the `expires` field uses a timezone-offset
format that `chrono` normalizes to UTC `Z` on re-serialization, breaking
the canonical-JSON byte sequence that the original signature was computed
over (0 of 5 signatures verify, threshold is 3). We embed root v12 (the
same workaround the original `sigstore` 0.13 crate uses) and pass it via
`TufConfig::custom`, so `tough` walks v12 -> v13 -> v14 successfully.
The TUF root file is added to `.prettierignore` because reformatting would
invalidate its signatures.

Verified end-to-end:
- cargo check, cargo clippy --all-targets, cargo fmt --all -- --check, taplo fmt
- cargo test --bin mise (747 tests pass)
- cargo test -p mise-sigstore (4 tests pass)
- mise run test:e2e -- test_aqua_github_attestations passes against
  goreleaser 2.14.1 (live download + TUF fetch + bundle verification)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
jdx added a commit that referenced this pull request May 1, 2026
Replace the external `sigstore-verification` dependency with a local
`mise-sigstore` adapter built on `sigstore-verify` 0.6.5 from sigstore-rust.
The wrapper at `src/github/sigstore.rs` and the `vfox` crate are rerouted to
the new adapter; mise's call sites are unchanged because the wrapper from
PR #9307 already abstracts the underlying verifier.

Why a local adapter and not just bumping `sigstore-verification`: the upstream
`sigstore-verify` 0.6.x exposes a different (and richer) API surface. The
adapter keeps mise's existing helper-style API (`verify_github_attestation`,
`verify_cosign_signature`, `verify_slsa_provenance`, `detect_attestations`)
so call sites do not need to change.

Why we ship a custom TUF root and bypass `TrustedRoot::production()`: the
`sigstore-trust-root` crate embeds TUF root v1 (from 2021), which fails
verification under `tough` because the `expires` field uses a timezone-offset
format that `chrono` normalizes to UTC `Z` on re-serialization, breaking
the canonical-JSON byte sequence that the original signature was computed
over (0 of 5 signatures verify, threshold is 3). We embed root v12 (the
same workaround the original `sigstore` 0.13 crate uses) and pass it via
`TufConfig::custom`, so `tough` walks v12 -> v13 -> v14 successfully.
The TUF root file is added to `.prettierignore` because reformatting would
invalidate its signatures.

Verified end-to-end:
- cargo check, cargo clippy --all-targets, cargo fmt --all -- --check, taplo fmt
- cargo test --bin mise (747 tests pass)
- cargo test -p mise-sigstore (4 tests pass)
- mise run test:e2e -- test_aqua_github_attestations passes against
  goreleaser 2.14.1 (live download + TUF fetch + bundle verification)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants