Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion knowledge/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": [
Expand Down
1 change: 1 addition & 0 deletions knowledge/comparisons/hashes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
7 changes: 5 additions & 2 deletions knowledge/primitives/sha1-u32.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
7 changes: 7 additions & 0 deletions src/hashes/sha1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ digest comparison.
| Configuration | Hashing script |
| --- | ---: |
| 32-byte input | <!-- metric:sha1_u32_32 -->209726<!-- /metric:sha1_u32_32 --> bytes |
| 64-byte prefix + 16-byte suffix from midstate | <!-- metric:sha1_u32_80_midstate -->209619<!-- /metric:sha1_u32_80_midstate --> bytes |

This fragment exceeds the repository optimizer's 32 KiB input cutoff and is
reported unoptimized.
Expand Down Expand Up @@ -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,
Expand Down
108 changes: 108 additions & 0 deletions src/hashes/sha1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -448,6 +516,46 @@ mod tests {
verify_digest(&[0x24; 130]);
}

#[test]
fn continues_from_midstate() {
let prefix = [0x42u8; 64];
let suffix: Vec<u8> = (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));
Expand Down
5 changes: 5 additions & 0 deletions tests/primitive_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3559,6 +3559,11 @@ fn metrics() -> Vec<Metric> {
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",
Expand Down