CShake and Kmac produce digests that disagree with SP800-185 whenever the bytepad-encoded prefix length is an exact multiple of the rate. The crate returns no error, so callers get a digest that no conforming implementation will match.
Reproducer (tiny-keccak 2.0.2, cshake feature):
use tiny_keccak::{CShake, Hasher};
fn main() {
let mut out = [0u8; 32];
let mut h = CShake::v128(b"", &[0u8; 161]);
h.update(b"hello");
h.finalize(&mut out);
println!("{}", out.iter().map(|b| format!("{:02x}", b)).collect::<String>());
}
Observed:
62299d1c9af7169fbc81a99abf6e0a5a05505d8d2a46388685d51867e24da94f
Expected:
dea803c5fcfd9d31669f57888e7d45ee9942823a009a185f28ae787183734083
The expected value comes from PyCryptodome cSHAKE128 with the same customization string and input. PyCryptodome was checked against the NIST SP800-185 cSHAKE sample vectors.
Only the exact-multiple case diverges. Holding everything else fixed and varying the customization length:
| customization |
prefix len |
prefix mod 168 |
result |
| 160 bytes |
167 |
167 |
4f7ccd0f0a79479b16f693917f4c2366cd54ea8a3130f89a6bb2bd69224b8c6c, matches |
| 161 bytes |
168 |
0 |
62299d1c..., diverges |
| 162 bytes |
169 |
1 |
29dd695943d7eb695bcdc408942e197284db25ee013de695bcd88bba78f2532a, matches |
For the failing case, left_encode(168) is 2 bytes, encode_string(b"") is 2 bytes, and encode_string of the 161-byte customization is 164 bytes. Total 168 bytes, exactly one rate block. SP800-185 section 2.3.3 defines bytepad as appending the minimum number of zero bytes to reach a rate multiple. Here that count is zero, so no extra permutation is defined.
Root cause: KeccakState::fill_block at src/lib.rs:464-467 calls keccak() unconditionally. CShake::new at src/cshake.rs:54 and Kmac::new at src/kmac.rs:51 call fill_block after absorbing the encoded prefix. When the prefix length is congruent to 0 mod rate, update() has already permuted at the block boundary and left offset at 0. The fill_block call permutes a second time, which is equivalent to absorbing one extra all-zero rate block that bytepad never emits.
Scope: cSHAKE128 when len(encode_string(N)) + len(encode_string(S)) + 2 is a multiple of 168, and cSHAKE256 for multiples of 136. KMAC128 with 163-byte keys and KMAC256 with 131-byte keys reproduce the same failure. TupleHash and ParallelHash build on CShake::new and inherit it.
CShakeandKmacproduce digests that disagree with SP800-185 whenever the bytepad-encoded prefix length is an exact multiple of the rate. The crate returns no error, so callers get a digest that no conforming implementation will match.Reproducer (tiny-keccak 2.0.2,
cshakefeature):Observed:
Expected:
The expected value comes from PyCryptodome cSHAKE128 with the same customization string and input. PyCryptodome was checked against the NIST SP800-185 cSHAKE sample vectors.
Only the exact-multiple case diverges. Holding everything else fixed and varying the customization length:
4f7ccd0f0a79479b16f693917f4c2366cd54ea8a3130f89a6bb2bd69224b8c6c, matches62299d1c..., diverges29dd695943d7eb695bcdc408942e197284db25ee013de695bcd88bba78f2532a, matchesFor the failing case,
left_encode(168)is 2 bytes,encode_string(b"")is 2 bytes, andencode_stringof the 161-byte customization is 164 bytes. Total 168 bytes, exactly one rate block. SP800-185 section 2.3.3 defines bytepad as appending the minimum number of zero bytes to reach a rate multiple. Here that count is zero, so no extra permutation is defined.Root cause:
KeccakState::fill_blockat src/lib.rs:464-467 callskeccak()unconditionally.CShake::newat src/cshake.rs:54 andKmac::newat src/kmac.rs:51 callfill_blockafter absorbing the encoded prefix. When the prefix length is congruent to 0 mod rate,update()has already permuted at the block boundary and leftoffsetat 0. Thefill_blockcall permutes a second time, which is equivalent to absorbing one extra all-zero rate block that bytepad never emits.Scope: cSHAKE128 when
len(encode_string(N)) + len(encode_string(S)) + 2is a multiple of 168, and cSHAKE256 for multiples of 136. KMAC128 with 163-byte keys and KMAC256 with 131-byte keys reproduce the same failure. TupleHash and ParallelHash build onCShake::newand inherit it.