Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ jobs:
run: cargo test
- name: Run tests (debug, --no-default-features)
run: cargo test --no-default-features
- name: Run zeroing tests (debug, --features wipe)
run: cargo test -p rind --features wipe --test zeroing

- name: Build (release)
run: cargo build -p rind --release
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This is one Rust workspace: the `rind` library, its `rind-<consumer>` adapter cr
- Test every primitive against Wycheproof where vectors exist, and differentially against aws-lc-rs (a dev-dependency only).
- Design the API so misuse fails to compile: types over runtime checks, one way to do each job. Borrow an aws-lc-rs or ring name only when it is as clear; the migration guide maps theirs to rind's.
- When in doubt about behavior (what to accept, refuse, or wipe), follow aws-lc-rs: it is the library rind would use if it didn't require C.
- Wipe keys, keyed state, and heap scratch derived from a private key before they are freed, since that scratch can be as good as the key. Wipe nothing else: digest contexts, decrypted messages, stack temporaries, other scratch, and copies inside a call stay unwiped. Safe Rust cannot read them, so wiping them costs speed for nothing.
- Wipe keys, keyed state, and heap scratch derived from a private key before they are freed, since that scratch can be as good as the key, but only under the `wipe` feature: wipe through `low::zeroise`, which does nothing without it. Wipe nothing else: digest contexts, decrypted messages, stack temporaries, other scratch, and copies inside a call stay unwiped. Safe Rust cannot read them, so wiping them costs speed for nothing.
- Benchmark performance-sensitive changes against aws-lc-rs.
- An adapter for a third-party crate lives in its own `rind-<consumer>` crate, so that crate's major bump never forces one in `rind`. The adapter's version begins with the breaking part of its upstream's (rind-rustls 0.23.x for rustls 0.23), so its own breaking change waits for upstream's.

Expand Down
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ run time. [MIGRATING.md][migrating] maps each ring and aws-lc-rs item to it.
from `public_key()`.
- Encodings, signatures, and shared secrets are owned values of fixed
capacity, such as `encoding::Pkcs8`, rather than caller buffers or
allocations. Keys wipe their secrets on drop, and their `Debug` shows only
their algorithm.
allocations. Keys wipe their secrets on drop under the `wipe` feature, and
their `Debug` shows only their algorithm.
- No call takes a random source: rind draws from the system's when it needs
one, and `rand::fill` gives callers the same.
- Fallible calls return `rind::Error`, whose variant says which kind of call
Expand Down
13 changes: 7 additions & 6 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ patches it in for every crate in its tree on ring 0.17.
| A `_FOR_LEGACY_USE_ONLY` suffix | a `legacy` submodule, such as `digest::legacy::SHA1` |
| An allocated `Document`, `Vec`, or `Box` output | an owned value of fixed capacity, such as `encoding::Pkcs8` |
| `error::Unspecified`, `error::KeyRejected` | `rind::Error`, by kind of call (see [error](#error)) |
| aws-lc-rs wipes keys and keyed state on drop; ring doesn't | rind wipes them only under its `wipe` feature: enable `rind/wipe` to keep aws-lc-rs's behavior |

## digest

Expand Down Expand Up @@ -136,7 +137,7 @@ protocols with their own integrity protection need. ring has none.
|---|---|
| `EphemeralPrivateKey::generate(alg, rng)`, `PrivateKey::generate(alg)` | `PrivateKey::generate(&alg)?` |
| `compute_public_key()?` | `public_key()`, which cannot fail |
| `agree_ephemeral(key, &UnparsedPublicKey::new(alg, peer), err, kdf)` | `key.agree(peer)?`, returning a `SharedSecret` wiped on drop |
| `agree_ephemeral(key, &UnparsedPublicKey::new(alg, peer), err, kdf)` | `key.agree(peer)?`, returning a `SharedSecret` wiped on drop under `wipe` |
| `agree(&key, peer, err, kdf)` | `key.agree(peer)?`, which borrows the key, so it agrees again |
| `UnparsedPublicKey::new(alg, point)`, `ParsedPublicKey` of a point | `PublicKey::new(&alg, point)?`, which decompresses a compressed point, then `key.agree(peer.as_ref())`; `agree` alone takes an uncompressed point |
| `ParsedPublicKey` of a `SubjectPublicKeyInfo` | `PublicKey::from_spki(&alg, der)?`, which also takes WebCrypto's `id-ecDH` |
Expand All @@ -145,7 +146,7 @@ protocols with their own integrity protection need. ring has none.
| `X25519`, `ECDH_P256`, `ECDH_P384`, `ECDH_P521` | the same |
| `PrivateKey::from_private_key(alg, bytes)` | `PrivateKey::from_scalar(&alg, bytes)` |
| `PrivateKey::from_private_key_der(alg, der)` | `from_pkcs8(&alg, der)` or `from_sec1(&alg, der)`, whichever the caller holds |
| `as_be_bytes()` to `EcPrivateKeyBin` or `Curve25519SeedBin` | `to_scalar()`, a `Scalar` wiped on drop |
| `as_be_bytes()` to `EcPrivateKeyBin` or `Curve25519SeedBin` | `to_scalar()`, a `Scalar` wiped on drop under `wipe` |
| `AsDer<Pkcs8V1Der>::as_der()` | `to_pkcs8()`, which X25519 keys also have |
| `PublicKey::as_der()` | `PublicKey::to_spki()`, with an uncompressed point |
| `AsDer<EcPrivateKeyRfc5915Der>::as_der()`, `PublicKey::as_be_bytes()` | none; an ECDSA key pair's `to_sec1()` |
Expand Down Expand Up @@ -209,12 +210,12 @@ ring has no `kem`.
| `RsaKeyPair::from_components`, `KeyPairComponents` | none: load the key as PKCS#1 or PKCS#8 |
| `Ed25519KeyPair::from_pkcs8_maybe_unchecked` | `from_pkcs8`, which takes v1 and v2 |
| `Ed25519KeyPair::from_seed_unchecked` | `from_seed`, or `from_seed_and_public_key`, which checks the public key |
| `to_pkcs8()`, `to_pkcs8v1()`, `public_key().as_der()`, each an allocated `Document` | `to_pkcs8()`, `to_pkcs8v1()` (Ed25519), `to_spki()`, each of fixed capacity; `Pkcs8` is wiped on drop |
| `EcdsaKeyPair::private_key().as_der()` | `to_sec1()`, an `encoding::Sec1` wiped on drop |
| `to_pkcs8()`, `to_pkcs8v1()`, `public_key().as_der()`, each an allocated `Document` | `to_pkcs8()`, `to_pkcs8v1()` (Ed25519), `to_spki()`, each of fixed capacity; `Pkcs8` is wiped on drop under `wipe` |
| `EcdsaKeyPair::private_key().as_der()` | `to_sec1()`, an `encoding::Sec1` wiped on drop under `wipe` |
| `EcdsaKeyPair::sign(rng, m)`, `RsaKeyPair::sign(alg, rng, m, &mut sig)`, `Ed25519KeyPair::try_sign` | `sign(m)`, `sign(alg, m)`, which take no random source and return a `Signature` |
| `rsa::KeySize`, `RsaKeyPair::generate(size)` | the same; `KeySize` adds `Rsa6144` |
| `EcdsaKeyPair::private_key().as_be_bytes()` (not in ring) | `to_scalar()`, an `EcdsaScalar` wiped on drop |
| `Ed25519KeyPair::seed()?.as_be_bytes()` (not in ring) | `to_seed()`, an `Ed25519Seed` wiped on drop |
| `EcdsaKeyPair::private_key().as_be_bytes()` (not in ring) | `to_scalar()`, an `EcdsaScalar` wiped on drop under `wipe` |
| `Ed25519KeyPair::seed()?.as_be_bytes()` (not in ring) | `to_seed()`, an `Ed25519Seed` wiped on drop under `wipe` |
| `public_modulus_len()`, `algorithm()`, `generate_fips` | none |
| `ED25519_PUBLIC_KEY_LEN` | none: always 32 |
| `rsa::PublicKey::from_der` | `PublicKey::new(&verify::RSA_PKCS1_SHA256, der)`, for one algorithm and PKCS#1 or `SubjectPublicKeyInfo` DER |
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,20 @@ Each crate's README shows its setup.
| [rind-rcgen][] | certificates through [rcgen](https://github.com/rustls/rcgen) |
| [rind-instant-acme][] | ACME through [instant-acme](https://github.com/djc/instant-acme), unpublished until instant-acme releases the interface it targets |

### Wiping keys

rind wipes keys, keyed state, and heap scratch derived from a private key
on drop only under its `wipe` feature, as RustCrypto does with `zeroize`;
aws-lc-rs always wipes, and ring never does. A memory disclosure such as
Heartbleed leaks every live key anyway, so the default build skips the
cost. The adapters don't forward the feature: enable it on `rind` itself,
and Cargo turns it on beneath every adapter too.

```toml
[dependencies]
rind = { version = "0.2", features = ["wipe"] }
```

### Platforms

rind runs on x86_64, aarch64, and 32-bit arm, and fails to compile for any
Expand Down
2 changes: 1 addition & 1 deletion REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ Check these first, on every path the change adds or touches:
- **Constant time**: trace each secret through the change. A secret-dependent branch, index, or variable-time instruction is a finding, and so is a new secret that no `low::ct::secret_slice` mark or `admin/ctgrind` test covers.
- **Panics**: find every `unwrap`, `expect`, index, slice, overflow, and `assert` that caller input can reach. Each is a finding unless the types rule it out.
- **Coverage**: a primitive or encoding missing its Wycheproof vectors or its differential test against aws-lc-rs is incomplete. Ask whether the tests reach the edges (zero, the modulus, maximal and empty lengths, malformed encodings), not only valid input.
- **Wiped secrets**: a type that owns a key or keyed state zeroises it on drop, a long-lived key type is covered by `rind/tests/zeroing.rs`, and heap scratch derived from a private key, such as RSA's power tables, is zeroised before it is freed. A wipe of anything else, such as a digest context, a decrypted message, a copy of a key or keyed state inside a call, stack scratch, or heap scratch no private key reaches, is a finding.
- **Wiped secrets**: a type that owns a key or keyed state zeroises it on drop through `low::zeroise`, which wipes only under the `wipe` feature, a long-lived key type is covered by `rind/tests/zeroing.rs`, and heap scratch derived from a private key, such as RSA's power tables, is zeroised before it is freed. A wipe of anything else, such as a digest context, a decrypted message, a copy of a key or keyed state inside a call, stack scratch, or heap scratch no private key reaches, is a finding.
5 changes: 3 additions & 2 deletions proofs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,5 +328,6 @@ Beyond Lean's kernel:
s2n-bignum's and Vale's own proofs.
- That rind's inline assembly is s2n-bignum's, aws-lc's, and Vale's object
code, as for `benchmarks/verified.toml`'s other entries.
- The translation ignores drops, which here wipe keys and keyed state and
restore CPU state, and computes nothing an output depends on.
- The translation ignores drops, which here wipe keys and keyed state under
the `wipe` feature and restore CPU state, and computes nothing an output
depends on.
1 change: 0 additions & 1 deletion quest/m2/speed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,3 @@ dual RSAZ only when the runner has IFMA.
- [AES-192-GCM on aarch64 is level with aws-lc-rs for large messages](/quest/m2/speed/aes192-gcm-aarch64.md) - s2n-bignum's stitched kernels skip 192-bit keys
- [The Ed25519 verify benchmark times every library the same way](/quest/m2/speed/ed25519-verify-bench.md) - rind's row parses the key inside the loop
- [HKDF-SHA256 expand is back to its earlier speed on x86_64](/quest/m2/speed/hkdf-expand-regression.md) - desktop measured 47 to 66 ns between two runs; confirm on a runner, then bisect
- [Wiping keys on drop is opt-in](/quest/m2/speed/wipe-feature.md) - a `wipe` feature; the default build skips it
20 changes: 0 additions & 20 deletions quest/m2/speed/wipe-feature.md

This file was deleted.

5 changes: 2 additions & 3 deletions quest/m4/fips-scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,5 @@ the validation itself is planned from its report. Note which consumers want
it (Bun's FIPS request, [oven-sh/bun#4869](https://github.com/oven-sh/bun/issues/4869),
and Deno's and Node's FIPS builds are the precedent) and whether a FIPS build
can stay a separate crate so rind's default build pays nothing.
Zeroization starts from the opt-in `wipe` feature
([wipe feature](/quest/m2/speed/wipe-feature.md)); say what FIPS adds to it,
such as wiping copies inside a call.
Zeroization starts from rind's opt-in `wipe` feature; say what FIPS adds to
it, such as wiping copies inside a call.
2 changes: 1 addition & 1 deletion rind-ring/src/aead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ impl From<hkdf::Okm<'_, &'static Algorithm>> for UnboundKey {
}
}

/// A key that seals or opens with any nonce the caller gives it; wiped on drop.
/// A key that seals or opens with any nonce the caller gives it; wiped on drop under rind's `wipe` feature.
///
/// A clone shares the expanded key.
#[derive(Clone)]
Expand Down
2 changes: 1 addition & 1 deletion rind-ring/src/aead/quic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ pub static CHACHA20: Algorithm = Algorithm {
id: AlgorithmId::CHACHA20,
};

/// A header-protection key; wiped on drop.
/// A header-protection key; wiped on drop under rind's `wipe` feature.
pub struct HeaderProtectionKey {
key: rind::aead::quic::HeaderProtectionKey,
algorithm: &'static Algorithm,
Expand Down
2 changes: 1 addition & 1 deletion rind-ring/src/agreement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub static ECDH_P384: Algorithm = Algorithm {
curve: CurveId::P384,
};

/// A private key used for one key agreement; wiped on drop.
/// A private key used for one key agreement; wiped on drop under rind's `wipe` feature.
pub struct EphemeralPrivateKey {
key: rind::agreement::PrivateKey,
algorithm: &'static Algorithm,
Expand Down
2 changes: 1 addition & 1 deletion rind-ring/src/hmac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ impl hkdf::KeyType for Algorithm {
}
}

/// An HMAC key, keyed once and reused for every message; wiped on drop.
/// An HMAC key, keyed once and reused for every message; wiped on drop under rind's `wipe` feature.
#[derive(Clone)]
pub struct Key {
inner: rind::hmac::Key,
Expand Down
2 changes: 1 addition & 1 deletion rind-ring/src/pkcs8.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! PKCS#8 private key documents.

/// A PKCS#8 private key document, wiped on drop.
/// A PKCS#8 private key document, wiped on drop under rind's `wipe` feature.
pub struct Document(rind::encoding::Pkcs8);

impl Document {
Expand Down
2 changes: 2 additions & 0 deletions rind/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ rust-version.workspace = true

[features]
default = []
# Wipes keys, keyed state, and private-key heap scratch on drop.
wipe = []
__ctgrind = ["dep:crabgrind"]

[dependencies]
Expand Down
2 changes: 1 addition & 1 deletion rind/src/aead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ pub static CHACHA20_POLY1305: Algorithm = Algorithm {
expand: Sealer::<ChaCha20Poly1305>::expand,
};

/// An AEAD key that seals with any nonce the caller gives it; wiped on drop.
/// An AEAD key that seals with any nonce the caller gives it; wiped on drop under the `wipe` feature.
///
/// The caller owns nonce uniqueness: sealing two messages with one nonce and
/// key loses confidentiality and lets anyone forge messages. Derive nonces
Expand Down
2 changes: 1 addition & 1 deletion rind/src/aead/gcm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ impl fmt::Debug for Iv<'_> {
}

/// An AES-GCM key that takes an IV of any length and makes tags of one
/// [`TagLength`], as WebCrypto's AES-GCM does; wiped on drop.
/// [`TagLength`], as WebCrypto's AES-GCM does; wiped on drop under the `wipe` feature.
///
/// Prefer [`super::Key`], whose 96-bit nonces and full tags are what
/// protocols use; GCM hashes an IV of any other length into its counter,
Expand Down
2 changes: 1 addition & 1 deletion rind/src/aead/quic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ pub static CHACHA20: Algorithm = Algorithm {
},
};

/// A header-protection key, expanded once for its algorithm and wiped on drop.
/// A header-protection key, expanded once for its algorithm and wiped on drop under the `wipe` feature.
pub struct HeaderProtectionKey(Key);

impl HeaderProtectionKey {
Expand Down
6 changes: 3 additions & 3 deletions rind/src/high/agreement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub static ECDH_P521: Algorithm = Algorithm {
parse_public: p521_public_key,
};

/// A private key, which agrees with any number of peers, wiped on drop.
/// A private key, which agrees with any number of peers, wiped on drop under the `wipe` feature.
pub struct PrivateKey(Private);

// `repr(u8)` puts `X25519` at tag zero, and every variant's key is plain
Expand Down Expand Up @@ -202,7 +202,7 @@ impl fmt::Debug for PrivateKey {
}

/// A private key as its scalar, which [`PrivateKey::from_scalar`]
/// loads, wiped on drop.
/// loads, wiped on drop under the `wipe` feature.
pub struct Scalar(Buffer<MAX_SCALAR_LEN>);

impl AsRef<[u8]> for Scalar {
Expand Down Expand Up @@ -537,7 +537,7 @@ impl fmt::Debug for PublicKey {
}
}

/// The secret both parties agree on, wiped on drop.
/// The secret both parties agree on, wiped on drop under the `wipe` feature.
pub struct SharedSecret(Secret);

// As `Private`: all-bits-zero is a valid `X25519` secret.
Expand Down
2 changes: 1 addition & 1 deletion rind/src/high/hkdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ impl fmt::Debug for Salt {
}
}

/// A pseudorandom key, from which [`Prk::expand`] derives any number of keys; wiped on drop.
/// A pseudorandom key, from which [`Prk::expand`] derives any number of keys; wiped on drop under the `wipe` feature.
//
// Held as an HMAC key so each expand block skips the two key compressions.
pub struct Prk(hmac::Key);
Expand Down
4 changes: 2 additions & 2 deletions rind/src/high/hmac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ pub static HMAC_SHA512: Algorithm = Algorithm {
key: Keying::<Sha512>::with_key,
};

/// An HMAC key, keyed once and reused for every message; wiped on drop.
/// An HMAC key, keyed once and reused for every message; wiped on drop under the `wipe` feature.
#[derive(Clone)]
pub struct Key(Keyed);

Expand Down Expand Up @@ -218,7 +218,7 @@ impl fmt::Debug for Key {
}
}

/// An in-progress tag computation, for a message given in pieces; wiped on drop.
/// An in-progress tag computation, for a message given in pieces; wiped on drop under the `wipe` feature.
///
/// A clone forks the computation, so messages sharing a prefix process it once.
#[derive(Clone)]
Expand Down
4 changes: 2 additions & 2 deletions rind/src/high/kem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub static ML_KEM_768: Algorithm = Algorithm {
id: AlgorithmId::MlKem768,
};

/// A decapsulation key, which decapsulates any number of ciphertexts, wiped on drop.
/// A decapsulation key, which decapsulates any number of ciphertexts, wiped on drop under the `wipe` feature.
pub struct DecapsulationKey(mlkem768::DecapKey);

impl DecapsulationKey {
Expand Down Expand Up @@ -130,7 +130,7 @@ impl AsRef<[u8]> for Ciphertext {
}
}

/// The secret both parties share, wiped on drop.
/// The secret both parties share, wiped on drop under the `wipe` feature.
pub struct SharedSecret(mlkem768::SharedSecret);

impl AsRef<[u8]> for SharedSecret {
Expand Down
2 changes: 1 addition & 1 deletion rind/src/high/pbkdf2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub static PBKDF2_HMAC_SHA384: Algorithm = Algorithm(&hmac::HMAC_SHA384);
/// PBKDF2-HMAC-SHA512.
pub static PBKDF2_HMAC_SHA512: Algorithm = Algorithm(&hmac::HMAC_SHA512);

/// A secret, such as a password, keyed once for every salt it derives with; wiped on drop.
/// A secret, such as a password, keyed once for every salt it derives with; wiped on drop under the `wipe` feature.
pub struct Secret(hmac::Key);

impl fmt::Debug for Secret {
Expand Down
Loading