Skip to content

godaddy/hardware-rust-crypto

hardware-rust-crypto

Hardware-only AES-256-GCM, AES-256-GCM-SIV, and key/nonce generation for Rust. Every AES round and every GF(2^128) multiply executes as a CPU instruction; no software fallback is compiled in.

CI Z3/sympy proofs Kani SAW crux-mir F* Constant-time Miri Sanitizers

Proven secure, not just asserted secure

Most crypto libraries ask you to trust that their code is correct. This one is built so you do not have to: the security-critical logic is machine-checked from several independent angles, the cryptographic primitives are computed by validated CPU silicon rather than by this code, and every check runs in CI where you can re-run it yourself.

The cryptography is the CPU's job; the orchestration is ours, and we prove the orchestration. Every AES round and every GF(2^128) multiplication is a single dedicated hardware instruction - AES-NI / PCLMULQDQ on x86_64, the ARMv8 Cryptographic Extensions (AES / PMULL) on aarch64 - the same cryptographic silicon that terminates TLS across the internet. There is no software S-box, no table lookup, no bitsliced fallback, so the classic AES cache-timing attack surface does not exist here: the S-box never touches memory. What this library adds is the thin, non-secret plumbing around those instructions - counter increments, the J0 construction, nonce generation, length framing, the tag-comparison decision, key lifecycle - and that plumbing is what we have proven exhaustively.

The composition logic is checked by independent verification engines

The intrinsic-free glue is verified for all inputs by tools that share no code and no solver heritage - they would each have to fail the same way to let a bug through. The single most safety-critical line - the counter increment, since a wrong increment silently breaks nonce uniqueness and is catastrophic for GCM - is independently confirmed by five of them:

Engine Verifies over… Proves (among others)
Kani / CBMC the actual compiled machine code counter == SP 800-38D inc_32 / RFC 8452 increment over all 2¹²⁸ blocks; J0 layout; length validators; the nonce parser and envelope splitters never panic or read out of bounds; constant_time_eq accepts exactly the correct tags; generated nonces are injective in the counter (no reuse within an instance)
SAW (Galois) the LLVM bitcode increment_counter == inc_32, j0 == IV‖0³¹‖1, against a Cryptol spec
crux-mir (Galois) the Rust MIR increment_counter == inc_32
F* (via hax) F* extracted from the real Rust source j0 sets the pre-counter byte; increment_counter preserves the leading 96 bits - with a drift guard that the proved code matches a fresh extraction
Z3 / SMT an independently-written model decryption inverts encryption, the SIV key derivation, and the length framing == SP 800-38D / RFC 8452, with a built-in non-vacuity check
sympy symbolic algebra the Horner recurrence == the batch sum-of-powers, for every block count

The one piece of novel math is proven for every possible input

The GHASH/POLYVAL field multiply is the only hand-built cryptographic arithmetic in the crate. It is proven equal to the RFC 8452 specification for all inputs

  • not sampled, all of them - by an exhaustive argument over the 128×128 GF(2) basis (the operation is GF(2)-bilinear, so its values on a basis determine it everywhere), on both architectures. The model is first pinned byte-for-byte to the output of the real CPU instructions, so "the model" = "the shipped code" = "the spec" before any proof is trusted. Notably, two of the engines above (SAW and crux-mir) independently established that no source- or bitcode-level tool can see through the hardware carryless-multiply instruction itself - which is exactly why the field proof is anchored to that instruction's real output: the primitive is the CPU vendor's validated silicon, and we prove our use of it.

…and tested the way you test something you cannot afford to get wrong

  • Constant-time, in the shipped binary. The secret-handling routines are disassembled and verified branch-free over their secret inputs - a checkable property of the machine code, with a deliberately-leaky control that must be rejected - backed by a CI-gated dudect (Welch t-test) timing test on both decrypt paths.
  • Memory safety, five ways. The entire key-state lifecycle and the real AES/GHASH paths run clean under Miri's undefined-behavior checker (aliasing, provenance, out-of-bounds, uninitialized reads), Valgrind memcheck, AddressSanitizer, ThreadSanitizer, and continuous fuzzing of the decrypt parser.
  • Wire-format correctness against three independent lineages. Byte-for-byte agreement with RustCrypto, ring (BoringSSL-derived), and OpenSSL - implementations with no shared ancestry - plus NIST CAVP (750 vectors), RFC 8452 Appendix C.2, and Project Wycheproof (every tag-rejection and counter-wrap case included).
  • Randomness quality. The hardware AES-CTR key/nonce generator passes the PractRand and dieharder statistical batteries.
  • We test the tests. Mutation testing (cargo-mutants) confirms the suite actually catches injected bugs; the few intentional survivors are individually accounted for.
  • Supply chain. The production dependency graph contains no third-party cipher and no third-party crypto implementation - and CI fails the build if one ever appears.

None of this is a one-time claim, and none of it is hidden. Every item above runs in public continuous integration as its own re-runnable workflow: the badges at the top of this file link straight to the live runs, where anyone can read the exact commands that executed and their full output - the SAW and crux-mir proofs printing Proof succeeded, Kani discharging its harnesses, F* printing PROOF VERIFIED, the constant-time disassembly check, the differential vectors. The verification is open to inspection, reproducible from the scripts under proofs/, and re-runnable on demand from the Actions tab. For the exact statement of each property, its method, and an explicit trust level - compiled-code proof → all-inputs model proof → exhaustive vectors → tooling - see docs/proof-coverage.md; for the full narrative, see docs/assurance.md.

Will quantum computers break this? No - and AES-256 is the reason

A fair question, and the short answer for symmetric encryption is no. The algorithms quantum computers actually break are the asymmetric ones - RSA and elliptic-curve key exchange and signatures, via Shor's algorithm - and this library uses none of them. Symmetric ciphers like AES are only modestly affected: the best known quantum attack, Grover's algorithm, at most halves the effective key length. That takes AES-256 from a 256-bit to an estimated 128-bit security level - still far beyond the reach of any computer, classical or quantum - and Grover's speedup parallelizes so poorly that the practical margin is larger still. This is precisely why NIST and the NSA's CNSA 2.0 suite both list AES-256 as quantum-resistant and approved for protecting information into the post-quantum era, and why doubling the symmetric key size (to 256 bits) is the standard post-quantum guidance.

Because this library is AES-256 throughout - both AEAD modes and the key/nonce generator - the symmetric cryptography here is considered safe against quantum attack. (Post-quantum key exchange and signatures are a separate, asymmetric problem that a symmetric AEAD library neither solves nor needs to.)

Why: the performance and footprint delta

Leaning almost entirely on the CPU's dedicated cryptographic instructions - AES-NI/PCLMULQDQ on x86_64, ARMv8 AES/PMULL on aarch64 - is what produces the gap below. Encryption is faster than ring at every size from 16 bytes through 16 KiB, including bulk, because the encrypt path stitches the AES keystream and GHASH multiply into one software-pipelined loop so both execution pipelines stay busy. Bulk decryption uses the same policy as high-performance in-place AEAD APIs: write plaintext while authenticating, then wipe the caller buffer on tag failure. It runs an order of magnitude faster than a default RustCrypto build and carries the smallest reusable key state of the three; see docs/benchmarks.md.

Operation (lower is better, ns) this crate ring RustCrypto (default) RustCrypto (armv8 cfgs)
encrypt 1 KiB 157 217 5040 579
encrypt 16 KiB 1970 2380 76100 8860
decrypt 1 KiB 169 168 5040 591
decrypt 16 KiB 1910 2190 75900 8930

Reusable AES-256-GCM key-state size: this crate 368 B / ring 544 B / RustCrypto 992 B. Measured on a MacBook Pro (Apple M4 Max, single machine); at these ~200 ns scales run-to-run variance is roughly +/-10%. The allocation-free *_to APIs run faster still (1 KiB encrypt 131 ns, decrypt 143 ns). The full matrix, both RustCrypto build configurations, and methodology are in docs/benchmarks.md.

How it gets there: stitched encrypt and decrypt loops that software-pipeline eight-way interleaved hardware CTR against eight-block aggregated GHASH so the AES and carryless-multiply pipelines run concurrently, a register-resident key schedule, fused bulk paths that touch each message byte once, and allocation-free *_to APIs. Failed decrypt_to authentication zeroizes the plaintext-length prefix of the caller buffer before returning an error. Why that is also safer: the AES S-box never touches memory, so the classic cache-timing attack surface does not exist here; and there is no silent software fallback - if the required hardware is absent, construction fails with a typed error instead of quietly degrading (a default RustCrypto build on aarch64 silently runs ~8x-slower software AES, see docs/benchmarks.md).

AES-256-GCM-SIV: nonce-misuse-resistant, same hardware

The crate also implements AES-256-GCM-SIV (RFC 8452) on the same hardware AES and carryless-multiply backends - POLYVAL is the field operation that backend computes natively, so it reuses the eight-block aggregated reduction, and the CTR pass drives eight interleaved AES chains. SIV is a two-pass design (it authenticates the plaintext before deriving the counter, so it cannot use the fused GCM loop) and derives a fresh per-message key, so it is slower than this crate's own AES-256-GCM - but it beats RustCrypto's AES-256-GCM-SIV at every size, including RustCrypto's hardware-enabled configuration, and is 12x-27x faster than the default build a consumer gets unmodified. There is no ring column here because ring implements no GCM-SIV mode (its only AEADs are AES-128/256-GCM and ChaCha20-Poly1305), so RustCrypto aes-gcm-siv is the only external reference; it is also our interop oracle, including the RFC 8452 known-answer vectors.

Operation (lower is better, ns) this crate (SIV) this crate (GCM) RustCrypto SIV (default) RustCrypto SIV (armv8 cfgs)
encrypt 1 KiB 392 157 7350 891
encrypt 16 KiB 3110 1970 84600 9960
decrypt 1 KiB 395 169 7390 874
decrypt 16 KiB 3040 1910 85100 10040

Reusable AES-256-GCM-SIV key-state size: this crate 240 B / RustCrypto 960 B (unchanged across build configurations). At 240 bytes it is the leanest state in the crate - smaller than the 368-byte AES-256-GCM state - because the POLYVAL key powers are derived per message rather than precomputed into reusable state, so it holds only the key-generating AES schedule. Key setup is correspondingly cheap (23-41 ns versus 347 ns for RustCrypto). Same Apple M4 Max machine and +/-10% sub-microsecond caveat as above; the full SIV matrix, both RustCrypto configurations, and the per-message-overhead breakdown are in docs/benchmarks.md.

What it provides

A single crate, hardware-rust-crypto, with two modules:

  • aes_gcm: hardware-only AES-256-GCM with compact reusable key state. The default encrypt API generates a unique nonce and returns the self-framed ciphertext || tag || nonce envelope; decrypt parses the nonce from that envelope. Allocation-free inline owned prepared keys (HardwareAes256GcmKeyState) and caller-controlled storage placement (HardwareAes256GcmIn / UninitKeyStateSlot / key_state_layout) let the caller decide where keys and key-equivalent state live. Key state zeroizes on drop. The same module also provides nonce-misuse-resistant AES-256-GCM-SIV (RFC 8452) through a parallel set of types (HardwareAes256GcmSiv / HardwareAes256GcmSivKeyState / HardwareAes256GcmSivIn / SivUninitKeyStateSlot) with the same default envelope API, built on the same hardware AES and carryless-multiply backends with POLYVAL authentication. Its 240-byte reusable state is the leanest of the set (just the key-generating AES schedule) and 4x smaller than RustCrypto's Aes256GcmSiv; see docs/benchmarks.md.
  • random: a hardware-only AES-CTR key/nonce generator with fork detection and zeroized state. Initial seeding uses OS entropy (getrandom); reseeding blends CPU hardware-RNG entropy (RDSEED on x86_64, RNDRRS on aarch64 Graviton) through the generator state when available, falling back to the OS otherwise (cpu-rng-reseed feature, default on).

Interoperability tests against stock RustCrypto aes-gcm, aes-gcm-siv, and ring (including RFC 8452 known-answer vectors and the Project Wycheproof AES-256-GCM-SIV suite - counter-wrap and tag-rejection vectors included - for the SIV path), dudect-style constant-time timing harnesses for both the GCM and SIV decrypt paths, Criterion benchmark harnesses, and CI across five platforms (Linux x64, Linux arm64, macOS arm64, Windows x64, Windows arm64) round out the repo.

The production dependency graph contains no software cipher and no third-party crypto implementation: only getrandom, subtle, and zeroize (plus libc on Unix). ChaCha20 (via rand_chacha), Salsa20, ring, and stock RustCrypto appear only in dev-dependencies as benchmark/interop/timing baselines - and CI fails the build if any of them ever enters a production graph.

Where this code comes from

The cryptographic cores are vendored, with minimal adaptation, from the RustCrypto project and from rand_aes:

  • The GHASH authenticator is the RustCrypto ghash 0.5.1 GHASH-to-POLYVAL mapping over the polyval 0.6.2 hardware backends (PCLMULQDQ on x86/x86_64, PMULL on aarch64).
  • The AES-256 key expansion and block encryption paths, and the AES-256-CTR generator backend, are adapted from rand_aes 0.7.0's AES-NI and ARMv8 hardware backends.
  • The AES-GCM composition (J0 construction, CTR keystream, length-block authentication, tag handling) follows NIST SP 800-38D and the RustCrypto aes-gcm crate's design.
  • The AES-256-GCM-SIV composition (per-nonce key derivation, POLYVAL over the same carryless-multiply backend, SIV tag and little-endian CTR) follows RFC 8452 and the RustCrypto aes-gcm-siv crate's design. POLYVAL is the field operation the vendored backend computes natively; the GHASH path is the byte-reversed adaptation of it.

Exact upstream versions, commit SHAs, copyright holders, licenses, and the modifications made are recorded in NOTICE.

Upstream audit history

The RustCrypto aes-gcm crate and its dependencies (including the AES implementations and the POLYVAL authenticator this repository's GHASH backend derives from) received a security audit by NCC Group in February 2020, funded by MobileCoin, with no significant findings. The audit specifically noted that the implementations use the recommended techniques for constant-time execution.

To be precise about what that means here: the audit covered the upstream crates as they existed at the time, not this fork. What this repository inherits is the audited design and backend structure; what it changes is subtractive (removing the software fallback and runtime dispatch). Parity with the audited lineage is enforced mechanically by the test suite described below, and ring - an independently developed, BoringSSL-derived implementation that is among the most widely deployed crypto code anywhere - is used as a second cross-validation oracle.

What was removed, and why

This is a strict subset of the upstream functionality: AES-256-GCM and AES-256-GCM-SIV with 96-bit nonces, plus AES-256-CTR key generation. The deliberate deletions are:

  • The fixsliced software AES fallback (and its state: 960 bytes of software round keys plus batch buffers, versus 240 bytes of hardware key schedule).
  • The runtime autodetection enum and boxed dispatch state that let one type hold either a hardware or software backend.
  • Every code path that could silently fall back to table-based or bitsliced software AES.

The motivation is the cached-key tier of applications that hold many keys resident: cached key-equivalent state should be as small as a hardware key schedule actually needs (HardwareAes256Gcm state is 368 bytes: 15 AES round keys plus eight precomputed GHASH key powers), should fit guarded-memory placement policies, and should never be able to be a software AES state. If the required CPU features are absent, construction fails with a typed error instead of degrading.

Why this is safe to use in place of ring or RustCrypto directly

  1. Every secret-dependent operation executes in hardware. AES rounds use AES-NI (AESENC/AESENCLAST) on x86_64 and the ARMv8 Cryptographic Extensions (AESE/AESMC) on aarch64. GHASH's GF(2^128) multiplication uses carryless multiply (PCLMULQDQ / PMULL). These instructions have no key- or data-dependent timing, no table lookups at secret-dependent addresses, and no secret-dependent branches - the classic AES cache-timing attack surface does not exist in this code because the S-box never touches memory. Tag comparison is constant-time via the subtle crate. The Rust glue performs no secret-dependent branching or indexing.
  2. Wire-format compatibility is proven, not assumed. Both AEAD modes are held to the same bar:
    • AES-256-GCM produces byte-identical ciphertext to RustCrypto aes-gcm, cross-decrypts in both directions with both aes-gcm and ring, matches NIST SP 800-38D known-answer vectors, and passes the Project Wycheproof AES-256-GCM suite (66 vectors: 39 valid, 27 tag-rejection).
    • AES-256-GCM-SIV matches the RFC 8452 Appendix C.2 known-answer vectors, byte-matches RustCrypto aes-gcm-siv, and passes the Project Wycheproof AES-256-GCM-SIV suite (103 vectors, including 5 counter-wrap and 34 tag-rejection cases).
    • Both survive randomized differential sweeps and dense length sweeps across plaintext and AAD sizes (every length 0..=288 across the GHASH/POLYVAL aggregation seams), reject every single-bit tampering of ciphertext, tag, nonce, and AAD across many sizes, and reject wrong key / nonce / AAD over hundreds of random trials.
    • Both decrypt paths carry dudect-style constant-time timing harnesses (Welch t-test): tag-comparison timing is independent of mismatch position and decrypt timing is independent of ciphertext content (|t| far below threshold for both modes; see docs/constant-time.md).
    • The AES-CTR generator is checked against FIPS-197 vectors independently verified with OpenSSL.
    • Wycheproof vectors are vendored verbatim (downloaded, not transcribed; Apache-2.0, see NOTICE) and are dev-only - absent from the production dependency graph.
  3. Hardware support is verified, never guessed. Construction checks CPU features at runtime and returns UnsupportedCpu before accepting key material, and CI asserts the hardware paths are actually exercised so a green build cannot mean "tests silently skipped".
  4. Key material lifecycle is explicit. Key state zeroizes on drop (owned and caller-placed), encryption and decryption can target caller-controlled buffers (encrypt_to/decrypt_to) so decrypted keys never sit in unmanaged heap allocations, and the key generators detect process forks and reseed on interval.
  5. Nonce uniqueness is the library's job by default. GCM is catastrophic on nonce reuse, so encrypt/encrypt_to generate a unique 96-bit nonce per call - a per-instance 64-bit counter over a 96-bit salt drawn always from the OS and re-salted on fork - and append it to the ciphertext as ciphertext || tag || nonce. decrypt/decrypt_to parse the nonce only from that envelope. The same default envelope shape applies to AES-GCM-SIV; SIV additionally degrades gracefully under accidental reuse (identical (nonce, key, message) tuples produce identical ciphertext, but reuse does not leak the authentication key or earlier plaintexts the way GCM does). Low-level caller-supplied-nonce entry points for both modes are kept behind the non-default hazmat-explicit-nonce feature for test vectors, interoperability work, and benchmark investigations.

Why hardware-only is the right default in production

AES-NI and PCLMULQDQ have shipped in effectively every x86_64 server CPU since Intel Westmere (2010) and AMD Bulldozer (2011). The ARMv8 Cryptographic Extensions are present on every mainstream aarch64 server and client part - AWS Graviton, Ampere, Apple Silicon. A production fleet in 2026 that lacks these instructions essentially does not exist; the software fallback in general-purpose crypto libraries serves hardware we do not run, while costing cached key-state size and a larger audit surface. The silent-fallback risk is not hypothetical: a default build of stock aes-gcm on aarch64 quietly runs software AES and POLYVAL unless every consumer passes RUSTFLAGS="--cfg aes_armv8 --cfg polyval_armv8" - measured at roughly 8x slower, with nothing failing and nothing warning (see docs/benchmarks.md). Making hardware support a hard, typed requirement converts that silent risk into a loud startup failure.

ring remains in the repository as an interop and benchmark baseline.

Commands

# Tests (interop, KATs, differential sweeps, lifecycle) and lints:
cargo test --workspace --all-targets
cargo clippy --workspace --all-targets -- -D warnings

# Hardware sanity check and key-state footprint report:
cargo run --example assert_hardware
cargo run --release --example state_size

# Benchmarks (see docs/benchmarks.md for both RustCrypto configurations):
cargo bench --bench aes_gcm
cargo bench --bench aes_gcm_siv
cargo bench --bench random

# Constant-time verification (manual; see docs/constant-time.md):
cargo test --release --test timing_constant_time -- --ignored --nocapture
cargo test --release --test timing_constant_time_siv -- --ignored --nocapture

# Machine-checked field-arithmetic proofs (GHASH/POLYVAL core; see proofs/):
pip install z3-solver sympy
./proofs/run_all.sh

# Full-lifecycle Miri over the AES-256-GCM/SIV key-state paths (x86):
cargo +nightly miri test --lib aes_gcm

See docs/design.md for the implementation plan. See docs/benchmarks.md for locally measured numbers and how to reproduce them. See docs/constant-time.md for the emitted-assembly inspection procedure and the dudect-style timing harness. See docs/proof-coverage.md for the proof-coverage map: every verified property, by method, at an explicit trust level (compiled-code proof / all-inputs model proof / exhaustive vectors / tooling), in one table. See docs/assurance.md for the full assurance narrative: the test/proof layers in place, the machine-checked GHASH/POLYVAL proofs (proofs/), Miri/Valgrind/sanitizer coverage, and what an independent audit or CAVP/CMVP validation would still add. See docs/randomness-testing.md for the RNG statistical-test battery (PractRand/dieharder) and how to reproduce it. See docs/security-audit.md for the multi-model agentic security assessment (threat model, findings, standards conformance, and residual-risk register).

Acknowledgments

This work exists because of the RustCrypto project and its maintainers, whose carefully engineered, audited, and openly licensed implementations of AES-GCM, GHASH, and POLYVAL form the foundation of this repository - our sincere thanks to them for years of rigorous work that the wider Rust ecosystem builds on. Thanks also to Nils Hasenbanck for rand_aes, whose clean hardware AES-CTR backends this repository adapts, and to NCC Group and MobileCoin for the public audit of the RustCrypto AEAD stack. See NOTICE for formal attribution.

About

Hardware-only AES-256-GCM / AES-256-GCM-SIV and key/nonce generation for Rust. Every AES round and every GF(2^128) multiply executes as a CPU instruction; no software fallback is compiled in.

Resources

License

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors