diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e783c4590..fd7092596 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index a5f7c0f04..3a1e3d8a3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ This is one Rust workspace: the `rind` library, its `rind-` 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-` 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. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 67254d1e3..541d7ff5d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 diff --git a/MIGRATING.md b/MIGRATING.md index 47088daf4..481cfdcbd 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -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 @@ -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` | @@ -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::as_der()` | `to_pkcs8()`, which X25519 keys also have | | `PublicKey::as_der()` | `PublicKey::to_spki()`, with an uncompressed point | | `AsDer::as_der()`, `PublicKey::as_be_bytes()` | none; an ECDSA key pair's `to_sec1()` | @@ -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 | diff --git a/README.md b/README.md index b205d67f2..01f052f59 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/REVIEW.md b/REVIEW.md index 87363ff8b..ef430a524 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -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. diff --git a/proofs/README.md b/proofs/README.md index 68e5c4c29..02d42d205 100644 --- a/proofs/README.md +++ b/proofs/README.md @@ -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. diff --git a/quest/m2/speed/README.md b/quest/m2/speed/README.md index 6548684c6..3f3813805 100644 --- a/quest/m2/speed/README.md +++ b/quest/m2/speed/README.md @@ -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 diff --git a/quest/m2/speed/wipe-feature.md b/quest/m2/speed/wipe-feature.md deleted file mode 100644 index 510649a76..000000000 --- a/quest/m2/speed/wipe-feature.md +++ /dev/null @@ -1,20 +0,0 @@ -# [S] Wiping keys on drop is opt-in - -## Goal - -`rind` wipes keys, keyed state, and private-key heap scratch on drop only when its additive `wipe` Cargo feature is on. The default build skips the wipe, and the PR reports what that saves against aws-lc-rs. - -## Plan - -Decided with the maintainer on 2026-11-07: make the wipe opt-in now, without measuring first, and plan it in m2 speed. Their reasoning: a memory disclosure such as Heartbleed leaks every live session's keys anyway, so wiping only the dropped ones buys little for the default build. RustCrypto made the same call with its opt-in `zeroize` feature. aws-lc and BoringSSL always cleanse, and ring doesn't wipe. - -- Add `wipe = []` to `rind/Cargo.toml`. Without it, `zeroise` and `zeroise_value` in `rind/src/low/generic/zeroise.rs` become no-ops. Keep the Drop impls unchanged so they compile away, rather than putting a `cfg` on each. -- Keep today's scope: keys, keyed state (AEAD keys and their GHASH tables, `hmac::Key` and `Context`, signing and agreement keys, ML-KEM decap keys and shared secrets) and RSA's private-key heap scratch. Re-adding the in-call copies [moq-dev/rind#394](https://github.com/moq-dev/rind/pull/394) dropped is the FIPS scope's question, not this quest's. -- Adapters don't forward the feature. Cargo unifies features, so a consumer who wants it enables `rind/wipe` directly. Say so in the README. -- Build and test with `--features wipe` somewhere in CI so the wipe keeps compiling, with any existing wipe tests gated on it. -- Rewrite AGENTS.md's Required wipe rule to match. The maintainer approved this; suggested wording: "Wipe keys, keyed state, and heap scratch derived from a private key on drop, but only under the `wipe` feature. Wipe nothing else: ...". -- Put CI bench rows before and after in the PR (one-shot HMAC, HKDF, ECDH and other handshake-sized ops) against aws-lc-rs, taken from the GitHub bench runners. Record the result, but don't gate on it. - -## Related - -- [FIPS scope](/quest/m4/fips-scope.md): a FIPS build turns `wipe` on and may extend it to in-call copies. diff --git a/quest/m4/fips-scope.md b/quest/m4/fips-scope.md index fc9118de9..92060d582 100644 --- a/quest/m4/fips-scope.md +++ b/quest/m4/fips-scope.md @@ -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. diff --git a/rind-ring/src/aead.rs b/rind-ring/src/aead.rs index 1cbec77d7..788b019cd 100644 --- a/rind-ring/src/aead.rs +++ b/rind-ring/src/aead.rs @@ -209,7 +209,7 @@ impl From> 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)] diff --git a/rind-ring/src/aead/quic.rs b/rind-ring/src/aead/quic.rs index c8aa4a441..47dd76844 100644 --- a/rind-ring/src/aead/quic.rs +++ b/rind-ring/src/aead/quic.rs @@ -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, diff --git a/rind-ring/src/agreement.rs b/rind-ring/src/agreement.rs index 32a05d0f8..0f46f731a 100644 --- a/rind-ring/src/agreement.rs +++ b/rind-ring/src/agreement.rs @@ -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, diff --git a/rind-ring/src/hmac.rs b/rind-ring/src/hmac.rs index efd5754e0..424bd3f0f 100644 --- a/rind-ring/src/hmac.rs +++ b/rind-ring/src/hmac.rs @@ -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, diff --git a/rind-ring/src/pkcs8.rs b/rind-ring/src/pkcs8.rs index 1fa7d34ca..9fd4ebd8d 100644 --- a/rind-ring/src/pkcs8.rs +++ b/rind-ring/src/pkcs8.rs @@ -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 { diff --git a/rind/Cargo.toml b/rind/Cargo.toml index e8ce3628e..587647f9e 100644 --- a/rind/Cargo.toml +++ b/rind/Cargo.toml @@ -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] diff --git a/rind/src/aead.rs b/rind/src/aead.rs index 14b8134ca..b1ffba40f 100644 --- a/rind/src/aead.rs +++ b/rind/src/aead.rs @@ -121,7 +121,7 @@ pub static CHACHA20_POLY1305: Algorithm = Algorithm { expand: Sealer::::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 diff --git a/rind/src/aead/gcm.rs b/rind/src/aead/gcm.rs index 818f25bcd..55ca2cb99 100644 --- a/rind/src/aead/gcm.rs +++ b/rind/src/aead/gcm.rs @@ -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, diff --git a/rind/src/aead/quic.rs b/rind/src/aead/quic.rs index 367f6f842..ae42957b6 100644 --- a/rind/src/aead/quic.rs +++ b/rind/src/aead/quic.rs @@ -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 { diff --git a/rind/src/high/agreement.rs b/rind/src/high/agreement.rs index faab62944..4d681123f 100644 --- a/rind/src/high/agreement.rs +++ b/rind/src/high/agreement.rs @@ -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 @@ -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); impl AsRef<[u8]> for Scalar { @@ -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. diff --git a/rind/src/high/hkdf.rs b/rind/src/high/hkdf.rs index 4ca221f7b..0fe98e4fa 100644 --- a/rind/src/high/hkdf.rs +++ b/rind/src/high/hkdf.rs @@ -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); diff --git a/rind/src/high/hmac.rs b/rind/src/high/hmac.rs index beeebd432..4ca068c9a 100644 --- a/rind/src/high/hmac.rs +++ b/rind/src/high/hmac.rs @@ -167,7 +167,7 @@ pub static HMAC_SHA512: Algorithm = Algorithm { key: Keying::::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); @@ -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)] diff --git a/rind/src/high/kem.rs b/rind/src/high/kem.rs index 8f6ff5a21..6fed046e6 100644 --- a/rind/src/high/kem.rs +++ b/rind/src/high/kem.rs @@ -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 { @@ -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 { diff --git a/rind/src/high/pbkdf2.rs b/rind/src/high/pbkdf2.rs index d4ddfbee5..17bf0aba4 100644 --- a/rind/src/high/pbkdf2.rs +++ b/rind/src/high/pbkdf2.rs @@ -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 { diff --git a/rind/src/high/signature.rs b/rind/src/high/signature.rs index 30f8ddc6c..31dfa8cf3 100644 --- a/rind/src/high/signature.rs +++ b/rind/src/high/signature.rs @@ -417,7 +417,7 @@ impl fmt::Debug for Signature { } } -/// A private key encoded as PKCS#8 DER, wiped on drop. +/// A private key encoded as PKCS#8 DER, wiped on drop under the `wipe` feature. pub struct Pkcs8(pub(super) Buffer<{ rsa::MAX_PKCS8_LEN }>); impl AsRef<[u8]> for Pkcs8 { @@ -455,7 +455,7 @@ impl fmt::Debug for Spki { } /// An ECDSA private key encoded as a SEC1 (RFC 5915) `ECPrivateKey` in -/// DER, wiped on drop. +/// DER, wiped on drop under the `wipe` feature. pub struct Sec1(Buffer<{ sec1::MAX_LEN }>); impl AsRef<[u8]> for Sec1 { @@ -477,7 +477,7 @@ impl Drop for Sec1 { } /// An ECDSA private key as a big-endian scalar exactly the curve's length, -/// wiped on drop. +/// wiped on drop under the `wipe` feature. pub struct EcdsaScalar(Buffer); impl AsRef<[u8]> for EcdsaScalar { @@ -498,7 +498,7 @@ impl Drop for EcdsaScalar { } } -/// An Ed25519 private key as its 32-byte seed, wiped on drop. +/// An Ed25519 private key as its 32-byte seed, wiped on drop under the `wipe` feature. pub struct Ed25519Seed([u8; 32]); impl AsRef<[u8]> for Ed25519Seed { diff --git a/rind/src/low/generic/zeroise.rs b/rind/src/low/generic/zeroise.rs index 25b5393b4..d181a58fe 100644 --- a/rind/src/low/generic/zeroise.rs +++ b/rind/src/low/generic/zeroise.rs @@ -5,14 +5,22 @@ use core::mem::{size_of, size_of_val}; use crate::low::zero_bytes; -/// Writes zeroes over the whole of the `v` slice. +// Without the `wipe` feature these do nothing, so the `Drop` impls that call +// them compile away; `cfg!` rather than `#[cfg]` keeps `zero_bytes` compiled, +// and its test running, in every build. + +/// Writes zeroes over the whole of the `v` slice, under the `wipe` feature. pub(crate) fn zeroise(v: &mut [T]) { - zero_bytes(v.as_mut_ptr().cast(), size_of_val(v)); + if cfg!(feature = "wipe") { + zero_bytes(v.as_mut_ptr().cast(), size_of_val(v)); + } } -/// Writes zeroes over the whole of the `v` value. +/// Writes zeroes over the whole of the `v` value, under the `wipe` feature. pub(crate) fn zeroise_value(v: &mut T) { - zero_bytes((v as *mut T).cast(), size_of::()); + if cfg!(feature = "wipe") { + zero_bytes((v as *mut T).cast(), size_of::()); + } } /// Marker trait for types who have valid all-bits-zero values. diff --git a/rind/src/low/tests.rs b/rind/src/low/tests.rs index fb3d2775b..26c11b421 100644 --- a/rind/src/low/tests.rs +++ b/rind/src/low/tests.rs @@ -17,21 +17,21 @@ fn bignum_mux() { } #[test] -fn zeroise() { +fn zero_bytes() { for n in 0..1024 { for offset in 0..4 { - zeroise_equiv(offset, n); + zero_bytes_equiv(offset, n); } } } // Zeroes `len` bytes from `offset` in, so the wide stores start unaligned too, // and checks the bytes either side are untouched. -fn zeroise_equiv(offset: usize, len: usize) { +fn zero_bytes_equiv(offset: usize, len: usize) { let mut expect = vec![0xffu8; offset + len + 1]; expect[offset..offset + len].fill(0); let mut bytes = vec![0xffu8; offset + len + 1]; - super::zeroise(&mut bytes[offset..offset + len]); + super::zero_bytes(bytes[offset..].as_mut_ptr(), len); assert_eq!(expect, bytes); } diff --git a/rind/tests/zeroing.rs b/rind/tests/zeroing.rs index 4e5302235..2c024d7f2 100644 --- a/rind/tests/zeroing.rs +++ b/rind/tests/zeroing.rs @@ -1,5 +1,5 @@ -//! Verification that every value owning a secret (a key, keyed state, or a -//! shared secret) is zeroed on drop. +//! Verification that, under the `wipe` feature, every value owning a secret +//! (a key, keyed state, or a shared secret) is zeroed on drop. //! //! Some of this is subject to whims of the compiler, and //! hand-written drop impls, so we test it here. @@ -41,7 +41,7 @@ //! been zeroised upon drop, even if the value was a `Key::Key128` //! that only initialized half of that space. -#![cfg(any(target_os = "linux", target_os = "macos"))] +#![cfg(all(feature = "wipe", any(target_os = "linux", target_os = "macos")))] use core::mem::size_of; use core::ops::Deref; diff --git a/sh/armv7.sh b/sh/armv7.sh index 8944041ea..a868bdf16 100755 --- a/sh/armv7.sh +++ b/sh/armv7.sh @@ -40,3 +40,5 @@ export CARGO_PROFILE_RELEASE_OVERFLOW_CHECKS=true # RIND_TEST_ARM_* fails the run that lands on the wrong path. QEMU_CPU=max RIND_TEST_ARM_NEON=1 RIND_TEST_ARM_CRYPTO_EXT=1 cargo test "${packages[@]}" --locked --release --target "$target" QEMU_CPU=cortex-a15 RIND_CPU_DISABLE_neon=1 RIND_TEST_ARM_NEON=0 RIND_TEST_ARM_CRYPTO_EXT=0 cargo test "${packages[@]}" --locked --release --target "$target" +QEMU_CPU=max RIND_TEST_ARM_NEON=1 RIND_TEST_ARM_CRYPTO_EXT=1 cargo test -p rind --features wipe --test zeroing --locked --release --target "$target" +QEMU_CPU=cortex-a15 RIND_CPU_DISABLE_neon=1 RIND_TEST_ARM_NEON=0 RIND_TEST_ARM_CRYPTO_EXT=0 cargo test -p rind --features wipe --test zeroing --locked --release --target "$target" diff --git a/sh/dispatch.sh b/sh/dispatch.sh index 4dcb58147..a2362bdaa 100755 --- a/sh/dispatch.sh +++ b/sh/dispatch.sh @@ -181,6 +181,11 @@ for module in "${selected[@]}"; do # `cargo test`, it passes a selection with no tests, such as the # benches alone. cargo nextest run "${packages[@]}" --locked --no-tests=warn + # Only the zeroing tests see the opt-in wipe, which the default + # build turns into no-ops. + if ((${#packages[@]} == 0)) || printf '%s\n' "${packages[@]}" | grep -qxE 'rind|--workspace'; then + cargo nextest run -p rind --features wipe --test zeroing --locked + fi # Doc tests need a library. No packages at all means the workspace. mapfile -t libs < <(sh/select.sh "$changed" "$all" --lib) if ((${#libs[@]} > 0 || ${#packages[@]} == 0)); then