Skip to content

Commit 14c4e66

Browse files
fix: make Hash duplex sponge portable across pointer widths
`Hash<D>::squeeze` and `squeeze_end` fold the squeeze block-counter and the read-length into the hash state via `usize::to_be_bytes()`. `usize` is 8 bytes on 64-bit targets and 4 bytes on 32-bit (e.g. wasm32), so the absorbed bytes — and therefore the squeezed output — differ by target. A 64-bit prover and a 32-bit verifier then derive different Fiat-Shamir challenges, breaking verification (this surfaced verifying a 64-bit-produced transcript inside a wasm32 verifier). Encode both counters as fixed-width `u64`. On 64-bit this is byte-identical to the previous output (`i as u64 == i`), so existing transcripts/vectors are unchanged; only 32-bit output changes, now matching 64-bit. The permutation `DuplexSponge` is unaffected — its `usize` fields are buffer indices, never absorbed. Add a multi-block squeeze known-answer test (counter increments past 0, plus a squeeze_end) that pins the output; running it on both a 64-bit and a 32-bit target guards the portability property.
1 parent 335eb5e commit 14c4e66

1 file changed

Lines changed: 44 additions & 3 deletions

File tree

  • spongefish/src/instantiations

spongefish/src/instantiations/hash.rs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,12 @@ impl<D: BlockSizeUser + Digest + Clone + FixedOutputReset> DuplexSpongeInterface
8686
self.squeeze(&mut output[len..])
8787
// Squeeze another digest
8888
} else if let Mode::Squeeze(i) = self.mode {
89-
// Add the squeeze mask, current digest, and index
89+
// Add the squeeze mask, current digest, and index.
90+
// Encode the index as a fixed-width `u64` (not `usize`) so the absorbed
91+
// bytes are identical on 32- and 64-bit targets; otherwise a 64-bit
92+
// prover and a 32-bit verifier (e.g. wasm32) derive different outputs.
9093
let mut output_hasher_prefix = self.hasher.clone();
91-
Digest::update(&mut output_hasher_prefix, i.to_be_bytes());
94+
Digest::update(&mut output_hasher_prefix, (i as u64).to_be_bytes());
9295
let digest = output_hasher_prefix.finalize();
9396
// Copy the digest into the output, and store the rest for later
9497
let chunk_len = usize::min(output.len(), Self::DIGEST_SIZE);
@@ -147,7 +150,9 @@ impl<D: BlockSizeUser + Digest + Clone + Reset> Hash<D> {
147150
let mut squeeze_hasher = D::new();
148151
Digest::update(&mut squeeze_hasher, Self::mask_squeeze_end());
149152
Digest::update(&mut squeeze_hasher, &self.cv);
150-
Digest::update(&mut squeeze_hasher, byte_count.to_be_bytes());
153+
// Fixed-width `u64` encoding for cross-architecture portability — see the
154+
// matching note in `squeeze`.
155+
Digest::update(&mut squeeze_hasher, (byte_count as u64).to_be_bytes());
151156
self.cv = Digest::finalize(squeeze_hasher);
152157

153158
// set the sponge state in absorb mode
@@ -259,3 +264,39 @@ fn test_shosha() {
259264
sho.squeeze(&mut got[..63]);
260265
assert_eq!(&got[..63], expected);
261266
}
267+
268+
/// Cross-architecture known-answer test for the squeeze path.
269+
///
270+
/// `squeeze` and `squeeze_end` absorb the squeeze block-counter / read-length
271+
/// into the hash state. These must be encoded at a fixed width (`u64`), not the
272+
/// native `usize`, or a 64-bit prover and a 32-bit verifier (e.g. `wasm32`)
273+
/// produce different output. This pins a multi-block squeeze (the counter
274+
/// increments past 0) plus a `squeeze_end`; the value MUST be identical on every
275+
/// target — run this under a 32-bit target (e.g. `wasm32-unknown-unknown`) as
276+
/// well as 64-bit to guard the property.
277+
#[cfg(all(test, feature = "sha2"))]
278+
#[test]
279+
fn squeeze_is_pointer_width_independent() {
280+
let mut sho = Hash::<sha2::Sha256>::default();
281+
sho.absorb(b"portability");
282+
sho.ratchet();
283+
sho.absorb(b"check");
284+
// 200 bytes spans multiple 32-byte SHA-256 squeeze blocks, exercising the
285+
// `Mode::Squeeze(i)` counter (i = 0..6) folded into the hash.
286+
let mut got = [0u8; 200];
287+
sho.squeeze(&mut got);
288+
sho.squeeze_end();
289+
let mut tail = [0u8; 40];
290+
sho.squeeze(&mut tail);
291+
292+
assert_eq!(
293+
hex::encode(got),
294+
"694d33cf42aed220f5cbbc49bf16d4f2494dbfa6d42cb50d23ec50d398fca6286cb54bd8989f97db6da16f1a0395e0c79d6eb3624e5dff81da81b75d1380d2b0c029419d5dde093607c5a910a9da16778ca145776ba961dcdb4b3581b5d0d237329b94b85ac05a1b1cf51394fbcf5a742a2c1d5447f6b823ad044ebd85356418e3ea6e6c67be32e209d832968101fbebbf24e352c6f1fda5f3142e6dbff79479f09948c96d663f21ebf3cf4c4739d2587213e698101e13c07d8e1ec2e13241f5580703770da745e0",
295+
"200-byte squeeze KAT changed (or is pointer-width dependent)"
296+
);
297+
assert_eq!(
298+
hex::encode(tail),
299+
"871f6366393e4fef9d77b0289953f1ec85d54b5ec95eb1d26397cdc5ae21139cfdc77ab9919726f8",
300+
"post-squeeze_end KAT changed (or is pointer-width dependent)"
301+
);
302+
}

0 commit comments

Comments
 (0)