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_64requiresaes,ssse3,popcnt,avx,avx2,adx,bmi2, andpclmulqdqCPU features. (This is most x86_64 CPUs made since around ~2014.)- 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. 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.
Algorithms
Each cell names whose code runs for that algorithm on that backend: the
sources below, rind's own code, or Graviola's own Rust and
intrinsics. A ✓ marks code its source formally verifies, to the extent
Sources states. A ⚡ marks unverified code chosen because it
measured faster than a verified alternative; other unverified code has none,
or a quest covers it. The table describes
main, and changes alongside the code it describes.
| Algorithm | x86_64 | aarch64 | armv7 |
|---|---|---|---|
| AES-128-GCM, AES-256-GCM | VAES: BoringSSL interleaved AES-CTR and GHASH (copied) ⚡, on 512-bit vectors from 2 KB with AVX-512 and 256-bit vectors from 256 bytes without; otherwise, and for the key schedule, single-block AES, and GHASH of the AAD and lengths: Graviola | crypto extension: s2n-bignum CTR and GHASH of whole blocks ⚡, by-8 for AES-256 on cores with SHA3, run in place, which its proofs exclude; Graviola key schedule, H powers, single-block AES (H, the tag mask, and a final partial block), and GHASH of the AAD, final partial block, and lengths; otherwise: aws-lc NEON AES and GHASH (copied), rind GCM | crypto extension: aws-lc AES and GHASH (copied), rind GCM; otherwise: libcrux-aes |
| AES block (header protection), AES-CTR (SRTP) | Graviola block, rind CTR | crypto extension: Graviola block, rind CTR; otherwise: aws-lc NEON block and CTR (copied) | crypto extension: aws-lc block and CTR (copied); otherwise: libcrux-aes block (copied), rind CTR |
| ChaCha20 (header protection) | Graviola; HACL*'s block function measured level for one block, so it isn't worth a libcrux dependency here | Graviola; as x86_64 | aws-lc (copied) ⚡ |
| ChaCha20-Poly1305, XChaCha20-Poly1305 | Graviola ⚡ | Graviola ⚡ | aws-lc ChaCha20 (copied) ⚡; NEON: aws-lc Poly1305 (copied) ⚡; otherwise: HACL* Poly1305 ✓; Graviola AEAD; rind HChaCha20 over HACL* rounds ✓ (copied) |
| SHA-1 (legacy only) | rind | crypto extension: rind; otherwise: aws-lc scalar (copied) | crypto extension: aws-lc (copied); otherwise: rind |
| SHA-256 | Graviola ⚡ | crypto extension: Graviola ⚡; otherwise: aws-lc scalar (copied) | crypto extension: aws-lc (copied) ⚡; otherwise: Graviola ⚡ |
| SHA-384, SHA-512, SHA-512/256 | Intel: aws-lc AVX (copied) ✓; otherwise: aws-lc scalar (copied) ⚡ | SHA-512 instructions: aws-lc (copied) ✓; otherwise: aws-lc scalar (copied) ✓ | NEON: aws-lc (copied) ⚡; otherwise: aws-lc scalar (copied) ⚡ |
| HMAC | Graviola ⚡ | Graviola ⚡ | Graviola ⚡ |
| HKDF | rind ⚡ | rind ⚡ | rind ⚡ |
| PBKDF2 | rind; SHA as in its rows | rind; SHA as in its rows | rind; SHA as in its rows |
| Constant-time comparison | Graviola | Graviola | rind |
| X25519 | s2n-bignum ✓ | s2n-bignum ✓ | NEON: BoringSSL shared secret (copied) ⚡, curve25519-dalek public key ⚡; otherwise: curve25519-dalek ⚡ |
| Ed25519 | s2n-bignum point and scalar arithmetic ✓; Graviola EdDSA ⚡; SHA-512 as in its row | s2n-bignum point and scalar arithmetic ✓; Graviola EdDSA ⚡; aws-lc SHA-512 ✓ | curve25519-dalek point and scalar arithmetic ⚡; rind EdDSA ⚡; SHA-512 as in its row |
| P-256 ECDH, ECDSA | s2n-bignum field and point arithmetic, and secret scalar multiplication ✓; Graviola verification's scalar multiplication ⚡ and ECDSA ⚡ | s2n-bignum field and point arithmetic, and secret scalar multiplication ✓; Graviola verification's scalar multiplication ⚡ and ECDSA ⚡ | HACL* ✓; Graviola ECDSA nonce |
| P-384 ECDH, ECDSA | s2n-bignum field and point arithmetic, and other secret scalar multiplication ✓; rind comb multiplication of the generator ⚡; Graviola verification's other scalar multiplication ⚡, and ECDSA | s2n-bignum field and point arithmetic, and other secret scalar multiplication ✓; rind comb multiplication of the generator ⚡; Graviola verification's other scalar multiplication ⚡, and ECDSA | fiat-crypto field and scalar arithmetic ✓; scalar multiplication over it ⚡, from rind point formulas, inversion, table lookup, and comb multiplication of the generator, and Graviola other scalar multiplication; Graviola ECDSA |
| P-521 ECDH, ECDSA | s2n-bignum field and point arithmetic, and other scalar multiplication ✓; rind comb multiplication of the generator ⚡; Graviola ECDSA | s2n-bignum field and point arithmetic, and other secret scalar multiplication ✓; rind comb multiplication of the generator ⚡, and verification's other scalar multiplication ⚡; Graviola ECDSA | fiat-crypto field arithmetic ✓; rind arithmetic mod the group order ⚡; scalar multiplication over them ⚡, from rind point formulas, inversion, table lookup, and comb multiplication of the generator; Graviola ECDSA |
| ML-KEM-768 | s2n-bignum polynomial arithmetic and Keccak-f ✓; Graviola FIPS 203 and AVX-512 sampling ⚡ | s2n-bignum polynomial arithmetic and Keccak-f ✓; Graviola FIPS 203 ⚡ | libcrux-ml-kem ✓, except its SHA-3 |
| RSA PKCS#1 v1.5 and PSS signatures, key generation | s2n-bignum bignum arithmetic ✓; Graviola exponentiation ⚡, CRT, padding, and key generation | s2n-bignum bignum arithmetic ✓; Graviola exponentiation ⚡, CRT, padding, and key generation | HACL* bignum arithmetic and exponentiation ✓; rind CRT and key generation; Graviola padding |
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 |
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, libcrux-p256, 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. |
| 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, 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, the ARMv8 crypto extension (AES, GHASH, SHA-1, SHA-256), and NEON Poly1305, in Rust global_asm!, and the C that drives that Poly1305, ported to Rust. aws-lc takes its Poly1305 from BoringSSL and the rest from OpenSSL |
Apache-2.0; Poly1305 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. 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, all the arithmetic is provided by s2n-bignum, under the exponentiation described below; armv7 uses HACL*'s arithmetic and exponentiation instead.
The RSA private operation always uses the CRT optimisation.
Modular exponentiation uses 4-bit fixed exponent window, 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. Unlike other private key operations in rind, RSA key generation is currently not side-channel safe. Avoid doing it in untrusted multi-tenant or physical environments.
ECC
On x86_64 and aarch64, all ECC field and scalar arithmetic are provided by s2n-bignum, and the rest of this section describes those backends.
P256 base point multiplication uses a 7-bit exponent window with Booth encoding (this costs a 148KB constant table). Variable point multiplication uses a 5-bit exponent window with Booth encoding. Both use the same exponent representations for "public" and "secret" exponents -- however the table selection for "public" exponents is specialized at compile-time.
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, except for ECDSA
verification's public scalar, which uses a 5-bit Booth window over the
multiples of the public key, computed once when the key is parsed.
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, for
ECDSA verification's public scalar too, except on aarch64, where a 5-bit Booth
window over the point's cached multiples measured faster for that one.
s2n-bignum's P521 point addition requires points that are not at infinity,
equal, or opposite, so the comb and the window keep to that, and select or
branch around the cases they reach.
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.
On x86_64 and aarch64, X25519 directly uses the s2n-bignum implementation.
ML-KEM-768
On x86_64 and aarch64, 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.
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). Without SHA-NI, x86_64
selects at runtime an AVX2 message schedule with scalar rounds for SHA1 and
SHA256, as aws-lc does; 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.
On x86_64, AES and GHASH always use the CPU's AES and carry-less multiply instructions (there are no fallbacks).
On x86_64, we have a stitched by-8 AES-CTR and a by-8 GHASH (they are not currently interleaved; this is future work.) 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.
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.