Conversation
On ARMv7 and x86, HMAC-SHA-256's init and finalize and PBKDF2-HMAC-SHA-256's iterate were the last HMAC/PBKDF2 functions with an implementation of their own, proven against SHA-256-specific contracts (initSha256Contract with 76 words of scratch, finalizeSha256OutContract with 86, iterateSha256Contract). They are now the one HMAC and PBKDF2 implementation for every streaming hash function (Impl/Hmac/Generic, Impl/Pbkdf2/Generic), calling SHA-256's verified streaming functions, and proven against sha256I's generic contracts (104 words of scratch), as SHA-1, MD5 and the SHA-512 family already are on these targets and SHA-256 is on x86-64 and AArch64. On x86, SHA-256 keeps its variant interface (Variants/Sha256/X86/): a backend now gives its streaming update and finalize (Sha256Stream), and HMAC and PBKDF2 are proven once for every backend (Proof/Hmac/Generic/X86/Instances.lean, Proof/Pbkdf2/Generic/X86/Instances.lean: the code between the calls depends only on SHA-256's sizes, so the kernel checks its taint once) and emitted for each from Generic/Sha256/X86/, so the scalar and SHA-NI backends both get them, and a new one would too. The SHA-256-specific implementations and proofs are deleted (scrypt's ARM proof only used one lemma of them, wp_eor, now its own). Spec/ is unchanged: the SHA-256-specific contracts are now unused, for a separate trust-change PR to remove. The Rust HMAC and PBKDF2 for SHA-256 on ARMv7 and x86 use the streaming_hmac! and streaming_pbkdf2! macros, matching exhaustively on Sha256Backend, as the other hashes do; the PBKDF2 key states are now zeroized. The generic iteration is slower than the SHA-256-specific one, which compressed twice per step: it copies each 96-byte state byte by byte and calls update and finalize. On x86 (i686, measured on x86-64 hardware), a PBKDF2 iteration goes from 393 to 606 ns (scalar) and from 136 to 307 ns (SHA-NI); HMAC of 64 bytes from 1174 to 1376 ns and from 453 to 661 ns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjfTK5YMk2jDsiKRYs2dbn
|
On 42984e3 the Compare with base checks fail for arm, x86 and x86 with
The cause is described in the PR body. The generic 32-bit code copies its state byte by byte and runs a full update/finalize on every PBKDF2 iteration. The old SHA-256-specific code compressed directly. The fixed overhead is worst where compression is cheap, as with SHA-NI. I'm keeping this as a draft until we've decided how to win the speed back:
Generated by Claude Code |
|
I'm closing this in favour of #583. That PR moves SHA-256 on ARMv7 and x86 onto the same generic contracts as this one, but uses the compression-level HMAC Generated by Claude Code |
Draft: this regresses PBKDF2 performance on x86 (and likely ARMv7); see below. Opened so CI's Benchmarks measure it on real hardware before deciding how to proceed.
On ARMv7 and x86, HMAC-SHA-256's
init/finalizeand PBKDF2-HMAC-SHA-256'siteratewere the last HMAC/PBKDF2 functions with implementations of their own, proven against SHA-256-specific contracts (initSha256Contractwith 76 words of scratch,finalizeSha256OutContractwith 86,iterateSha256Contract). They are now the one HMAC and PBKDF2 implementation for every streaming hash function (Impl/Hmac/Generic,Impl/Pbkdf2/Generic), calling SHA-256's verified streaming functions, and proven againstSpec.Hmac.sha256I's generic contracts (104 words) — as SHA-1, MD5 and the SHA-512 family already are on these targets, and SHA-256 is on x86-64 and AArch64.sha256OKand the instances inProof/{Hmac,Pbkdf2}/Generic/Arm; the registration files are like SHA-1's.Variants/Sha256/X86/): a backend now provides its streamingupdate/finalize(Sha256Stream); HMAC and PBKDF2 are proven once for any backend (the code between calls depends only on SHA-256's sizes, so its taint is checked once) and emitted per backend fromGeneric/Sha256/X86/— so the scalar and SHA-NI (Accelerate x86 SHA-256, HMAC and PBKDF2 with SHA-NI #468) backends both get them, as would any new one.Impl/Hmac/{Arm,X86}.lean,Impl/Hmac/Sha256/X86.lean,Impl/Pbkdf2/{Arm,X86}.lean,Impl/Pbkdf2/Sha256/X86.lean,Proof/Hmac/{Arm,X86,Sha256}/…,Proof/Pbkdf2/{Arm,X86,Sha256}/…,Proof/Sha256/X86/Stream/{CompressAt,FinalizeVariant}.lean). Scrypt's ARM proof used one lemma from them (wp_eor), now its own.src/hmac/sha256.rsandsrc/pbkdf2/sha256.rsusestreaming_hmac!/streaming_pbkdf2!on every target (the hand-writtenSha256HmacState,sha256_key_statesanditerateare gone), matching exhaustively onSha256Backend; PBKDF2's key states are now zeroized, and ARMv7 no longer skips the backend match.Spec/orTCB/changes. The SHA-256-specific contracts inSpec/Hmac/Contract.leanandSpec/Pbkdf2/Contract.leanbecome unused; a separate trust-change PR removes them after this one.Performance regression
The SHA-256-specific iteration made two compression calls per step. The generic one, for each of the inner and outer states, copies the 96-byte state byte by byte,
updates (buffering the 32-byteUbyte by byte) andfinalizes, then XORsTbyte by byte. On i686 (musl build on an x86-64 SHA-NI machine, min of 7 runs):ARMv7 wasn't measured locally; from the emitted code, the two 96-byte copies alone are ~1,350 instructions per iteration, so expect something similar. The same costs already apply to SHA-1, MD5 and the SHA-512 family on these targets. Ways to recover it (word-wise state copies and XOR in the generic code, or porting the 64-bit targets' compression-level
Mddesign to 32-bit) are follow-ups.Checked locally: full
lake build,Emit.leanregenerated and--check, everyci/check,cargo fmt/clippy(native, i686, armv7, aarch64),cargo testwith Wycheproof natively and on i686 (musl) withVG_CPU_FEATURES=none,sha,ssse3and unrestricted (both x86 backends). Not run locally: ARMv7 tests.🤖 Generated with Claude Code
https://claude.ai/code/session_01RjfTK5YMk2jDsiKRYs2dbn
Generated by Claude Code