From c124961a0a9d73f442577f1da515b862ab25b815 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 9 Nov 2026 17:22:30 -0800 Subject: [PATCH 1/5] Revert "feat(cipher)!: AES-CBC encryption takes a typed CbcIv (#530)" This reverts commit 8b2dd4c4f36a1d2f5e7ced86176e82224db5c5a6. --- MIGRATING.md | 6 ++-- TESTING.md | 2 +- fuzz/fuzzer/aes_cbc.rs | 4 +-- quest/m2/api/cbc-iv.md | 11 +++++++ rind-aws-lc-rs/src/cipher.rs | 12 ++------ rind-bench/benches/cipher.rs | 6 ++-- rind-timing/src/cases.rs | 7 ++--- rind/src/cipher.rs | 2 +- rind/src/mid/aes_cbc.rs | 59 +++++++----------------------------- rind/tests/aws_lc.rs | 5 ++- rind/tests/crosschecks.rs | 10 +++--- rind/tests/wycheproof.rs | 9 ++---- 12 files changed, 46 insertions(+), 87 deletions(-) create mode 100644 quest/m2/api/cbc-iv.md diff --git a/MIGRATING.md b/MIGRATING.md index ec18171f3..b56ff5c76 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -123,11 +123,11 @@ protocols with their own integrity protection need. ring has none. |---|---| | `UnboundCipherKey::new(&AES_128, key)` and `EncryptingKey::ctr` or `DecryptingKey::ctr` | `AesCtr::new(&AES_128, key, 128)?`, whose last argument is how many of the counter block's rightmost bits count, as WebCrypto's `length` | | `less_safe_encrypt(in_out, EncryptionContext::Iv128(iv))`, `decrypt` | `apply_keystream(&iv, in_out)?`, with `iv` the `[u8; 16]` counter block | -| `PaddedBlockEncryptingKey::cbc_pkcs7(key)` and `less_safe_encrypt(in_out, EncryptionContext::Iv128(iv))` | `AesCbcEncryptingKey::new(&AES_128, key)?` and `encrypt(CbcIv::assume_unpredictable(iv), in_out)`, which cannot fail | +| `PaddedBlockEncryptingKey::cbc_pkcs7(key)` and `less_safe_encrypt(in_out, EncryptionContext::Iv128(iv))` | `AesCbcEncryptingKey::new(&AES_128, key)?` and `encrypt(&iv, in_out)`, which cannot fail | | `PaddedBlockDecryptingKey::cbc_pkcs7(key)` and `decrypt(in_out, DecryptionContext::Iv128(iv))` | `AesCbcDecryptingKey::new(&AES_128, key)?` and `decrypt(&iv, in_out)?`, which clears `in_out` if the padding is malformed | -| `EncryptingKey::cbc`, `DecryptingKey::cbc`, unpadded | `encrypt_blocks(CbcIv::assume_unpredictable(iv), in_out)?` and `decrypt_blocks(&iv, in_out)?` on the keys above | +| `EncryptingKey::cbc`, `DecryptingKey::cbc`, unpadded | `encrypt_blocks(&iv, in_out)?` and `decrypt_blocks(&iv, in_out)?` on the keys above | | `AES_128`, `AES_192`, `AES_256` | the same | -| `EncryptingKey::encrypt` and `PaddedBlockEncryptingKey::encrypt`, which pick a random IV | for CBC, `CbcIv::random()?`, read with `as_bytes` before encrypting; for CTR, pick the counter block with `rand::fill` | +| `EncryptingKey::encrypt` and `PaddedBlockEncryptingKey::encrypt`, which pick a random IV | none: pick the nonce or IV with `rand::fill` | | `DES_EDE3_FOR_LEGACY_USE_ONLY` and the other DES, the ECB, CFB, and OFB modes, `StreamingEncryptingKey`, `StreamingDecryptingKey` | none; [aws-lc-rs extras](quest/m3/parity/aws-lc-extras.md) | | none | `Aes::new(&alg, key)?.encrypt_block(block)`, one AES block | | none | `ChaCha20::new(&key).apply_keystream(nonce, counter, in_out)?` | diff --git a/TESTING.md b/TESTING.md index 9b579e0a2..3c7963dbb 100644 --- a/TESTING.md +++ b/TESTING.md @@ -106,7 +106,7 @@ constant-time cell is empty by design. | QUIC header protection | `aead::quic::{Algorithm, AES_128, AES_256, CHACHA20, HeaderProtectionKey, SAMPLE_LEN}` | RFC 9001: `aead::quic::rfc9001` | none, [planned](/quest/m3/confidence/fuzz-every-primitive.md) | `crosschecks::cipher::quic_header_protection_mask` | ctgrind: AES's key, as below, and ChaCha20's `mid::chacha20::new`; qemu: `mid::aes::ct_qemu_aes` | | AES block | `cipher::{Aes, Algorithm, AES_128, AES_192, AES_256}` | `cavp/aes`, `aws_lc::aes`, `aws_lc::cipher` | none, [planned](/quest/m3/confidence/fuzz-every-primitive.md) | `crosschecks::cipher::aes_block` | ctgrind: `mid::aes::expand_key`, `mid::aes::encrypt_block`; qemu: `mid::aes::ct_qemu_aes` | | AES-CTR | `cipher::AesCtr` | SP 800-38A: `mid::aes::sp800_38a`, RFC 3686: `mid::aes::rfc3686`, RFC 3711: `mid::aes::srtp_aes_cm`, `aws_lc::cipher` | none, [planned](/quest/m3/confidence/fuzz-every-primitive.md) | `crosschecks::cipher::aes_ctr` | ctgrind: `mid::aes::expand_key`, `mid::aes::apply_keystream`; qemu: `mid::aes::ct_qemu_aes` | -| AES-CBC | `cipher::{AesCbcDecryptingKey, AesCbcEncryptingKey, CbcIv}` | `aes_cbc_pkcs5_test`, SP 800-38A: `mid::aes_cbc::sp800_38a`, `aws_lc::cipher` | `fuzz::aes_cbc` | `crosschecks::cipher::aes_cbc_pkcs7`, `crosschecks::cipher::aes_cbc_unpadded` | ctgrind: the key `mid::aes_cbc::new`; qemu: `mid::aes_cbc::ct_qemu_aes_cbc` | +| AES-CBC | `cipher::{AesCbcDecryptingKey, AesCbcEncryptingKey}` | `aes_cbc_pkcs5_test`, SP 800-38A: `mid::aes_cbc::sp800_38a`, `aws_lc::cipher` | `fuzz::aes_cbc` | `crosschecks::cipher::aes_cbc_pkcs7`, `crosschecks::cipher::aes_cbc_unpadded` | ctgrind: the key `mid::aes_cbc::new`; qemu: `mid::aes_cbc::ct_qemu_aes_cbc` | | ChaCha20 | `cipher::ChaCha20` | `chacha20_counter_test`, RFC 8439: `mid::chacha20::rfc8439` | none, [planned](/quest/m3/confidence/fuzz-every-primitive.md) | `crosschecks::cipher::chacha20_keystream`, `crosschecks::cipher::chacha20_every_length` | ctgrind: the key `mid::chacha20::new`, `low::x86_64::chacha20::paths_match`, `low::arm::chacha_armv4::neon_matches_nohw` | | AES-KW, AES-KWP | `key_wrap::{AesKek, AES_128, AES_192, AES_256}` | `aes_wrap_test`, `aes_kwp_test`, RFC 3394: `mid::aes_kw::rfc3394`, RFC 5649: `mid::aes_kw::rfc5649`, `aws_lc::aes` | `fuzz::key_wrap`, but not AES-192, which aws-lc-rs lacks | `crosschecks::key_wrap::wrap_and_unwrap`, `crosschecks::key_wrap::wrap_and_unwrap_padded` | ctgrind: the key `mid::aes_kw::new`; qemu: `mid::aes_kw::ct_qemu_aes_kw` | | MD5 | `digest::legacy::MD5` | RFC 1321: `mid::md5::rfc1321` | none, [planned](/quest/m3/confidence/fuzz-every-primitive.md) | `crosschecks::hashes_match_aws_lc_rs`, `crosschecks::hashes_every_length` | ctgrind: `high::digest::secret_message` | diff --git a/fuzz/fuzzer/aes_cbc.rs b/fuzz/fuzzer/aes_cbc.rs index 712c523b9..20dabd21a 100644 --- a/fuzz/fuzzer/aes_cbc.rs +++ b/fuzz/fuzzer/aes_cbc.rs @@ -5,7 +5,7 @@ use aws_lc_rs::cipher::{ UnboundCipherKey, }; use libfuzzer_sys::{arbitrary, arbitrary::Arbitrary, fuzz_target}; -use rind::cipher::{AES_128, AES_192, AES_256, AesCbcDecryptingKey, AesCbcEncryptingKey, CbcIv}; +use rind::cipher::{AES_128, AES_192, AES_256, AesCbcDecryptingKey, AesCbcEncryptingKey}; mod edit; @@ -22,7 +22,7 @@ fuzz_target!(|input: (Operation, [u8; 16], Vec, edit::Edit)| { let mut ct = msg.clone(); AesCbcEncryptingKey::new(ours, key) .unwrap() - .encrypt(CbcIv::assume_unpredictable(iv), &mut ct); + .encrypt(&iv, &mut ct); let mut right = msg.clone(); PaddedBlockEncryptingKey::cbc_pkcs7(theirs()) .unwrap() diff --git a/quest/m2/api/cbc-iv.md b/quest/m2/api/cbc-iv.md new file mode 100644 index 000000000..4f10e1cfb --- /dev/null +++ b/quest/m2/api/cbc-iv.md @@ -0,0 +1,11 @@ +# [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. diff --git a/rind-aws-lc-rs/src/cipher.rs b/rind-aws-lc-rs/src/cipher.rs index c9421cab7..f3bebe123 100644 --- a/rind-aws-lc-rs/src/cipher.rs +++ b/rind-aws-lc-rs/src/cipher.rs @@ -296,7 +296,7 @@ impl EncryptingKey { let iv = context.iv()?; match &self.key { Encrypting::Cbc(key) => key - .encrypt_blocks(rind::cipher::CbcIv::assume_unpredictable(*iv), in_out) + .encrypt_blocks(iv, in_out) .map_err(crate::error::unspecified)?, Encrypting::Ctr(key) => key .apply_keystream(iv, in_out) @@ -433,10 +433,7 @@ impl PaddedBlockEncryptingKey { where InOut: AsMut<[u8]> + for<'in_out> Extend<&'in_out u8>, { - let iv = rind::cipher::CbcIv::random().map_err(crate::error::unspecified)?; - let context = EncryptionContext::Iv128((*iv.as_bytes()).into()); - self.key.encrypt(iv, in_out); - Ok(context.into()) + self.less_safe_encrypt(in_out, EncryptionContext::Iv128(FixedLength::new()?)) } /// Pads `in_out` and encrypts it in place under the caller's IV, which @@ -449,10 +446,7 @@ impl PaddedBlockEncryptingKey { where InOut: AsMut<[u8]> + for<'in_out> Extend<&'in_out u8>, { - self.key.encrypt( - rind::cipher::CbcIv::assume_unpredictable(*context.iv()?), - in_out, - ); + self.key.encrypt(context.iv()?, in_out); Ok(context.into()) } } diff --git a/rind-bench/benches/cipher.rs b/rind-bench/benches/cipher.rs index d3e8acb07..0b4f683a9 100644 --- a/rind-bench/benches/cipher.rs +++ b/rind-bench/benches/cipher.rs @@ -49,12 +49,12 @@ fn bench_aes_cbc(c: &mut Criterion) { for op in ["encrypt", "decrypt"] { let mut group = c.benchmark_group(format!("{name}-{op}")); for (size, size_name) in criterion::SIZES { - use rind::cipher::{AesCbcDecryptingKey, AesCbcEncryptingKey, CbcIv}; + use rind::cipher::{AesCbcDecryptingKey, AesCbcEncryptingKey}; let algorithm = rind_aes(key_len); let mut ct = vec![0u8; size]; AesCbcEncryptingKey::new(algorithm, key) .unwrap() - .encrypt(CbcIv::assume_unpredictable(iv), &mut ct); + .encrypt(&iv, &mut ct); let input = if op == "encrypt" { vec![0u8; size] } else { ct }; let mut buf = Vec::with_capacity(size + 16); group.throughput(Throughput::Bytes(size as u64)); @@ -91,7 +91,7 @@ fn bench_aes_cbc(c: &mut Criterion) { buf.clear(); buf.extend_from_slice(&input); if op == "encrypt" { - enc.encrypt(CbcIv::assume_unpredictable(iv), &mut buf); + enc.encrypt(&iv, &mut buf); } else { dec.decrypt(&iv, &mut buf).unwrap(); } diff --git a/rind-timing/src/cases.rs b/rind-timing/src/cases.rs index 776f5b319..345786f5d 100644 --- a/rind-timing/src/cases.rs +++ b/rind-timing/src/cases.rs @@ -327,8 +327,7 @@ fn cipher_cases(cases: &mut Vec) { let plaintexts = classes(VALUES, fixed(random::<256>()), random::<256>); measure(config, &plaintexts, |plaintext| { let mut buf = *plaintext; - key.encrypt_blocks(cipher::CbcIv::assume_unpredictable(iv), &mut buf) - .map(|()| buf) + key.encrypt_blocks(&iv, &mut buf).map(|()| buf) }) })); cases.push(case("cipher/aes-128-cbc/decrypt-key", move |config| { @@ -353,9 +352,7 @@ fn cipher_cases(cases: &mut Vec) { let mut buf = [16; 64]; buf[..48].copy_from_slice(&prefix); buf[48 + at] ^= random::<1>()[0] | 1; - encrypt - .encrypt_blocks(cipher::CbcIv::assume_unpredictable(iv), &mut buf) - .unwrap(); + encrypt.encrypt_blocks(&iv, &mut buf).unwrap(); buf }; let anywhere = || wrong_at(usize::from(random::<1>()[0]) % 15); diff --git a/rind/src/cipher.rs b/rind/src/cipher.rs index 1d6d0cb6a..a7dd8a0b0 100644 --- a/rind/src/cipher.rs +++ b/rind/src/cipher.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0 pub use crate::mid::aes::{Aes, AesCtr}; -pub use crate::mid::aes_cbc::{AesCbcDecryptingKey, AesCbcEncryptingKey, CbcIv}; +pub use crate::mid::aes_cbc::{AesCbcDecryptingKey, AesCbcEncryptingKey}; pub use crate::mid::chacha20::ChaCha20; /// An AES key size, which fixes the key length a key must have. diff --git a/rind/src/mid/aes_cbc.rs b/rind/src/mid/aes_cbc.rs index 4c9db2839..8d0a132a6 100644 --- a/rind/src/mid/aes_cbc.rs +++ b/rind/src/mid/aes_cbc.rs @@ -17,7 +17,7 @@ use crate::low::{self, AesDecKey, AesKey, Entry, Zeroable, aes_cbc, zeroise_valu /// with each message, as encrypt-then-MAC and RFC 7518's `A128CBC-HS256` /// do, or use [`crate::aead`] instead. /// -/// Each message needs a fresh [`CbcIv`], normally [`CbcIv::random`]: an IV +/// Each message needs a fresh random IV from [`crate::rand::fill`]: an IV /// an attacker can predict, such as a counter, leaks whether a plaintext /// block equals one they chose. pub struct AesCbcEncryptingKey { @@ -41,26 +41,26 @@ impl AesCbcEncryptingKey { /// Pads `in_out` with PKCS#7, 1 to 16 bytes each holding the count, and /// encrypts it in place under `iv`. - pub fn encrypt(&self, iv: CbcIv, in_out: &mut InOut) + pub fn encrypt(&self, iv: &[u8; 16], in_out: &mut InOut) where InOut: AsMut<[u8]> + for<'a> Extend<&'a u8>, { let _entry = Entry::new_secret(); let pad = 16 - in_out.as_mut().len() % 16; in_out.extend(&[pad as u8; 16][..pad]); - self.encrypt_in_place(&iv.0, in_out.as_mut().as_chunks_mut().0); + self.encrypt_in_place(iv, in_out.as_mut().as_chunks_mut().0); } /// Encrypts `in_out` in place under `iv`, without padding. /// /// Fails with [`Error::WrongLength`], leaving `in_out` unchanged, if /// `in_out` is not a whole number of 16-byte blocks. - pub fn encrypt_blocks(&self, iv: CbcIv, in_out: &mut [u8]) -> Result<(), Error> { + pub fn encrypt_blocks(&self, iv: &[u8; 16], in_out: &mut [u8]) -> Result<(), Error> { let _entry = Entry::new_secret(); let (blocks, []) = in_out.as_chunks_mut() else { return Err(Error::WrongLength); }; - self.encrypt_in_place(&iv.0, blocks); + self.encrypt_in_place(iv, blocks); Ok(()) } @@ -71,41 +71,6 @@ impl AesCbcEncryptingKey { } } -/// A CBC IV that encrypts one message: random, or one the caller promises an -/// attacker cannot predict. -/// -/// It is not `Clone`, so one value encrypts at most one message. Read it with -/// [`CbcIv::as_bytes`] before encrypting to send it alongside the ciphertext. -pub struct CbcIv([u8; 16]); - -impl CbcIv { - /// Draws an IV from the system's secure random source. - /// - /// Fails with [`Error::RngFailed`] if the system random source does. - pub fn random() -> Result { - let mut iv = [0; 16]; - crate::rand::fill(&mut iv)?; - Ok(Self(iv)) - } - - /// Wraps `iv`, which the caller promises an attacker cannot predict: - /// never a constant, zero, or counter. - pub fn assume_unpredictable(iv: [u8; 16]) -> Self { - Self(iv) - } - - /// The IV's bytes, which the receiver needs to decrypt. - pub fn as_bytes(&self) -> &[u8; 16] { - &self.0 - } -} - -impl fmt::Debug for CbcIv { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("CbcIv").finish_non_exhaustive() - } -} - /// An AES key that decrypts in CBC mode; wiped on drop. /// /// Decryption is unauthenticated: check the MAC over the IV and ciphertext @@ -267,15 +232,14 @@ mod tests { ] { let (enc, dec) = keys(&hex::decode(key).unwrap()); let mut buf = pt.clone(); - enc.encrypt_blocks(CbcIv::assume_unpredictable(*iv), &mut buf) - .unwrap(); + enc.encrypt_blocks(iv, &mut buf).unwrap(); assert_eq!(hex::encode(&buf), ct); dec.decrypt_blocks(iv, &mut buf).unwrap(); assert_eq!(buf, pt); // padded, the same blocks then one of padding let mut buf = pt.clone(); - enc.encrypt(CbcIv::assume_unpredictable(*iv), &mut buf); + enc.encrypt(iv, &mut buf); assert_eq!(hex::encode(&buf[..64]), ct); assert_eq!(buf.len(), 80); assert_eq!(dec.decrypt(iv, &mut buf).unwrap(), pt); @@ -323,13 +287,13 @@ mod tests { for len in 0..=150usize { let msg: Vec = (0..len as u8).collect(); let mut buf = msg.clone(); - enc.encrypt(CbcIv::assume_unpredictable(iv), &mut buf); + enc.encrypt(&iv, &mut buf); assert_eq!(buf.len(), (len / 16 + 1) * 16, "{len}"); assert_eq!(dec.decrypt(&iv, &mut buf).unwrap(), msg, "{len}"); let whole = len.is_multiple_of(16); let mut buf = msg.clone(); - let result = enc.encrypt_blocks(CbcIv::assume_unpredictable(iv), &mut buf); + let result = enc.encrypt_blocks(&iv, &mut buf); assert_eq!(result.is_ok(), whole, "{len}"); if !whole { assert_eq!(buf, msg, "refused encrypt wrote"); @@ -355,8 +319,7 @@ mod tests { let iv = [0x24; 16]; let check = |padded: &[u8]| { let mut buf = padded.to_vec(); - enc.encrypt_blocks(CbcIv::assume_unpredictable(iv), &mut buf) - .unwrap(); + enc.encrypt_blocks(&iv, &mut buf).unwrap(); let result = dec.decrypt(&iv, &mut buf).map(|pt| pt.len()); if result.is_err() { assert!(buf.iter().all(|b| *b == 0), "failure left plaintext"); @@ -403,7 +366,7 @@ mod tests { let iv = bytes(16).try_into().unwrap(); let mut msg = bytes(6 * 16 + 5); - ct_qemu::traced(|| enc.encrypt(CbcIv::assume_unpredictable(iv), &mut msg)); + ct_qemu::traced(|| enc.encrypt(&iv, &mut msg)); ct_qemu::traced(|| dec.decrypt(&iv, &mut msg)).unwrap(); } } diff --git a/rind/tests/aws_lc.rs b/rind/tests/aws_lc.rs index c7f82e895..4903a177a 100644 --- a/rind/tests/aws_lc.rs +++ b/rind/tests/aws_lc.rs @@ -10,7 +10,7 @@ use rind::aead::{ }; use rind::agreement::{self, ECDH_P256, ECDH_P384, ECDH_P521}; use rind::cipher::{ - AES_128, AES_192, AES_256, Aes, AesCbcDecryptingKey, AesCbcEncryptingKey, AesCtr, CbcIv, + AES_128, AES_192, AES_256, Aes, AesCbcDecryptingKey, AesCbcEncryptingKey, AesCtr, }; use rind::hmac; use rind::key_wrap::AesKek; @@ -210,8 +210,7 @@ fn cipher() { m if m.ends_with("-CBC") => { let mut got = pt.clone(); let enc = AesCbcEncryptingKey::new(algorithm(), &key).unwrap(); - enc.encrypt_blocks(CbcIv::assume_unpredictable(iv()), &mut got) - .unwrap(); + enc.encrypt_blocks(&iv(), &mut got).unwrap(); assert_eq!(got, ct, "{test}"); let dec = AesCbcDecryptingKey::new(algorithm(), &key).unwrap(); dec.decrypt_blocks(&iv(), &mut got).unwrap(); diff --git a/rind/tests/crosschecks.rs b/rind/tests/crosschecks.rs index 00ff5620d..43a379ce9 100644 --- a/rind/tests/crosschecks.rs +++ b/rind/tests/crosschecks.rs @@ -176,9 +176,7 @@ mod cipher { }; use proptest::prelude::*; use rind::Error; - use rind::cipher::{ - self, Aes, AesCbcDecryptingKey, AesCbcEncryptingKey, AesCtr, CbcIv, ChaCha20, - }; + use rind::cipher::{self, Aes, AesCbcDecryptingKey, AesCbcEncryptingKey, AesCtr, ChaCha20}; fn aes_key() -> impl Strategy> { prop::sample::select(&[16, 24, 32][..]) @@ -268,7 +266,7 @@ mod cipher { let theirs_dec = PaddedBlockDecryptingKey::cbc_pkcs7(aws_aes_key(&key)).unwrap(); let mut ours = msg.clone(); - enc.encrypt(CbcIv::assume_unpredictable(iv), &mut ours); + enc.encrypt(&iv, &mut ours); let mut theirs = msg.clone(); PaddedBlockEncryptingKey::cbc_pkcs7(aws_aes_key(&key)) .unwrap() @@ -283,7 +281,7 @@ mod cipher { forged[len - 1] = tail.0; forged[len - 2] = if tail.1 == 0 { tail.0 } else { forged[len - 2] }; let mut ct = forged.clone(); - enc.encrypt_blocks(CbcIv::assume_unpredictable(iv), &mut ct).unwrap(); + enc.encrypt_blocks(&iv, &mut ct).unwrap(); let mut ours = ct.clone(); let ours = dec.decrypt(&iv, &mut ours).map(|pt| pt.to_vec()); let mut theirs = ct.clone(); @@ -307,7 +305,7 @@ mod cipher { let mut ours = msg.clone(); AesCbcEncryptingKey::new(algorithm, &key) .unwrap() - .encrypt_blocks(CbcIv::assume_unpredictable(iv), &mut ours) + .encrypt_blocks(&iv, &mut ours) .unwrap(); let mut theirs = msg.clone(); diff --git a/rind/tests/wycheproof.rs b/rind/tests/wycheproof.rs index 8f4f82cc7..08b20fff7 100644 --- a/rind/tests/wycheproof.rs +++ b/rind/tests/wycheproof.rs @@ -6,9 +6,7 @@ use rind::aead::{ AES_128_GCM, AES_192_GCM, AES_256_GCM, CHACHA20_POLY1305, Key, Nonce, RandomNonce, RandomizedNonceKey, Tag, gcm, }; -use rind::cipher::{ - AES_128, AES_192, AES_256, AesCbcDecryptingKey, AesCbcEncryptingKey, CbcIv, ChaCha20, -}; +use rind::cipher::{AES_128, AES_192, AES_256, AesCbcDecryptingKey, AesCbcEncryptingKey, ChaCha20}; use rind::key_wrap::{self, AesKek}; use rind::rsa::{self, DecryptingKey, OaepAlgorithm}; use rind::signature::verify::legacy; @@ -662,7 +660,7 @@ fn test_aes_cbc_pkcs7() { (ExpectedResult::Valid, Ok(pt)) => { assert_eq!(pt, test.msg); let mut ct = test.msg.clone(); - enc.encrypt(CbcIv::assume_unpredictable(iv), &mut ct); + enc.encrypt(&iv, &mut ct); assert_eq!(ct, test.ct); } // Every invalid test is a whole number of blocks whose @@ -677,8 +675,7 @@ fn test_aes_cbc_pkcs7() { // plaintext and encrypts back. let mut in_out = test.ct.clone(); dec.decrypt_blocks(&iv, &mut in_out).unwrap(); - enc.encrypt_blocks(CbcIv::assume_unpredictable(iv), &mut in_out) - .unwrap(); + enc.encrypt_blocks(&iv, &mut in_out).unwrap(); assert_eq!(in_out, test.ct); } } From cc1e28caf79258777c9183c43ef116b79cb1752a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 9 Nov 2026 17:22:47 -0800 Subject: [PATCH 2/5] fix(release): set rind-rustls 0.23.4 and rind-rcgen 0.14.4 by hand release-plz proposed rind-rustls 0.24.0 and rind-rcgen 0.15.0, which the adapter version rule refuses: an adapter's version begins with its upstream's breaking part. Both changes restore upstream's API, so each is a patch release. Co-Authored-By: Claude --- Cargo.lock | 4 ++-- quest/m2/api/cbc-iv.md | 1 + rind-rcgen/Cargo.toml | 2 +- rind-rustls/Cargo.toml | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdef59fd3..a06e45d40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "rind-rcgen" -version = "0.14.3" +version = "0.14.4" dependencies = [ "crabgrind 0.4.0", "rcgen", @@ -3273,7 +3273,7 @@ dependencies = [ [[package]] name = "rind-rustls" -version = "0.23.3" +version = "0.23.4" dependencies = [ "crabgrind 0.4.0", "env_logger", diff --git a/quest/m2/api/cbc-iv.md b/quest/m2/api/cbc-iv.md index 4f10e1cfb..7ac59bf3d 100644 --- a/quest/m2/api/cbc-iv.md +++ b/quest/m2/api/cbc-iv.md @@ -6,6 +6,7 @@ ## Plan +- Landed in [moq-dev/rind#530](https://github.com/moq-dev/rind/pull/530) on 2026-11-10 and reverted the same day (maintainer: ✅ revert it and release 0.3.3 now; rejected: hold the whole release for 0.4; ship 0.4.0 with this change alone). rind's version was hand-set to 0.3.3 and unpublished, so the pending release would have published this break as a patch. Re-land #530's change as it was, once the other two breaking changes are ready, so rind 0.4 carries all three. Until 0.3.3 is on crates.io, merging it repeats the hazard: check `cargo search rind` against `rind/Cargo.toml` first. - 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. diff --git a/rind-rcgen/Cargo.toml b/rind-rcgen/Cargo.toml index 2808b881d..7ed076eb1 100644 --- a/rind-rcgen/Cargo.toml +++ b/rind-rcgen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rind-rcgen" -version = "0.14.3" +version = "0.14.4" edition = "2024" repository = "https://github.com/moq-dev/rind" license = "Apache-2.0 OR ISC OR MIT-0" diff --git a/rind-rustls/Cargo.toml b/rind-rustls/Cargo.toml index f67e2d8f3..8e2c67532 100644 --- a/rind-rustls/Cargo.toml +++ b/rind-rustls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rind-rustls" -version = "0.23.3" +version = "0.23.4" edition = "2024" repository = "https://github.com/moq-dev/rind" license = "Apache-2.0 OR ISC OR MIT-0" From e814faa5856ded95bd6e8de635e7389baf6ee5cd Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 9 Nov 2026 18:18:53 -0800 Subject: [PATCH 3/5] Reapply "feat(cipher)!: AES-CBC encryption takes a typed CbcIv (#530)" This reverts commit c124961a0a9d73f442577f1da515b862ab25b815. --- MIGRATING.md | 6 ++-- TESTING.md | 2 +- fuzz/fuzzer/aes_cbc.rs | 4 +-- quest/m2/api/cbc-iv.md | 12 -------- rind-aws-lc-rs/src/cipher.rs | 12 ++++++-- rind-bench/benches/cipher.rs | 6 ++-- rind-timing/src/cases.rs | 7 +++-- rind/src/cipher.rs | 2 +- rind/src/mid/aes_cbc.rs | 59 +++++++++++++++++++++++++++++------- rind/tests/aws_lc.rs | 5 +-- rind/tests/crosschecks.rs | 10 +++--- rind/tests/wycheproof.rs | 9 ++++-- 12 files changed, 87 insertions(+), 47 deletions(-) delete mode 100644 quest/m2/api/cbc-iv.md diff --git a/MIGRATING.md b/MIGRATING.md index b56ff5c76..ec18171f3 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -123,11 +123,11 @@ protocols with their own integrity protection need. ring has none. |---|---| | `UnboundCipherKey::new(&AES_128, key)` and `EncryptingKey::ctr` or `DecryptingKey::ctr` | `AesCtr::new(&AES_128, key, 128)?`, whose last argument is how many of the counter block's rightmost bits count, as WebCrypto's `length` | | `less_safe_encrypt(in_out, EncryptionContext::Iv128(iv))`, `decrypt` | `apply_keystream(&iv, in_out)?`, with `iv` the `[u8; 16]` counter block | -| `PaddedBlockEncryptingKey::cbc_pkcs7(key)` and `less_safe_encrypt(in_out, EncryptionContext::Iv128(iv))` | `AesCbcEncryptingKey::new(&AES_128, key)?` and `encrypt(&iv, in_out)`, which cannot fail | +| `PaddedBlockEncryptingKey::cbc_pkcs7(key)` and `less_safe_encrypt(in_out, EncryptionContext::Iv128(iv))` | `AesCbcEncryptingKey::new(&AES_128, key)?` and `encrypt(CbcIv::assume_unpredictable(iv), in_out)`, which cannot fail | | `PaddedBlockDecryptingKey::cbc_pkcs7(key)` and `decrypt(in_out, DecryptionContext::Iv128(iv))` | `AesCbcDecryptingKey::new(&AES_128, key)?` and `decrypt(&iv, in_out)?`, which clears `in_out` if the padding is malformed | -| `EncryptingKey::cbc`, `DecryptingKey::cbc`, unpadded | `encrypt_blocks(&iv, in_out)?` and `decrypt_blocks(&iv, in_out)?` on the keys above | +| `EncryptingKey::cbc`, `DecryptingKey::cbc`, unpadded | `encrypt_blocks(CbcIv::assume_unpredictable(iv), in_out)?` and `decrypt_blocks(&iv, in_out)?` on the keys above | | `AES_128`, `AES_192`, `AES_256` | the same | -| `EncryptingKey::encrypt` and `PaddedBlockEncryptingKey::encrypt`, which pick a random IV | none: pick the nonce or IV with `rand::fill` | +| `EncryptingKey::encrypt` and `PaddedBlockEncryptingKey::encrypt`, which pick a random IV | for CBC, `CbcIv::random()?`, read with `as_bytes` before encrypting; for CTR, pick the counter block with `rand::fill` | | `DES_EDE3_FOR_LEGACY_USE_ONLY` and the other DES, the ECB, CFB, and OFB modes, `StreamingEncryptingKey`, `StreamingDecryptingKey` | none; [aws-lc-rs extras](quest/m3/parity/aws-lc-extras.md) | | none | `Aes::new(&alg, key)?.encrypt_block(block)`, one AES block | | none | `ChaCha20::new(&key).apply_keystream(nonce, counter, in_out)?` | diff --git a/TESTING.md b/TESTING.md index 3c7963dbb..9b579e0a2 100644 --- a/TESTING.md +++ b/TESTING.md @@ -106,7 +106,7 @@ constant-time cell is empty by design. | QUIC header protection | `aead::quic::{Algorithm, AES_128, AES_256, CHACHA20, HeaderProtectionKey, SAMPLE_LEN}` | RFC 9001: `aead::quic::rfc9001` | none, [planned](/quest/m3/confidence/fuzz-every-primitive.md) | `crosschecks::cipher::quic_header_protection_mask` | ctgrind: AES's key, as below, and ChaCha20's `mid::chacha20::new`; qemu: `mid::aes::ct_qemu_aes` | | AES block | `cipher::{Aes, Algorithm, AES_128, AES_192, AES_256}` | `cavp/aes`, `aws_lc::aes`, `aws_lc::cipher` | none, [planned](/quest/m3/confidence/fuzz-every-primitive.md) | `crosschecks::cipher::aes_block` | ctgrind: `mid::aes::expand_key`, `mid::aes::encrypt_block`; qemu: `mid::aes::ct_qemu_aes` | | AES-CTR | `cipher::AesCtr` | SP 800-38A: `mid::aes::sp800_38a`, RFC 3686: `mid::aes::rfc3686`, RFC 3711: `mid::aes::srtp_aes_cm`, `aws_lc::cipher` | none, [planned](/quest/m3/confidence/fuzz-every-primitive.md) | `crosschecks::cipher::aes_ctr` | ctgrind: `mid::aes::expand_key`, `mid::aes::apply_keystream`; qemu: `mid::aes::ct_qemu_aes` | -| AES-CBC | `cipher::{AesCbcDecryptingKey, AesCbcEncryptingKey}` | `aes_cbc_pkcs5_test`, SP 800-38A: `mid::aes_cbc::sp800_38a`, `aws_lc::cipher` | `fuzz::aes_cbc` | `crosschecks::cipher::aes_cbc_pkcs7`, `crosschecks::cipher::aes_cbc_unpadded` | ctgrind: the key `mid::aes_cbc::new`; qemu: `mid::aes_cbc::ct_qemu_aes_cbc` | +| AES-CBC | `cipher::{AesCbcDecryptingKey, AesCbcEncryptingKey, CbcIv}` | `aes_cbc_pkcs5_test`, SP 800-38A: `mid::aes_cbc::sp800_38a`, `aws_lc::cipher` | `fuzz::aes_cbc` | `crosschecks::cipher::aes_cbc_pkcs7`, `crosschecks::cipher::aes_cbc_unpadded` | ctgrind: the key `mid::aes_cbc::new`; qemu: `mid::aes_cbc::ct_qemu_aes_cbc` | | ChaCha20 | `cipher::ChaCha20` | `chacha20_counter_test`, RFC 8439: `mid::chacha20::rfc8439` | none, [planned](/quest/m3/confidence/fuzz-every-primitive.md) | `crosschecks::cipher::chacha20_keystream`, `crosschecks::cipher::chacha20_every_length` | ctgrind: the key `mid::chacha20::new`, `low::x86_64::chacha20::paths_match`, `low::arm::chacha_armv4::neon_matches_nohw` | | AES-KW, AES-KWP | `key_wrap::{AesKek, AES_128, AES_192, AES_256}` | `aes_wrap_test`, `aes_kwp_test`, RFC 3394: `mid::aes_kw::rfc3394`, RFC 5649: `mid::aes_kw::rfc5649`, `aws_lc::aes` | `fuzz::key_wrap`, but not AES-192, which aws-lc-rs lacks | `crosschecks::key_wrap::wrap_and_unwrap`, `crosschecks::key_wrap::wrap_and_unwrap_padded` | ctgrind: the key `mid::aes_kw::new`; qemu: `mid::aes_kw::ct_qemu_aes_kw` | | MD5 | `digest::legacy::MD5` | RFC 1321: `mid::md5::rfc1321` | none, [planned](/quest/m3/confidence/fuzz-every-primitive.md) | `crosschecks::hashes_match_aws_lc_rs`, `crosschecks::hashes_every_length` | ctgrind: `high::digest::secret_message` | diff --git a/fuzz/fuzzer/aes_cbc.rs b/fuzz/fuzzer/aes_cbc.rs index 20dabd21a..712c523b9 100644 --- a/fuzz/fuzzer/aes_cbc.rs +++ b/fuzz/fuzzer/aes_cbc.rs @@ -5,7 +5,7 @@ use aws_lc_rs::cipher::{ UnboundCipherKey, }; use libfuzzer_sys::{arbitrary, arbitrary::Arbitrary, fuzz_target}; -use rind::cipher::{AES_128, AES_192, AES_256, AesCbcDecryptingKey, AesCbcEncryptingKey}; +use rind::cipher::{AES_128, AES_192, AES_256, AesCbcDecryptingKey, AesCbcEncryptingKey, CbcIv}; mod edit; @@ -22,7 +22,7 @@ fuzz_target!(|input: (Operation, [u8; 16], Vec, edit::Edit)| { let mut ct = msg.clone(); AesCbcEncryptingKey::new(ours, key) .unwrap() - .encrypt(&iv, &mut ct); + .encrypt(CbcIv::assume_unpredictable(iv), &mut ct); let mut right = msg.clone(); PaddedBlockEncryptingKey::cbc_pkcs7(theirs()) .unwrap() diff --git a/quest/m2/api/cbc-iv.md b/quest/m2/api/cbc-iv.md deleted file mode 100644 index 7ac59bf3d..000000000 --- a/quest/m2/api/cbc-iv.md +++ /dev/null @@ -1,12 +0,0 @@ -# [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 - -- Landed in [moq-dev/rind#530](https://github.com/moq-dev/rind/pull/530) on 2026-11-10 and reverted the same day (maintainer: ✅ revert it and release 0.3.3 now; rejected: hold the whole release for 0.4; ship 0.4.0 with this change alone). rind's version was hand-set to 0.3.3 and unpublished, so the pending release would have published this break as a patch. Re-land #530's change as it was, once the other two breaking changes are ready, so rind 0.4 carries all three. Until 0.3.3 is on crates.io, merging it repeats the hazard: check `cargo search rind` against `rind/Cargo.toml` first. -- 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. diff --git a/rind-aws-lc-rs/src/cipher.rs b/rind-aws-lc-rs/src/cipher.rs index f3bebe123..c9421cab7 100644 --- a/rind-aws-lc-rs/src/cipher.rs +++ b/rind-aws-lc-rs/src/cipher.rs @@ -296,7 +296,7 @@ impl EncryptingKey { let iv = context.iv()?; match &self.key { Encrypting::Cbc(key) => key - .encrypt_blocks(iv, in_out) + .encrypt_blocks(rind::cipher::CbcIv::assume_unpredictable(*iv), in_out) .map_err(crate::error::unspecified)?, Encrypting::Ctr(key) => key .apply_keystream(iv, in_out) @@ -433,7 +433,10 @@ impl PaddedBlockEncryptingKey { where InOut: AsMut<[u8]> + for<'in_out> Extend<&'in_out u8>, { - self.less_safe_encrypt(in_out, EncryptionContext::Iv128(FixedLength::new()?)) + let iv = rind::cipher::CbcIv::random().map_err(crate::error::unspecified)?; + let context = EncryptionContext::Iv128((*iv.as_bytes()).into()); + self.key.encrypt(iv, in_out); + Ok(context.into()) } /// Pads `in_out` and encrypts it in place under the caller's IV, which @@ -446,7 +449,10 @@ impl PaddedBlockEncryptingKey { where InOut: AsMut<[u8]> + for<'in_out> Extend<&'in_out u8>, { - self.key.encrypt(context.iv()?, in_out); + self.key.encrypt( + rind::cipher::CbcIv::assume_unpredictable(*context.iv()?), + in_out, + ); Ok(context.into()) } } diff --git a/rind-bench/benches/cipher.rs b/rind-bench/benches/cipher.rs index 0b4f683a9..d3e8acb07 100644 --- a/rind-bench/benches/cipher.rs +++ b/rind-bench/benches/cipher.rs @@ -49,12 +49,12 @@ fn bench_aes_cbc(c: &mut Criterion) { for op in ["encrypt", "decrypt"] { let mut group = c.benchmark_group(format!("{name}-{op}")); for (size, size_name) in criterion::SIZES { - use rind::cipher::{AesCbcDecryptingKey, AesCbcEncryptingKey}; + use rind::cipher::{AesCbcDecryptingKey, AesCbcEncryptingKey, CbcIv}; let algorithm = rind_aes(key_len); let mut ct = vec![0u8; size]; AesCbcEncryptingKey::new(algorithm, key) .unwrap() - .encrypt(&iv, &mut ct); + .encrypt(CbcIv::assume_unpredictable(iv), &mut ct); let input = if op == "encrypt" { vec![0u8; size] } else { ct }; let mut buf = Vec::with_capacity(size + 16); group.throughput(Throughput::Bytes(size as u64)); @@ -91,7 +91,7 @@ fn bench_aes_cbc(c: &mut Criterion) { buf.clear(); buf.extend_from_slice(&input); if op == "encrypt" { - enc.encrypt(&iv, &mut buf); + enc.encrypt(CbcIv::assume_unpredictable(iv), &mut buf); } else { dec.decrypt(&iv, &mut buf).unwrap(); } diff --git a/rind-timing/src/cases.rs b/rind-timing/src/cases.rs index 345786f5d..776f5b319 100644 --- a/rind-timing/src/cases.rs +++ b/rind-timing/src/cases.rs @@ -327,7 +327,8 @@ fn cipher_cases(cases: &mut Vec) { let plaintexts = classes(VALUES, fixed(random::<256>()), random::<256>); measure(config, &plaintexts, |plaintext| { let mut buf = *plaintext; - key.encrypt_blocks(&iv, &mut buf).map(|()| buf) + key.encrypt_blocks(cipher::CbcIv::assume_unpredictable(iv), &mut buf) + .map(|()| buf) }) })); cases.push(case("cipher/aes-128-cbc/decrypt-key", move |config| { @@ -352,7 +353,9 @@ fn cipher_cases(cases: &mut Vec) { let mut buf = [16; 64]; buf[..48].copy_from_slice(&prefix); buf[48 + at] ^= random::<1>()[0] | 1; - encrypt.encrypt_blocks(&iv, &mut buf).unwrap(); + encrypt + .encrypt_blocks(cipher::CbcIv::assume_unpredictable(iv), &mut buf) + .unwrap(); buf }; let anywhere = || wrong_at(usize::from(random::<1>()[0]) % 15); diff --git a/rind/src/cipher.rs b/rind/src/cipher.rs index a7dd8a0b0..1d6d0cb6a 100644 --- a/rind/src/cipher.rs +++ b/rind/src/cipher.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0 pub use crate::mid::aes::{Aes, AesCtr}; -pub use crate::mid::aes_cbc::{AesCbcDecryptingKey, AesCbcEncryptingKey}; +pub use crate::mid::aes_cbc::{AesCbcDecryptingKey, AesCbcEncryptingKey, CbcIv}; pub use crate::mid::chacha20::ChaCha20; /// An AES key size, which fixes the key length a key must have. diff --git a/rind/src/mid/aes_cbc.rs b/rind/src/mid/aes_cbc.rs index 8d0a132a6..4c9db2839 100644 --- a/rind/src/mid/aes_cbc.rs +++ b/rind/src/mid/aes_cbc.rs @@ -17,7 +17,7 @@ use crate::low::{self, AesDecKey, AesKey, Entry, Zeroable, aes_cbc, zeroise_valu /// with each message, as encrypt-then-MAC and RFC 7518's `A128CBC-HS256` /// do, or use [`crate::aead`] instead. /// -/// Each message needs a fresh random IV from [`crate::rand::fill`]: an IV +/// Each message needs a fresh [`CbcIv`], normally [`CbcIv::random`]: an IV /// an attacker can predict, such as a counter, leaks whether a plaintext /// block equals one they chose. pub struct AesCbcEncryptingKey { @@ -41,26 +41,26 @@ impl AesCbcEncryptingKey { /// Pads `in_out` with PKCS#7, 1 to 16 bytes each holding the count, and /// encrypts it in place under `iv`. - pub fn encrypt(&self, iv: &[u8; 16], in_out: &mut InOut) + pub fn encrypt(&self, iv: CbcIv, in_out: &mut InOut) where InOut: AsMut<[u8]> + for<'a> Extend<&'a u8>, { let _entry = Entry::new_secret(); let pad = 16 - in_out.as_mut().len() % 16; in_out.extend(&[pad as u8; 16][..pad]); - self.encrypt_in_place(iv, in_out.as_mut().as_chunks_mut().0); + self.encrypt_in_place(&iv.0, in_out.as_mut().as_chunks_mut().0); } /// Encrypts `in_out` in place under `iv`, without padding. /// /// Fails with [`Error::WrongLength`], leaving `in_out` unchanged, if /// `in_out` is not a whole number of 16-byte blocks. - pub fn encrypt_blocks(&self, iv: &[u8; 16], in_out: &mut [u8]) -> Result<(), Error> { + pub fn encrypt_blocks(&self, iv: CbcIv, in_out: &mut [u8]) -> Result<(), Error> { let _entry = Entry::new_secret(); let (blocks, []) = in_out.as_chunks_mut() else { return Err(Error::WrongLength); }; - self.encrypt_in_place(iv, blocks); + self.encrypt_in_place(&iv.0, blocks); Ok(()) } @@ -71,6 +71,41 @@ impl AesCbcEncryptingKey { } } +/// A CBC IV that encrypts one message: random, or one the caller promises an +/// attacker cannot predict. +/// +/// It is not `Clone`, so one value encrypts at most one message. Read it with +/// [`CbcIv::as_bytes`] before encrypting to send it alongside the ciphertext. +pub struct CbcIv([u8; 16]); + +impl CbcIv { + /// Draws an IV from the system's secure random source. + /// + /// Fails with [`Error::RngFailed`] if the system random source does. + pub fn random() -> Result { + let mut iv = [0; 16]; + crate::rand::fill(&mut iv)?; + Ok(Self(iv)) + } + + /// Wraps `iv`, which the caller promises an attacker cannot predict: + /// never a constant, zero, or counter. + pub fn assume_unpredictable(iv: [u8; 16]) -> Self { + Self(iv) + } + + /// The IV's bytes, which the receiver needs to decrypt. + pub fn as_bytes(&self) -> &[u8; 16] { + &self.0 + } +} + +impl fmt::Debug for CbcIv { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("CbcIv").finish_non_exhaustive() + } +} + /// An AES key that decrypts in CBC mode; wiped on drop. /// /// Decryption is unauthenticated: check the MAC over the IV and ciphertext @@ -232,14 +267,15 @@ mod tests { ] { let (enc, dec) = keys(&hex::decode(key).unwrap()); let mut buf = pt.clone(); - enc.encrypt_blocks(iv, &mut buf).unwrap(); + enc.encrypt_blocks(CbcIv::assume_unpredictable(*iv), &mut buf) + .unwrap(); assert_eq!(hex::encode(&buf), ct); dec.decrypt_blocks(iv, &mut buf).unwrap(); assert_eq!(buf, pt); // padded, the same blocks then one of padding let mut buf = pt.clone(); - enc.encrypt(iv, &mut buf); + enc.encrypt(CbcIv::assume_unpredictable(*iv), &mut buf); assert_eq!(hex::encode(&buf[..64]), ct); assert_eq!(buf.len(), 80); assert_eq!(dec.decrypt(iv, &mut buf).unwrap(), pt); @@ -287,13 +323,13 @@ mod tests { for len in 0..=150usize { let msg: Vec = (0..len as u8).collect(); let mut buf = msg.clone(); - enc.encrypt(&iv, &mut buf); + enc.encrypt(CbcIv::assume_unpredictable(iv), &mut buf); assert_eq!(buf.len(), (len / 16 + 1) * 16, "{len}"); assert_eq!(dec.decrypt(&iv, &mut buf).unwrap(), msg, "{len}"); let whole = len.is_multiple_of(16); let mut buf = msg.clone(); - let result = enc.encrypt_blocks(&iv, &mut buf); + let result = enc.encrypt_blocks(CbcIv::assume_unpredictable(iv), &mut buf); assert_eq!(result.is_ok(), whole, "{len}"); if !whole { assert_eq!(buf, msg, "refused encrypt wrote"); @@ -319,7 +355,8 @@ mod tests { let iv = [0x24; 16]; let check = |padded: &[u8]| { let mut buf = padded.to_vec(); - enc.encrypt_blocks(&iv, &mut buf).unwrap(); + enc.encrypt_blocks(CbcIv::assume_unpredictable(iv), &mut buf) + .unwrap(); let result = dec.decrypt(&iv, &mut buf).map(|pt| pt.len()); if result.is_err() { assert!(buf.iter().all(|b| *b == 0), "failure left plaintext"); @@ -366,7 +403,7 @@ mod tests { let iv = bytes(16).try_into().unwrap(); let mut msg = bytes(6 * 16 + 5); - ct_qemu::traced(|| enc.encrypt(&iv, &mut msg)); + ct_qemu::traced(|| enc.encrypt(CbcIv::assume_unpredictable(iv), &mut msg)); ct_qemu::traced(|| dec.decrypt(&iv, &mut msg)).unwrap(); } } diff --git a/rind/tests/aws_lc.rs b/rind/tests/aws_lc.rs index 4903a177a..c7f82e895 100644 --- a/rind/tests/aws_lc.rs +++ b/rind/tests/aws_lc.rs @@ -10,7 +10,7 @@ use rind::aead::{ }; use rind::agreement::{self, ECDH_P256, ECDH_P384, ECDH_P521}; use rind::cipher::{ - AES_128, AES_192, AES_256, Aes, AesCbcDecryptingKey, AesCbcEncryptingKey, AesCtr, + AES_128, AES_192, AES_256, Aes, AesCbcDecryptingKey, AesCbcEncryptingKey, AesCtr, CbcIv, }; use rind::hmac; use rind::key_wrap::AesKek; @@ -210,7 +210,8 @@ fn cipher() { m if m.ends_with("-CBC") => { let mut got = pt.clone(); let enc = AesCbcEncryptingKey::new(algorithm(), &key).unwrap(); - enc.encrypt_blocks(&iv(), &mut got).unwrap(); + enc.encrypt_blocks(CbcIv::assume_unpredictable(iv()), &mut got) + .unwrap(); assert_eq!(got, ct, "{test}"); let dec = AesCbcDecryptingKey::new(algorithm(), &key).unwrap(); dec.decrypt_blocks(&iv(), &mut got).unwrap(); diff --git a/rind/tests/crosschecks.rs b/rind/tests/crosschecks.rs index 43a379ce9..00ff5620d 100644 --- a/rind/tests/crosschecks.rs +++ b/rind/tests/crosschecks.rs @@ -176,7 +176,9 @@ mod cipher { }; use proptest::prelude::*; use rind::Error; - use rind::cipher::{self, Aes, AesCbcDecryptingKey, AesCbcEncryptingKey, AesCtr, ChaCha20}; + use rind::cipher::{ + self, Aes, AesCbcDecryptingKey, AesCbcEncryptingKey, AesCtr, CbcIv, ChaCha20, + }; fn aes_key() -> impl Strategy> { prop::sample::select(&[16, 24, 32][..]) @@ -266,7 +268,7 @@ mod cipher { let theirs_dec = PaddedBlockDecryptingKey::cbc_pkcs7(aws_aes_key(&key)).unwrap(); let mut ours = msg.clone(); - enc.encrypt(&iv, &mut ours); + enc.encrypt(CbcIv::assume_unpredictable(iv), &mut ours); let mut theirs = msg.clone(); PaddedBlockEncryptingKey::cbc_pkcs7(aws_aes_key(&key)) .unwrap() @@ -281,7 +283,7 @@ mod cipher { forged[len - 1] = tail.0; forged[len - 2] = if tail.1 == 0 { tail.0 } else { forged[len - 2] }; let mut ct = forged.clone(); - enc.encrypt_blocks(&iv, &mut ct).unwrap(); + enc.encrypt_blocks(CbcIv::assume_unpredictable(iv), &mut ct).unwrap(); let mut ours = ct.clone(); let ours = dec.decrypt(&iv, &mut ours).map(|pt| pt.to_vec()); let mut theirs = ct.clone(); @@ -305,7 +307,7 @@ mod cipher { let mut ours = msg.clone(); AesCbcEncryptingKey::new(algorithm, &key) .unwrap() - .encrypt_blocks(&iv, &mut ours) + .encrypt_blocks(CbcIv::assume_unpredictable(iv), &mut ours) .unwrap(); let mut theirs = msg.clone(); diff --git a/rind/tests/wycheproof.rs b/rind/tests/wycheproof.rs index 08b20fff7..8f4f82cc7 100644 --- a/rind/tests/wycheproof.rs +++ b/rind/tests/wycheproof.rs @@ -6,7 +6,9 @@ use rind::aead::{ AES_128_GCM, AES_192_GCM, AES_256_GCM, CHACHA20_POLY1305, Key, Nonce, RandomNonce, RandomizedNonceKey, Tag, gcm, }; -use rind::cipher::{AES_128, AES_192, AES_256, AesCbcDecryptingKey, AesCbcEncryptingKey, ChaCha20}; +use rind::cipher::{ + AES_128, AES_192, AES_256, AesCbcDecryptingKey, AesCbcEncryptingKey, CbcIv, ChaCha20, +}; use rind::key_wrap::{self, AesKek}; use rind::rsa::{self, DecryptingKey, OaepAlgorithm}; use rind::signature::verify::legacy; @@ -660,7 +662,7 @@ fn test_aes_cbc_pkcs7() { (ExpectedResult::Valid, Ok(pt)) => { assert_eq!(pt, test.msg); let mut ct = test.msg.clone(); - enc.encrypt(&iv, &mut ct); + enc.encrypt(CbcIv::assume_unpredictable(iv), &mut ct); assert_eq!(ct, test.ct); } // Every invalid test is a whole number of blocks whose @@ -675,7 +677,8 @@ fn test_aes_cbc_pkcs7() { // plaintext and encrypts back. let mut in_out = test.ct.clone(); dec.decrypt_blocks(&iv, &mut in_out).unwrap(); - enc.encrypt_blocks(&iv, &mut in_out).unwrap(); + enc.encrypt_blocks(CbcIv::assume_unpredictable(iv), &mut in_out) + .unwrap(); assert_eq!(in_out, test.ct); } } From 4765360fdba489a9d34b8464da15ce1e4e15a5e6 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 9 Nov 2026 18:19:28 -0800 Subject: [PATCH 4/5] fix(release): set rind 0.4.0, rind-ring 0.17.4, and rind-str0m 0.25.3 by hand main holds two breaking rind changes at a hand-set 0.3.3, which the release PR would publish as a patch. Setting 0.4.0 makes them, and any later one, publish under the right version. The adapters require rind 0.4.0 and keep their upstream's version prefix. Co-Authored-By: Claude --- Cargo.lock | 6 +++--- README.md | 4 ++-- rind-aws-lc-rs/Cargo.toml | 2 +- rind-dart/Cargo.toml | 2 +- rind-dimpl/Cargo.toml | 2 +- rind-jsonwebtoken/Cargo.toml | 2 +- rind-object-store/Cargo.toml | 2 +- rind-python/Cargo.toml | 2 +- rind-rcgen/Cargo.toml | 2 +- rind-ring/Cargo.toml | 4 ++-- rind-rustls/Cargo.toml | 2 +- rind-str0m/Cargo.toml | 4 ++-- rind/Cargo.toml | 2 +- 13 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a634b419..dbd943e89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3081,7 +3081,7 @@ dependencies = [ [[package]] name = "rind" -version = "0.3.3" +version = "0.4.0" dependencies = [ "aws-lc-rs", "aws-lc-sys", @@ -3260,7 +3260,7 @@ dependencies = [ [[package]] name = "rind-ring" -version = "0.17.3" +version = "0.17.4" dependencies = [ "aws-lc-rs", "crabgrind 0.4.0", @@ -3292,7 +3292,7 @@ dependencies = [ [[package]] name = "rind-str0m" -version = "0.25.2" +version = "0.25.3" dependencies = [ "crabgrind 0.4.0", "dimpl", diff --git a/README.md b/README.md index 478f5d489..2eaf22bee 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ ring = { package = "rind-ring", version = "0.17" } aws-lc-rs = { package = "rind-aws-lc-rs", version = "1" } # Or use rind's own API. -rind = "0.3" +rind = "0.4" ``` ### With rustls @@ -143,7 +143,7 @@ and wipes the adapter's own copies of secrets. ```toml [dependencies] -rind = { version = "0.3", features = ["wipe"] } +rind = { version = "0.4", features = ["wipe"] } ``` ## Data-independent timing diff --git a/rind-aws-lc-rs/Cargo.toml b/rind-aws-lc-rs/Cargo.toml index a5eedd34e..b0e1c6236 100644 --- a/rind-aws-lc-rs/Cargo.toml +++ b/rind-aws-lc-rs/Cargo.toml @@ -39,7 +39,7 @@ unstable = [] wipe = ["rind/wipe"] [dependencies] -rind = { version = "0.3.2", path = "../rind" } +rind = { version = "0.4.0", path = "../rind" } untrusted = "0.7.1" zeroize = "1.9" diff --git a/rind-dart/Cargo.toml b/rind-dart/Cargo.toml index a82ccf890..5f40f3189 100644 --- a/rind-dart/Cargo.toml +++ b/rind-dart/Cargo.toml @@ -17,7 +17,7 @@ name = "rind_dart" crate-type = ["cdylib"] [dependencies] -rind = { version = "0.3.2", path = "../rind" } +rind = { version = "0.4.0", path = "../rind" } [dev-dependencies] # Requests reach valgrind only once `rind/__ctgrind` turns on crabgrind's default diff --git a/rind-dimpl/Cargo.toml b/rind-dimpl/Cargo.toml index 68ec25d65..c0aa60324 100644 --- a/rind-dimpl/Cargo.toml +++ b/rind-dimpl/Cargo.toml @@ -15,7 +15,7 @@ readme = "README.md" # Without it a custom provider cannot enforce that, and it adds only the DER # parsers this crate already needs to read certificates. dimpl = { version = "0.9", default-features = false, features = ["_crypto-common"] } -rind = { version = "0.3.2", path = "../rind" } +rind = { version = "0.4.0", path = "../rind" } x509-cert = { version = "0.3", default-features = false } [dev-dependencies] diff --git a/rind-jsonwebtoken/Cargo.toml b/rind-jsonwebtoken/Cargo.toml index c03854368..25868772f 100644 --- a/rind-jsonwebtoken/Cargo.toml +++ b/rind-jsonwebtoken/Cargo.toml @@ -10,7 +10,7 @@ rust-version.workspace = true readme = "README.md" [dependencies] -rind = { version = "0.3.2", path = "../rind" } +rind = { version = "0.4.0", path = "../rind" } jsonwebtoken = { version = "11.1", default-features = false } pkcs1 = { version = "0.7.5", default-features = false, features = ["alloc"] } diff --git a/rind-object-store/Cargo.toml b/rind-object-store/Cargo.toml index fa2e26e2c..4d58cd8c1 100644 --- a/rind-object-store/Cargo.toml +++ b/rind-object-store/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" # object_store exposes its CryptoProvider only with a cloud backend enabled; # gcp-base is the one that adds no dependencies beyond the shared cloud-base. object_store = { version = "0.14", default-features = false, features = ["gcp-base"] } -rind = { version = "0.3.2", path = "../rind" } +rind = { version = "0.4.0", path = "../rind" } rustls-pki-types = { version = "1.9", default-features = false, features = ["std"] } [dev-dependencies] diff --git a/rind-python/Cargo.toml b/rind-python/Cargo.toml index 078bf9673..3974e2bd7 100644 --- a/rind-python/Cargo.toml +++ b/rind-python/Cargo.toml @@ -21,7 +21,7 @@ test = false doctest = false [dependencies] -rind = { version = "0.3.2", path = "../rind" } +rind = { version = "0.4.0", path = "../rind" } # The stable ABI from 3.11, the oldest CPython still supported and the first # whose stable ABI has the buffer protocol, so one wheel per platform covers # every CPython since. diff --git a/rind-rcgen/Cargo.toml b/rind-rcgen/Cargo.toml index a470367dd..dcf7f57be 100644 --- a/rind-rcgen/Cargo.toml +++ b/rind-rcgen/Cargo.toml @@ -22,7 +22,7 @@ zeroize = [] [dependencies] base64ct = { version = "1.8", optional = true, features = ["alloc"] } rcgen = { version = "0.14", default-features = false } -rind = { version = "0.3.2", path = "../rind" } +rind = { version = "0.4.0", path = "../rind" } rustls-pki-types = "1.12" zeroize = "1.9" diff --git a/rind-ring/Cargo.toml b/rind-ring/Cargo.toml index 29564e264..dfa1a0685 100644 --- a/rind-ring/Cargo.toml +++ b/rind-ring/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rind-ring" -version = "0.17.3" +version = "0.17.4" edition = "2024" repository = "https://github.com/moq-dev/rind" license = "Apache-2.0 OR ISC OR MIT-0" @@ -26,7 +26,7 @@ dev_urandom_fallback = [] std = ["alloc"] [dependencies] -rind = { version = "0.3.2", path = "../rind" } +rind = { version = "0.4.0", path = "../rind" } untrusted = "0.9" [dev-dependencies] diff --git a/rind-rustls/Cargo.toml b/rind-rustls/Cargo.toml index 8e2c67532..fcd403539 100644 --- a/rind-rustls/Cargo.toml +++ b/rind-rustls/Cargo.toml @@ -10,7 +10,7 @@ rust-version.workspace = true readme = "README.md" [dependencies] -rind = { version = "0.3.2", path = "../rind" } +rind = { version = "0.4.0", path = "../rind" } rustls = { version = "0.23.20", default-features = false, features = ["std", "tls12"] } [dev-dependencies] diff --git a/rind-str0m/Cargo.toml b/rind-str0m/Cargo.toml index 71032bebd..106c7d928 100644 --- a/rind-str0m/Cargo.toml +++ b/rind-str0m/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rind-str0m" -version = "0.25.2" +version = "0.25.3" edition = "2024" repository = "https://github.com/moq-dev/rind" license = "Apache-2.0 OR ISC OR MIT-0" @@ -12,7 +12,7 @@ readme = "README.md" [dependencies] dimpl = { version = "0.9", default-features = false } rcgen = { version = "0.14", default-features = false } -rind = { version = "0.3.2", path = "../rind" } +rind = { version = "0.4.0", path = "../rind" } rind-dimpl = { version = "0.9.3", path = "../rind-dimpl" } rind-rcgen = { version = "0.14.3", path = "../rind-rcgen" } # Its default features compile aws-lc-rs. diff --git a/rind/Cargo.toml b/rind/Cargo.toml index 8710622b0..fa3c4bfad 100644 --- a/rind/Cargo.toml +++ b/rind/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rind" -version = "0.3.3" +version = "0.4.0" edition = "2024" repository = "https://github.com/moq-dev/rind" license = "(Apache-2.0 OR ISC OR MIT-0) AND (Apache-2.0 OR ISC OR MIT) AND Apache-2.0 AND ISC AND BSD-3-Clause" From 301b408ced141ff311d78c48086d435c64e4d0fa Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:42:42 -0800 Subject: [PATCH 5/5] fix(release): return rind to the published 0.3.2 so release-plz proposes 0.4.0 Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- README.md | 4 ++-- quest/m0/adapters/README.md | 2 +- quest/m2/api/release-0.4.md | 4 ++-- rind-aws-lc-rs/Cargo.toml | 2 +- rind-dart/Cargo.toml | 2 +- rind-dimpl/Cargo.toml | 2 +- rind-jsonwebtoken/Cargo.toml | 2 +- rind-object-store/Cargo.toml | 2 +- rind-python/Cargo.toml | 2 +- rind-rcgen/Cargo.toml | 2 +- rind-ring/Cargo.toml | 2 +- rind-rustls/Cargo.toml | 2 +- rind-str0m/Cargo.toml | 2 +- rind/Cargo.toml | 2 +- 15 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dbd943e89..eb446bf7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3081,7 +3081,7 @@ dependencies = [ [[package]] name = "rind" -version = "0.4.0" +version = "0.3.2" dependencies = [ "aws-lc-rs", "aws-lc-sys", diff --git a/README.md b/README.md index ca76bab5c..951eddfd9 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ ring = { package = "rind-ring", version = "0.17" } aws-lc-rs = { package = "rind-aws-lc-rs", version = "1" } # Or use rind's own API. -rind = "0.4" +rind = "0.3" ``` ### With rustls @@ -144,7 +144,7 @@ and wipes the adapter's own copies of secrets. ```toml [dependencies] -rind = { version = "0.4", features = ["wipe"] } +rind = { version = "0.3", features = ["wipe"] } ``` ## Data-independent timing diff --git a/quest/m0/adapters/README.md b/quest/m0/adapters/README.md index 6f787bba5..cf2b3fb36 100644 --- a/quest/m0/adapters/README.md +++ b/quest/m0/adapters/README.md @@ -20,4 +20,4 @@ Decided with the maintainer on 2026-11-09: - Releases: one rind 0.4 that carries all three breaking changes, and a MoQ-side PR ready before the adapter patch releases publish ✅ (rejected: the 0.4 quest alone, accepting that MoQ's build breaks until fixed; leaving both to release-plz). rind 0.4 is in [rind 0.4 ships once](/quest/m2/api/release-0.4.md). - The children change calls MoQ's `main` makes: `default_provider()` in rind-rustls, rind-jsonwebtoken, and rind-object-store, and `KeyPair::generate` and `to_pkcs8` in rind-rcgen. MoQ requires those adapters by caret (moq-dev/moq's `Cargo.toml:218-221`), so its first lockfile update after the patch releases stops building until its calls change. No crate MoQ has published names a rind adapter yet, so only `main` and its next cut are exposed. - The adapter patch releases therefore publish only once MoQ's PR adapting those calls is ready to merge. Publishing happens only when a release PR merges (`CONTRIBUTING.md:138`), so the maintainer leaves the release PR that carries them unmerged until then, and MoQ's PR merges the day they publish. That PR is a quest in moq-dev/moq's own tree. -- Since rind was set to 0.4.0 by hand on 2026-11-09 ([moq-dev/rind#548](https://github.com/moq-dev/rind/pull/548)), these fixes publish in the same release as rind 0.4, each adapter requiring it, so MoQ's PR moves to both at once. +- Since rind 0.4 is pending on `main` ([moq-dev/rind#548](https://github.com/moq-dev/rind/pull/548) leaves release-plz to propose it), these fixes publish in the same release as rind 0.4, each adapter requiring it, so MoQ's PR moves to both at once. diff --git a/quest/m2/api/release-0.4.md b/quest/m2/api/release-0.4.md index e9be03471..10842e1dc 100644 --- a/quest/m2/api/release-0.4.md +++ b/quest/m2/api/release-0.4.md @@ -9,7 +9,7 @@ rind 0.4.0 on crates.io carries all three of the API epic's breaking changes: th Decided with the maintainer on 2026-11-09: - Releases: one rind 0.4 that carries all three breaking changes, and a MoQ-side PR ready before the adapter fixes publish ✅ (rejected: the 0.4 quest alone, accepting that MoQ's build breaks until fixed; leaving both to release-plz). Three breaking releases would re-pin every adapter three times, and each would leave a consumer that pins rind 0.3 with two copies of rind until it follows. -- Two of the three merged the same day, while rind's version was hand-set to 0.3.3 and unpublished: the typed CBC IV ([moq-dev/rind#530](https://github.com/moq-dev/rind/pull/530)) and the agreement keys ([moq-dev/rind#529](https://github.com/moq-dev/rind/pull/529)). A version set by hand ahead of the published one is kept as is (`CONTRIBUTING.md:146`), so release PR [moq-dev/rind#511](https://github.com/moq-dev/rind/pull/511) proposed publishing both as rind 0.3.3. The maintainer chose ✅ to set rind to 0.4.0 by hand and hold the release for the last change (rejected: set 0.4.0 and ship it now, with the CPU-check removal as 0.5; revert both and release 0.3.3). [moq-dev/rind#548](https://github.com/moq-dev/rind/pull/548) sets it, so every later `!` change to rind publishes under the right version. +- Two of the three merged the same day, while rind's version was hand-set to 0.3.3 and unpublished: the typed CBC IV ([moq-dev/rind#530](https://github.com/moq-dev/rind/pull/530)) and the agreement keys ([moq-dev/rind#529](https://github.com/moq-dev/rind/pull/529)). A version set by hand ahead of the published one is kept as is (`CONTRIBUTING.md:146`), so release PR [moq-dev/rind#511](https://github.com/moq-dev/rind/pull/511) proposed publishing both as rind 0.3.3. On 2026-11-10 the maintainer chose ✅ to return rind's manifest to the published 0.3.2, so release-plz proposes 0.4.0 itself from the `!` commits, and hold the release for the last change (rejected: hand-set 0.4.0; set 0.4.0 and ship it now, with the CPU-check removal as 0.5; revert both and release 0.3.3). [moq-dev/rind#548](https://github.com/moq-dev/rind/pull/548) sets it; any later `!` change to rind then lands inside the same 0.4.0. - So the adapter fixes of [Adapters offer exactly their upstream's API](/quest/m0/adapters/README.md) publish in the same release as rind 0.4, not before it, and MoQ's PR moves to both at once. What remains is [rind needs no runtime CPU check](/quest/m2/api/cpu-check.md), which waits on that epic. @@ -19,7 +19,7 @@ The hold uses only what the release tooling has. Publishing happens only when a Before the release PR merges, check what it proposes: - `rind` 0.4.0. -- Each adapter at its next patch version, requiring rind 0.4.0. A `!` commit or a removed item makes release-plz propose an adapter's next breaking version, which `sh/adapter-versions.sh` fails (`CONTRIBUTING.md:144`). #548 set rind-rustls, rind-rcgen, rind-ring, and rind-str0m by hand for the changes merged by then; a later adapter PR with a `!` title sets its own patch version, as [moq-dev/rind#522](https://github.com/moq-dev/rind/pull/522) did. +- Each adapter at its next patch version, requiring rind 0.4.0. A `!` commit or a removed item makes release-plz propose an adapter's next breaking version, which `sh/adapter-versions.sh` fails (`CONTRIBUTING.md:144`). [moq-dev/rind#548](https://github.com/moq-dev/rind/pull/548) set rind-rustls, rind-rcgen, rind-ring, and rind-str0m by hand for the changes merged by then; a later adapter PR with a `!` title sets its own patch version, as [moq-dev/rind#522](https://github.com/moq-dev/rind/pull/522) did. - No breaking version for an adapter from the rind 0.4 requirement alone: its API did not change, so set its patch version by hand if release-plz proposes one. Each change documents its own migration in MIGRATING.md, so this quest adds none. Delete it once 0.4.0 is published. diff --git a/rind-aws-lc-rs/Cargo.toml b/rind-aws-lc-rs/Cargo.toml index b0e1c6236..a5eedd34e 100644 --- a/rind-aws-lc-rs/Cargo.toml +++ b/rind-aws-lc-rs/Cargo.toml @@ -39,7 +39,7 @@ unstable = [] wipe = ["rind/wipe"] [dependencies] -rind = { version = "0.4.0", path = "../rind" } +rind = { version = "0.3.2", path = "../rind" } untrusted = "0.7.1" zeroize = "1.9" diff --git a/rind-dart/Cargo.toml b/rind-dart/Cargo.toml index 5f40f3189..a82ccf890 100644 --- a/rind-dart/Cargo.toml +++ b/rind-dart/Cargo.toml @@ -17,7 +17,7 @@ name = "rind_dart" crate-type = ["cdylib"] [dependencies] -rind = { version = "0.4.0", path = "../rind" } +rind = { version = "0.3.2", path = "../rind" } [dev-dependencies] # Requests reach valgrind only once `rind/__ctgrind` turns on crabgrind's default diff --git a/rind-dimpl/Cargo.toml b/rind-dimpl/Cargo.toml index c0aa60324..68ec25d65 100644 --- a/rind-dimpl/Cargo.toml +++ b/rind-dimpl/Cargo.toml @@ -15,7 +15,7 @@ readme = "README.md" # Without it a custom provider cannot enforce that, and it adds only the DER # parsers this crate already needs to read certificates. dimpl = { version = "0.9", default-features = false, features = ["_crypto-common"] } -rind = { version = "0.4.0", path = "../rind" } +rind = { version = "0.3.2", path = "../rind" } x509-cert = { version = "0.3", default-features = false } [dev-dependencies] diff --git a/rind-jsonwebtoken/Cargo.toml b/rind-jsonwebtoken/Cargo.toml index 25868772f..c03854368 100644 --- a/rind-jsonwebtoken/Cargo.toml +++ b/rind-jsonwebtoken/Cargo.toml @@ -10,7 +10,7 @@ rust-version.workspace = true readme = "README.md" [dependencies] -rind = { version = "0.4.0", path = "../rind" } +rind = { version = "0.3.2", path = "../rind" } jsonwebtoken = { version = "11.1", default-features = false } pkcs1 = { version = "0.7.5", default-features = false, features = ["alloc"] } diff --git a/rind-object-store/Cargo.toml b/rind-object-store/Cargo.toml index 4d58cd8c1..fa2e26e2c 100644 --- a/rind-object-store/Cargo.toml +++ b/rind-object-store/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" # object_store exposes its CryptoProvider only with a cloud backend enabled; # gcp-base is the one that adds no dependencies beyond the shared cloud-base. object_store = { version = "0.14", default-features = false, features = ["gcp-base"] } -rind = { version = "0.4.0", path = "../rind" } +rind = { version = "0.3.2", path = "../rind" } rustls-pki-types = { version = "1.9", default-features = false, features = ["std"] } [dev-dependencies] diff --git a/rind-python/Cargo.toml b/rind-python/Cargo.toml index 3974e2bd7..078bf9673 100644 --- a/rind-python/Cargo.toml +++ b/rind-python/Cargo.toml @@ -21,7 +21,7 @@ test = false doctest = false [dependencies] -rind = { version = "0.4.0", path = "../rind" } +rind = { version = "0.3.2", path = "../rind" } # The stable ABI from 3.11, the oldest CPython still supported and the first # whose stable ABI has the buffer protocol, so one wheel per platform covers # every CPython since. diff --git a/rind-rcgen/Cargo.toml b/rind-rcgen/Cargo.toml index dcf7f57be..a470367dd 100644 --- a/rind-rcgen/Cargo.toml +++ b/rind-rcgen/Cargo.toml @@ -22,7 +22,7 @@ zeroize = [] [dependencies] base64ct = { version = "1.8", optional = true, features = ["alloc"] } rcgen = { version = "0.14", default-features = false } -rind = { version = "0.4.0", path = "../rind" } +rind = { version = "0.3.2", path = "../rind" } rustls-pki-types = "1.12" zeroize = "1.9" diff --git a/rind-ring/Cargo.toml b/rind-ring/Cargo.toml index 11ee1d734..79b48bff4 100644 --- a/rind-ring/Cargo.toml +++ b/rind-ring/Cargo.toml @@ -33,7 +33,7 @@ unstable-testing-arm-no-neon = [] wasm32_unknown_unknown_js = [] [dependencies] -rind = { version = "0.4.0", path = "../rind" } +rind = { version = "0.3.2", path = "../rind" } untrusted = "0.9" [dev-dependencies] diff --git a/rind-rustls/Cargo.toml b/rind-rustls/Cargo.toml index fcd403539..8e2c67532 100644 --- a/rind-rustls/Cargo.toml +++ b/rind-rustls/Cargo.toml @@ -10,7 +10,7 @@ rust-version.workspace = true readme = "README.md" [dependencies] -rind = { version = "0.4.0", path = "../rind" } +rind = { version = "0.3.2", path = "../rind" } rustls = { version = "0.23.20", default-features = false, features = ["std", "tls12"] } [dev-dependencies] diff --git a/rind-str0m/Cargo.toml b/rind-str0m/Cargo.toml index 106c7d928..8bf5065d7 100644 --- a/rind-str0m/Cargo.toml +++ b/rind-str0m/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" [dependencies] dimpl = { version = "0.9", default-features = false } rcgen = { version = "0.14", default-features = false } -rind = { version = "0.4.0", path = "../rind" } +rind = { version = "0.3.2", path = "../rind" } rind-dimpl = { version = "0.9.3", path = "../rind-dimpl" } rind-rcgen = { version = "0.14.3", path = "../rind-rcgen" } # Its default features compile aws-lc-rs. diff --git a/rind/Cargo.toml b/rind/Cargo.toml index e6309c188..00a83edef 100644 --- a/rind/Cargo.toml +++ b/rind/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rind" -version = "0.4.0" +version = "0.3.2" edition = "2024" repository = "https://github.com/moq-dev/rind" license = "(Apache-2.0 OR ISC OR MIT-0) AND (Apache-2.0 OR ISC OR MIT) AND Apache-2.0 AND ISC AND BSD-3-Clause"