Skip to content

Commit 58a6d1c

Browse files
committed
Compute digests through utils::digest helpers
Add sha256_hex_of, sha1_hex_of, sha512_base64_of and sha512_sri_of to utils::digest and move every inline sha256, sha1 and sha512 SRI computation in production files no open PR changes onto them. Delete the private copies they replace: ledger_snapshots::sha256_hex, vlt_preflight::sha512_sri, nuget_feed::content_hash, the npm_pack and bun_lock SRI blocks, and api::client::is_valid_sha256_hex (now digest::is_hex(s, 64)). No user-visible change: every helper produces the same lowercase hex or padded base64 the inline copies did, pinned by known vectors. A ratchet test fails on any new inline digest in production code and lists the six files left for slice 2. Refs #706. Assisted-by: Claude Code:claude-opus-5-5
1 parent 548501f commit 58a6d1c

19 files changed

Lines changed: 211 additions & 113 deletions

File tree

‎crates/socket-patch-core/src/api/client.rs‎

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use crate::api::types::*;
2020
use crate::api::vendor_prefetch::VendorPrefetch;
2121
pub use crate::api::vendor_prefetch::VendorPrefetchGuard;
2222
use crate::constants::USER_AGENT as USER_AGENT_VALUE;
23+
use crate::utils::digest::is_hex;
2324
use crate::utils::env_compat::{is_debug_enabled, is_offline_env, proxy_url_from_env};
2425
use crate::utils::notice::{notice_once, Notice};
2526
use crate::utils::socket_cli_config;
@@ -1055,7 +1056,7 @@ impl ApiClient {
10551056
/// slug are available, otherwise falls back to the public proxy.
10561057
pub async fn fetch_blob(&self, hash: &str) -> Result<Option<BinaryBody>, ApiError> {
10571058
// Validate hash format: SHA-256 = 64 hex characters
1058-
if !is_valid_sha256_hex(hash) {
1059+
if !is_hex(hash, 64) {
10591060
return Err(ApiError::InvalidHash(format!(
10601061
"Invalid hash format: {}. Expected SHA256 hash (64 hex characters).",
10611062
hash
@@ -2541,11 +2542,6 @@ fn truncate_to_chars(s: &str, max_chars: usize) -> String {
25412542
format!("{}...", truncated)
25422543
}
25432544

2544-
/// Validate that a string is a 64-character hex string (SHA-256).
2545-
fn is_valid_sha256_hex(s: &str) -> bool {
2546-
s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
2547-
}
2548-
25492545
/// Validate the standard 8-4-4-4-12 UUID hex grouping.
25502546
fn is_valid_uuid(s: &str) -> bool {
25512547
let parts: Vec<&str> = s.split('-').collect();
@@ -3120,13 +3116,14 @@ mod tests {
31203116
#[test]
31213117
fn test_is_valid_sha256_hex() {
31223118
let valid = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
3123-
assert!(is_valid_sha256_hex(valid));
3119+
assert!(is_hex(valid, 64));
31243120

31253121
// Too short
3126-
assert!(!is_valid_sha256_hex("abcdef"));
3122+
assert!(!is_hex("abcdef", 64));
31273123
// Non-hex
3128-
assert!(!is_valid_sha256_hex(
3129-
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
3124+
assert!(!is_hex(
3125+
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
3126+
64
31303127
));
31313128
}
31323129

@@ -3232,7 +3229,7 @@ mod tests {
32323229

32333230
/// `fetch_blob` must reject a malformed hash *before* any network I/O:
32343231
/// the client points at a closed port, so a regression that bypasses the
3235-
/// `is_valid_sha256_hex` guard surfaces as `ApiError::Network` instead
3232+
/// `is_hex(hash, 64)` guard surfaces as `ApiError::Network` instead
32363233
/// of `InvalidHash` (mirrors `invalid_uuid_is_failed_without_network`;
32373234
/// `fetch_diff`'s twin guard is already covered).
32383235
#[tokio::test]
@@ -3635,31 +3632,31 @@ mod tests {
36353632
#[test]
36363633
fn test_sha256_uppercase_valid() {
36373634
let upper = "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789";
3638-
assert!(is_valid_sha256_hex(upper));
3635+
assert!(is_hex(upper, 64));
36393636
}
36403637

36413638
#[test]
36423639
fn test_sha256_65_chars_invalid() {
36433640
let too_long = "a".repeat(65);
3644-
assert!(!is_valid_sha256_hex(&too_long));
3641+
assert!(!is_hex(&too_long, 64));
36453642
}
36463643

36473644
#[test]
36483645
fn test_sha256_63_chars_invalid() {
36493646
let too_short = "a".repeat(63);
3650-
assert!(!is_valid_sha256_hex(&too_short));
3647+
assert!(!is_hex(&too_short, 64));
36513648
}
36523649

36533650
#[test]
36543651
fn test_sha256_empty_invalid() {
3655-
assert!(!is_valid_sha256_hex(""));
3652+
assert!(!is_hex("", 64));
36563653
}
36573654

36583655
#[test]
36593656
fn test_sha256_mixed_case_valid() {
36603657
let mixed = "aAbBcCdDeEfF0123456789aAbBcCdDeEfF0123456789aAbBcCdDeEfF01234567";
36613658
assert_eq!(mixed.len(), 64);
3662-
assert!(is_valid_sha256_hex(mixed));
3659+
assert!(is_hex(mixed, 64));
36633660
}
36643661

36653662
// ── UUID validation tests ───────────────────────────────────────

‎crates/socket-patch-core/src/hosted/npm_manifest.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub async fn fetch_hosted_npm_manifest(
4646
#[cfg(test)]
4747
mod tests {
4848
use super::*;
49-
use crate::patch::redirect::vlt_preflight::sha512_sri;
49+
use crate::utils::digest::sha512_sri_of;
5050

5151
fn tgz(entries: &[(&str, &[u8])]) -> Vec<u8> {
5252
let mut builder = tar::Builder::new(flate2::write::GzEncoder::new(
@@ -67,7 +67,7 @@ mod tests {
6767
fn decodes_the_manifest_and_checks_the_sha512() {
6868
let manifest = br#"{"name":"uuid","bin":{"uuid":"./dist/bin/uuid"}}"#;
6969
let bytes = tgz(&[("package/package.json", manifest)]);
70-
let sri = sha512_sri(&bytes);
70+
let sri = sha512_sri_of(&bytes);
7171
assert_eq!(
7272
decode_hosted_npm_manifest(&bytes, Some(&sri))
7373
.unwrap()
@@ -78,7 +78,7 @@ mod tests {
7878
decode_hosted_npm_manifest(&bytes, None).unwrap().as_bytes(),
7979
manifest
8080
);
81-
let other = sha512_sri(b"other");
81+
let other = sha512_sri_of(b"other");
8282
assert!(decode_hosted_npm_manifest(&bytes, Some(&other))
8383
.unwrap_err()
8484
.contains("sha512"));

‎crates/socket-patch-core/src/patch/redirect/upstream/client.rs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,7 @@ impl UpstreamClient {
289289
if let Some(sri) = dist.integrity.as_deref() {
290290
crate::vendor::registry_fetch::verify_sri(&bytes, sri)?;
291291
} else if let Some(sha1) = dist.shasum.as_deref() {
292-
use sha1::Digest as _;
293-
if hex::encode(sha1::Sha1::digest(&bytes)) != sha1 {
292+
if crate::utils::digest::sha1_hex_of(&bytes) != sha1 {
294293
return Err("registry archive checksum mismatch".into());
295294
}
296295
} else {
@@ -659,7 +658,7 @@ fn gosumdb_base(module: &str) -> Option<String> {
659658
pub(crate) fn go_mod_h1(go_mod: &[u8]) -> String {
660659
use base64::Engine as _;
661660
use sha2::{Digest, Sha256};
662-
let file_sum = hex::encode(Sha256::digest(go_mod));
661+
let file_sum = crate::utils::digest::sha256_hex_of(go_mod);
663662
let summary = format!("{file_sum} go.mod\n");
664663
format!(
665664
"h1:{}",

‎crates/socket-patch-core/src/patch/redirect/vlt_preflight.rs‎

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
use std::collections::{BTreeMap, BTreeSet};
1010
use std::time::Duration;
1111

12-
use base64::Engine as _;
13-
use sha2::{Digest, Sha512};
12+
use crate::utils::digest::sha512_sri_of;
1413

1514
use super::{vlt, DepOverride};
1615
use crate::api::client::{ApiClient, MAX_VENDOR_PACKAGE_BYTES};
@@ -66,14 +65,6 @@ impl ArtifactProbe {
6665
}
6766
}
6867

69-
/// The SRI form of `bytes`' sha512.
70-
pub fn sha512_sri(bytes: &[u8]) -> String {
71-
format!(
72-
"sha512-{}",
73-
base64::engine::general_purpose::STANDARD.encode(Sha512::digest(bytes))
74-
)
75-
}
76-
7768
fn fetch_error(error: impl Into<String>) -> ArtifactProbe {
7869
ArtifactProbe {
7970
error: Some(error.into()),
@@ -131,7 +122,7 @@ async fn fetch_capped(client: &reqwest::Client, url: &str, max: u64) -> Artifact
131122
Ok(bytes) => ArtifactProbe {
132123
status: Some(status),
133124
content_encoding,
134-
sha512: Some(sha512_sri(&bytes)),
125+
sha512: Some(sha512_sri_of(&bytes)),
135126
body: Some(bytes),
136127
error: None,
137128
},
@@ -270,7 +261,7 @@ mod tests {
270261
.mount(&server)
271262
.await;
272263
let probe = probe_of(&server, "/a.tgz").await;
273-
assert_eq!(probe.failure(&sha512_sri(BODY)), None);
264+
assert_eq!(probe.failure(&sha512_sri_of(BODY)), None);
274265
assert_eq!(probe.body.as_deref(), Some(BODY));
275266
}
276267

@@ -286,7 +277,9 @@ mod tests {
286277
.mount(&server)
287278
.await;
288279
assert_eq!(
289-
probe_of(&server, "/a.tgz").await.failure(&sha512_sri(BODY)),
280+
probe_of(&server, "/a.tgz")
281+
.await
282+
.failure(&sha512_sri_of(BODY)),
290283
None
291284
);
292285
}
@@ -319,7 +312,7 @@ mod tests {
319312
.respond_with(ResponseTemplate::new(500))
320313
.mount(&server)
321314
.await;
322-
let expected = sha512_sri(BODY);
315+
let expected = sha512_sri_of(BODY);
323316

324317
let gz_probe = probe_of(&server, "/gz.tgz").await;
325318
assert_eq!(
@@ -328,7 +321,7 @@ mod tests {
328321
);
329322
assert_eq!(
330323
gz_probe.sha512,
331-
Some(sha512_sri(&gz)),
324+
Some(sha512_sri_of(&gz)),
332325
"the body is hashed as received, never decoded"
333326
);
334327
assert_eq!(
@@ -411,7 +404,7 @@ mod tests {
411404
.respond_with(ResponseTemplate::new(200).set_body_bytes(BODY))
412405
.mount(&server)
413406
.await;
414-
let expected = sha512_sri(BODY);
407+
let expected = sha512_sri_of(BODY);
415408
assert_eq!(probe_of(&server, "/r1").await.failure(&expected), None);
416409
assert!(probe_of(&server, "/r0")
417410
.await
@@ -430,7 +423,7 @@ mod tests {
430423
let probe = fetch_capped(api(None).plain_http(), &url, 4).await;
431424
assert!(
432425
probe
433-
.failure(&sha512_sri(BODY))
426+
.failure(&sha512_sri_of(BODY))
434427
.is_some_and(|r| r.starts_with("fetch error ") && r.contains("too large")),
435428
"{probe:?}"
436429
);

‎crates/socket-patch-core/src/policy/mod.rs‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ use std::collections::BTreeMap;
1515
use std::io::Read;
1616
use std::path::{Path, PathBuf};
1717

18-
use sha2::{Digest, Sha256};
19-
2018
use crate::api::ranking::max_severity_order;
2119
use crate::api::types::PatchSearchResult;
2220
use crate::crawlers::Ecosystem;
@@ -579,7 +577,7 @@ impl SelectionPolicy {
579577
Some((name, bytes, parsed)) if !parsed.empty => {
580578
let source = PolicySource::File {
581579
path: name.to_string(),
582-
sha256: hex::encode(Sha256::digest(&bytes)),
580+
sha256: crate::utils::digest::sha256_hex_of(&bytes),
583581
};
584582
let block = parsed.patches.clone().unwrap_or_default();
585583
Self::from_parts(name, source, &parsed.project_ignore_paths, &block)?

‎crates/socket-patch-core/src/policy/tests.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::*;
2+
use sha2::{Digest, Sha256};
23

34
fn mem(files: &[(&str, &str)]) -> MemoryPolicyFs {
45
let mut fs = MemoryPolicyFs::default();

‎crates/socket-patch-core/src/update/download.rs‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
use std::io::Read;
1919
use std::path::{Path, PathBuf};
2020

21-
use sha2::{Digest, Sha256};
22-
2321
use super::release::{UpdateEndpoints, UpdateTimeouts};
2422
use super::UpdateError;
2523
use crate::utils::http::read_capped;
@@ -345,7 +343,7 @@ pub async fn download_and_stage(
345343
let archive = fetch_archive(endpoints, timeouts, version, asset).await?;
346344

347345
// 3. Checksum BEFORE extraction.
348-
let actual_sha = hex::encode(Sha256::digest(&archive));
346+
let actual_sha = crate::utils::digest::sha256_hex_of(&archive);
349347
if actual_sha != expected_sha {
350348
return Err(UpdateError::ChecksumMismatch {
351349
asset: asset.to_string(),

0 commit comments

Comments
 (0)