Skip to content

HMAC: finalize_reset, the MAC and the computation keyed again in place - #2706

Closed
alex wants to merge 1 commit into
claude/hmac-inplacefrom
claude/hmac-finalize-reset
Closed

alex wants to merge 1 commit into
claude/hmac-inplacefrom
claude/hmac-finalize-reset

Conversation

@alex

@alex alex commented Oct 18, 2026

Copy link
Copy Markdown
Member

New public API, stacked on #2687. Rust only: no Lean, src/asm/, TCB/ or Spec/ changes.

Hmac::finalize_reset(&mut self) -> H::Output returns the MAC of everything absorbed and leaves the computation keyed again with the same key, ready for the next message. It closes the last dead-stack copy #2687 couldn't reach: finalize(self) takes the state by value, so a caller's new + update + finalize has Rust copy its variable to pass it, and leaves the original unwiped in the caller's frame. finalize_reset takes &mut self and never moves the state.

The name and semantics follow what the dead-stack design (#2680's session) uses for the other streaming types, and RustCrypto's FixedOutputReset::finalize_reset. For HMAC, "reset" means keyed again. The coordinator approved it as a PR of its own, with a benchmark. The rustls provider (#991) will switch to it.

How

  • The saved hash value: the state gains ipad: [u8; V], the hash value of K₀ ⊕ ipad, which is the inner state once keyed (V = state size − block size: 16, 20, 32 or 64 bytes). hmac_init copies it in place after the verified init.
  • The reset: hmac_reset copies ipad back into the inner state in place and sets the inner length to one block.
    • A streaming state of one whole block is its hash value alone. ReprFrom (Spec/Sha256.lean, and the same shape for each hash) reads length % B bytes of the buffer, here none.
    • So the restored state represents K₀ ⊕ ipad, as init left it. The verified finalize's precondition holds for every later message, as after new.
  • The wipe: StreamingHmacState is #[repr(C)], so ipad directly follows outer (byte arrays, no padding), and Drop wipes both with one zeroize_raw.
  • set_length: streaming_hash! gains a pub(crate) set_length for the reset.

Dead-stack harness

The harness is #2674's (key A, key B, key A from a dirtied stack, after a warm-up), on a tree with #2674's and #2696's asm and this branch's Rust. It measures a caller's own out-of-line new + update + then either:

Key-dependent dead-stack bytes after the caller returns finalize(self) finalize_reset(&mut self)
x86-64, default, avx,avx2,bmi1,bmi2 and none 108 (MD5) – 251 (SHA-512) 0, every hash, 32- and 200-byte keys
AArch64 (qemu) — 0 (4 for SHA-1/-224/-512/224: the end of their 20- and 28-byte MACs, public, which the harness's 8-byte MAC windows miss, as in #2696)

Tests

hmac::<hash>::tests::finalize_reset, generated for every hash, checks:

  • a sequence of messages, of 0 bytes, one block and longer, each with finalize_reset;
  • that each MAC equals Hmac::mac's, with a short key and with one longer than a block;
  • that verify and finalize after a reset see only what follows it.

It runs in every CPU-feature configuration, since ci/cpu_tests.py's groups name the hmac::<hash> modules. It passes on x86-64 with VG_CPU_FEATURES unset, none, avx,avx2,bmi1,bmi2 and sha,ssse3, and on AArch64 under qemu.

Cost

Measured by instruction count under callgrind, x86-64 AVX2, 64-byte message, against #2687:

Per call #2687 this PR
Hmac::<Sha256>::mac 11,152 11,150
new + update + verify, SHA-256 11,568 11,581 (+0.1%)
Hmac::<Sha512>::mac 12,586 12,581
new + update + verify, SHA-512 13,307 13,330 (+0.2%)

The ipad copy costs a few instructions, and the wipe stays one call. A first version wiped ipad with a second zeroize and cost about 80–100 instructions; merging the two wipes removed that.

Benchmark

hmac-<hash>-reset, in bench/benches/primitives/hmac_*.rs, through a shared hmac_reset_group:

  • verified-garbage: keyed once, then update + finalize_reset per message.
  • OpenSSL: a Signer made from the same PKey per message, since rust-openssl has no reset.
  • aws-lc-rs: one reused hmac::Key with hmac::sign, which is the same work.

Locally (x86-64 with SHA-NI, unscoped timing), HMAC-SHA-256 per message:

64 B 1 KiB 16 KiB
verified-garbage ~157 ns ~791 ns ~11.1 µs
aws-lc-rs ~266 ns ~920 ns ~11.1 µs

A scoped Benchmarks dispatch (the hmac_* modules) will follow, when the queue allows; I'll link it here.

Checks

  • cargo fmt --check (crate and bench/); cargo clippy --all-targets -- -D warnings; ci/check_arch_gates.py; cargo bench --no-run.
  • HMAC and PBKDF2 tests (Wycheproof included) on x86-64 (4 configurations) and AArch64 (qemu).

Generated by Claude Code

`Hmac::finalize_reset(&mut self)` returns the MAC of everything absorbed and
leaves the computation keyed with the same key, ready for the next message.
Unlike `finalize(self)` it never moves the state, so it leaves no copy of it
where the caller's variable was (one Rust makes to pass a variable by value).

The state keeps the hash value of K0 ^ ipad (`ipad`, the keyed inner state of
one block, whose buffer it does not read), copied in place after `init`;
`hmac_reset` copies it back in place and sets the inner length to a block.
`ipad` follows `outer` in C's layout, so one wipe covers both.

Benchmarked next to OpenSSL (a `Signer` from the same `PKey` per message,
as rust-openssl has no reset) and aws-lc-rs (one `hmac::Key`, reused):
`hmac-<hash>-reset`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013RJ8JEAFPoUJwyYZ7WE48r

alex commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

Closing: stack-residue work dropped by the maintainer.


Generated by Claude Code

@alex alex closed this Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants