hashes: Fix sha3_256 incremental hashing
What changed, and why it matters
This commit fixes a bug in the SHA3-256 hashing code in rust-bitcoin. Previously, when data was fed in chunks (rather than all at once), the code incorrectly added end-of-message padding after every chunk. That produced the wrong hash for any multi-chunk input. The fix buffers partial blocks and only applies padding once at the end. The commit adds a regression test that hashes byte-by-byte and compares the result to the known correct output.
Upgrade to the fixed version. If you used SHA3-256 in rust-bitcoin to hash data supplied in multiple input() calls, recompute and compare those hashes against a correct implementation; any multi-chunk hashes produced before this fix are incorrect and should not be trusted for integrity checks, signatures, or consensus.
Security signals we found
Incorrect cryptographic output for incremental hashing
Padding applied per input() call instead of once at finalization
Domain-separation padding moved to finalize()
Regression test added for byte-by-byte incremental hashing
Evidence from the diff
The SHA3-256 HashEngine’s input() method used to pad and absorb every chunk passed to input(), so incremental hashing computed Pad(a)||Pad(b) instead of Pad(a||b). The patch refactors input() to use a shared engine_input_impl! macro that buffers up to RATE bytes, introduces process_block() to absorb+permute a full RATE block, and moves SHA3 domain-separation padding (0x06, final bit 0x80) into finalize(), where it is applied exactly once. A regression test hashes each byte separately and verifies the digest matches the all-at-once reference vectors.
Changed components
hashes/src/sha3_256/mod.rsHashEngine::input()HashEngine::finalize()SHA3-256 incremental hashingInspect captured patch +31 / −21
diff --git a/hashes/src/sha3_256/mod.rs b/hashes/src/sha3_256/mod.rs
index 9838da1f..e345b14d 100644
--- a/hashes/src/sha3_256/mod.rs
+++ b/hashes/src/sha3_256/mod.rs
@@ -18,7 +18,7 @@
// To read this file, follow the example code: https://keccak.team/keccak_specs_summary.html
// For a detailed specification: https://keccak.team/files/Keccak-reference-3.0.pdf
-use core::fmt;
+use core::{cmp, fmt};
crate::internal_macros::general_hash_type! {
256,
@@ -165,15 +165,22 @@ fn keccakf1600(state: &mut KeccakState) {
}
/// Engine to compute the Sha3-256 hash function.
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone)]
pub struct HashEngine {
state: KeccakState,
bytes_hashed: u64,
+ buffer: [u8; RATE],
+}
+
+impl Default for HashEngine {
+ fn default() -> Self { Self::new() }
}
impl HashEngine {
/// Construct a new Sha3-256 hash engine.
- pub const fn new() -> Self { Self { state: KeccakState::new(), bytes_hashed: 0 } }
+ pub const fn new() -> Self {
+ Self { state: KeccakState::new(), bytes_hashed: 0, buffer: [0; RATE] }
+ }
fn absorb(&mut self, block: [u8; RATE]) {
for lane in 0..RATE_LANES {
@@ -185,6 +192,11 @@ impl HashEngine {
self.state.xor_assign(x, y, shuffle);
}
}
+
+ fn process_block(&mut self) {
+ self.absorb(self.buffer);
+ keccakf1600(&mut self.state);
+ }
}
impl crate::HashEngine for HashEngine {
@@ -192,27 +204,17 @@ impl crate::HashEngine for HashEngine {
type Bytes = [u8; 32];
const BLOCK_SIZE: usize = RATE;
- fn input(&mut self, mut data: &[u8]) {
- while data.len().ge(&RATE) {
- let mut block = [0u8; RATE];
- block.copy_from_slice(&data[..RATE]);
- self.bytes_hashed += RATE as u64;
- self.absorb(block);
- keccakf1600(&mut self.state);
- data = &data[RATE..];
- }
- let mut final_block = [0u8; RATE];
- final_block[..data.len()].copy_from_slice(data);
- self.bytes_hashed += data.len() as u64;
- final_block[data.len()] = 0x06;
- final_block[RATE - 1] ^= 0x80;
- self.absorb(final_block);
- keccakf1600(&mut self.state);
- }
+ crate::internal_macros::engine_input_impl!();
fn n_bytes_hashed(&self) -> u64 { self.bytes_hashed }
- fn finalize(self) -> Self::Hash {
+ fn finalize(mut self) -> Self::Hash {
+ let incomplete_block_len = crate::incomplete_block_len(&self);
+ self.buffer[incomplete_block_len + 1..].fill(0);
+ self.buffer[incomplete_block_len] = 0x06;
+ self.buffer[RATE - 1] ^= 0x80;
+ self.process_block();
+
let mut out = [0u8; 32];
out[..8].copy_from_slice(&self.state.lane(0, 0).to_le_bytes());
out[8..16].copy_from_slice(&self.state.lane(1, 0).to_le_bytes());
@@ -249,6 +251,14 @@ mod tests {
sha3.input(&input_bytes);
let hash = sha3.finalize();
assert_eq!(hash.to_string(), test.output);
+
+ // Hash through engine, checking that we can input byte by byte
+ let mut sha3 = super::HashEngine::new();
+ for ch in input_bytes {
+ sha3.input(&[ch]);
+ }
+ let hash = sha3.finalize();
+ assert_eq!(hash.to_string(), test.output);
}
}
}
Why this scored 62/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.