Files
emsha-rs/docs/CLAUDE-AUDIT.md
T
kyle 7755be49e9 Fix SHA-256 padding bug for messages of length 63 mod 64
pad_message wrote 0x00 into the last byte of the current block and
deferred the 0x80 pad byte to the next block when mbi == 63, producing
incorrect digests for every message whose length is 63 mod 64 (and
correspondingly wrong HMAC tags). Always write the pad byte in the
current block first, then flush an extra block only when the 8-byte
length field no longer fits.

Adds regression tests for SHA-256 boundary lengths 55/56/63/64/65 and
HMAC with a 63-byte message. Bumps to 1.0.4 for crates.io publication.

Ref: docs/LAGUNA-AUDIT.md
2026-09-17 12:25:23 -07:00

5.2 KiB
Raw Blame History

Claude audit: local SHA-256 padding fix (1.0.4)

Date: 2026-09-17
Mode: local uncommitted changes vs HEAD
Scope: Cargo.toml, Cargo.lock, src/sha256.rs, tests/sha256.rs, tests/hmac.rs
Diff: 5 files, +63 / 18
Passes: security review, cloud multi-agent review, code review (high effort)
Verdict: The padding rewrite is correct. No vulnerabilities and no correctness bugs. One latent panic closed with a debug_assert!.

Summary

Three independent review passes examined the 1.0.4 padding fix. Two found nothing; the third found one unreachable panic and one intentional packaging change. All three confirmed the rewrite is correct SHA-256 padding and that it repairs a real digest bug.

The old pad_message mishandled mbi == 63 (message length ≡ 63 mod 64): it wrote 0x00 into the last byte of the current block and deferred the 0x80 pad byte to index 0 of the next block. Every such message hashed wrong, and HMAC tags over those lengths were nonstandard. The new path writes the pad byte into the current block unconditionally and starts an extra block only when the 8-byte length field no longer fits (mbi > MB_SIZE - 8).

All three passes are scoped to the diff. None of them audited the unchanged source.

Passes

1 — security review (/security-review)

Sub-agent review of the diff for newly introduced vulnerabilities, with false-positive filtering. No findings.

It walked pad_message for every entry value of mbi in 0..=63, confirming the 0x80 byte lands directly after the message and the length occupies the final 8 bytes of the last block in each case. It established that no stale buffer bytes can reach a compressed block — in the two-block path every byte of the final block is rewritten, and in the one-block path everything after the 0x80 is zeroed — so padding stays injective and distinct messages cannot collide through it. It confirmed src/hmac.rs is unchanged, uses only update/finalize/reset, and gains no forgery or length-extension avenue.

The pass reported comparing 2107 SHA-256 and HMAC-SHA-256 digests against Python hashlib/hmac with no mismatches, covering message lengths 0300 across six update split points and HMAC key lengths 0130.

2 — cloud multi-agent review (ultrareview)

Multi-agent review of the same 5-file diff, run in the cloud. No findings.

3 — code review (/code-review high)

High-effort correctness and quality review of the diff. Two low findings, both below.

It confirmed the old-code bug directly, verified all six new test vectors against Python hashlib/hmac, and reported an exhaustive comparison against hashlib for every input length 0299. cargo clippy --all-targets was clean.

Issues

1 — low — resolved — src/sha256.rs:174

pad_message wrote self.mb[self.mbi] = pc unconditionally, resting on the invariant that mbi < MB_SIZE on entry. update has one hole in that invariant: the byte store and mbi += 1 happen before self.add_length(8)?, while the block flush that resets mbi happens after it. If add_length returns InputTooLong on the byte that just filled the block, mbi is left at 64 with hresult still OK and complete still false, so a later finalize reaches pad_message and indexes out of bounds.

Reaching this requires mlen to overflow u64 — roughly 2^61 bytes through a single context — and the consequence is a panic, not a wrong digest. The old code happened to survive the state because its update_message_block call reset mbi first, making this a narrow regression rather than a new class of problem.

Resolved: debug_assert!(self.mbi < MB_SIZE) added at the top of pad_message, documenting the invariant the comment already asserted. The flush ordering in update was left as-is.

2 — not a defect — Cargo.toml:8

Removing publish = ["kellnr"] changes what a bare cargo publish does: with the key present it errors out unless --registry kellnr is given, and without it the crate defaults to publishable and 1.0.4 goes to crates.io. Both the code review and the security review raised it for confirmation.

Confirmed intentional — 1.0.4 is meant for crates.io.

Verification

Checked directly rather than taken from a review pass:

  • The six new test vectors match sha256sum and Python hmac exactly — SHA-256 at 55/56/63/64/65 bytes, and HMAC-SHA-256 over a 63-byte message with the 20-byte 0x0b key.
  • cargo test passes: 11 tests across the unit, integration, and doc suites, including the new boundary cases.

Counts

Severity Count State
high 0
medium 0
low 1 resolved
packaging 1 confirmed intent

Coverage

Every pass here reviewed the diff, not the repository. Reviewers read surrounding files such as src/hmac.rs, src/lib.rs, and src/common.rs for context, but findings were anchored to changed lines and unchanged code was not itself audited. A full-repository scan was offered and declined during this session, so the crate outside this diff remains unreviewed by these passes.