Skip to content

HMAC-SHA-256 and PBKDF2-HMAC-SHA-256 on ARMv7 and x86: the generic contracts and code - #583

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

alex merged 10 commits into
mainfrom
claude/inspiring-pascal-labmxq-sha256-md

Conversation

@alex

@alex alex commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Summary

HMAC-SHA-256 and PBKDF2-HMAC-SHA-256 on ARMv7 and 32-bit x86 now use the same contracts and code as every other hash on every target:

SHA-256's own 32-bit HMAC and PBKDF2 implementations and proofs are deleted. This replaces #563.

It is implementation only: no changes to Spec/ or TCB/. The old SHA-256-specific contracts in Spec (initSha256*, finalizeSha256Out*, iterateSha256*) are now unused. A separate Spec-only PR will delete them.

ARMv7

  • SHA-256 becomes one more instance of Impl/Pbkdf2/Md/Arm.lean, like SHA-224 (Proof/Pbkdf2/Md/Arm/Sha256.lean, Proof/Hmac/Generic/Arm/Sha256.lean).
  • The whole-PBKDF2 SHA-256 instance is rewired onto it.

x86

  • The SHA-256 backend interface (Proof/Sha256/X86/Variants/Interface.lean) now carries:
    • each backend's compression function and its facts;
    • its streaming functions (Sha256Stream).
  • Generic/Sha256/X86/{Hmac,Pbkdf2}.lean emit sha256I's init, finalize, iterate and pbkdf2 for each backend, built from the Md x86 code. The SHA-NI backend gets the _shani suffix.
  • The proofs are written once for every backend: Proof/Hmac/Generic/X86/Sha256.lean, Proof/Pbkdf2/Md/X86/Sha256.lean, Proof/Pbkdf2/Whole/X86/Sha256.lean.

scrypt

main's scrypt (#573) calls the whole PBKDF2-HMAC-SHA256, so its proofs now follow the moved instances:

  • on ARMv7, SHA-256's HashOK is Proof.Hmac.Generic.Arm.sha256OK;
  • on x86, a backend's PBKDF2 is Backend.F.

Deleted

  • Impl/Hmac/{X86,Arm}.lean, Impl/Hmac/Sha256/X86.lean, Impl/Pbkdf2/{X86,Arm}.lean and Impl/Pbkdf2/Sha256/X86.lean, with their proofs.
  • Proof/Pbkdf2/Whole/X86/Sha256Fns.lean, Proof/Sha256/X86/Stream/{CompressAt,FinalizeVariant}.lean and Proof/Framework/TaintWeaken.lean.

Rust

src/hmac/sha256.rs uses streaming_hmac! on every architecture, with SHA-NI in its match on x86. The 32-bit HmacHash implementation and its state type are removed.

Performance (instructions)

main (before #578/#579) this PR
ARMv7 finalize 5190 5035
ARMv7 iterate, per iteration 4810 4810
i686 scalar finalize 7085 7125
i686 scalar iterate, per iteration 6812 6817

PBKDF2 matches main. With SHA-NI, natively, 3M iterations take 412 ms, against 426 ms on main.

A short HMAC on x86 is still a few percent slower than with the old SHA-256 code, because HMAC's init calls the streaming init and update. #586, which builds on this PR, gives init the compression-level design and makes a short HMAC-SHA-256 on x86 about 14% faster than main was.

Validation

All of the following ran on the branch after merging main, which now contains #575, #578, #579 and #582:

  • Lean: a full lake build, then lake env lean --run Emit.lean --check. The axiom, compiler-override and Spec-origin audits pass.
  • CI scripts: check_lean_imports, check_lean_speed, check_vectors, check_arch_gates, check_variants, check_mcdt and algorithms_table all pass.
  • Rust lint: cargo fmt --check, and cargo clippy --all-targets -- -D warnings on the host, i686 and armv7.
  • Tests: cargo test with Wycheproof passes on i686-musl (238 passed, 0 failed). Before the merge it also passed on the host, and on i686 with VG_CPU_FEATURES=none.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RjfTK5YMk2jDsiKRYs2dbn

claude added 8 commits October 2, 2026 05:25
…on (MD5, SHA-1, SHA-224, SHA-384, SHA-512, SHA-512/224, SHA-512/256)

Port the compression-level design of x86-64 and AArch64 to 32-bit ARM for
every Merkle-Damgard hash function but SHA-256 (which keeps its own code).

New `Impl/Pbkdf2/Md/Arm.lean` (`Hash`: the streaming `Hash` plus the hash
value size N, the length field L, byte order, compression scratch offset,
digest-writing code and compression function):

* PBKDF2 `iterate` lays out the message block once in scratch
  (U || 0x80 || zeros || the length of B + D bytes, as word stores). Each
  step copies the key's inner hash value into scratch word-wise, compresses
  the block once, writes the digest back over U (restoring the padding it
  overwrites when N > D), does the same with the outer hash value, and XORs
  U into T word-wise. Two compressions per step, no calls of update or
  finalize, no byte loops.
* HMAC `finalize` calls the streaming finalize for the inner hash, then
  copies the outer hash value word-wise, pads the digest into a fixed block
  with word stores, compresses it once and writes the digest to out (via
  scratch and a word copy when D < N).

Proofs (`Proof/Pbkdf2/Md/Arm/`), once for every hash function, against the
`iterG`/`finG` contracts at 16 bytes of stack, moved to the shared
`Spec.Hmac.*I.iterateContract`/`finalizeContract`: correctness against
`Proof.MdStream.Md` (`Iterate.lean`, `HmacFin.lean`, with the
target-independent `Md.iterate_hmac` and `Md.hmac_outer` in
`Proof/Pbkdf2/MdStep.lean`); constant time with RelCT, the code between
calls by `taint_decide` and the calls by `compressBlock_rel` and `fin_rel`
(`IterateCT.lean`, `HmacFinCT.lean`); and the instances
(`Instances.lean`, `Sha224.lean`).

The whole `vg_pbkdf2_hmac_<hash>` (#564) now calls the new functions
(`Proof/Pbkdf2/Whole/Arm/Instances.lean` and `Sha224.lean`, `fnsOf`).

Deleted the streaming-level generic `iterate` and `finalize` on ARMv7
(`Impl/Pbkdf2/Generic/Arm.lean`, `Proof/Pbkdf2/Generic/Arm/`,
`Proof/Hmac/Generic/Arm/Finalize.lean`, `finalize` in
`Impl/Hmac/Generic/Arm.lean`); HMAC `init` is unchanged. No changes to
TCB/ or Spec/.

Instructions per PBKDF2 iteration on ARMv7 (counted by emulating the
generated code; compressions included):

  MD5         3472 -> 1378
  SHA-1       5494 -> 3296
  SHA-224     7256 -> 4812
  SHA-384    22554 -> 18006
  SHA-512    22762 -> 18010
  SHA-512/224 22294 -> 17996
  SHA-512/256 22346 -> 17998

HMAC finalize (20-byte message): SHA-1 4679 -> 3542, SHA-512 21023 -> 18486.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfTK5YMk2jDsiKRYs2dbn
…al-labmxq-md-arm

# Conflicts:
#	lean/VerifiedGarbage/Artifacts/HmacMd5/Arm.lean
#	lean/VerifiedGarbage/Artifacts/HmacSha1/Arm.lean
#	lean/VerifiedGarbage/Artifacts/HmacSha224/Arm.lean
#	lean/VerifiedGarbage/Artifacts/HmacSha384/Arm.lean
#	lean/VerifiedGarbage/Artifacts/HmacSha512/Arm.lean
#	lean/VerifiedGarbage/Artifacts/HmacSha512_224/Arm.lean
#	lean/VerifiedGarbage/Artifacts/HmacSha512_256/Arm.lean
#	lean/VerifiedGarbage/Artifacts/Pbkdf2Md5/Arm.lean
#	lean/VerifiedGarbage/Artifacts/Pbkdf2Sha1/Arm.lean
#	lean/VerifiedGarbage/Artifacts/Pbkdf2Sha224/Arm.lean
#	lean/VerifiedGarbage/Artifacts/Pbkdf2Sha384/Arm.lean
#	lean/VerifiedGarbage/Artifacts/Pbkdf2Sha512/Arm.lean
#	lean/VerifiedGarbage/Artifacts/Pbkdf2Sha512_224/Arm.lean
#	lean/VerifiedGarbage/Artifacts/Pbkdf2Sha512_256/Arm.lean
#	lean/VerifiedGarbage/Proof/Hmac/Generic/Arm/Finalize.lean
#	lean/VerifiedGarbage/Proof/Hmac/Generic/Arm/Instances.lean
#	lean/VerifiedGarbage/Proof/Hmac/Generic/X86/Finalize.lean
#	lean/VerifiedGarbage/Proof/Hmac/Generic/X86/Instances.lean
#	lean/VerifiedGarbage/Proof/Pbkdf2/Generic/Arm/Instances.lean
#	lean/VerifiedGarbage/Proof/Pbkdf2/Generic/Arm/Sha224.lean
#	lean/VerifiedGarbage/Proof/Pbkdf2/Generic/X86/Instances.lean
#	lean/VerifiedGarbage/Proof/Pbkdf2/Generic/X86/Iterate.lean
#	lean/VerifiedGarbage/Proof/Pbkdf2/Generic/X86/IterateCT.lean
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
…ntracts and code

SHA-256 is now one more Merkle–Damgård hash function of the compression-level
design (`Impl/Pbkdf2/Md/Arm.lean`, `Impl/Pbkdf2/Md/X86.lean`), with the
contracts of `Spec.Hmac.sha256I` (`initApi`, `finalizeApi`, `iterateApi`,
`pbkdf2Api`), as every other hash function on every target:

* ARMv7: `sha256H` and its `HashOK` (`Proof/Hmac/Generic/Arm/Sha256.lean`),
  `sha256Md` (`Proof/Pbkdf2/Md/Arm/Sha256.lean`, where SHA-224's file now
  takes `sha256_comp` from), HMAC's `init` the generic one, and the whole of
  PBKDF2 rewired onto them (`Proof/Pbkdf2/Whole/Arm/Sha256.lean`).
* x86: the backends' code (`Proof/Sha256/X86/Variants/Code.lean`: SHA-256's
  streaming functions, its Md `Hash` and the whole of PBKDF2's `Fns`, for a
  backend's compression and streaming functions), proven once for every
  backend (`Proof/Hmac/Generic/X86/Sha256.lean`,
  `Proof/Pbkdf2/Md/X86/Sha256.lean`, `Proof/Pbkdf2/Whole/X86/Sha256.lean`;
  taint checks evaluated once, on the sizes). `Backend` now carries the
  compression function, the streaming functions (`Sha256Stream`) and the
  `spSafe`/no-`esp`/stack facts of the code built on them; the fields only
  the specialized code used are gone. `Generic/Sha256/X86/{Hmac,Pbkdf2}.lean`
  emit `sha256I`'s init, finalize, iterate and pbkdf2 for each backend (with
  `_shani` for SHA-NI).

Deleted: SHA-256's own 32-bit HMAC and PBKDF2 implementations and proofs
(`Impl/Hmac/Arm.lean`, `Impl/Pbkdf2/Arm.lean`, `Impl/Hmac/X86.lean`,
`Impl/Hmac/Sha256/X86.lean`, `Impl/Pbkdf2/X86.lean`,
`Impl/Pbkdf2/Sha256/X86.lean`, `Proof/Hmac/Arm/`, `Proof/Pbkdf2/Arm/`,
`Proof/Hmac/X86/`, `Proof/Hmac/Sha256/X86/`, `Proof/Pbkdf2/X86/`,
`Proof/Pbkdf2/Sha256/X86.lean`, `Proof/Pbkdf2/Whole/X86/Sha256Fns.lean`) and
what only they used (`Proof/Sha256/X86/Stream/{CompressAt,FinalizeVariant}.lean`,
`Proof/Framework/TaintWeaken.lean`). Scrypt's ARM proof gets its own
`wp_eor`; `sha256_repr` moves to `Proof/Hmac/Generic/Common.lean`.

No change to `TCB/` or `Spec/`: the old SHA-256-specific 32-bit contracts
there are now unused, for a later Spec-only PR to delete.

Rust: `src/hmac/sha256.rs` uses `streaming_hmac!` on every architecture (the
32-bit `HmacHash` implementation is gone); `src/pbkdf2/sha256.rs` already
used `whole_pbkdf2!`.

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

`streaming_hmac!`'s `init` wrote the two streaming states into locals, moved
them into the computation and wiped the locals; `finalize` copied the inner
state out, finalized the copy and wiped it. They now work on the
computation's own states (`state_mut`, replacing `state`), which its drop
wipes: no copies, and 288 fewer bytes to wipe per MAC (on i686, about 450
fewer instructions per HMAC-SHA-256 MAC of a short message).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfTK5YMk2jDsiKRYs2dbn
claude added 2 commits October 2, 2026 12:27
#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
main's scrypt (#573) calls the whole PBKDF2-HMAC-SHA256 through names this
branch replaced: SHA-256's HashOK on ARMv7 is Proof.Hmac.Generic.Arm.sha256OK,
and on x86 a backend's PBKDF2 is Backend.F, with its streaming functions'
stack bounds in Backend.stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfTK5YMk2jDsiKRYs2dbn
@alex
alex marked this pull request as ready for review October 2, 2026 12:46
@alex
alex enabled auto-merge October 2, 2026 12:47
alex pushed a commit that referenced this pull request Oct 2, 2026
The scrypt proofs take this branch's side: it moved the streaming helpers
to VG.{Impl,Proof}.Pbkdf2.Stream.*.

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 572277d Oct 2, 2026
87 of 89 checks passed
@alex
alex deleted the claude/inspiring-pascal-labmxq-sha256-md branch October 2, 2026 13:15
alex pushed a commit that referenced this pull request Oct 2, 2026
#583 is now on main; this branch already carries it and builds on it, so
its side is kept in every file both changed. The merge adds only #537.

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