diff --git a/src/sha224.nr b/src/sha224.nr index c9c4d5c..993baab 100644 --- a/src/sha224.nr +++ b/src/sha224.nr @@ -59,7 +59,7 @@ unconstrained fn __sha224_var( * @return finalized sha24 hash */ pub fn partial_sha224_var_end( - mut h: [u32; 8], + h: [u32; 8], msg: [u8; N], message_size: u32, real_message_size: u32, diff --git a/src/sha224/tests.nr b/src/sha224/tests.nr index 62498fc..8d6d4fb 100644 --- a/src/sha224/tests.nr +++ b/src/sha224/tests.nr @@ -237,6 +237,14 @@ fn same_msg_len_variable_padding() { assert_eq(var_length_hash_576, var_full_length_hash); } +// Regression test: partial_sha224_var_end delegates to partial_sha256_var_end, so +// the same out-of-capacity guard must reject a tail that extends beyond `msg`. +#[test(should_fail_with = "does not fit in the input array")] +fn regression_partial_end_tail_beyond_capacity_sha224() { + let msg = [7; 64]; + let _ = partial_sha224_var_end(sha224_constants::INITIAL_STATE_SHA224, msg, 64, 100); +} + #[test] fn fuzz_test_partial_hash_sha224(data: [u8; 192]) { let mut data0 = [0; 128]; @@ -271,6 +279,105 @@ fn fuzz_test_partial_hash_interstitial_sha224(data: [u8; 192]) { assert_eq(hash, correct_hash); } +// Dual-mode coverage for the SHA-224 partial-hash API, mirroring the SHA-256 +// tests. The defect in partial_sha256_var_end is reachable verbatim through +// partial_sha224_var_end, so these exercise the same non-block-aligned totals. + +unconstrained fn unconstrained_partial_sha224_var_end( + h: [u32; 8], + msg: [u8; N], + message_size: u32, + real_message_size: u32, +) -> sha224_constants::HASH_SHA224 { + partial_sha224_var_end(h, msg, message_size, real_message_size) +} + +fn assert_partial_end_matches_reference_sha224(real_message_size: u32) { + let mut msg = [0; 192]; + for i in 0..msg.len() { + msg[i] = ((i * 7 + 1) % 256) as u8; + } + let message_size = (real_message_size / 64) * 64; + let hash = partial_sha224_var_end( + sha224_constants::INITIAL_STATE_SHA224, + msg, + message_size, + real_message_size, + ); + let reference = sha224_var(msg, real_message_size); + assert_eq(hash, reference); +} + +#[test] +fn test_partial_end_sha224_size_1() { + assert_partial_end_matches_reference_sha224(1); +} + +#[test] +fn test_partial_end_sha224_size_63() { + assert_partial_end_matches_reference_sha224(63); +} + +#[test] +fn test_partial_end_sha224_size_65() { + assert_partial_end_matches_reference_sha224(65); +} + +#[test] +fn test_partial_end_sha224_size_100() { + assert_partial_end_matches_reference_sha224(100); +} + +#[test] +fn test_partial_end_sha224_size_127() { + assert_partial_end_matches_reference_sha224(127); +} + +#[test] +fn test_partial_end_sha224_size_191() { + assert_partial_end_matches_reference_sha224(191); +} + +#[test] +fn test_partial_end_sha224_size_64() { + assert_partial_end_matches_reference_sha224(64); +} + +#[test] +fn fuzz_partial_end_sha224_implementations_agree(msg: [u8; 192], real_message_size: u32) { + let real = real_message_size % 193; // 0..=192 + let message_size = (real / 64) * 64; + // Safety: test function + let oracle = unsafe { + unconstrained_partial_sha224_var_end( + sha224_constants::INITIAL_STATE_SHA224, + msg, + message_size, + real, + ) + }; + let circuit = partial_sha224_var_end( + sha224_constants::INITIAL_STATE_SHA224, + msg, + message_size, + real, + ); + assert_eq(circuit, oracle); +} + +#[test] +fn fuzz_partial_end_sha224_matches_reference(msg: [u8; 192], real_message_size: u32) { + let real = real_message_size % 193; // 0..=192 + let message_size = (real / 64) * 64; + let hash = partial_sha224_var_end( + sha224_constants::INITIAL_STATE_SHA224, + msg, + message_size, + real, + ); + assert_eq(hash, sha224_var(msg, real)); +} + #[test] fn fuzz_test_partial_hash_end_sha224(data: [u8; 192]) { let mut data0 = [0; 64]; diff --git a/src/sha256.nr b/src/sha256.nr index 125fe9e..d17e1c0 100644 --- a/src/sha256.nr +++ b/src/sha256.nr @@ -266,11 +266,7 @@ fn add_padding_byte_and_compress_if_needed( } } -pub(crate) fn finalize_sha256_blocks( - message_size: u32, - mut h: STATE, - mut msg_block: MSG_BLOCK, -) -> HASH { +pub(crate) fn finalize_sha256_blocks(message_size: u32, h: STATE, msg_block: MSG_BLOCK) -> HASH { let msg_byte_ptr = message_size % BLOCK_SIZE; let (h, mut msg_block) = add_padding_byte_and_compress_if_needed(msg_block, msg_byte_ptr, h); @@ -291,11 +287,12 @@ pub(crate) fn finalize_sha256_blocks( * @return the intermediate hash state after compressing in msg to h */ pub fn partial_sha256_var_interstitial( - mut h: [u32; 8], + h: [u32; 8], msg: [u8; N], message_size: u32, ) -> [u32; 8] { assert(message_size % BLOCK_SIZE == 0, "Message size must be a multiple of the block size"); + assert(message_size <= N, "Message does not fit in the input array"); if std::runtime::is_unconstrained() { // Safety: running as an unconstrained function unsafe { @@ -326,6 +323,15 @@ pub fn partial_sha256_var_end( real_message_size: u32, ) -> [u8; 32] { assert(message_size % BLOCK_SIZE == 0, "Message size must be a multiple of the block size"); + assert( + real_message_size >= message_size, + "Real message size must be at least the message size", + ); + // The block-aligned prefix plus the trailing partial block must lie within `msg`. + assert( + message_size + real_message_size % BLOCK_SIZE <= N, + "Message does not fit in the input array", + ); if std::runtime::is_unconstrained() { // Safety: running as an unconstrained function unsafe { @@ -335,10 +341,12 @@ pub fn partial_sha256_var_end( // This case is only hit if the msg is less than the block size, // or our message cannot be evenly split into blocks. - finalize_last_sha256_block(h, real_message_size, msg) + finalize_last_sha256_block(h, message_size, real_message_size, msg) } } else { - let (h, msg_block) = process_full_blocks(msg, message_size, h); + // The final chunk is the block-aligned `message_size` plus the trailing partial block. + let chunk_size = message_size + (real_message_size % BLOCK_SIZE); + let (h, msg_block) = process_full_blocks(msg, chunk_size, h); finalize_sha256_blocks(real_message_size, h, msg_block) } } @@ -358,19 +366,26 @@ unconstrained fn __sha_partial_var_interstitial( h } -// Helper function to finalize the message block with padding and length +// Helper function to finalize the message block with padding and length. +// +// The trailing partial block is read from `msg` starting at `partial_message_size` (the +// block-aligned bytes already compressed into `h`), while `real_message_size` supplies the +// length encoded into the padding. unconstrained fn finalize_last_sha256_block( - mut h: STATE, - message_size: u32, + h: STATE, + partial_message_size: u32, + real_message_size: u32, msg: [u8; N], ) -> HASH { - let msg_byte_ptr = message_size % BLOCK_SIZE; + let msg_byte_ptr = real_message_size % BLOCK_SIZE; - // We now build the final un-filled block. + // We now build the final un-filled block from the trailing bytes. let msg_block: MSG_BLOCK = if msg_byte_ptr != 0 { - let num_full_blocks = message_size / BLOCK_SIZE; - let msg_start = BLOCK_SIZE * num_full_blocks; - build_msg_block(msg, message_size, msg_start) + build_msg_block( + msg, + partial_message_size + msg_byte_ptr, + partial_message_size, + ) } else { // If the message size is a multiple of the block size (i.e. `msg_byte_ptr == 0`) then this block will be empty, // so we short-circuit in this case. @@ -379,7 +394,7 @@ unconstrained fn finalize_last_sha256_block( // Once built, we need to add the necessary padding bytes and encoded length let (h, mut msg_block) = add_padding_byte_and_compress_if_needed(msg_block, msg_byte_ptr, h); - msg_block = attach_len_to_msg_block(msg_block, message_size); + msg_block = attach_len_to_msg_block(msg_block, real_message_size); hash_final_block(msg_block, h) } diff --git a/src/sha256/tests.nr b/src/sha256/tests.nr index ec92fbb..1b10ecb 100644 --- a/src/sha256/tests.nr +++ b/src/sha256/tests.nr @@ -313,6 +313,18 @@ fn test_attach_len_to_msg_block() { assert_eq(msg_block[15], 8); } +#[test] +fn regression_test_partial_hash_non_block_aligned() { + let mut data = [0; 100]; + for i in 0..data.len() { + // Non-zero, position-dependent bytes so the tail actually changes the hash. + data[i] = ((i * 7 + 1) % 256) as u8; + } + let hash = partial_sha256_var_end(INITIAL_STATE, data, 64, data.len()); + let correct_hash = sha256_var(data, data.len()); + assert_eq(hash, correct_hash); +} + #[test] fn fuzz_test_partial_hash(data: [u8; 192]) { let mut data0 = [0; 128]; @@ -362,3 +374,195 @@ fn fuzz_test_partial_hash_end(data: [u8; 192]) { let correct_hash = sha256_var(data, data.len()); assert_eq(hash, correct_hash); } + +// Dual-mode coverage for the partial-hash API. +// +// These tests use the standalone `partial_sha256_var_end` usage (starting from +// INITIAL_STATE with the whole preimage in `msg`), which is the unambiguous +// case for a non-block-aligned total: the final chunk begins at absolute offset +// 0, so `message_size` (full blocks) and the trailing `real_message_size % 64` +// bytes both index into `msg` directly. + +unconstrained fn unconstrained_partial_sha256_var_end( + h: [u32; 8], + msg: [u8; N], + message_size: u32, + real_message_size: u32, +) -> [u8; 32] { + partial_sha256_var_end(h, msg, message_size, real_message_size) +} + +unconstrained fn unconstrained_partial_sha256_var_interstitial( + h: [u32; 8], + msg: [u8; N], + message_size: u32, +) -> [u32; 8] { + partial_sha256_var_interstitial(h, msg, message_size) +} + +fn assert_partial_end_matches_reference(real_message_size: u32) { + let mut msg = [0; 192]; + for i in 0..msg.len() { + msg[i] = ((i * 7 + 1) % 256) as u8; + } + let message_size = (real_message_size / 64) * 64; + let hash = partial_sha256_var_end(INITIAL_STATE, msg, message_size, real_message_size); + let reference = sha256_var(msg, real_message_size); + assert_eq(hash, reference); +} + +// Non-block-aligned totals: should equal a plain sha256_var over the same bytes. +#[test] +fn test_partial_end_size_1() { + assert_partial_end_matches_reference(1); +} + +#[test] +fn test_partial_end_size_33() { + assert_partial_end_matches_reference(33); +} + +#[test] +fn test_partial_end_size_63() { + assert_partial_end_matches_reference(63); +} + +#[test] +fn test_partial_end_size_65() { + assert_partial_end_matches_reference(65); +} + +#[test] +fn test_partial_end_size_100() { + assert_partial_end_matches_reference(100); +} + +#[test] +fn test_partial_end_size_127() { + assert_partial_end_matches_reference(127); +} + +#[test] +fn test_partial_end_size_129() { + assert_partial_end_matches_reference(129); +} + +#[test] +fn test_partial_end_size_191() { + assert_partial_end_matches_reference(191); +} + +// Block-aligned totals: these should already pass today. +#[test] +fn test_partial_end_size_64() { + assert_partial_end_matches_reference(64); +} + +#[test] +fn test_partial_end_size_128() { + assert_partial_end_matches_reference(128); +} + +// Constrained vs unconstrained must agree for any total size. +#[test] +fn fuzz_partial_end_implementations_agree(msg: [u8; 192], real_message_size: u32) { + let real = real_message_size % 193; // 0..=192 + let message_size = (real / 64) * 64; + // Safety: test function + let oracle = + unsafe { unconstrained_partial_sha256_var_end(INITIAL_STATE, msg, message_size, real) }; + let circuit = partial_sha256_var_end(INITIAL_STATE, msg, message_size, real); + assert_eq(circuit, oracle); +} + +// Constrained partial-end must match the reference sha256_var for any total size. +#[test] +fn fuzz_partial_end_matches_reference(msg: [u8; 192], real_message_size: u32) { + let real = real_message_size % 193; // 0..=192 + let message_size = (real / 64) * 64; + let hash = partial_sha256_var_end(INITIAL_STATE, msg, message_size, real); + assert_eq(hash, sha256_var(msg, real)); +} + +// Regression tests: the size arguments must not describe bytes outside `msg`. +// +// If `message_size` plus the trailing `real_message_size % 64` bytes exceeds the +// array capacity `N`, the call reads past the end of `msg`. This must be rejected +// (in both runtimes) rather than silently hashing the missing bytes as zeros in +// the constrained path while the unconstrained path reads out of bounds. + +#[test(should_fail_with = "does not fit in the input array")] +fn regression_partial_end_tail_beyond_capacity() { + // msg holds 64 bytes, but real=100 claims a 36-byte tail at msg[64..100]. + let msg = [7; 64]; + let _ = partial_sha256_var_end(INITIAL_STATE, msg, 64, 100); +} + +#[test(should_fail_with = "does not fit in the input array")] +fn regression_partial_end_tail_beyond_capacity_unconstrained() { + let msg = [7; 64]; + // Safety: test function + let _ = unsafe { unconstrained_partial_sha256_var_end(INITIAL_STATE, msg, 64, 100) }; +} + +#[test(should_fail_with = "does not fit in the input array")] +fn regression_partial_interstitial_message_beyond_capacity() { + // message_size (128) exceeds the array capacity N (64). + let msg = [7; 64]; + let _ = partial_sha256_var_interstitial(INITIAL_STATE, msg, 128); +} + +// Composed (interstitial + end) hashing of a non-block-aligned message, with the +// final chunk re-based to offset 0. Both the constrained and unconstrained paths +// must match a plain sha256_var over the whole preimage. + +#[test] +fn test_composed_non_aligned_tail_only() { + // 100 bytes: interstitial absorbs one block, end handles a 36-byte tail. + let mut full = [0; 100]; + for i in 0..full.len() { + full[i] = ((i * 7 + 1) % 256) as u8; + } + let state = partial_sha256_var_interstitial(INITIAL_STATE, full, 64); + let mut tail = [0; 36]; + for i in 0..tail.len() { + tail[i] = full[64 + i]; + } + let circuit = partial_sha256_var_end(state, tail, 0, 100); + // Safety: test function + let oracle = unsafe { unconstrained_partial_sha256_var_end(state, tail, 0, 100) }; + let reference = sha256_var(full, 100); + assert_eq(circuit, reference); + assert_eq(oracle, reference); +} + +#[test] +fn test_composed_non_aligned_block_plus_tail() { + // 200 bytes: interstitial absorbs two blocks, end handles one block + 8-byte tail. + let mut full = [0; 200]; + for i in 0..full.len() { + full[i] = ((i * 7 + 1) % 256) as u8; + } + let state = partial_sha256_var_interstitial(INITIAL_STATE, full, 128); + let mut chunk = [0; 72]; + for i in 0..chunk.len() { + chunk[i] = full[128 + i]; + } + let circuit = partial_sha256_var_end(state, chunk, 64, 200); + // Safety: test function + let oracle = unsafe { unconstrained_partial_sha256_var_end(state, chunk, 64, 200) }; + let reference = sha256_var(full, 200); + assert_eq(circuit, reference); + assert_eq(oracle, reference); +} + +// The interstitial step (whole blocks only) must agree across modes. +#[test] +fn fuzz_partial_interstitial_implementations_agree(msg: [u8; 192], num_blocks: u32) { + let message_size = (num_blocks % 4) * 64; // 0, 64, 128, 192 + // Safety: test function + let oracle = + unsafe { unconstrained_partial_sha256_var_interstitial(INITIAL_STATE, msg, message_size) }; + let circuit = partial_sha256_var_interstitial(INITIAL_STATE, msg, message_size); + assert_eq(circuit, oracle); +}