SHA3 bug fixes: around partial-byte handling - #91
Conversation
… cleanups Bugs: - SHAKE squeeze_partial_byte_final[_out] bypassed the SHAKE "1111" domain suffix when it was the first squeeze, returning raw Keccak output instead of SHAKE output. Now routes through squeeze_out(). - The same function returned the high bits of the output byte; FIPS 202 B.1 bit ordering (and our own input-side convention) makes the first bits the low bits. Now returns the low num_bits bits; XOF trait doc updated to match. - SHA3 do_final_partial_bits[_out] did not validate num_partial_bits: >=16 panicked with a shift overflow, 8..=15 silently hashed garbage. Now returns HashError::InvalidLength for anything above 7. - SHAKE absorb_last_partial_byte now accepts 0 partial bits (consistent with SHA3) and error strings state the actual accepted range. Cleanups: - std::marker::PhantomData -> core::marker::PhantomData (no_std goal). - Blanket `impl HashAlgParams for SHA3Internal<P>` forwarding to the params struct, replacing four hand-duplicated impls and stale commented constants. - Crate docs: added Memory Usage and Security Considerations sections, fixed typo, documented the *_NAME constants. - Removed .clone() on Copy types and redundant branch in do_final_out. - keccak_tests::test_keccak now asserts instead of printing. Regression tests added for all of the above. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_out Mirrors the sha2 structure: do_final_out is the zero-partial-bits case of a single spec-commented finalize(), and do_final_partial_bits_out validates num_partial_bits then delegates. Removes the second hand-rolled suffix path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- lib.rs: drop internal sponge/stack detail from Memory Usage; remove the "SHAKE does not implement Hash", absorb-after-squeeze, constant-time and KeyMaterial-caveat bullets from Security Considerations (CT claim awaits bcgit#75; KeyMaterial caveat belongs on KeyMaterial itself). - sha3.rs: rename private shared finalizer finalize() -> do_final_bits_out() per naming convention; drop redundant comments. - shake.rs: replace range-check / bit-ordering comments with a one-liner each. - core traits: document that num_partial_bits = 0 is valid for do_final_partial_bits* and absorb_last_partial_byte, and explain on XOF why absorb-after-squeeze (duplex) is rejected. - tests: assert the 7-bit upper boundary is accepted by absorb_last_partial_byte (kills the shake.rs `>` -> `>=` mutant found by cargo mutants). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the vendored copies in crypto/sha3/tests/data/ with the same lookup convention used by the mldsa/mlkem crates: read SHA3TestVectors.txt and SHAKETestVectors.txt from ../bc-test-data/crypto (or ../../../bc-test-data when run from the crate directory), printing a one-time warning and skipping the vector tests if the repo is not checked out. The vector files were byte-identical apart from the download URL in the header comment. Requested in PR bcgit#87 review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tail
Adds crypto/sha3/tests/cavp_tests.rs reading the SHA3VS .rsp files from
../bc-test-data/crypto/sha3/{bit-oriented,byte-oriented}/ with the same
lookup/skip-with-warning convention as the other crates: SHA3 ShortMsg,
LongMsg and Monte (s. 6.2.2) for SHA3-224/256/384/512, and SHAKE ShortMsg,
LongMsg, VariableOut and Monte (s. 6.2.3) for SHAKE128/256 — 40 tests,
~13k message cases (~7.7k bit-length inputs, ~1.7k bit-length outputs).
Bit ordering confirmed from the vectors: SHA-3 CAVP follows FIPS 202 B.1 and
packs excess input and output bits in the least significant bits of the
final byte (100% of partial cases have zero high bits), matching the
Hash/XOF partial-bit API directly, unlike SHA-2 CAVP which is MSB-first.
The harness found a bug: KeccakInternal::absorb_bits(_, 0) returned early
without switching to the squeezing phase, so when 4 trailing message bits
plus the SHAKE "1111" suffix exactly filled a byte, absorb_last_partial_byte
left squeezing == false and the first squeeze applied the suffix a second
time. Every SHAKE message with Len % 8 == 4 was wrong; the NIST example
vectors (5/30/1605/1630 bits) cannot reach this case. absorb_bits(_, 0) now
pads and switches phase after the usual state checks. Regression tests: the
CAVP SHAKE128 Len = 4 vector in shake_tests, and a keccak unit test pinning
absorb_bits' range and phase behaviour.
Note: cargo mutants runs in a copied tree where ../bc-test-data does not
resolve, so vector-file tests skip during mutation testing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The num_partial_bits message bits are taken from the least significant bits of partial_byte (FIPS 202 Appendix B.1) for every hash family, including SHA-2 where FIPS 180-4 defines no packing. Notes that NIST CAVP SHAVS (SHA-2) vectors pack MSB-first and need shifting, while SHA3VS vectors already use the LSB convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…A3/SHAKE Follows bench_mldsa_mem_usage / bench_mlkem_mem_usage: print_struct_sizes() reports size_of for SHA3_224..SHA3_512, SHAKE128/256 (440 bytes) and SUSPENDED_SHA3_STATE_LEN (415), which are the numbers in the crate's Memory Usage table; the remaining entry points (one-shot hash, streaming, XOF squeeze, suspend/resume) are for valgrind --tool=massif stack measurement. The crate docs now point at the bench as the source of the table. Requested in PR bcgit#87 review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…artial-bit validation * `XOF::squeeze_partial_byte_final()` squeezed raw Keccak, skipping the SHAKE `1111` suffix (FIPS 202 s. 6.2) when it was the first squeeze, and returned the high rather than the low `num_bits` bits. Now goes via `squeeze_out()` and masks the low bits. The old test's output byte happened to be `0xFF`, which hid the high/low error. * `KeccakInternal::absorb_bits()` returned early for `bits == 0` without switching to squeezing, so a suffix already folded into a whole byte was applied a second time. Broke every SHAKE message of bit length 4 mod 8 (6 mod 8 for SHA-3). Now accepts 0..=7 and always switches phase. * `num_partial_bits` was unvalidated before use as a shift amount: SHA-3 absorbed garbage for 8..15 and panicked at >= 16; SHAKE rejected 0. Both now accept 0..=7 (0 meaning the message ends on a byte boundary) and return `HashError::InvalidLength` otherwise. * `Hash` / `XOF` docs: state the FIPS 202 Appendix B.1 bit ordering, note the opposite MSB-first packing in the CAVP SHAVS (SHA-2) vector files, and explain why absorb-after-squeeze is rejected (duplex, not SHAKE). * Regression tests for each fix, including the 4-bit SHAKE128 vector from the CAVP SHA3VS bit-oriented set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Review Tasks (for me):
I plan to just make changes directly to the branch rather than use github's review tool. |
|
dhg says:
|
bea169a to
c026339
Compare
…artial-bit validation * `XOF::squeeze_partial_byte_final()` squeezed raw Keccak, skipping the SHAKE `1111` suffix (FIPS 202 s. 6.2) when it was the first squeeze, and returned the high rather than the low `num_bits` bits. Now goes via `squeeze_out()` and masks the low bits. The old test's output byte happened to be `0xFF`, which hid the high/low error. * `KeccakInternal::absorb_bits()` returned early for `bits == 0` without switching to squeezing, so a suffix already folded into a whole byte was applied a second time. Broke every SHAKE message of bit length 4 mod 8 (6 mod 8 for SHA-3). Now accepts 0..=7 and always switches phase. * `num_partial_bits` was unvalidated before use as a shift amount: SHA-3 absorbed garbage for 8..15 and panicked at >= 16; SHAKE rejected 0. Both now accept 0..=7 (0 meaning the message ends on a byte boundary) and return `HashError::InvalidLength` otherwise. * `Hash` / `XOF` docs: state the FIPS 202 Appendix B.1 bit ordering, note the opposite MSB-first packing in the CAVP SHAVS (SHA-2) vector files, and explain why absorb-after-squeeze is rejected (duplex, not SHAKE). * Regression tests for each fix, including the 4-bit SHAKE128 vector from the CAVP SHA3VS bit-oriented set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> # Conflicts: # alpha_0.1.3_release_notes.md # crypto/sha3/src/sha3.rs
…-rust into fix/sha3-shake-partial-bits
|
I fixed the things that I noticed during my review, plus a few more that I / Claude found in the process. |
|
Oh shoot. I rebased it on top of release/0.1.3alpha. Once the aes stuff gets rebased back out, all the commits on this branch will need to get cherry-picked back out. |
|
Merged manually in 2f8d5a7 |
…offsets A cargo-mutants run over the sha3 crate after #91 left 11 survivors. Two are the documented OR/XOR equivalents in the partial-bit suffix code; the other nine are all offset or length arithmetic in the Suspendable serialization in keccak.rs: swapping or aliasing the two 1-byte KDF metadata slots (offsets 402 and 403), or enlarging KECCAK_SERIALIZED_LEN / SHA3_FAMILY_STATE_LEN, went unnoticed. The root cause is structural: the KDF entry points are one-shot and consume self, so via the public API the kdf_key_type / kdf_security_strength / kdf_entropy fields are always at their defaults (all zero) whenever a state is suspended. The integration round-trip tests can therefore never tell the slots apart, and an oversized buffer round-trips because trailing zeros are ignored. Add two tests to the keccak.rs test module: - serialized_state_lengths_are_pinned asserts the three length constants (401 / 412 / 415), which are wire-format constants in the public Suspendable contract. - sha3_family_state_round_trips_kdf_metadata drives the crate-private serialize / deserialize helpers directly with distinct non-zero values for the tag, key type, security strength and entropy, checking each byte offset and the round trip. With these, re-running mutants over the serialization region gives 0 missed (73 mutants: 64 caught, 9 unviable). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Break up of #87, so this is actually dgh's submission. So I will be reviewing / approving.
XOF::squeeze_partial_byte_final()squeezed raw Keccak, skipping the SHAKE1111suffix (FIPS 202 s. 6.2) when it was the first squeeze, and returned the high rather than the lownum_bitsbits. Now goes viasqueeze_out()and masks the low bits. The old test's output byte happened to be0xFF, which hid the high/low error.KeccakInternal::absorb_bits()returned early forbits == 0without switching to squeezing, so a suffix already folded into a whole byte was applied a second time. Broke every SHAKE message of bit length 4 mod 8 (6 mod 8 for SHA-3). Now accepts 0..=7 and always switches phase.num_partial_bitswas unvalidated before use as a shift amount: SHA-3 absorbed garbage for 8..15 and panicked atHash/XOFdocs: state the FIPS 202 Appendix B.1 bit ordering, note the opposite MSB-first packing in the CAVP SHAVS (SHA-2) vector files, and explain why absorb-after-squeeze is rejected (duplex, not SHAKE).