{"record":{"id":"7e80a9afb266cd73","repo":"jdx/mise","slug":"lockfile-generation-would-downgrade-recorded-provenance","errorCode":null,"errorMessage":"lockfile generation would downgrade recorded provenance; previous files were preserved","messagePattern":"lockfile generation would downgrade recorded provenance; previous files were preserved","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/lockfile/generate.rs","lineNumber":783,"sourceCode":"        lockfile.set_uv_lock(&ba.short, &tv.version, &backend_name, &options, graph)?;\n    }\n    Ok(())\n}\n\nfn ensure_no_downgrade(old: &PlatformInfo, new: &PlatformInfo, backend: &str) -> Result<()> {\n    // A verified Packslip signer is the replacement trust baseline for an\n    // artifact authenticated by its signed release manifest. Older incremental\n    // lock updates could carry detected GitHub provenance into a Packslip entry,\n    // but complete generation intentionally does not persist that unverified\n    // link. Without a signer, retain the ordinary provenance ratchet.\n    let packslip_signer_replaces_provenance =\n        backend.starts_with(\"packslip:\") && new.signer.is_some();\n    if provenance_is_downgrade(\n        old.provenance.as_ref(),\n        new.provenance.as_ref(),\n        packslip_signer_replaces_provenance,\n    ) {\n        bail!(\n            \"lockfile generation would downgrade recorded provenance; previous files were preserved\"\n        );\n    }\n    if let Some(signer) = &old.signer\n        && (new.signer.as_ref() != Some(signer) || new.attested_by != old.attested_by)\n    {\n        bail!(\n            \"lockfile generation would change the recorded signer; previous files were preserved\"\n        );\n    }\n    // Preserve identities across reordering, then pair replaced URLs in their\n    // configured order so version upgrades retain the previous trust baseline.\n    let mut replacements = new.additional_artifacts.iter().filter(|artifact| {\n        !old.additional_artifacts\n            .iter()\n            .any(|old| old.url == artifact.url)\n    });\n    for artifact in &old.additional_artifacts {","sourceCodeStart":765,"sourceCodeEnd":801,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/lockfile/generate.rs#L765-L801","documentation":"mise's lockfile records cryptographic provenance (e.g. SLSA attestations) for each locked artifact. During `lockfile generate`, `ensure_no_downgrade` compares the existing recorded provenance with the newly computed one; if the new provenance is weaker or absent (including the packslip-signer-replaces-provenance case), generation aborts so previously written lockfiles are preserved untouched. This is a trust-safety guard, not a bug.","triggerScenarios":"Running lockfile generation (`mise lock` / internal `generate`) for a tool whose existing lockfile entry has provenance (or a packslip signer) recorded, but the regenerated entry's provenance is missing or weaker per `provenance_is_downgrade` — e.g. the backend stopped serving attestations, the packslip manifest no longer includes them, or settings for attestations were disabled.","commonSituations":"Regenerating a lockfile after a registry/backend change dropped attestation data; switching a backend from packslip to a non-attesting backend; CI regenerating lockfiles after attestations were temporarily unavailable upstream; toggling `github_attestations` settings off.","solutions":["Check why the new provenance is weaker: verify the packslip manifest/attestations are still published for the target version","Re-enable attestation settings (e.g. `github_attestations`) that were disabled in settings.toml","Regenerate with the backend that produced the original provenance (e.g. packslip:) instead of a fallback backend","If the downgrade is intentional, delete/review the existing lockfile entry first so generation starts from a clean trusted state"],"exampleFix":"# before: attestations disabled, regeneration downgrades provenance\n[settings.ruby]\ngithub_attestations = false\n# after\n[settings.ruby]\ngithub_attestations = true","handlingStrategy":"validation","validationCode":"let old = existing_entry.provenance.as_ref();\nlet new = computed_entry.provenance.as_ref();\nif new.is_none() || new.map(|n| n.kind()) != old.map(|o| o.kind()) {\n    // provenance would be downgraded — regenerate with attesting backend first\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep attestation settings (github_attestations) enabled consistently across environments","Always regenerate lockfiles with the same backend that produced the original provenance","Don't disable attestations in CI before regenerating lockfiles","Review lockfile provenance fields after backend or registry changes"],"tags":["lockfile","provenance","security"],"backgroundTag":"checksum-mismatch","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}