Skip to content

HMAC finalize and PBKDF2 iterate on x86 call the compression function - #579

Merged
alex merged 3 commits into
mainfrom
claude/inspiring-pascal-labmxq-md-x86
Oct 2, 2026
Merged

alex merged 3 commits into
mainfrom
claude/inspiring-pascal-labmxq-md-x86

Conversation

@alex

@alex alex commented Oct 2, 2026

Copy link
Copy Markdown
Member

Summary

On 32-bit x86, HMAC finalize and PBKDF2 iterate now call the compression function directly for MD5, SHA-1, SHA-384, SHA-512, SHA-512/224 and SHA-512/256. This is the design already used on x86-64 and AArch64, and on ARMv7 in #578. It is written once over a description of a Merkle–Damgård hash and proven once against Proof.MdStream.Md.

Before this change, these functions were generic over the hash's streaming functions:

  • each PBKDF2 iteration copied a state byte by byte, then called update and finalize, twice;
  • HMAC's outer hash went through update and finalize too.

This PR is implementation only, with no changes to Spec/ or TCB/. The functions are proven against the existing Spec.Hmac.Instance.finalizeContract and iterateContract, at the same stack as before (48).

Some things stay as they are:

Design

Impl/Pbkdf2/Md/X86.lean is new. Its Hash holds:

  • the hash's streaming functions, which HMAC init and the inner finalize call;
  • the stored hash value size N;
  • the length field size L and its byte order;
  • the compression function, with its scratch size;
  • the digest writer.

It covers both the 64-byte-block hashes (Impl/MdStream/X86.lean) and the SHA-512 family. The block always sits right after the hash value.

iterate. The prologue copies U into the block and writes the padding of a B + D-byte message, once. Each step then:

  1. loads the inner hash value from key and compresses;
  2. writes the digest into the block, loads the outer hash value and compresses;
  3. writes the digest again, and computes T ⊕= U word-wise in t.

So each step is exactly two compressions, with no update/finalize and no byte loops.

HMAC finalize. It makes one call of the streaming finalize for the inner hash. It then reuses the inner state as the outer block, with word copies and constant word stores for the padding and length, and does one compression. The digest goes to out, through scratch and a word copy for the truncated hashes. The helpers (copyW, storeW, pad, digest, cmp) are shared with iterate.

Proofs

  • Target-independent lemmas go in a new module, Proof/Pbkdf2/MdHmac.lean: Link.hmac_outer, blockAt_tailPad and Link.hash_outer.
  • x86 proofs are in Proof/Pbkdf2/Md/X86/:
    • Block (shared pieces, and MdOk, which bundles what is needed of a hash);
    • Iterate and IterateCT;
    • HmacFin and HmacFinCT;
    • Hashes, Lit and Instances.
  • Constant time: taint_decide covers the code between calls; cmp_rel and fin_rel relate the calls through their contracts.
  • Whole PBKDF2 (Whole PBKDF2-HMAC (vg_pbkdf2_hmac_<hash>) on ARMv7 and 32-bit x86 #564, Proof/Pbkdf2/Whole/X86/{Lit,Instances}.lean) now calls the new iterate and finalize.
  • Deleted:
    • Impl/Pbkdf2/Generic/X86.lean, and Proof/Pbkdf2/Generic/X86/{Iterate,IterateCT,Instances}.lean;
    • the finalize part of Impl/Hmac/Generic/X86.lean, with the proofs trimmed to what init needs (FinalizeCT.lean becomes InitCT.lean).
  • Speed: there are no resource-limit options, and the largest new declarations take about 2 s.

Instructions executed (i686, callgrind)

before after change
PBKDF2-MD5, per iteration 3776 1261 −67%
PBKDF2-SHA-1, per iteration 5464 2836 −48%
PBKDF2-SHA-384, per iteration 36048 30403 −16%
PBKDF2-SHA-512, per iteration 36288 30399 −16%
HMAC finalize MD5 2871 1587 −45%
HMAC finalize SHA-1 4515 3159 −30%
HMAC finalize SHA-384 33985 31061 −9%
HMAC finalize SHA-512 34161 31036 −9%

There is no 32-bit OpenSSL here, so the bench crate doesn't build for i686. CI's "Compare with base (x86)" check will measure the change.

Validation

  • Lean: a full lake build, then lake env lean --run Emit.lean --check. The axiom and compiler-override audits pass, and the generated code matches.
  • CI scripts: check_lean_imports, check_lean_speed, check_vectors, check_arch_gates, check_variants and check_mcdt pass, and algorithms_table needs no README change.
  • Lint: cargo fmt --check passes, and so does cargo clippy --all-targets -- -D warnings on the host and for i686.
  • Tests:
    • cargo test with Wycheproof passes on the host.
    • On i686, it passes with default features (SHA-NI host) and with VG_CPU_FEATURES=none. These ran on i686-unknown-linux-musl, because the -gnu target can't link in this environment; CI runs -gnu.

Merging with #578

The two PRs conflict only in comments. #578 edited doc references in x86 files that this PR deletes or trims. Whichever merges second keeps this PR's side of those files.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RjfTK5YMk2jDsiKRYs2dbn


Generated by Claude Code

claude added 2 commits October 2, 2026 06:44
On 32-bit x86, HMAC's `finalize` and PBKDF2's `iterate` for MD5, SHA-1,
SHA-384, SHA-512, SHA-512/224 and SHA-512/256 are now written once over a
description of a Merkle-Damgard hash function (`Impl/Pbkdf2/Md/X86.lean`:
its streaming functions, hash value and length-field sizes, byte order,
compression function and digest code), proven once against
`Proof.MdStream.Md` and instantiated per hash.

* `iterate` lays the block out once (U, 0x80, zeros, the length of a
  B + D-byte message), and each step is two compressions: the key's inner
  hash value with that block, then the outer one with the digest written
  word by word into it. The padding a truncated digest overwrites is
  written back, and T ^= U is computed word by word.
* HMAC `finalize` calls the streaming `finalize` for the inner hash, then
  computes the outer hash with one compression of a fixed-layout block:
  the outer hash value and the digest copied word by word, the padding and
  length written as word stores, the MAC written to `out` (or, truncated,
  to scratch and copied).

The whole PBKDF2 derivation calls the new functions. The streaming-level
x86 `iterate` (`Impl/Pbkdf2/Generic/X86.lean`) and HMAC `finalize`, and
their proofs, are removed; HMAC's `init` is unchanged. No change to TCB/
or Spec/, to the contracts, or to other targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfTK5YMk2jDsiKRYs2dbn
Conflicts were in the docs main trimmed (#571, #572): the registration
files' boilerplate and the proofs' "Untrusted" sentence, dropped here too,
including from the new modules. The streaming-level x86 files main edited
stay deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfTK5YMk2jDsiKRYs2dbn
The conflicts were #578's doc-only edits to x86 files this branch deletes
or trims; this branch's side is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfTK5YMk2jDsiKRYs2dbn
@alex
alex added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 3712136 Oct 2, 2026
49 checks passed
@alex
alex deleted the claude/inspiring-pascal-labmxq-md-x86 branch October 2, 2026 12:24
alex pushed a commit that referenced this pull request Oct 2, 2026
#578, #579 and #582 are now on main; this branch already carries #578 and
#579 and builds on them, so its side is kept in the files both changed.
src/hmac/mod.rs takes main's, which adds #582's uninitialized scratch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfTK5YMk2jDsiKRYs2dbn
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