diff --git a/knowledge/catalog.json b/knowledge/catalog.json index 496c3eb..2929620 100644 --- a/knowledge/catalog.json +++ b/knowledge/catalog.json @@ -1740,7 +1740,9 @@ "implementation": "src/hashes/sha1/mod.rs", "documentation": "src/hashes/sha1/README.md", "tests": [ - "hashes::sha1::tests::hashes_standard_vectors" + "hashes::sha1::tests::hashes_standard_vectors", + "hashes::sha1::tests::continues_from_midstate", + "hashes::sha1::tests::rejects_extra_suffix_bytes" ], "references": [ "fips-180-4", @@ -1771,6 +1773,26 @@ "metric_keys": [ "sha1_u32_32" ] + }, + { + "id": "message-80-midstate", + "label": "64-byte prefix plus 16-byte suffix from midstate", + "parameters": { + "prefix_bytes": 64, + "suffix_bytes": 16 + }, + "includes": "fragment-only: one-block midstate continuation, final padding, and digest restoration; excludes prefix authentication, suffix pushes, digest comparison, and terminal predicate", + "script_bytes": 209619, + "witness_bytes": null, + "witness_bytes_max": null, + "max_stack_items": null, + "executed_opcodes": null, + "validation_weight": null, + "setup_script_bytes": null, + "per_use_script_bytes": 209619, + "metric_keys": [ + "sha1_u32_80_midstate" + ] } ], "limitations": [ diff --git a/knowledge/comparisons/hashes.md b/knowledge/comparisons/hashes.md index b2e332e..16989c8 100644 --- a/knowledge/comparisons/hashes.md +++ b/knowledge/comparisons/hashes.md @@ -7,6 +7,7 @@ Measured fragments exclude input pushes and output comparison. | BLAKE3 sparse direct u4 | 32-byte input | 59,529 | differentially-validated | Fixed length at generation time; at most 32 bytes | | BLAKE3 limb29 | 64-byte input | 72,293 | differentially-validated | Single 1,024-byte chunk only; includes table memory | | SHA-1 u32 | 32-byte input | 209,726 | differentially-validated | Collision-broken compatibility hash | +| SHA-1 u32 midstate | 64-byte prefix + 16-byte suffix | 209,619 | differentially-validated | Requires an authenticated one-block midstate; collision-broken | | RIPEMD-160 u32 | 32-byte input | 244,063 | differentially-validated | 160-bit output | | SHA-256 u4 | 32-byte input | 332,942 | differentially-validated | Large research fragment | | SHA-256 u32 | 32-byte input | 512,428 | differentially-validated | Larger than local u4 variant | diff --git a/knowledge/primitives/sha1-u32.md b/knowledge/primitives/sha1-u32.md index b225b14..257d911 100644 --- a/knowledge/primitives/sha1-u32.md +++ b/knowledge/primitives/sha1-u32.md @@ -7,10 +7,13 @@ bytes. collision resistance is required. - **Evidence:** differentially validated against standard reference digests and internal round checks. -- **Representative result:** a 32-byte hashing fragment is 209,726 bytes. +- **Representative results:** a 32-byte hashing fragment is 209,726 bytes; a + 16-byte suffix continuation from a one-block midstate is 209,619 bytes. - **Deployment:** operation-heavy research fragment; complete consensus and policy feasibility are configuration-dependent and not established here. -- **Stack contract:** one byte item per input byte; 20 byte items returned. +- **Stack contract:** one byte item per input byte; 20 byte items returned. The + midstate continuation consumes exactly 16 suffix bytes and returns the + digest for a 64-byte prefix plus that suffix. See the [implementation README](../../src/hashes/sha1/README.md) and catalog record `hash/sha1-u32`. diff --git a/src/hashes/sha1/README.md b/src/hashes/sha1/README.md index 24560b7..7ac866e 100644 --- a/src/hashes/sha1/README.md +++ b/src/hashes/sha1/README.md @@ -19,6 +19,7 @@ digest comparison. | Configuration | Hashing script | | --- | ---: | | 32-byte input | 209726 bytes | +| 64-byte prefix + 16-byte suffix from midstate | 209619 bytes | This fragment exceeds the repository optimizer's 32 KiB input cutoff and is reported unoptimized. @@ -58,6 +59,12 @@ value in `0..=255`. temporary lookup table and message schedule are removed, and the altstack is restored to its starting depth. +`sha1_80bytes_from_midstate(midstate)` consumes exactly 16 suffix bytes and +continues from the state after a 64-byte prefix. The generated fragment +includes the final 80-bit length encoding for the resulting 80-byte message; +the caller is responsible for authenticating the supplied midstate and its +prefix binding. + ## Operational notes Padding uses SHA-1's big-endian length encoding with a zero high 32-bit word, diff --git a/src/hashes/sha1/mod.rs b/src/hashes/sha1/mod.rs index 6cae303..24b13bb 100644 --- a/src/hashes/sha1/mod.rs +++ b/src/hashes/sha1/mod.rs @@ -58,6 +58,39 @@ pub fn sha1(num_bytes: usize) -> Script { } } +/// Continues SHA-1 from the state after one 64-byte block over a 16-byte +/// suffix, producing the digest of the resulting 80-byte message. +pub fn sha1_80bytes_from_midstate(midstate: [u32; 5]) -> Script { + let mut state = midstate; + state.reverse(); + script! { + { push_reverse_bytes_to_alt(16) } + { u8_push_xor_table() } + for _ in 0..16 { + OP_FROMALTSTACK + } + 0x80 + { push_to_stack(0, 39) } + { u32_push(0) } + { u32_push(640) } + for i in 1..16 { + { u32_roll(i as u32) } + } + for word in state { + { u32_push(word) } + } + { sha1_transform(16) } + { sha1_final() } + for _ in 0..5 { + { u32_toaltstack() } + } + { u8_drop_xor_table() } + for _ in 0..5 { + { u32_fromaltstack() } + } + } +} + fn push_reverse_bytes_to_alt(num_bytes: usize) -> Script { script! { for i in 1..=num_bytes { @@ -307,6 +340,7 @@ fn majority(words_above_table: usize) -> Script { mod tests { use super::*; use crate::arithmetic::u32::stack::{u32_equal, u32_push}; + use crate::support::execution::execute_script_with_inputs; use bitcoin::hashes::{sha1 as reference_sha1, Hash}; fn push_message(message: &[u8]) -> Script { @@ -332,6 +366,40 @@ mod tests { assert!(result.success, "{result}"); } + fn compress(state: &mut [u32; 5], block: &[u8; 64]) { + let mut words = [0u32; 80]; + for (word, bytes) in words.iter_mut().zip(block.chunks_exact(4).take(16)) { + *word = u32::from_be_bytes(bytes.try_into().unwrap()); + } + for t in 16..80 { + words[t] = (words[t - 3] ^ words[t - 8] ^ words[t - 14] ^ words[t - 16]).rotate_left(1); + } + + let [mut a, mut b, mut c, mut d, mut e] = *state; + for (t, word) in words.iter().enumerate() { + let function = if t < 20 { + d ^ (b & (c ^ d)) + } else if t < 40 || t >= 60 { + b ^ c ^ d + } else { + (b & c) | (d & (b | c)) + }; + let constant = round_constant(t); + let next = a + .rotate_left(5) + .wrapping_add(function) + .wrapping_add(e) + .wrapping_add(constant) + .wrapping_add(*word); + (a, b, c, d, e) = (next, a, b.rotate_left(30), c, d); + } + state[0] = state[0].wrapping_add(a); + state[1] = state[1].wrapping_add(b); + state[2] = state[2].wrapping_add(c); + state[3] = state[3].wrapping_add(d); + state[4] = state[4].wrapping_add(e); + } + #[test] fn round_functions_match_reference() { let a = 0x0123_4567u32; @@ -448,6 +516,46 @@ mod tests { verify_digest(&[0x24; 130]); } + #[test] + fn continues_from_midstate() { + let prefix = [0x42u8; 64]; + let suffix: Vec = (0..16).collect(); + let mut midstate = INITIAL_STATE; + compress(&mut midstate, &prefix); + let mut message = prefix.to_vec(); + message.extend_from_slice(&suffix); + let expected = reference_sha1::Hash::hash(&message).to_byte_array(); + let result = crate::support::execution::execute_script_without_stack_limit(script! { + { push_message(&suffix) } + { sha1_80bytes_from_midstate(midstate) } + for byte in expected { + { byte } + OP_EQUALVERIFY + } + OP_TRUE + }); + + assert!(result.success, "{result}"); + } + + #[test] + fn rejects_extra_suffix_bytes() { + let result = execute_script_with_inputs( + script! { + { sha1_80bytes_from_midstate(INITIAL_STATE) } + for _ in 0..20 { + OP_DROP + } + OP_DEPTH + OP_0 + OP_EQUAL + }, + vec![vec![0x42]; 17], + ); + + assert!(!result.success); + } + #[test] fn rejects_unsupported_message_length() { let panic = std::panic::catch_unwind(|| sha1(512)); diff --git a/tests/primitive_metrics.rs b/tests/primitive_metrics.rs index 581dbce..62cc81a 100644 --- a/tests/primitive_metrics.rs +++ b/tests/primitive_metrics.rs @@ -3559,6 +3559,11 @@ fn metrics() -> Vec { key: "sha1_u32_32", value: script_len(sha1::sha1(32)), }, + Metric { + readme: "src/hashes/sha1/README.md", + key: "sha1_u32_80_midstate", + value: script_len(sha1::sha1_80bytes_from_midstate([0; 5])), + }, Metric { readme: "src/hashes/sha256/README.md", key: "sha2_u32_32",