rind is a compendium of high quality, fast and easy to build cryptography for Rust, aimed at use with rustls and MoQ.
rind began as a fork of Graviola by Joe
Birr-Pixton, and no longer tracks it. It adds what MoQ needs to drop C
cryptography entirely; the plan lives in quest/. Coming from
ring or aws-lc-rs? MIGRATING.md maps their API to rind's, and
rind-ring offers ring's API as is.
High quality: rind incorporates assembler routines from the s2n-bignum project. These have been formally proven to correctly implement the desired mathematical operation.
Fast: rind beats or is competitive with other cryptography
libraries for Rust. cargo bench compares it with aws-lc-rs, ring,
and RustCrypto.
Easy and fast to build: no C compiler, assembler or other tooling needed: just the Rust compiler. Compiles in seconds.
Status
This project is very new, so exercise due caution. The overriding
goal of this crate is for use with rustls via rind-rustls,
but there is also a public API for general-purpose use.
Goals
- Fast and simple compilation
-
cargo buildtakes seconds, and requires only rustc
-
- Competitive performance (with ring, aws-lc-rs, and rustcrypto)
- Uses formally-verified code from other projects (where it is as fast)
- Intended to provide algorithms in wide use on web
- Intended for use as a rustls
CryptoProvider, via rind-rustls.
Limitations
aarch64, x86_64, and 32-bit arm architectures only.
aarch64requires theneonCPU feature. Where the CPU has the crypto extension (aes,pmull, andsha2), as the Raspberry Pi 5 and most aarch64 servers and phones do, AES, GHASH, SHA1, and SHA256 run on its instructions. Without it, as on the Raspberry Pi 4, CM4, and Zero 2 W, they run aws-lc's NEON and scalar assembly instead (see Algorithms),rind::cpu::has_aes_hardware()is false, and rind-rustls and rind-dimpl prefer ChaCha20-Poly1305.x86_64runs on every x86_64 CPU. Withoutaesandpclmulqdq, as on Core 2 and Nehalem, AES and GHASH run aws-lc's SSSE3 assembly instead (see Algorithms),rind::cpu::has_aes_hardware()is false, and rind-rustls and rind-dimpl prefer ChaCha20-Poly1305. Withoutavx2, as on Sandy Bridge, Ivy Bridge, and AMD's Bulldozer and Jaguar cores, ChaCha20-Poly1305 runs aws-lc's SSE4.1 assembly, ChaCha20, SHA1, and SHA256 its SSSE3 assembly, or for SHA its AVX assembly on Intel's cores, ML-KEM's arithmetic runs a Rust port of mlkem-native's portable C, and table lookups use SSE2 (see Algorithms). Withoutsse4.1, as on the first Core 2s and Atoms and AMD's Bobcat, ChaCha20 and Poly1305 run in separate passes. Withoutssse3either, as on Intel's CPUs before Core 2, AMD's before Bobcat and Bulldozer, and QEMU's defaultqemu64model, AES and GHASH run Rust ports of aws-lc's constant-time SSE2 and integer C, and ChaCha20, SHA1, and SHA256 its integer assembly. Withoutadx, as on Haswell and AMD's cores before Zen, s2n-bignum's_altroutines replace those that use it, and aws-lc's Montgomery multiplication replaces its Karatsuba routines for RSA; withavx2there, as on Haswell, RSA-2048 signing runs aws-lc's RSAZ exponentiation. Withavx512ifma, as on Ice Lake, Sapphire Rapids, and AMD's Zen 4 and 5, RSA-2048, 3072, and 4096 signing runs aws-lc's dual AVX-512 RSAZ.- 32-bit
arm(tested onarmv7-unknown-linux-gnueabihf, and built forarmv7-linux-androideabi) runs portable Rust instead, mostly from libcrux (see Algorithms), and needs no CPU features. RSA's Montgomery multiplication runs aws-lc's assembly, with NEON where the CPU has it. Where the CPU has the ARMv8 crypto extension, as most 32-bit Android devices do, AES, GHASH, SHA1, and SHA256 run aws-lc's assembly for it instead. Without it,rind::cpu::has_aes_hardware()is false, and rind-rustls and rind-dimpl prefer ChaCha20-Poly1305, which is faster there: ChaCha20 runs aws-lc's assembly, and so does Poly1305 where the CPU has NEON. SHA-384, SHA-512, and SHA-512/256 always run aws-lc's assembly, with NEON where the CPU has it, and with NEON, X25519's shared secret runs BoringSSL's assembly.
Every operation panics on a CPU missing one of these. Call rind::cpu::check()
at startup to get an error naming the missing feature instead.
Building and Testing
Compile:
cargo build --release
Run tests:
git submodule update --init thirdparty/cavp
git submodule update --init thirdparty/wycheproof
cargo test
Run performance benchmarks:
cargo bench
With Nix, nix develop (or direnv) provides CI's
toolchain, and just check runs CI's gate over what the branch changed.
On Linux, just armv7 runs the nightly armv7 job: it builds and lints for
32-bit ARM, then runs the tests under qemu with the ARMv8 crypto extension,
with NEON alone, and with NEON denied.
nix develop .#ctgrind --command admin/ctgrind runs the nightly
constant-time checks under valgrind, and nix develop .#sde --command admin/ct-sde those of the AVX-512 code valgrind cannot run, under Intel SDE.
Algorithms
Each cell gives rind's time over the fastest of aws-lc-rs and ring on that
path, in the benchmark where rind trails most, then whose code runs: the
sources below, rind's own code, or Graviola's. A bold ratio
is a gap, above the noise of about 1.02. A ✓ marks code its source formally
verifies, to the extent Sources states, and a ⚡ unverified code
chosen because it measured faster than a verified alternative. A cell naming
no source runs the same sources as the one to its left, and an empty cell
runs the same code, so it isn't timed. The rind-rustls rows time full and
resumed TLS 1.3 handshakes and bulk transfer against rustls over aws-lc-rs's
and ring's providers. RSA key generation, whose time swings by up to 3x from
run to run, stays out of the ratios. The nightly bench workflow times every
cell and keeps each benchmark's numbers in its summary. These are from
run 41378010740, and the P-256, P-384, and P-521 cells from
run 41399702862, except armv7's P-256, from run 41518446383; a change
that moves a cell refreshes it.
ML-KEM-768's cells are from run 41400124007, whose x86_64 job drew an EPYC
9V74 (Zen 4) and no AVX2 job an EPYC 7763. RSA's no ADX and no AVX2 cells are
from run 41469252502, whose no ADX job drew a Xeon 6973P-C (Granite Rapids)
and no AVX2 job an EPYC 7763.
x86_64 runs every feature of an AMD EPYC 7763 (Zen 3: ADX, AVX2, SHA-NI, and
VAES, but no AVX-512), except where noted below. Each column to its right denies more, as bench.yml
describes, and compares with aws-lc-rs alone, since ring cannot be denied a
feature: no ADX denies ADX (Haswell); no AVX2 also AVX2, BMI2, SHA-NI,
AVX-512, VAES, and VPCLMULQDQ (Sandy Bridge); no AVX also AVX (Westmere); no
AES-NI also AES-NI, PCLMULQDQ, and SSE4.1 (Core 2); and SSE2 also SSSE3.
GitHub's pool ran no ADX on an EPYC 7763 too, no AVX2 on an EPYC 9V74 (Zen
4), no AVX and SSE2 on an EPYC 9V45 (Zen 5), and no AES-NI on a Xeon Platinum
8573C (Emerald Rapids).
The 1 MB transfer row is from run 41401180882, which ran x86_64 on an
EPYC 9V74, whose AVX-512 runs AES-GCM's 512-bit paths rather than the 256-bit
VAES ones Zen 3 runs, except its no AVX2 cell, from run 41468954426 on an
EPYC 9V45. The AES-GCM row is from run 41519474246, which ran x86_64 on an
EPYC 9V45, whose AVX-512 runs AES-GCM's 512-bit paths, no AVX2, no AVX, and
no AES-NI on an EPYC 7763, and SSE2 on a Xeon Platinum 8573C.
The SHA, HMAC, and HKDF rows are from run 41398664891, which ran every
column on an EPYC 7763, except x86_64's HKDF cell, from run 41519474246 on
an EPYC 9V45, and its PBKDF2 cell, from run 41464027605 on an EPYC 7763. The SHA-256
cells are from run 41485960394, which ran x86_64 on an EPYC 9V74, no AVX2 on
an EPYC 7763, no AVX on an EPYC 9V45, and SSE2 on a Xeon Platinum 8573C.
The x86_64 P-256 cells take key generation, signing, and ECDH from
run 41467306881, which ran x86_64 and no AVX2 on an EPYC 7763 and no ADX
on an EPYC 9V74, and verification from run 41399702862, which ran x86_64
on an EPYC 7763, no ADX on an EPYC 9V45, and no AVX2 on a Xeon 6975P-C
(Granite Rapids).
Intel CPUs without SHA-NI run aws-lc's AVX SHA-1 and SHA-256, and every
Intel CPU its verified ✓ AVX SHA-384 and SHA-512.
| Algorithm | x86_64 | no ADX | no AVX2 | no AVX | no AES-NI | SSE2 |
|---|---|---|---|---|---|---|
| AES-GCM | 0.99 BoringSSL ⚡ + Graviola | 0.99 aws-lc ✓ + Graviola | 0.94 Graviola | 1.01 aws-lc + rind | 0.89 | |
| AES block, AES-CTR | 1.00 Graviola + aws-lc | 0.99 | 0.99 | 1.01 aws-lc | 0.85 | |
| ChaCha20 (header protection) | 0.96 Graviola | 0.95 aws-lc ⚡ | 0.93 | 0.95 | 0.92 | |
| ChaCha20-Poly1305, XChaCha20-Poly1305 | 1.02 aws-lc ⚡ | 1.00 | 1.12 aws-lc ⚡ + Graviola ⚡ | 1.10 | ||
| SHA-1 | 1.00 rind | 1.00 aws-lc | 1.00 | 0.94 | ||
| SHA-256 | 1.13 Graviola ⚡ | 1.00 aws-lc ⚡ | 1.02 | 1.03 | ||
| SHA-384, SHA-512, SHA-512/256 | 1.01 aws-lc ⚡ | 1.00 | 1.01 | 1.00 | ||
| HMAC | 1.00 Graviola ⚡ | 1.01 | ||||
| HKDF | 0.93 rind ⚡ | 0.87 | ||||
| PBKDF2 | 0.94 rind | 0.93 | ||||
| X25519 | 1.01 s2n-bignum ✓ | 1.00 | 1.02 | |||
| Ed25519 | 1.04 s2n-bignum ✓ + Graviola ⚡ | 1.03 | 1.03 | |||
| P-256 ECDH, ECDSA | 0.99 s2n-bignum ✓ + aws-lc ⚡ + Graviola ⚡ | 1.00 | 1.09 | |||
| P-384 ECDH, ECDSA | 0.90 s2n-bignum ✓ + rind ⚡ + Graviola ⚡ | 0.96 | 0.92 | |||
| P-521 ECDH, ECDSA | 0.94 s2n-bignum ✓ + rind ⚡ | 0.94 | 0.94 | |||
| ML-KEM-768 | 0.79 s2n-bignum ✓ + mlkem-native ✓ + Graviola ⚡ | 0.92 mlkem-native ⚡ + s2n-bignum ✓ | ||||
| RSA signatures, key generation | 1.07 s2n-bignum ✓ + aws-lc ⚡ | 1.22 | 1.03 | |||
| rind-rustls handshakes | 0.98 | 1.05 | ||||
| rind-rustls 1 MB transfer | 1.00 | 1.00 |
aarch64 runs a Neoverse-N2, with the crypto extension and the SHA-512 and SHA-3 instructions; no crypto denies them, as a Raspberry Pi 4 lacks them. armv7 runs on the same core in 32-bit mode, with NEON and the crypto extension; without them it runs the code Symmetric cryptography names. The aarch64 and no crypto cells for RSA and rind-rustls handshakes are from run 41397278464, which times aarch64 RSA's current Montgomery multiplication. The SHA-256 cells and aarch64's HMAC and PBKDF2 cells are from run 41485960394, the AES-GCM and HKDF rows from run 41519474246, the other SHA and HMAC cells from run 41398664891, and armv7's RSA and handshake cells from run 41401089431.
| Algorithm | aarch64 | no crypto | armv7 |
|---|---|---|---|
| AES-GCM | 1.04 Graviola | 1.02 aws-lc + rind | 0.99 |
| AES block, AES-CTR | 1.18 Graviola + rind | 1.01 aws-lc | 0.99 |
| ChaCha20 (header protection) | 1.03 Graviola | 0.96 | 0.87 aws-lc ⚡ |
| ChaCha20-Poly1305, XChaCha20-Poly1305 | 1.01 aws-lc ⚡ | 1.00 | 1.06 |
| SHA-1 | 1.01 rind | 1.00 aws-lc | 1.00 |
| SHA-256 | 1.02 aws-lc ⚡ | 1.00 aws-lc | 0.99 aws-lc ⚡ |
| SHA-384, SHA-512, SHA-512/256 | 1.00 aws-lc ✓ | 1.00 | 1.00 aws-lc ⚡ |
| HMAC | 1.00 Graviola ⚡ | 1.00 | 1.00 |
| HKDF | 0.86 rind ⚡ | 0.88 | 1.00 |
| PBKDF2 | 0.91 rind | 0.93 | 0.98 |
| X25519 | 0.81 s2n-bignum ✓ | 0.80 | 0.79 BoringSSL ⚡ + curve25519-dalek ⚡ |
| Ed25519 | 0.89 s2n-bignum ✓ + Graviola ⚡ | 0.77 | 0.88 curve25519-dalek ⚡ + rind ⚡ |
| P-256 ECDH, ECDSA | 1.02 s2n-bignum ✓ + Graviola ⚡ | 0.99 | 0.58 fiat-crypto ✓ + rind ⚡ + Graviola ⚡ |
| P-384 ECDH, ECDSA | 0.78 s2n-bignum ✓ + rind ⚡ + Graviola ⚡ | 0.78 | 1.03 fiat-crypto ✓ + rind ⚡ |
| P-521 ECDH, ECDSA | 0.64 s2n-bignum ✓ + rind ⚡ | 0.65 | 1.04 fiat-crypto ✓ + rind ⚡ |
| ML-KEM-768 | 1.04 s2n-bignum ✓ + Graviola ⚡ | 0.93 | 1.05 libcrux-ml-kem ✓ |
| RSA signatures, key generation | 0.97 s2n-bignum ✓ + aws-lc ⚡ + Graviola ⚡ | 0.97 | 1.34 aws-lc ⚡ + Graviola ⚡ + rind ⚡ |
| rind-rustls handshakes | 0.96 | 0.90 | 1.29 |
| rind-rustls 1 MB transfer | 1.00 | 1.00 | 1.01 |
The table describes main, and changes alongside the code it describes. The
tests use Wycheproof's vectors.
Sources
| Source | How rind uses it | License | What is verified |
|---|---|---|---|
| Graviola | copied: rind began as a copy of it, and rind-rustls of its rustls-graviola; its aarch64 ChaCha20 and its Poly1305 began as cifra, CC0-1.0 | Apache-2.0 OR ISC OR MIT-0 | nothing |
| rind | its own code | Apache-2.0 OR ISC OR MIT-0 | nothing |
| s2n-bignum | copied: its assembly, transliterated into Rust asm! by admin/parse-asm, which ends each x86_64 routine using 256-bit registers with vzeroupper |
Apache-2.0 OR ISC OR MIT-0; its ML-KEM and aarch64 Keccak-f routines (from mlkem-native) Apache-2.0 OR ISC OR MIT; its x86_64 Keccak-f (from OpenSSL) Apache-2.0 | HOL Light proofs (x86_64, aarch64) that each routine's object code computes its specified result for all inputs. They check s2n-bignum's own build, not rind's transliteration, and do not cover memory safety. |
| HACL* | dependency, through libcrux-hacl-rs and libcrux-poly1305; copied: the ChaCha20 rounds, for HChaCha20, from libcrux-chacha20poly1305 | Apache-2.0 | F* proofs of memory safety, functional correctness, and secret independence of the F* source. libcrux's Rust is compiled mechanically from HACL*'s verified C, and that step is not itself verified. |
| libcrux-ml-kem | dependency | Apache-2.0 | hax and F* proofs of panic freedom and functional correctness for its portable arithmetic, NTT, serialization, and high-level code. Its SHA-3, from libcrux-sha3, is unverified. |
| mlkem-native | ported: its portable C polynomial arithmetic (the NTT, inverse NTT, base multiplication, reductions, packing, and rejection sampling), to Rust, for x86_64 CPUs without AVX2; copied, by admin/import-aws-lc from aws-lc's copy: its x86_64 AVX2 ciphertext compression, which rind ends with vzeroupper |
Apache-2.0 OR ISC OR MIT | CBMC proofs of memory safety and coefficient bounds for its C. They do not cover rind's port. HOL Light proofs that its AVX2 compression's object code computes its specified result for all inputs, of mlkem-native's own build. |
| libcrux-aes | dependency for AES-GCM; copied: the block cipher | Apache-2.0 | nothing: libcrux marks it pre-verification |
| aws-lc | copied, by admin/import-aws-lc: its SHA-384/512 assembly for x86_64 and aarch64, its x86_64 AVX SHA-256 routine and its aarch64 one for the SHA-256 instructions, Montgomery multiplication and the x86_64 exponentiation's bn_power5 (with one deliberate change: rind's bn_sqr8x_mont and bn_powerx5 zero two scratch words their mulx squaring reads unwritten, which valgrind reports though results and timing do not depend on them), AVX2 and dual AVX-512 RSAZ exponentiation, AES-NI AES-CTR, AVX AES-GCM (aesni_gcm), P-256's generator multiplication (its row scan and mixed addition), and SSE4.1 and AVX2 ChaCha20-Poly1305 (with one deliberate change: rind's copy reads the choice of AVX2 from the block rind passes it rather than aws-lc's OPENSSL_ia32cap_P), its aarch64 NEON ChaCha20-Poly1305, its x86_64 assembly for CPUs without AVX2 (SSSE3 and integer ChaCha20, SSSE3 and AVX SHA-1, and SSSE3 SHA-256) without AES-NI (SSSE3 AES and GHASH), and without SSSE3 (integer SHA-1 and SHA-256), its aarch64 Montgomery multiplication, and its aarch64 assembly for CPUs without the crypto extension (NEON AES, NEON GHASH, and scalar SHA-1 and SHA-256), as Rust naked functions; its 32-bit ARM assembly for ChaCha20, SHA-384/512, Montgomery multiplication, the ARMv8 crypto extension (AES, GHASH, SHA-1, SHA-256), and NEON Poly1305, in Rust global_asm!; and the C that drives that Poly1305, RSAZ, and bn_power5, that sets up its x86_64 SSSE3 GHASH table, and that computes its constant-time SSE2 AES and integer GHASH for x86_64 CPUs without SSSE3, ported to Rust. aws-lc takes its Poly1305, ChaCha20-Poly1305, SSSE3 GHASH, and constant-time AES and GHASH from BoringSSL and the rest from OpenSSL |
Apache-2.0; Poly1305, ChaCha20-Poly1305, SSSE3 and integer GHASH, and constant-time AES ISC | SAW and NSym proofs that its x86_64 AVX and aarch64 (with and without the SHA-512 instructions) SHA-384/512 routines compute their specified result for all inputs, and SAW proofs that its x86_64 AVX AES-GCM does, for AES-256 and whole blocks. They check aws-lc's own build, not rind's copy, and do not cover its 32-bit ARM code. |
| BoringSSL | copied, by admin/import-boringssl: its x86_64 VAES AES-GCM assembly, aes-gcm-avx2-x86_64 and aes-gcm-avx512-x86_64, as Rust naked functions; its 32-bit ARM NEON X25519 assembly, in Rust global_asm!, which BoringSSL takes from SUPERCOP's neon2, public domain |
Apache-2.0 | nothing |
| curve25519-dalek | dependency, with its serial 32-bit backend | BSD-3-Clause | nothing: that backend's field arithmetic is hand-written. Its fiat backend would run fiat-crypto's verified field arithmetic instead, but only a --cfg flag for the whole build selects it, so rind refuses it rather than let the build pick. |
| fiat-crypto | dependency | MIT OR Apache-2.0 OR BSD-1-Clause | Coq proofs that each generated field operation is correct. rind's point formulas, inversion, and table lookup over them are not covered. |
rind's own code is licensed under Apache-2.0 OR ISC OR MIT-0. Copied code
keeps its source's copyright and license, which LICENSE lists with the
texts they require; dependencies ship their own. Every file under
rind/src names its license in a SPDX-License-Identifier comment, and
files Graviola wrote keep their Written for Graviola by ... header. The
rind crate as a whole is therefore (Apache-2.0 OR ISC OR MIT-0) AND (Apache-2.0 OR ISC OR MIT) AND Apache-2.0 AND ISC. The adapter crates copy
nothing under another license, so they stay Apache-2.0 OR ISC OR MIT-0.
rind-ring runs ring's own tests, which keep ring's licenses and stay out of
the published crate.
A change that copies code from a new source, or moves an algorithm to a different one, updates this table and LICENSE in the same PR.
Assorted technical details
RSA
On x86_64 and aarch64, the arithmetic is provided by s2n-bignum, under the
exponentiation described below, except that x86_64 runs aws-lc's Montgomery
multiplication as aws-lc's bn_mul_mont picks it (bn_mul4x_mont for moduli
whose width in 64-bit words is a multiple of four, bn_sqr8x_mont for
squaring a multiple of eight, each by mulx with ADX, and bn_mul_mont_nohw
for other widths), other than 1024- and 2048-bit moduli on CPUs with ADX,
where s2n-bignum's Karatsuba routines are as fast or faster. aarch64 runs
aws-lc's bn_mul_mont, which picks its own squaring or 4x routine by the
width, at every size: on a Neoverse-N2 it measured 1.15 to 2 times as fast as
s2n-bignum's generic routines, and faster than its Karatsuba routines for
1024- and 2048-bit moduli.
armv7 runs the same exponentiation over aws-lc's 32-bit ARM bn_mul_mont
routines as aws-lc picks them (bn_mul8x_mont_neon with NEON for moduli whose
width in 32-bit words is a multiple of eight, and bn_mul_mont_nohw
otherwise), and the rest of the arithmetic in rind's portable Rust.
On x86_64 CPUs with AVX2 but without ADX, the private operation's exponentiation modulo each 1024-bit
prime of RSA-2048 runs aws-lc's RSAZ, as aws-lc does there: Montgomery
products on AVX2 over 29-bit limbs, and a 5-bit fixed window whose table
entry is gathered by masking all 32 entries. Otherwise x86_64 runs aws-lc's
5-bit window, as BN_mod_exp_mont_consttime does, gathering each entry by
s2n-bignum's verified routine, which reads every entry. For moduli of a
multiple of 512 bits other than 1024 and 2048 bits with ADX, it runs aws-lc's
bn_power5 instead: five squarings and the product by an entry gathered by
masking all 32, in one call.
On x86_64 CPUs with AVX-512 IFMA, both exponentiations of the CRT for primes
of 1024, 1536, or 2048 bits run at once by aws-lc's dual RSAZ, as aws-lc's
BN_mod_exp_mont_consttime_x2 does there: almost Montgomery products on
256-bit vectors over 52-bit limbs, two moduli at a time, and a 5-bit fixed
window whose table entries are gathered by masking all 32. Valgrind runs no
AVX-512, so admin/ct-sde checks it under Intel SDE instead.
The RSA private operation always uses the CRT optimisation.
On aarch64 and armv7, modular exponentiation uses a 5-bit fixed exponent window, as aws-lc's does, and the term is selected by the exponent bits from the table of base powers in a side-channel-free way. The private operation is always followed by the public operation to verify the result (and the result compared in a side-channel-free way).
Only RSA signing and verification are provided. Signing keys have 2048- to
8192-bit moduli; verification also takes 1024-bit moduli and SHA-1, but only
under signature::verify::legacy. Every key refuses a public exponent that
is even, 1, or over 33 bits, and a private key whose components disagree is
refused when it loads, as aws-lc's RSA_check_key does. RSA encryption is not provided:
OAEP waits on a consumer, and PKCS#1 v1.5 encryption, which invites
padding-oracle attacks, is refused.
RSA key generation is supported for five fixed key sizes between 2048-8192-bits. All generated keys have e = 0x10001 and, as in aws-lc, d reduced mod lcm(p - 1, q - 1). Generation runs in constant time in the primes, revealing only which candidates it discards.
ECC
On x86_64 and aarch64, all ECC field and scalar arithmetic are provided by s2n-bignum, except P256's on x86_64 where aws-lc's is named below, and the rest of this section describes those backends.
P256 base point multiplication on x86_64 is aws-lc's: 7-bit Booth windows over
37 rows of 64 affine points (a 152KB constant table), each row scanned whole
by its SSE2 or AVX2 routine and added by its mixed addition, with mulq or
with BMI2 and ADX, as aws-lc's ecp_nistz256_point_mul_base runs them. On
aarch64 it is s2n-bignum's p256_scalarmulbase, over 6-bit Booth windows and
a table of each window's 32 multiples (an 88KB constant table). Variable
point multiplication is s2n-bignum's p256_montjscalarmul.
armv7 runs the aarch64 algorithms for P256 in portable Rust, over
fiat-crypto's field and scalar arithmetic, except its Montgomery
multiplication and squaring, which are rind's. LLVM compiles fiat-crypto's
32-bit multiplication to save and restore the one carry flag 171 times a
call, between its interleaved carry chains; rind's carry in 64-bit
accumulators (umaal) instead, in 560 instructions run to fiat-crypto's
1,375. fiat-crypto cannot generate an unsaturated P256, as it does P521:
its bounds checks fail at every 32-bit limb count from 9 to 16.
P384 base point multiplication uses a comb of 20 tables of 16 affine points
(a 30KB constant table) over 5-bit Booth windows, as aws-lc does. Variable
point multiplication is s2n-bignum's p384_montjscalarmul.
P521's prime is a Mersenne prime, so its field arithmetic is direct rather
than Montgomery's. Base point multiplication uses a comb of 27 tables of 16
affine points (a 62KB constant table) over 5-bit Booth windows, as aws-lc
does. Variable point multiplication is s2n-bignum's p521_jscalarmul.
s2n-bignum's P521 point addition requires points that are not at infinity,
equal, or opposite, so the comb keeps to that, selecting around the cases
it reaches.
ECDSA verification computes u1 G + u2 Q in one chain of doublings, as
aws-lc does, adding terms from width-5 non-adjacent forms of the public
scalars: u2's over the public key's odd multiples, computed once when the
key is parsed, and u1's over the first row of the base point table, at
width 6 for P256, as its aarch64 row holds 32 multiples. An addition of equal
or opposite points gives a zero z, as infinity does, so only then is it
redone by cases.
ECDSA follows RFC6979 for generation of k, but adds additional non-critical random input.
We do this to avoid the theoretical fragility of RFC6979 under fault conditions.
This is allowed for by RFC6979, and the HMAC-DRBG that it builds on.
The code is structured such that we pass the RFC6979 test vectors.
The code which selects a term from a table of points is non-verified, and is written in AVX2/Neon intrinsics, or for P256 on x86_64 is aws-lc's SSE2 and AVX2 assembly.
On x86_64 and aarch64, X25519 directly uses the s2n-bignum implementation.
ML-KEM-768
On aarch64, and on x86_64 CPUs with AVX2, the core polynomial arithmetic -- the NTT, inverse NTT, base multiplication and reduction -- is provided by s2n-bignum (contributed by the mlkem-native developers). The surrounding FIPS-203 construction (key generation, encapsulation and decapsulation) is written in Rust. With AVX2, ciphertext compression runs mlkem-native's AVX2 assembly.
x86_64 CPUs without AVX2 run the same construction over a Rust port of mlkem-native's portable C arithmetic instead, in natural coefficient order as aarch64 keeps it, and Keccak-f four states one at a time, chosen once per process. It measured 1.3x faster than libcrux's portable ML-KEM for key generation and encapsulation there, and 2x for decapsulation.
Compression and decompression are division-free, avoiding the variable-time division behind the KyberSlash attacks.
For sampling, which is a significant factor in ML-KEM performance, we have some different options depending on CPU features:
- We have a
SampleNTT()which does 8-wide Keccak-f permutation followed by rejection sampling using AVX-512 (F, BW, VBMI, VBMI2) on x86-64. - We have a 4-wide Keccak-f using AVX2 on x86-64.
- We have a 2-wide Keccak-f using Neon+SHA3 on aarch64.
- We have a basic Keccak-f using scalar instructions on aarch64.
Symmetric cryptography
SHA1 and SHA256 have straightforward implementations using hashing intrinsics
(aka "SHA-NI" on x86_64, "sha" extension on aarch64), except that aarch64 runs
aws-lc's SHA256 assembly for those instructions: it keeps a register copy off
each round's dependency chain, where LLVM's code for the intrinsics puts one
on it. Without SHA-NI, x86_64
selects at runtime an AVX2 message schedule with scalar rounds for SHA1, as
aws-lc does. For SHA256 it runs aws-lc's AVX routine on Intel, as aws-lc
picks, and the same AVX2 approach elsewhere, since the AVX routine rotates with
shrd, which pre-Zen AMD runs slowly. Without AVX2 either, both run aws-lc's
AVX routines on Intel and its SSSE3 ones elsewhere, as aws-lc picks; they are
2.2x (SHA256) and 1.7x (SHA1) faster than the pure Rust ones on Intel.
Without SSSE3 too, both run aws-lc's integer routines, as aws-lc does: 2.2x
(SHA256) and 1.25x (SHA1) faster than the pure Rust ones, and 1.3-1.5x
faster than HACL*'s SHA256.
aarch64 without the crypto extension runs aws-lc's
scalar SHA1 and SHA256 assembly. 32-bit arm runs aws-lc's SHA1 and SHA256 assembly
where the CPU has the crypto extension, and the pure Rust SHA1 and SHA256
otherwise: libcrux has no SHA1, and the pure Rust SHA256 is faster than
libcrux's.
SHA384, SHA512, and SHA512/256 run aws-lc's assembly everywhere. On x86_64,
its AVX routine on Intel and its scalar routine on other CPUs, as aws-lc picks:
the AVX routine is 1.1x faster than the scalar one on Intel, and 1.5x slower on
AMD's Zen. On aarch64, its routine for the SHA-512 instructions where the CPU
has them, and its scalar routine otherwise; on 32-bit arm, its NEON routine
where the CPU has NEON, and its scalar routine otherwise.
x86_64 decides once per process whether the CPU has AES-NI and PCLMULQDQ,
and SSSE3. Without the first two, as on Core 2 and Nehalem, AES and AES-CTR
run aws-lc's vpaes-x86_64 assembly and GHASH its ghash-ssse3-x86_64,
composed into AES-GCM as aws-lc composes them, taking turns over 3 KB; both
modes measured level with aws-lc's. Without SSSE3 either, as on QEMU's
qemu64, they run Rust ports of the C aws-lc runs there: aes_nohw.c, which
bitslices eight blocks into SSE2 registers and computes the S-box as a
circuit, and gcm_nohw.c, which multiplies with integer multiplies of
operands masked to every fourth bit, so no carry reaches a bit it keeps.
Neither branches or indexes memory by a secret. Composed the same way,
AES-GCM and AES-CTR measured 0.82-0.92x aws-lc's time; libcrux's portable
AES-GCM measured 3-7x slower, and its bitsliced block level with the port's
for one block. With AES-NI and PCLMULQDQ, AES-CTR runs aws-lc's
aes_hw_ctr32_encrypt_blocks, 1.1-2x faster than rind's by-8 intrinsics at
every length, with or without AVX.
AES-GCM has a stitched by-8 AES-CTR and a by-8 GHASH, SSE-encoded on every
CPU: VEX encoding measured no faster with AVX, and without it, as on Westmere
and the Goldmont and Tremont Atoms, this is 1.02-1.28x faster than aws-lc's
SSE aesni-x86_64 and ghash-x86_64 routines. With AVX and MOVBE but no
VAES, as from Haswell through Comet Lake and on Zen 1 and 2, aws-lc's
verified aesni_gcm routine, which interleaves AES-CTR and GHASH six blocks
at a time, takes messages from 288 bytes to encrypt and 96 to decrypt, as
aws-lc and ring run it, and the by-8 code takes their tails; on an i7-8700
(Coffee Lake) this measured 1.2-1.7x faster than the by-8 code alone from
1200 bytes to 16 KB. Every 256- and 512-bit kernel rind runs ends with
vzeroupper, so SSE code after one pays no AVX-SSE transition or false
dependency. There is also an optional AVX-512 by-16 AES-CTR
and GHASH. It uses dynamic CPU feature detection, so AVX-512 is not required at runtime.
On cores with VAES, BoringSSL's kernels, which interleave AES-CTR and GHASH, take
long messages: its 512-bit kernel from 2 KB where the CPU has AVX-512 (the by-16
path is quicker below that), and its 256-bit kernel from 256 bytes otherwise.
The key picks its kernel once, when it is made.
aarch64 decides once per process whether the CPU has the crypto extension.
With it, AES-GCM runs s2n-bignum's kernels, which interleave a by-4 AES-CTR
and GHASH over whole blocks. An AES-256 key made on a core with SHA3 runs their
by-8 kernels instead, which use its three-way XOR. They run in place, outside
their proofs, which assume separate input and output buffers.
admin/parse-asm/inplace.py walks them at every length to 64 blocks, checking
that each block is loaded before it is stored and that their branches and
addresses depend only on the length.
Without it, AES runs aws-lc's vpaes-armv8, Mike Hamburg's vector permutation
AES, whose S-box is NEON tbl lookups into registers, and GHASH runs aws-lc's
ghash-neon-armv8, which builds each product from NEON's 8-bit polynomial
multiplies; neither reads memory or branches by a secret. AES-GCM runs AES-CTR
and GHASH over each eight blocks in turn, as aws-lc composes them.
32-bit arm decides once per process from AT_HWCAP2: with the crypto
extension, AES and AES-GCM keys use aws-lc's aesv8-armx and ghashv8-armx
assembly, composed as aws-lc does on that architecture; without it, libcrux's
bitsliced AES and AES-GCM.
x86_64 CPUs without AVX2 run ChaCha20 on aws-lc's chacha-x86_64 assembly,
as aws-lc picks: its integer routine up to 128 bytes, and its SSSE3 routine,
four blocks at a time, above that. Both measured faster than the pure Rust
block function at every length: 1.7x for one block, and 4.5x from 256 bytes.
Without SSSE3, the integer routine runs at every length, as in aws-lc, and
measured 1.2x faster than HACL*'s ChaCha20 at every length, and HChaCha20,
for XChaCha20, rotates with SSE2 shifts rather than SSSE3's byte shuffles.
ChaCha20-Poly1305, and XChaCha20-Poly1305 after HChaCha20, run aws-lc's
chacha20_poly1305 assembly, which interleaves ChaCha20 with Poly1305 in one
pass: on x86_64, its AVX2 half where the CPU has AVX2 and BMI2 and its SSE4.1
half otherwise, as aws-lc picks them, and on aarch64, its NEON routine.
aws-lc's x86_64 routine reads its pick from OPENSSL_ia32cap_P; rind's copy
reads the same bits from the block rind passes it, so the path is decided
once per process like rind's others. Without SSE4.1, ChaCha20 and Poly1305
run in separate passes, as in aws-lc.
32-bit arm runs ChaCha20 on aws-lc's chacha-armv4 assembly, switching from
its integer routine to its NEON one at 192 bytes as aws-lc does, where
AT_HWCAP reports NEON. Poly1305 runs aws-lc's NEON code there too, and
HACL*'s verified Poly1305 without NEON.
Architecture
rind's API is its own, shaped so that misuse fails to compile rather than at run time. MIGRATING.md maps each ring and aws-lc-rs item to it.
- Each module takes an algorithm as a
&'staticconstant, such asaead::AES_128_GCMorsignature::verify::ECDSA_P256_SHA256_ASN1, and has one key type per role. - Algorithms only old protocols should use sit in
legacysubmodules, such asdigest::legacy::SHA1. - Key pairs name the DER format they load or encode (
from_pkcs8,from_sec1,to_spki), and every private key returns its public half frompublic_key(). - Encodings, signatures, and shared secrets are owned values of fixed
capacity, such as
signature::Pkcs8, rather than caller buffers or allocations. Keys wipe their secrets on drop. - No call takes a random source: rind draws from the system's when it needs
one, and
rand::fillgives callers the same. - Fallible calls return
rind::Error, whose variant says which kind of call failed, such asKeyRejectedorBadSignature.
The modules:
aead: AES-GCM and ChaCha20-Poly1305 keys with caller nonces, an XChaCha20-Poly1305 key that picks random nonces, and QUIC header-protection keys.cipher: bare AES, AES-CTR, and ChaCha20, for DTLS record number protection and SRTP.agreement: ephemeral X25519 and ECDH on P-256, P-384, and P-521.kem: ML-KEM-768.signature: ECDSA on P-256, P-384, and P-521, RSA PKCS#1 v1.5 and PSS, and Ed25519 key pairs and verification, plus RSA keys given as their components, and SHA-1 and 1024-bit RSA for verifying old signatures.digest: SHA-1 (legacy only), SHA-256, SHA-384, SHA-512, and SHA-512/256.hmac: HMAC over those digests, keyed once and reused.hkdf: HKDF over those HMACs, whose output becomes anaead, QUIC header-protection, orhmackey.pbkdf2: PBKDF2 over those HMACs, deriving or verifying a key from a password.constant_time: comparing secrets such as MACs without a timing leak.rand: random bytes from the system.cpu: the startup check for required CPU features, and whether AES runs in hardware.
The public modules wrap three private layers:
low: low level primitives. platform-specific. unsafe allowed. minimal std and alloc.mid: constructions, protocols and encodings. platform agnostic. no unsafe. minimal std and alloc.high: high level encodings and operations. platform agnostic. no unsafe.
low code should not refer to mid, nor mid to high.
low must present the same interface irrespective of platform. To this end,
low::generic contains pure-rust polyfills for items we don't have assembler-
or intrinsic-based implementations for a certain platform.