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
1 change: 1 addition & 0 deletions quest/m2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion quest/m2/adoption/targets/x86.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
24 changes: 24 additions & 0 deletions quest/m2/api/README.md
Original file line number Diff line number Diff line change
@@ -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).
14 changes: 14 additions & 0 deletions quest/m2/api/agreement-key-state.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions quest/m2/api/cbc-iv.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions quest/m2/api/cpu-check.md
Original file line number Diff line number Diff line change
@@ -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`.
13 changes: 13 additions & 0 deletions quest/m2/api/hkdf-chain.md
Original file line number Diff line number Diff line change
@@ -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<Okm<'_, &'static hkdf::Algorithm>> 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.
18 changes: 18 additions & 0 deletions quest/m2/api/hygiene.md
Original file line number Diff line number Diff line change
@@ -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.