Skip to content

feat: store provenance verification results in lockfile#8495

Merged
jdx merged 55 commits intomainfrom
feat/lockfile-provenance
Mar 7, 2026
Merged

feat: store provenance verification results in lockfile#8495
jdx merged 55 commits intomainfrom
feat/lockfile-provenance

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Mar 7, 2026

Summary

Store provenance verification results in the lockfile so that subsequent installs can detect downgrade/stripping attacks. When a lockfile records that a tool was installed with a specific provenance mechanism (e.g., SLSA, GitHub attestations, minisign, cosign), future installs will refuse to proceed if that mechanism is disabled or unavailable.

Key changes

  • ProvenanceType enum with ordered variants: Minisign < Cosign < Slsa < GithubAttestations

    • Uses strum::Display and strum::EnumIs for ergonomic variant checks
    • Slsa variant carries an optional URL for the .intoto.jsonl provenance file
    • Custom serde: SLSA serializes as provenance = { slsa = { url = "..." } }, others as plain strings
    • Ord impl ensures max() always preserves the highest-priority provenance
  • Aqua backend (src/backend/aqua.rs):

    • Records provenance from all verification mechanisms (attestations, SLSA, cosign, minisign)
    • Exact-match skip logic: when lockfile specifies a provenance type, only that mechanism runs
    • Short-circuit logic: lower-priority mechanisms skip when a higher-priority one already succeeded
    • Enforces provenance match after all verification completes
    • Warns on GitHub attestation API failures (non-fatal during mise lock)
  • GitHub backend (src/backend/github.rs):

    • verify_attestations_or_slsa returns Option<ProvenanceType> for recording
    • Detects provenance type during mise lock via attestation API probe
    • Exact-match skip logic for attestations vs SLSA
  • Core plugins (zig, ruby):

    • Zig enforces minisign provenance expectation from lockfile
    • Ruby enforces provenance for precompiled binary verification
  • Lockfile (src/lockfile.rs):

    • merge_with uses std::cmp::max() to prevent provenance downgrade
    • Backwards-compatible parsing: reads legacy provenance_url field into Slsa { url }
    • set_platform_info preserves highest-priority provenance across updates
  • E2E test (e2e/lockfile/test_lockfile_provenance):

    • Verifies SLSA provenance is written for sops
    • Tests downgrade attack detection (provenance in lockfile + verification disabled → error)

Test plan

  • cargo check passes
  • All lints pass (mise run lint)
  • mise run test:e2e test_lockfile_provenance passes
  • Unit tests for ProvenanceType roundtrip, legacy compat, merge, ordering

🤖 Generated with Claude Code


Note

High Risk
Touches core install/verification logic and changes lockfile serialization format; mistakes could block installs or weaken/overly-tighten supply-chain verification behavior.

Overview
mise lock now records which provenance mechanism (e.g., github-attestations, slsa, cosign, minisign) was used per tool+platform in mise.lock, and subsequent installs fail if that expected verification is disabled/unavailable (downgrade attack protection).

This introduces a new provenance field and ProvenanceType in the lockfile model (including SLSA URL support + legacy compat) and updates backends/plugins (notably aqua, github, ruby precompiled, and zig) to detect, write, and strictly validate provenance during verification; the lockfile writer also changes platform entries from inline tables to dotted-key subtables. Adds new e2e coverage for provenance writing/enforcement and updates the generated mise.lock accordingly.

Written by Cursor Bugbot for commit 196d502. This will update automatically on new commits. Configure here.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the security of mise by embedding software supply chain provenance verification directly into the lockfile. It introduces new fields to PlatformInfo to persistently store details about successful provenance checks. This ensures that once a tool's origin and integrity are verified, mise will enforce this verification in future installations, actively mitigating risks like downgrade attacks and unauthorized modifications.

Highlights

  • Lockfile Enhancement: Introduced provenance and provenance_url fields to the PlatformInfo struct within the lockfile to store details about software supply chain verification.
  • Provenance Recording: Implemented recording of provenance verification results (type and URL) in mise.lock upon successful Sigstore/SLSA/GitHub attestation verification during tool installation.
  • Security Enforcement: Added a security mechanism to enforce provenance verification: if a lockfile records provenance for a tool, but verification cannot be performed on subsequent installs, mise will now treat this as a security error, preventing downgrade or stripping attacks.
  • Backend Integration: Integrated provenance recording for github and aqua backends, supporting "github-attestations" and "slsa" types.
Changelog
  • src/backend/aqua.rs
    • Updated get_platform_info to use ..Default::default() for PlatformInfo initialization.
    • Added logic to record SLSA and GitHub artifact attestation provenance in the lockfile upon successful verification.
  • src/backend/conda.rs
    • Updated get_platform_info to use ..Default::default() for PlatformInfo initialization.
  • src/backend/github.rs
    • Modified verify_attestations_or_slsa to return provenance type and URL upon successful verification.
    • Updated install_version to record provenance verification results in lock_platforms.
    • Added a check to enforce provenance verification if specified in the lockfile, raising an error if not performed.
  • src/backend/mod.rs
    • Updated get_platform_info implementations in the Backend trait to use ..Default::default() for PlatformInfo initialization.
  • src/lockfile.rs
    • Extended PlatformInfo struct with new provenance and provenance_url fields.
    • Updated is_empty, merge_with, TryFrom<toml::Value>, and From<PlatformInfo> implementations to correctly handle the new provenance fields.
    • Modified Lockfile::merge_platform_info to preserve provenance fields during merges.
    • Updated From<ToolVersionList> to clone PlatformInfo directly.
    • Added new unit tests for provenance fields, covering roundtrip serialization, merge preservation, and is_empty checks.
  • src/plugins/core/bun.rs
    • Updated get_platform_info to use ..Default::default() for PlatformInfo initialization.
  • src/plugins/core/deno.rs
    • Updated get_platform_info to use ..Default::default() for PlatformInfo initialization.
  • src/plugins/core/go.rs
    • Updated get_platform_info to use ..Default::default() for PlatformInfo initialization.
  • src/plugins/core/node.rs
    • Updated get_platform_info to use ..Default::default() for PlatformInfo initialization.
  • src/plugins/core/python.rs
    • Updated get_platform_info to use ..Default::default() for PlatformInfo initialization.
  • src/plugins/core/ruby.rs
    • Updated get_platform_info to use ..Default::default() for PlatformInfo initialization.
  • src/plugins/core/ruby_common.rs
    • Updated resolve_rubyinstaller_lock_info to use ..Default::default() for PlatformInfo initialization.
  • src/plugins/core/zig.rs
    • Updated get_platform_info to use ..Default::default() for PlatformInfo initialization.
Activity
  • New provenance and provenance_url fields were added to the PlatformInfo struct in the lockfile.
  • Logic was implemented to record successful provenance verification (Sigstore/SLSA/GitHub attestations) directly into mise.lock during tool installation.
  • A security measure was introduced to prevent downgrade or stripping attacks by enforcing provenance verification if it's already recorded in the lockfile.
  • The github and aqua backends were updated to support and record "github-attestations" and "slsa" provenance types.
  • The pull request includes a test plan detailing passing existing lockfile unit tests, three new unit tests for provenance handling (roundtrip serialization, merge preservation, is_empty check), passing lint, and pending manual testing with goreleaser.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR implements provenance verification persistence in mise.lock, enabling detection of downgrade and stripping attacks. When a tool is installed with a provenance mechanism (SLSA, GitHub attestations, cosign, or minisign), the type is recorded in the lockfile's per-platform PlatformInfo. Subsequent installs fail if that mechanism is disabled or produces a different result.

Verified findings:

  • The sops SLSA provenance is correctly written to mise.lock for all platform entries (verification confirmed)
  • One issue flagged: asymmetric error handling in SLSA verification between missing assets (soft warning) vs. missing attestations (hard error) for first-time installs

Architecture summary:

  • New ProvenanceType enum with ordinal-based priority ordering (Minisign < Cosign < Slsa < GithubAttestations), custom Ord/PartialEq (intentionally non-structural for Slsa variants), and merge() to preserve the highest-priority type and associated URLs.
  • Aqua backend: exact-match skip flags, per-mechanism is_none() guards, short-circuit logic for higher-priority mechanisms, and downgrade check via std::mem::discriminant comparison.
  • GitHub backend: detect_provenance_type with live attestation API probe, matching skip/enforcement logic, and pre-checks for early returns like GitLab/Forgejo.
  • Zig and Ruby plugins enforce recorded provenance expectations post-download.
  • Lockfile TOML format uses dotted-key subtables for richer per-platform fields.

The core security logic is well-structured and correctly prevents downgrades. The e2e test fixtures and sed portability concerns were verified and found to be non-issues in the current code.

Confidence Score: 4/5

  • Core provenance verification logic is correct and prevents downgrade attacks. E2E tests validate the security model. One minor asymmetry in error handling (missing assets vs. missing attestations) doesn't affect safety but could improve UX.
  • The PR implements a well-designed provenance verification system that correctly records and enforces verification types in the lockfile. The security logic is sound: skip flags prevent wrong mechanisms, short-circuits avoid redundant checks, and the discriminant-based downgrade detection works correctly. The one remaining comment highlights an asymmetry in how SLSA errors are handled for first-time installs, which is a design concern rather than a correctness issue. The fixtures and portability concerns from the prior review were verified to be non-issues. This merits 4/5 because the core functionality is solid and safe, with only a minor point about error handling consistency worth addressing."
  • src/backend/aqua.rs — Consider aligning SLSA error handling (missing asset soft warning vs. missing attestations hard error) for consistency, though current behavior may be intentional.

Comments Outside Diff (1)

  1. src/backend/aqua.rs, line 1270-1276 (link)

    NoAttestations is treated as a hard blocking error, but only for SLSA, creating an asymmetry with other verification paths.

    When locked_provenance = None (first install) and the SLSA provenance file exists but contains no attestations, the install fails with an unrecoverable error. However, if the SLSA asset file is missing entirely (lines 1215–1218), the code returns Ok(()) with a warning and continues.

    This asymmetry means:

    • Missing asset: harmless, continues with warning ✓
    • Asset exists, no attestations: hard error, install blocked ✗

    For first-time installs with no lockfile expectation to enforce, this strict behavior may be overly strict if older releases are added to the registry without retroactive attestation support.

    Suggestion: Document whether this behavior is intentional, or consider applying the same "soft warning on missing SLSA" approach here for consistency with the asset-not-found case.

Fix All in Claude Code

Last reviewed commit: 196d502

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 adds provenance verification result storage to the lockfile, which is a great security enhancement. When a tool's artifact is verified using sigstore/SLSA/GitHub attestations, this information is now recorded. Subsequent installations will fail if this recorded provenance cannot be re-verified, preventing potential downgrade or stripping attacks. The changes are well-implemented across the github and aqua backends, and the lockfile format has been updated accordingly with new tests. I have one minor suggestion to improve code robustness.

Note: Security Review did not run due to the size of the PR.

pi.provenance_url = Some(
provenance_path
.file_name()
.unwrap()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using .unwrap() here could lead to a panic if provenance_path.file_name() returns None. While it's unlikely in this context, it's safer to handle this case gracefully to prevent unexpected crashes. Consider using ok_or_else or a match statement to provide a more informative error message if the filename cannot be extracted.

Suggested change
.unwrap()
.file_name().ok_or_else(|| eyre!("Could not get filename from provenance path: {:?}", provenance_path))?

.await
} else {
None
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant API call per platform in provenance detection

Medium Severity

detect_provenance_type is called from resolve_lock_info_for_target once per platform target during mise lock. Each call fetches the same GitHub release via API (line 383-390). With ~11 platform targets, this produces ~11 duplicate API calls for the identical release data. The release and its assets are the same regardless of target platform, so this detection only needs to happen once.

Fix in Cursor Fix in Web

@jdx jdx force-pushed the feat/lockfile-provenance branch from 9a0492c to 92cb984 Compare March 7, 2026 14:09
@jdx
Copy link
Copy Markdown
Owner Author

jdx commented Mar 7, 2026

Re: "Redundant API call per platform in provenance detection" — this is not an issue. get_release_for_url (in src/github.rs:265-273) uses an in-memory cache via get_or_try_init_async. The release is fetched once from the API and subsequent calls for the same (api_url, repo, tag) key return the cached result. So while detect_provenance_type is called per platform, only one actual API call is made.

This comment was generated by Claude Code.

@jdx
Copy link
Copy Markdown
Owner Author

jdx commented Mar 7, 2026

bugbot run

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@jdx
Copy link
Copy Markdown
Owner Author

jdx commented Mar 7, 2026

bugbot run

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 7, 2026

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.4 x -- echo 23.7 ± 0.6 22.6 25.7 1.00 ± 0.03
mise x -- echo 23.6 ± 0.4 22.9 25.1 1.00

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.4 env 22.9 ± 0.8 22.1 29.0 1.01 ± 0.04
mise env 22.8 ± 0.3 22.3 24.8 1.00

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.4 hook-env 23.4 ± 0.3 22.8 25.2 1.00
mise hook-env 23.5 ± 0.2 22.9 25.2 1.00 ± 0.02

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.4 ls 22.6 ± 0.2 22.0 24.4 1.00
mise ls 22.8 ± 0.5 22.2 28.9 1.01 ± 0.02

xtasks/test/perf

Command mise-2026.3.4 mise Variance
install (cached) 149ms 148ms +0%
ls (cached) 81ms 81ms +0%
bin-paths (cached) 84ms 84ms +0%
task-ls (cached) 809ms 810ms +0%

src/lockfile.rs Outdated

// Merging empty with provenance picks up provenance from other
let merged = without.merge_with(&with_provenance);
assert_eq!(merged.provenance, Some("github-attestations".to_string()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Test contradicts merge_with provenance implementation

Medium Severity

The test test_provenance_merge_preserves_existing asserts that without.merge_with(&with_provenance) returns Some("github-attestations"), but merge_with always uses self.provenance.clone() (line 131). Since without is a default PlatformInfo with provenance: None, the result will be None, causing this assertion to fail. Either the merge_with implementation needs to fall back to other.provenance (like it does for url), or the test expectation is wrong.

Additional Locations (1)

Fix in Cursor Fix in Web

@jdx
Copy link
Copy Markdown
Owner Author

jdx commented Mar 7, 2026

bugbot run

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.


# Inject provenance into the lockfile (simulating a previously-verified install)
ESCAPED_PLATFORM="${PLATFORM//./\\.}"
sed "/\"platforms\.$ESCAPED_PLATFORM\"\]/a provenance = \"github-attestations\"" mise.lock >mise.lock.tmp && mv mise.lock.tmp mise.lock
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

E2e test creates duplicate TOML keys causing parse failure

Medium Severity

Since github_attestations defaults to true (confirmed in settings.toml), mise lock at line 31 will already write provenance = "github-attestations" for jqlang/jq in the lockfile. The sed injection on line 37 then appends another provenance = "github-attestations" line after the platform section header, creating a duplicate TOML key. This makes the lockfile unparseable, so the subsequent mise install fails with a TOML parse error instead of the expected "downgrade attack" message, causing assert_fail_contains on line 46 to fail.

Fix in Cursor Fix in Web

- Remove speculative GithubAttestations from detect_provenance_type
  since we cannot verify attestations exist without downloading the
  artifact. Recording is deferred to install-time after verification.
- Remove dead provenance_url sort key from lockfile serializer
- Improve downgrade error messages: show "no verification" instead of
  "None" when no provenance mechanism ran

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

}

None
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optimistic provenance detection causes false install failures

Medium Severity

detect_provenance_type records provenance in the lockfile based solely on aqua registry configuration (e.g., slsa_provenance field exists and is enabled), without verifying that the actual provenance artifacts are available. During install, verify_slsa can hit early-return paths (e.g., "no asset configured" or "no asset found") that warn! and return Ok(()) without recording provenance. The post-verification downgrade check then sees expected=SLSA but got=None and raises a misleading "downgrade attack" error, blocking installation of tools whose registry metadata is incomplete or whose provenance files don't actually exist.

Additional Locations (1)

Fix in Cursor Fix in Web

};
HTTP.download_file(&url, &checksum_path, Some(ctx.pr.as_ref()))
.await?;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cosign skip not applied to checksum download condition

Low Severity

When the lockfile specifies non-cosign provenance (e.g., SLSA), skip_cosign is true, but the checksum file download condition (needs_checksum || !skip_cosign) still downloads the checksum file if needs_checksum is true. This is mostly fine, but if needs_checksum is false and skip_cosign is true, the checksum file isn't downloaded, which is correct. However, the inverse case — when needs_checksum is false and skip_cosign is false (i.e., provenance is cosign) — the checksum file IS downloaded solely for cosign, but the checksum parse and set at line 1053-1061 is gated by needs_checksum, so no checksum is written. This logic works but makes the download condition harder to reason about because the two concerns (checksum and cosign) are conflated in one conditional.

Fix in Cursor Fix in Web

…ixes

- Remove speculative SLSA from detect_provenance_type — verify_slsa
  has early-return paths (missing asset, no config) that silently skip
  verification, causing false downgrade errors when the lockfile
  expects SLSA but verification never ran
- Tighten minisign short-circuit to check is_slsa()/is_github_attestations()
  explicitly since cosign runs later and cannot be set at that point
- Rename !skip_cosign to needs_cosign for clarity in download condition
- Drain unconsumed map entries in ProvenanceType's visit_map to satisfy
  strict deserializers
- Document intentional Ord/PartialEq inconsistency on ProvenanceType:
  Ord compares by priority ordinal only, not inner data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

}

None
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Aqua SLSA/attestation provenance not detected during lock

High Severity

detect_provenance_type in the aqua backend only returns Cosign or Minisign, never Slsa or GithubAttestations. Since mise lock only calls resolve_lock_info (which calls detect_provenance_type) and does NOT run the install/verify path, SLSA and GitHub attestation provenance is never recorded in the lockfile for aqua tools. The E2E test test_lockfile_provenance expects provenance.slsa for sops after mise lock, but sops has SLSA provenance (not cosign or minisign), so this assertion will fail. The repo's mise.lock also contains github-attestations provenance for aqua tools like actionlint/jq/gh, which cannot have been generated by this code path.

Additional Locations (1)

Fix in Cursor Fix in Web

fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.ordinal().cmp(&other.ordinal())
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ord and PartialEq/Hash contract violation on ProvenanceType

Medium Severity

ProvenanceType derives PartialEq, Eq, and Hash (which compare all fields including the inner url of Slsa), but manually implements Ord using only ordinal() (ignoring inner data). This violates Rust's requirement that a.cmp(b) == Equal implies a == b. Two Slsa variants with different URLs are Equal under Ord but unequal under PartialEq/Hash. While documented with a comment and clippy allow, this can cause subtle bugs in any sorted collection and violates the standard library trait contract.

Fix in Cursor Fix in Web

jdx and others added 2 commits March 7, 2026 19:48
When a higher-priority mechanism (e.g., GithubAttestations) has
already recorded provenance, skip cosign verification to prevent
hard errors from cosign failures blocking a legitimately verified
install. Also skips the unnecessary checksum file download in this
case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Re-add SLSA to detect_provenance_type (above cosign, below
  GithubAttestations) — SLSA registry config is reliable for
  detection unlike attestations which need API probing
- Fix Ord/PartialEq/Hash contract: implement PartialEq, Eq, and Hash
  manually to compare by ordinal only, consistent with Ord. Two Slsa
  variants with different URLs are now considered equal under all
  comparison traits. Tests updated to use pattern matching for URL
  verification instead of assert_eq.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

name.contains(".intoto.jsonl")
|| name.ends_with(".sigstore.json")
|| name.ends_with(".sigstore")
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SLSA detection heuristic mismatches actual verification asset selection

High Severity

detect_provenance_type treats .sigstore.json and .sigstore files as SLSA evidence and records Slsa provenance in the lockfile. However, pick_best_provenance (used by try_verify_slsa at install time) only matches .intoto.jsonl, provenance, and .attestation — it does not match .sigstore.json/.sigstore. This causes try_verify_slsa to find no provenance asset and return NoAttestations, triggering a false-positive "downgrade attack" error that blocks installation. The existing _security_features method (line 128-138) already correctly classifies .sigstore.json/.sigstore as GitHub Attestations, not SLSA.

Fix in Cursor Fix in Web

detect_provenance_type was matching .sigstore.json/.sigstore files as
SLSA evidence, but pick_best_provenance only matches .intoto.jsonl,
provenance, and .attestation. This caused false-positive downgrade
errors when .sigstore files were present but no SLSA provenance file
existed. The .sigstore files are GitHub Attestations (already handled
by the attestation API probe), not SLSA.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Note that ProvenanceType should not be used as a BTreeMap/HashSet key
due to intentional ordinal-only equality, and that merge() should be
preferred over max() to preserve inner data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

let locked_provenance = tv
.lock_platforms
.get(&platform_key)
.and_then(|pi| pi.provenance.clone());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

GitHub backend doesn't clear lockfile provenance before verification

Medium Severity

In verify_attestations_or_slsa, locked_provenance is read via .clone() (preserving the original in lock_platforms), while the aqua backend uses .take() to clear it before re-verification. When provenance_result is Some(...), the caller unconditionally overwrites platform_info.provenance, which works. But if in the future a code path returns Ok(None) without hitting the downgrade check at line 1249 (e.g., if locked_provenance were None but provenance still existed in lock_platforms from a different source), stale provenance data could persist unverified.

Additional Locations (1)

Fix in Cursor Fix in Web

…e vs take

- Document why cosign_already_verified is safe to cache in aqua verify
- Note ruby's speculative GithubAttestations assumption in doc comment
- Explain github.rs clone vs aqua's take approach for locked_provenance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

});
if has_slsa {
return Some(ProvenanceType::Slsa { url: None });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lock-time SLSA detection ignores platform, install-time filters by platform

Medium Severity

detect_provenance_type at lock-time checks if any release asset matches SLSA patterns (.intoto.jsonl, provenance, .attestation) using release.assets.iter().any(...) without platform filtering. At install-time, try_verify_slsa uses picker.pick_best_provenance(&asset_names) which filters and scores by platform. A release may have a provenance file for linux but not for macos. Lock-time would record SLSA provenance for all platforms, but install-time would fail with NoAttestations on the platform lacking a matching provenance asset, producing a misleading downgrade-attack error.

Additional Locations (1)

Fix in Cursor Fix in Web

jdx and others added 2 commits March 7, 2026 21:26
- github.rs: detect_provenance_type now accepts PlatformTarget for
  platform-aware SLSA asset detection via pick_best_provenance
- lockfile.rs: Slsa{url:None} serializes as plain string "slsa" instead
  of orphaned table header [provenance.slsa]
- e2e: remove existing provenance lines before sed injection to avoid
  duplicate TOML keys; update assertion to match new serialization
- aqua.rs: document SLSA vs attestations detection priority and stale
  checksum trust assumptions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx
Copy link
Copy Markdown
Owner Author

jdx commented Mar 7, 2026

bugbot run

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

let platform_key = self.get_platform_key();
let platform_info = tv.lock_platforms.entry(platform_key).or_default();
platform_info.provenance = Some(provenance_type);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

GitHub backend doesn't clear stale provenance on successful verification

Medium Severity

The verify_attestations_or_slsa method reads locked_provenance via .clone() (since tv is &ToolVersion) and the comment claims "the caller unconditionally overwrites platform_info.provenance." But the caller only writes when provenance_result is Some. When verification succeeds and returns Some, it correctly overwrites. However, if a tool previously had provenance in the lockfile and on a subsequent install verify_attestations_or_slsa returns Ok(Some(...)) with a different type than what was locked, no downgrade check fires because the check only runs when no verification succeeded (Ok(None)). The locked_provenance value is read but never compared against the actual result when verification does succeed — a tool could silently switch from GithubAttestations to Slsa without triggering the downgrade detection.

Additional Locations (1)

Fix in Cursor Fix in Web

url_api: None,
conda_deps: None,
provenance: Some(ProvenanceType::Minisign),
..Default::default()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Zig lockfile records minisign provenance before verification succeeds

Medium Severity

In resolve_lock_info, the JSON-fetched path pre-sets provenance: Some(ProvenanceType::Minisign) at lock time (line 433). But the fallback path for numbered versions intentionally omits provenance (line 438 comment). This means mise lock records minisign provenance for all zig versions that have JSON metadata, even though minisign verification hasn't actually run yet. If minisign verification later fails at install time, the lockfile already claims minisign was verified, creating a mismatch that blocks future installs with a false "downgrade attack" error.

Additional Locations (1)

Fix in Cursor Fix in Web

Comment on lines +316 to +329
let legacy_provenance_url = match t.remove("provenance_url") {
Some(toml::Value::String(s)) => Some(s),
_ => None,
};
let provenance = match t.remove("provenance") {
Some(toml::Value::String(s)) => {
let mut prov: ProvenanceType = s
.parse()
.map_err(|_| eyre!("unrecognized provenance type {s:?} in lockfile"))?;
// Attach legacy provenance_url to SLSA if present
if let ProvenanceType::Slsa { ref mut url } = prov {
*url = legacy_provenance_url;
}
Some(prov)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

legacy_provenance_url is silently discarded when provenance is not Slsa

The backwards-compat migration reads provenance_url from the legacy lockfile field and then attaches it only to a Slsa provenance variant. If a legacy lockfile contains both provenance = "github-attestations" and provenance_url = "https://..." (an inconsistency that could arise from manual editing or a partially-migrated lockfile), the URL is silently discarded with no warning or parse error.

More importantly, if a lockfile was written with provenance = "slsa" (string, no URL) but also has a provenance_url key that this migration was designed to migrate, the url field will be set to legacy_provenance_url. However, if the new-format provenance = { slsa = { url = "..." } } was already written AND a stale provenance_url key also exists (orphaned from a previous format), the table-branch of the match at line 331 handles deserialization — but legacy_provenance_url is read first (line 316) and then never used. This is harmless but means the migration path has dead code when the new table format is present.

Consider adding a diagnostic warning when legacy_provenance_url is non-None but the provenance type doesn't accept it, to help users identify inconsistent lockfiles:

if legacy_provenance_url.is_some() && !matches!(provenance, Some(ProvenanceType::Slsa { .. })) {
    warn!("lockfile has provenance_url but provenance type is not slsa — url discarded");
}

Fix in Claude Code

- zig.rs: remove premature minisign provenance from resolve_lock_info;
  provenance is recorded in install_version_ after download() confirms
  minisign verification succeeded
- github.rs: add defense-in-depth checks that verify the provenance type
  returned by successful verification matches the lockfile expectation,
  preventing silent type switches between mechanisms

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx merged commit 8fa4cb7 into main Mar 7, 2026
36 checks passed
@jdx jdx deleted the feat/lockfile-provenance branch March 7, 2026 21:56
jdx pushed a commit that referenced this pull request Mar 7, 2026
### 🚀 Features

- **(vfox)** add `RUNTIME.envType` for libc variant detection by @malept
in [#8493](#8493)
- store provenance verification results in lockfile by @jdx in
[#8495](#8495)

### 🐛 Bug Fixes

- **(env)** skip remote version fetching for "latest" in prefer-offline
mode by @jdx in [#8500](#8500)
- **(tasks)** deduplicate shared deps across task delegation by
@vadimpiven in [#8497](#8497)
- **(windows)** correctly identify mise binary without extension by @jdx
in [#8503](#8503)

### 🚜 Refactor

- **(core)** migrate cmd! callers to async with kill_on_drop by @jdx in
[a63f7d2](a63f7d2)

### Chore

- **(ci)** temporarily disable `mise up` in release-plz by @jdx in
[#8504](#8504)
- consolidate all linters into hk.pkl by @jdx in
[#8498](#8498)

## 📦 Aqua Registry Updates

#### New Packages (1)

- [`apache/ant`](https://github.com/apache/ant)
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.

1 participant