Skip to content

Commit 14eeec6

Browse files
fix(vendor): accept sha1-only SRI as last-resort verification in registry_fetch (#134)
npm-era lockfile entries carry ONLY `integrity sha1-…` (yarn classic writes them for packages published before sha512 metadata existed). verify_sri ranked sha512/384/256 and refused everything else, so every legacy package became unvendorable whenever the prebuilt-artifact service missed and the local-build fallback fetched the pristine tarball: `vendor_fetch_failed: no usable hash in SRI sha1-…`. A clean vendored run on a real 2019-era monorepo failed 14 of 62 packages this way (ansi-regex@3.0.0, decode-uri-component@0.2.0, semver@5.3.0, …). sha1 now ranks BELOW sha256/384/512 (never preferred, only used when it is all the lockfile records) — the same trust the package manager itself enforces for those entries, and the same decision the LockIntegrity::Sha1Hex bare-hex arm already made in this file. New unit test pins: sha1-only verifies, sha1 mismatch refuses (not fail-open), sha1 never outranks a stronger hash in multi-hash SRIs, md5 still refused. Validated end-to-end on the real monorepo: all 14 previously-failing packages vendor, scan exits success, yarn 1 installs 62/62 vendored packages byte-identical to their patched blobs. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c8479ae commit 14eeec6

1 file changed

Lines changed: 34 additions & 0 deletions

File tree

crates/socket-patch-core/src/patch/vendor/registry_fetch.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,14 @@ fn verify_integrity(bytes: &[u8], integrity: &LockIntegrity) -> Result<(), Fetch
756756

757757
/// SRI verification: pick the strongest hash of a (possibly multi-hash,
758758
/// whitespace-separated) SRI string and compare base64 digests.
759+
///
760+
/// `sha1` is accepted as a LAST resort (never preferred over sha256+): it is
761+
/// the only integrity npm-era lockfile entries carry (yarn classic writes
762+
/// `integrity sha1-…` for them), and it is the exact guarantee the package
763+
/// manager itself enforces for those entries — refusing it would make every
764+
/// legacy package unvendorable whenever the prebuilt-artifact service misses
765+
/// (the 2026-07 strapi clean-run regression). The bare-hex twin of this trust
766+
/// decision already lives in the `LockIntegrity::Sha1Hex` arm above.
759767
fn verify_sri(bytes: &[u8], sri: &str) -> Result<(), String> {
760768
let mut best: Option<(u8, &str, &str)> = None;
761769
for token in sri.split_whitespace() {
@@ -766,6 +774,7 @@ fn verify_sri(bytes: &[u8], sri: &str) -> Result<(), String> {
766774
"sha512" => 3,
767775
"sha384" => 2,
768776
"sha256" => 1,
777+
"sha1" => 0,
769778
_ => continue,
770779
};
771780
if best.map(|(r, _, _)| rank > r).unwrap_or(true) {
@@ -779,6 +788,7 @@ fn verify_sri(bytes: &[u8], sri: &str) -> Result<(), String> {
779788
let actual = match algo {
780789
"sha512" => b64.encode(Sha512::digest(bytes)),
781790
"sha384" => b64.encode(Sha384::digest(bytes)),
791+
"sha1" => b64.encode(Sha1::digest(bytes)),
782792
_ => b64.encode(Sha256::digest(bytes)),
783793
};
784794
if actual == expect {
@@ -990,6 +1000,30 @@ mod tests {
9901000
);
9911001
}
9921002

1003+
#[test]
1004+
fn sri_sha1_is_accepted_as_last_resort() {
1005+
use base64::Engine as _;
1006+
let bytes = b"hello";
1007+
let sha1_b64 = base64::engine::general_purpose::STANDARD.encode(Sha1::digest(bytes));
1008+
// npm-era lockfile entries carry ONLY `sha1-…` (the strapi clean-run
1009+
// regression: `no usable hash in SRI`); it must verify…
1010+
assert!(
1011+
verify_sri(bytes, &format!("sha1-{sha1_b64}")).is_ok(),
1012+
"sha1-only SRI must be usable"
1013+
);
1014+
// …and still be a REAL check, not a fail-open.
1015+
let wrong = base64::engine::general_purpose::STANDARD.encode(Sha1::digest(b"other"));
1016+
assert!(
1017+
verify_sri(bytes, &format!("sha1-{wrong}")).is_err(),
1018+
"sha1 mismatch must refuse"
1019+
);
1020+
// sha1 never outranks a stronger hash: a correct sha1 alongside a
1021+
// wrong sha512 fails (strongest wins), the reverse passes.
1022+
let sha512_good = sri_of(bytes);
1023+
assert!(verify_sri(bytes, &format!("sha1-{sha1_b64} sha512-WRONG=")).is_err());
1024+
assert!(verify_sri(bytes, &format!("sha1-{wrong} {sha512_good}")).is_ok());
1025+
}
1026+
9931027
#[tokio::test]
9941028
async fn fetch_verifies_sri_and_extracts_with_modes() {
9951029
let tgz = make_tgz(&[

0 commit comments

Comments
 (0)