diff --git a/quest/m2/README.md b/quest/m2/README.md index 5f2fd4f26..c92a68252 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -8,4 +8,5 @@ does not block it, and adoption by projects beyond MoQ. ## Required - [Fastest at every algorithm](/quest/m2/speed/README.md) - every algorithm on every path as fast as any permissive source +- [The API steers callers away from misuse](/quest/m2/api/README.md) - the 2026-11-08 API audit's fixes - [Adoption beyond MoQ](/quest/m2/adoption/README.md) - popular projects and other languages can choose rind, with what they need from it diff --git a/quest/m2/adoption/targets/x86.md b/quest/m2/adoption/targets/x86.md index 2fdbb4399..0fc8f61ea 100644 --- a/quest/m2/adoption/targets/x86.md +++ b/quest/m2/adoption/targets/x86.md @@ -6,4 +6,4 @@ rind and rind-rustls build for i686-unknown-linux-gnu, i686-pc-windows-msvc, and ## Plan -Android x86 and 32-bit Windows are the likeliest consumers. libcrux-platform 0.0.4, which libcrux-aes pulls in, calls `__cpuid` outside `unsafe`, which compiles only on Rust newer than rind's 1.90 MSRV on x86: raise the MSRV, or get libcrux to fix it, before i686 can claim it. i586 is out: ring refuses it for lacking SSE2, and aws-lc-rs doesn't test it. aws-lc has x86 assembly, but it's out of scope until a consumer measures a need. A missing CPU feature still fails with an error naming it; the portable backend needs none. Add the targets to [PLATFORMS.md](/PLATFORMS.md) and the release checks in the same PR. +Android x86 and 32-bit Windows are the likeliest consumers. libcrux-platform 0.0.4, which libcrux-aes pulls in, calls `__cpuid` outside `unsafe`, which compiles only on Rust newer than rind's 1.90 MSRV on x86: raise the MSRV, or get libcrux to fix it, before i686 can claim it. i586 is out: ring refuses it for lacking SSE2, and aws-lc-rs doesn't test it. aws-lc has x86 assembly, but it's out of scope until a consumer measures a need. A backend requires only the target's compile-time features ([rind needs no runtime CPU check](/quest/m2/api/cpu-check.md)): i686 enables SSE2, and the portable backend needs none. Add the targets to [PLATFORMS.md](/PLATFORMS.md) and the release checks in the same PR. diff --git a/quest/m2/api/README.md b/quest/m2/api/README.md new file mode 100644 index 000000000..4fd442923 --- /dev/null +++ b/quest/m2/api/README.md @@ -0,0 +1,24 @@ +# The API steers callers away from misuse + +## Goal + +rind's public API steers callers away from the misuse and wasted work a 2026-11-08 security and performance audit found: chained HKDF expands written by hand, public keys recomputed on every call, caller-chosen AES-CBC IVs, and hazards nothing documents, and a startup CPU check every caller must make although it can no longer fail. Adapters change only to drop the workarounds they carry today. + +## Plan + +The audit checked every module of `rind` against AGENTS.md's rules and found nothing high-severity, and no panic reachable from public input. Decided with the maintainer on 2026-11-08: + +- Scope: the goal above ✅ (rejected: non-breaking changes only, docs only). +- Additive quests: HKDF chaining, cached agreement keys, and a hygiene and docs pass ✅, all three. +- Breaking changes: a typed AES-CBC IV, and deleting the runtime CPU check that can no longer fail ✅, with the adapters breaking in patch releases (see [its quest](/quest/m2/api/cpu-check.md)). Rejected, so don't raise them again without new evidence: + - Replacing `SharedSecret`'s `AsRef<[u8]>` with a named accessor. Documenting HKDF in the examples gets most of the value, and the rename would churn every TLS and HPKE caller. + - Moving GCM's 32- and 64-bit tags under a legacy name. WebCrypto requires them, and `TagLength`'s docs already warn about them. + - Collapsing `constant_time::verify`'s `WrongLength` and `NotEqual` into one error. Matching only `NotEqual` is a contrived misuse, and every caller uses `?`. +- Milestone: m2 ✅ (rejected: m1, m3). None of this blocks MoQ. +- Dropped as not worth a quest: + - Fixed buffers sized for RSA (`PublicKey` 3584 B, `encoding::Pkcs8` 4.8 KB, `Signature` 1032 B). They sit on no hot path, and fixed capacity is ARCHITECTURE.md's design. + - `RandomizedNonceKey`'s `getrandom` call per seal. Its only consumer seals rustls tickets. + - The per-packet copy in rind-str0m's SRTP. aws-lc-rs's ciphers are in place too, and MoQ doesn't use SRTP. + - Taking `agree(&PublicKey)` instead of bytes. TLS parses each peer key once per handshake anyway. + - A streaming `hmac::Context::verify`. It has no consumer; the hygiene quest documents signing and then calling `constant_time::verify`. +- Owned elsewhere: storing and loading ML-KEM decapsulation keys is in [ML-KEM-512 and ML-KEM-1024](/quest/m4/pq/ml-kem.md). diff --git a/quest/m2/api/agreement-key-state.md b/quest/m2/api/agreement-key-state.md new file mode 100644 index 000000000..9ef6b44cf --- /dev/null +++ b/quest/m2/api/agreement-key-state.md @@ -0,0 +1,14 @@ +# [S] Agreement keys keep their public key and algorithm + +## Goal + +`agreement::PrivateKey::public_key()` and `to_pkcs8()` return the public key computed when the key was generated or loaded, instead of a fresh base-point multiplication on every call. Agreement keys report their algorithm, and their `Debug` shows it, as the other key types' do. rind-rustls and rind-aws-lc-rs drop the public-key caches they keep to work around the cost. `signature::EcdsaKeyPair::public_key()` likewise stops re-parsing its cached point. + +## Plan + +- Today X25519's `public_key()` runs a full scalar multiplication (`mid/x25519.rs`), each NIST curve's runs `base_multiply`, and `to_pkcs8()` repeats it. PKCS#8 and SEC1 loaders that carry a public key already compute it to check consistency, then discard it. +- Store the encoded public key (at most 133 bytes) in the key at generate and load time, and reuse it for `public_key()`, `to_pkcs8()`, and the loaders' consistency check. It is public, so it is not wiped. +- Add `algorithm()` to `agreement::PrivateKey` and `PublicKey`, and `PartialEq`/`Eq` to `agreement::Algorithm`, as aws-lc-rs and ring have. `PrivateKey`'s `Debug` shows the algorithm, the 0.2.0 audit's open follow-up ([moq-dev/rind#383](https://github.com/moq-dev/rind/pull/383)). +- Adapter caches to remove: `rind-rustls/src/kx_group.rs` (around line 73), and the `algorithm` tracking and cached public key in `rind-aws-lc-rs/src/agreement.rs`. +- `EcdsaKeyPair::public_key()` re-parses its point and returns a `PublicKey` with empty verify caches. Build it directly from the cached point. +- Additive. Measure `public_key()` and a rind-rustls handshake before and after, against aws-lc-rs. diff --git a/quest/m2/api/cbc-iv.md b/quest/m2/api/cbc-iv.md new file mode 100644 index 000000000..404db2b6c --- /dev/null +++ b/quest/m2/api/cbc-iv.md @@ -0,0 +1,12 @@ +# [S] AES-CBC encryption takes a typed IV + +## Goal + +`cipher::AesCbcEncryptingKey`'s `encrypt` and `encrypt_blocks` take an IV type that is either drawn from the system's random source or explicitly asserted unpredictable by the caller. A zero, constant, or counter IV, which leaks whether a plaintext block equals one an attacker chose, is then never the easy path. + +## Plan + +- Decided with the maintainer on 2026-11-08: take this breaking change for rind 0.4 ✅ (rejected: document only). aws-lc-rs draws the IV itself and names the caller-IV path `less_safe_encrypt`. +- Recommended shape, following `aead::Nonce::assume_unique_for_key` and `gcm::Iv`: `cbc::Iv::random()?` and `Iv::assume_unpredictable([u8; 16])`, not `Clone`, consumed per call, with the bytes readable so the caller can send them. Decryption keeps a plain `&[u8; 16]`, since a received IV is public. One way per job: no second `encrypt` that takes raw bytes. +- Update every caller: rind-aws-lc-rs (`less_safe_encrypt` maps to `assume_unpredictable`, and its random-IV path to `random`), the benches, the timing cases, the fuzzer, the tests, and MIGRATING.md. The title takes `!` with the migration. +- Unrelated to [Check CBC consumers for padding-oracle exposure](/quest/m3/cbc-padding-oracle.md), which traces decryption. diff --git a/quest/m2/api/cpu-check.md b/quest/m2/api/cpu-check.md new file mode 100644 index 000000000..8978dded8 --- /dev/null +++ b/quest/m2/api/cpu-check.md @@ -0,0 +1,18 @@ +# [S] rind needs no runtime CPU check + +## Goal + +A backend requires only its target's compile-time features, so no CPU that runs a rind binary lacks one. `rind::cpu::check`, `cpu::MissingFeature`, and the panic every operation guards against are gone, and so is every adapter's `MissingFeature`: `default_provider()` and its kin become infallible, and pyrind, rind-dart, and rind-cpuid stop calling the check. + +## Plan + +`check()` can no longer fail on any target rind builds for. `missing_cpu_feature()` returns `None` on x86_64, arm, and portable, and aarch64 checks only `neon`, which every aarch64 target with `std` enables at compile time (`rustc --print cfg`, 2026-11-08); only the `no_std` `*-none-softfloat` targets lack it. The check dates from [moq-dev/rind#12](https://github.com/moq-dev/rind/pull/12), when x86_64 required AVX2, ADX, BMI2, and AES-NI, requirements that later tiers removed ([moq-dev/rind#172](https://github.com/moq-dev/rind/pull/172), [moq-dev/rind#178](https://github.com/moq-dev/rind/pull/178)). + +Decided with the maintainer on 2026-11-08: + +- Delete the runtime check and make requirements compile-time ✅ (rejected: keep `check()` documented as never failing; leave it). rind's removal ships in 0.4. +- `rind/build.rs` selects the aarch64 backend only when `CARGO_CFG_TARGET_FEATURE` has `neon`, and the portable backend otherwise, which is one backend per target, chosen at build time. +- AGENTS.md's Required rule changes from "a CPU missing a required feature fails with an error naming it" to say a backend requires only its target's compile-time features. The maintainer asked for this edit; follow `quest guide prompting`. +- Adapters break now, in their next patch releases, rather than waiting for their upstream's next major ✅ (rejected: wait for upstream majors; change the adapter version scheme). The maintainer: nobody uses them yet. This covers rind-rustls, rind-dimpl, rind-object-store, rind-instant-acme, rind-jsonwebtoken's docs, and rind-ring's and rind-aws-lc-rs's `cpu::check`. +- Delete what only existed for the check: `verify_cpu_features` and its per-operation atomic load in `low::entry`, `rind/tests/cpu.rs`'s per-feature panic test, and `RIND_CPU_DISABLE_neon` if nothing else uses it. Keep the optional-feature toggles that select tiers. +- Update README, PLATFORMS.md, MIGRATING.md, ARCHITECTURE.md's `cpu` line, and the adapters' docs, which tell callers to check first. Keep `cpu::has_aes_hardware` and `cpu::Dit`. diff --git a/quest/m2/api/hkdf-chain.md b/quest/m2/api/hkdf-chain.md new file mode 100644 index 000000000..32d263381 --- /dev/null +++ b/quest/m2/api/hkdf-chain.md @@ -0,0 +1,13 @@ +# [S] HKDF output keys the next expand + +## Goal + +A caller turns one HKDF-Expand's output into the PRK of the next expand through rind's types, as QUIC's Initial secrets, TLS 1.3's key schedule, and MLS chain them, without writing HKDF-Expand over `hmac::Context` or re-extracting the bytes, which runs an extra HMAC and derives different keys. rind-ring and rind-aws-lc-rs drop their hand-written copies of Expand. + +## Plan + +- Recommended shape: `impl KeyType for &'static hkdf::Algorithm` (hash-length output) and `From> for Prk`, matching the existing `aead::Key::from(okm)` and `hmac::Key::from(okm)`. This keeps rind's sealed `KeyType`, and adds no `Prk::new_less_safe`. +- The copies to delete: `rind-ring/src/hkdf.rs` `expand_into` and `rind-aws-lc-rs/src/hkdf.rs` (around line 123). Check whether either adapter can then map its own `Prk::from(okm)` and `Salt::from(okm)` onto the new conversion. +- Update MIGRATING.md's hkdf rows, which list `Prk::from(okm)` as "none". +- Test a chained expand against RFC 9001's Appendix A Initial secrets, and differentially against aws-lc-rs's `Prk::from(Okm)`. +- [HKDF-SHA256 expand is back to its earlier speed](/quest/m2/speed/hkdf-expand-regression.md) touches the same code for speed; merge with it rather than conflict. diff --git a/quest/m2/api/hygiene.md b/quest/m2/api/hygiene.md new file mode 100644 index 000000000..c9cd60a6e --- /dev/null +++ b/quest/m2/api/hygiene.md @@ -0,0 +1,18 @@ +# [S] The audit's hazards are documented or linted + +## Goal + +Each misuse hazard the 2026-11-08 API audit found, short of an API change, is documented where a caller meets it or caught by the compiler. Debug output stops printing tag bytes. + +## Plan + +All additive. The audit's findings: + +- `hmac::Tag`'s `Debug` prints its bytes, which are secret when HMAC is a PRF (rind-dimpl's DTLS PRF). `aead::gcm::Tag`'s prints all 16 bytes even when truncated. Show the algorithm only, as keys' `Debug` does. +- `#[must_use]` on `hkdf::Okm`, `hkdf::Salt::extract`, `digest::digest`, `hmac::sign`, and `cpu::has_aes_hardware`, so a discarded result warns. +- `agreement` and `kem`'s module examples derive keys through HKDF instead of comparing raw shared secrets, and MIGRATING.md's `agree_ephemeral(.., kdf)` row says to run a KDF. +- `hmac::verify` says how to check a MAC over a message in pieces, or a truncated tag a protocol fixes (SRTP's HMAC-SHA1-80): sign, then `constant_time::verify` the agreed prefix. +- `cipher::AesCtr` and `cipher::ChaCha20`: each call starts at a whole block, so continuing a stream after a length that isn't a multiple of the block size reuses keystream. Fix AesCtr's SRTP counter-width example, which says 16 bits while rind-str0m uses 128. +- `signature::PublicKey::verify`: ECDSA accepts both `(r, s)` and `(r, n-s)`, unlike Ed25519, PKCS#1, and PSS. +- `pbkdf2::Secret`: output beyond one hash adds defender cost but no strength, and iteration counts from untrusted input (PBES2, PKCS#12) need a caller cap. +- `RsaPssSaltLength::Custom`: a salt too long for the key fails at sign or verify as `KeyRejected`, not at construction.