diff --git a/Cargo.lock b/Cargo.lock index 3199dc0..9271f7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" dependencies = [ "cipher", "cpubits", @@ -66,9 +66,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.67" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "shlex", @@ -220,7 +220,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -393,9 +393,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "subtle", "typenum", @@ -413,9 +413,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "log" @@ -548,18 +548,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -630,9 +630,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -644,9 +644,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "zeroize", ] @@ -656,7 +656,9 @@ name = "rustls-rustcrypto" version = "0.0.2-alpha" dependencies = [ "aead", + "aes", "aes-gcm", + "chacha20", "chacha20poly1305", "crypto-common", "der", @@ -711,31 +713,31 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", ] [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -802,6 +804,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "typenum" version = "1.20.1" diff --git a/Cargo.toml b/Cargo.toml index e8e5de0..bf9acde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,9 @@ resolver = "1" # Hack to enable the `custom` feature of `getrandom` # default features often have std breaking no_std and potentially other unwanted [dependencies] aead = { version = "0.6", default-features = false } +aes = { version = "0.9", default-features = false } aes-gcm = { version = "0.11", default-features = false, features = ["aes", "alloc"] } +chacha20 = { version = "0.10", default-features = false } chacha20poly1305 = { version = "0.11", default-features = false } crypto-common = { version = "0.2", default-features = false } der = { version = "0.8", default-features = false } diff --git a/src/lib.rs b/src/lib.rs index 6332bca..cc5f54a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -205,44 +205,53 @@ const TLS12_SUITES: &[SupportedCipherSuite] = misc::const_concat_slices!( #[cfg(not(feature = "tls12"))] const TLS12_SUITES: &[SupportedCipherSuite] = &[]; +// Named statics so `&Tls13CipherSuite` is truly `'static` on MSRV when +// `quic: Some(...)` is set (avoids E0716 on rustc 1.85). + +static TLS13_AES_128_GCM_SHA256_INTERNAL: &Tls13CipherSuite = &Tls13CipherSuite { + common: CipherSuiteCommon { + suite: CipherSuite::TLS13_AES_128_GCM_SHA256, + hash_provider: hash::SHA256, + confidentiality_limit: u64::MAX, + }, + hkdf_provider: &rustls::crypto::tls13::HkdfUsingHmac(hmac::SHA256), + aead_alg: &aead::gcm::Tls13Aes128Gcm, + quic: Some(quic::AES_128_GCM), +}; + pub const TLS13_AES_128_GCM_SHA256: SupportedCipherSuite = - SupportedCipherSuite::Tls13(&Tls13CipherSuite { - common: CipherSuiteCommon { - suite: CipherSuite::TLS13_AES_128_GCM_SHA256, - hash_provider: hash::SHA256, - confidentiality_limit: u64::MAX, - }, - hkdf_provider: &rustls::crypto::tls13::HkdfUsingHmac(hmac::SHA256), - aead_alg: &aead::gcm::Tls13Aes128Gcm, - quic: None, - }); + SupportedCipherSuite::Tls13(TLS13_AES_128_GCM_SHA256_INTERNAL); + +static TLS13_AES_256_GCM_SHA384_INTERNAL: &Tls13CipherSuite = &Tls13CipherSuite { + common: CipherSuiteCommon { + suite: CipherSuite::TLS13_AES_256_GCM_SHA384, + hash_provider: hash::SHA384, + confidentiality_limit: u64::MAX, + }, + hkdf_provider: &rustls::crypto::tls13::HkdfUsingHmac(hmac::SHA384), + aead_alg: &aead::gcm::Tls13Aes256Gcm, + quic: Some(quic::AES_256_GCM), +}; pub const TLS13_AES_256_GCM_SHA384: SupportedCipherSuite = - SupportedCipherSuite::Tls13(&Tls13CipherSuite { - common: CipherSuiteCommon { - suite: CipherSuite::TLS13_AES_256_GCM_SHA384, - hash_provider: hash::SHA384, - confidentiality_limit: u64::MAX, - }, - hkdf_provider: &rustls::crypto::tls13::HkdfUsingHmac(hmac::SHA384), - aead_alg: &aead::gcm::Tls13Aes256Gcm, - quic: None, - }); + SupportedCipherSuite::Tls13(TLS13_AES_256_GCM_SHA384_INTERNAL); const TLS13_AES_SUITES: &[SupportedCipherSuite] = &[TLS13_AES_128_GCM_SHA256, TLS13_AES_256_GCM_SHA384]; +static TLS13_CHACHA20_POLY1305_SHA256_INTERNAL: &Tls13CipherSuite = &Tls13CipherSuite { + common: CipherSuiteCommon { + suite: CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + hash_provider: hash::SHA256, + confidentiality_limit: u64::MAX, + }, + hkdf_provider: &rustls::crypto::tls13::HkdfUsingHmac(hmac::SHA256), + aead_alg: &aead::chacha20::Chacha20Poly1305, + quic: Some(quic::CHACHA20_POLY1305), +}; + pub const TLS13_CHACHA20_POLY1305_SHA256: SupportedCipherSuite = - SupportedCipherSuite::Tls13(&Tls13CipherSuite { - common: CipherSuiteCommon { - suite: CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, - hash_provider: hash::SHA256, - confidentiality_limit: u64::MAX, - }, - hkdf_provider: &rustls::crypto::tls13::HkdfUsingHmac(hmac::SHA256), - aead_alg: &aead::chacha20::Chacha20Poly1305, - quic: None, - }); + SupportedCipherSuite::Tls13(TLS13_CHACHA20_POLY1305_SHA256_INTERNAL); const TLS13_SUITES: &[SupportedCipherSuite] = misc::const_concat_slices!( SupportedCipherSuite, diff --git a/src/quic.rs b/src/quic.rs index 08dd10d..e99e2f2 100644 --- a/src/quic.rs +++ b/src/quic.rs @@ -1,141 +1,709 @@ +//! QUIC packet and header protection (RFC 9001). +//! +//! Provides AEAD packet protection and header protection for the TLS 1.3 +//! cipher suites that QUIC uses on master: AES-128-GCM, AES-256-GCM, and +//! ChaCha20-Poly1305. + #![allow(clippy::duplicate_mod)] #[cfg(feature = "alloc")] use alloc::boxed::Box; -use aead::{AeadCore, AeadInOut}; -use chacha20poly1305::{KeyInit, KeySizeUser}; +use aead::{AeadInOut, KeyInit as AeadKeyInit, KeySizeUser}; +use aes::cipher::BlockCipherEncrypt; +use chacha20::{ + cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}, + ChaCha20, +}; use crypto_common::typenum::Unsigned; -use rustls::crypto::cipher::{self, AeadKey, Iv}; -use rustls::{quic, Error, Tls13CipherSuite}; +use rustls::crypto::cipher::{AeadKey, Iv, Nonce}; +use rustls::{quic, Error}; + +/// Sample length shared by all header-protection algorithms in RFC 9001. +const SAMPLE_LEN: usize = 16; + +/// Header-protection mask size: 1 byte for the first header byte + 4 for PN. +const MASK_LEN: usize = 5; + +// --------------------------------------------------------------------------- +// Header protection (enum + trait dispatch) +// --------------------------------------------------------------------------- -#[allow(dead_code)] // TODO -pub struct HeaderProtectionKey(AeadKey); +/// Algorithm-specific construction of the 5-byte header-protection mask. +/// +/// Implemented for each concrete cipher; [`HeaderProtectionKey`] dispatches to +/// the active variant via enum match (no trait-object vtable). +trait MaskSample { + fn new_mask(&self, sample: &[u8]) -> Result<[u8; MASK_LEN], Error>; +} + +/// Concrete header-protection key material for the AEAD in use. +/// +/// RFC 9001 §5.4: AES-GCM/CCM use AES-ECB; ChaCha20-Poly1305 uses raw ChaCha20. +#[allow(clippy::large_enum_variant)] +enum HeaderProtectionKey { + Aes128(aes::Aes128), + Aes256(aes::Aes256), + ChaCha20(chacha20::Key), +} impl HeaderProtectionKey { - pub fn new(key: AeadKey) -> Self { - Self(key) + fn new_aes128(key: AeadKey) -> Result { + aes::Aes128::new_from_slice(key.as_ref()) + .map(Self::Aes128) + .map_err(|_| Error::General("invalid AES-128 header protection key".into())) + } + + fn new_aes256(key: AeadKey) -> Result { + aes::Aes256::new_from_slice(key.as_ref()) + .map(Self::Aes256) + .map_err(|_| Error::General("invalid AES-256 header protection key".into())) + } + + fn new_chacha20(key: AeadKey) -> Result { + let key = chacha20::Key::try_from(key.as_ref()) + .map_err(|_| Error::General("invalid ChaCha20 header protection key".into()))?; + Ok(Self::ChaCha20(key)) + } +} + +impl MaskSample for HeaderProtectionKey { + fn new_mask(&self, sample: &[u8]) -> Result<[u8; MASK_LEN], Error> { + match self { + Self::Aes128(cipher) => cipher.new_mask(sample), + Self::Aes256(cipher) => cipher.new_mask(sample), + Self::ChaCha20(key) => key.new_mask(sample), + } + } +} + +// RFC 9001 §5.4.3 — AES-ECB over the 16-byte sample. +impl MaskSample for aes::Aes128 { + fn new_mask(&self, sample: &[u8]) -> Result<[u8; MASK_LEN], Error> { + aes_ecb_mask(self, sample) + } +} + +impl MaskSample for aes::Aes256 { + fn new_mask(&self, sample: &[u8]) -> Result<[u8; MASK_LEN], Error> { + aes_ecb_mask(self, sample) + } +} + +fn aes_ecb_mask(cipher: &impl BlockCipherEncrypt, sample: &[u8]) -> Result<[u8; MASK_LEN], Error> { + if sample.len() < SAMPLE_LEN { + return Err(Error::General("sample of invalid length".into())); + } + let mut block = sample[..SAMPLE_LEN] + .try_into() + .map_err(|_| Error::General("sample of invalid length".into()))?; + cipher.encrypt_block(&mut block); + block[..MASK_LEN] + .try_into() + .map_err(|_| Error::General("mask of invalid length".into())) +} + +// RFC 9001 §5.4.4 — ChaCha20 keystream over five zero bytes. +impl MaskSample for chacha20::Key { + fn new_mask(&self, sample: &[u8]) -> Result<[u8; MASK_LEN], Error> { + if sample.len() < SAMPLE_LEN { + return Err(Error::General("sample of invalid length".into())); + } + let counter = u32::from_le_bytes( + sample[0..4] + .try_into() + .map_err(|_| Error::General("sample of invalid length".into()))?, + ); + let nonce = sample[4..SAMPLE_LEN] + .try_into() + .map_err(|_| Error::General("sample of invalid length".into()))?; + let mut chacha = ChaCha20::new(self, &nonce); + chacha + .try_seek(counter) + .map_err(|_| Error::General("ChaCha20 seek failed".into()))?; + let mut mask = [0u8; MASK_LEN]; + chacha.apply_keystream(&mut mask); + Ok(mask) + } +} + +/// RFC 9001 §5.4.1 header-protection application, shared by all mask algorithms. +trait XorInPlace: MaskSample { + fn xor_in_place( + &self, + sample: &[u8], + first: &mut u8, + packet_number: &mut [u8], + masked: bool, + ) -> Result<(), Error> { + let mask = self.new_mask(sample)?; + let (first_mask, pn_mask) = mask + .split_first() + .ok_or_else(|| Error::General("mask of invalid length".into()))?; + + if packet_number.len() > pn_mask.len() { + return Err(Error::General("packet number too long".into())); + } + + const LONG_HEADER_FORM: u8 = 0x80; + let bits = if *first & LONG_HEADER_FORM == LONG_HEADER_FORM { + 0x0f // Long header: 4 bits masked + } else { + 0x1f // Short header: 5 bits masked + }; + + // When unmasking, recover the packet-number length from the unmasked first byte. + let first_plain = if masked { + *first ^ (first_mask & bits) + } else { + *first + }; + let pn_len = (first_plain & 0x03) as usize + 1; + + *first ^= first_mask & bits; + for (dst, m) in packet_number.iter_mut().zip(pn_mask).take(pn_len) { + *dst ^= m; + } + + Ok(()) } } +impl XorInPlace for T {} + impl quic::HeaderProtectionKey for HeaderProtectionKey { fn encrypt_in_place( &self, - _sample: &[u8], - _first: &mut u8, - _packet_number: &mut [u8], + sample: &[u8], + first: &mut u8, + packet_number: &mut [u8], ) -> Result<(), Error> { - todo!() + self.xor_in_place(sample, first, packet_number, false) } fn decrypt_in_place( &self, - _sample: &[u8], - _first: &mut u8, - _packet_number: &mut [u8], + sample: &[u8], + first: &mut u8, + packet_number: &mut [u8], ) -> Result<(), Error> { - todo!() + self.xor_in_place(sample, first, packet_number, true) } #[inline] fn sample_len(&self) -> usize { - todo!() + SAMPLE_LEN } } -pub struct PacketKey { - /// Computes unique nonces for each packet - iv: Iv, - - /// The cipher suite used for this packet key - #[allow(dead_code)] - suite: &'static Tls13CipherSuite, +// --------------------------------------------------------------------------- +// Packet keys (enum + trait dispatch) +// --------------------------------------------------------------------------- - crypto: chacha20poly1305::ChaCha20Poly1305, +/// Shared AEAD state for one QUIC packet-protection key. +struct PacketCipher { + key: A, + iv: Iv, + confidentiality_limit: u64, + integrity_limit: u64, } -impl PacketKey { - pub fn new(suite: &'static Tls13CipherSuite, key: AeadKey, iv: Iv) -> Self { +impl PacketCipher +where + A: AeadKeyInit + AeadInOut + Send + Sync, +{ + fn new(key: AeadKey, iv: Iv, confidentiality_limit: u64, integrity_limit: u64) -> Self { Self { + key: A::new_from_slice(key.as_ref()).expect("invalid AEAD key length"), iv, - suite, - crypto: chacha20poly1305::ChaCha20Poly1305::new_from_slice(key.as_ref()) - .expect("key should be valid"), + confidentiality_limit, + integrity_limit, + } + } + + fn encrypt_with_nonce( + &self, + nonce: &[u8; rustls::crypto::cipher::NONCE_LEN], + header: &[u8], + payload: &mut [u8], + ) -> Result { + let nonce = aead::Nonce::::try_from(&nonce[..]) + .map_err(|_| Error::General("invalid AEAD nonce length".into()))?; + let tag = self + .key + .encrypt_inout_detached(&nonce, header, payload.into()) + .map_err(|_| Error::EncryptError)?; + Ok(quic::Tag::from(tag.as_ref())) + } + + fn decrypt_with_nonce<'a>( + &self, + nonce: &[u8; rustls::crypto::cipher::NONCE_LEN], + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], Error> { + let tag_len = A::TagSize::to_usize(); + if payload.len() < tag_len { + return Err(Error::DecryptError); } + let (body, tag_bytes) = payload.split_at_mut(payload.len() - tag_len); + let nonce = aead::Nonce::::try_from(&nonce[..]) + .map_err(|_| Error::General("invalid AEAD nonce length".into()))?; + let tag = aead::Tag::::try_from(&tag_bytes[..]).map_err(|_| Error::DecryptError)?; + self.key + .decrypt_inout_detached(&nonce, header, body.into(), &tag) + .map_err(|_| Error::DecryptError)?; + let plain_len = payload.len() - tag_len; + Ok(&payload[..plain_len]) } } -impl quic::PacketKey for PacketKey { - fn encrypt_in_place( +/// Packet seal/open operations implemented per AEAD; [`PacketKey`] dispatches via +/// enum match. +trait PacketOps { + fn seal( + &self, + packet_number: u64, + header: &[u8], + payload: &mut [u8], + ) -> Result; + + fn seal_for_path( + &self, + path_id: u32, + packet_number: u64, + header: &[u8], + payload: &mut [u8], + ) -> Result; + + fn open<'a>( + &self, + packet_number: u64, + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], Error>; + + fn open_for_path<'a>( + &self, + path_id: u32, + packet_number: u64, + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], Error>; + + fn tag_len(&self) -> usize; + fn confidentiality_limit(&self) -> u64; + fn integrity_limit(&self) -> u64; +} + +impl PacketOps for PacketCipher +where + A: AeadKeyInit + AeadInOut + Send + Sync, +{ + fn seal( &self, packet_number: u64, - aad: &[u8], + header: &[u8], payload: &mut [u8], ) -> Result { - let nonce = cipher::Nonce::new(&self.iv, packet_number).0; + self.encrypt_with_nonce(&Nonce::new(&self.iv, packet_number).0, header, payload) + } - let tag = self - .crypto - .encrypt_inout_detached(&nonce.into(), aad, payload.into()) - .map_err(|_| rustls::Error::EncryptError)?; - Ok(quic::Tag::from(tag.as_ref())) + fn seal_for_path( + &self, + path_id: u32, + packet_number: u64, + header: &[u8], + payload: &mut [u8], + ) -> Result { + self.encrypt_with_nonce( + &Nonce::for_path(path_id, &self.iv, packet_number).0, + header, + payload, + ) } - /// Decrypt a QUIC packet - /// - /// Takes the packet `header`, which is used as the additional authenticated - /// data, and the `payload`, which includes the authentication tag. - /// - /// If the return value is `Ok`, the decrypted payload can be found in - /// `payload`, up to the length found in the return value. - fn decrypt_in_place<'a>( + fn open<'a>( + &self, + packet_number: u64, + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], Error> { + self.decrypt_with_nonce(&Nonce::new(&self.iv, packet_number).0, header, payload) + } + + fn open_for_path<'a>( &self, + path_id: u32, packet_number: u64, - aad: &[u8], + header: &[u8], payload: &'a mut [u8], ) -> Result<&'a [u8], Error> { - let mut payload_ = payload.to_vec(); - let payload_len = payload_.len(); - let nonce = chacha20poly1305::Nonce::from(cipher::Nonce::new(&self.iv, packet_number).0); + self.decrypt_with_nonce( + &Nonce::for_path(path_id, &self.iv, packet_number).0, + header, + payload, + ) + } - AeadInOut::decrypt_in_place(&self.crypto, &nonce, aad, &mut payload_) - .map_err(|_| rustls::Error::DecryptError)?; + #[inline] + fn tag_len(&self) -> usize { + A::TagSize::to_usize() + } - // Unfortunately the lifetime bound on decrypt_in_place sucks - payload.copy_from_slice(&payload_); + fn confidentiality_limit(&self) -> u64 { + self.confidentiality_limit + } - let plain_len = payload_len - self.tag_len(); - Ok(&payload[..plain_len]) + fn integrity_limit(&self) -> u64 { + self.integrity_limit + } +} + +/// Concrete packet-protection key for the negotiated AEAD. +#[allow(clippy::large_enum_variant)] +enum PacketKey { + Aes128Gcm(PacketCipher), + Aes256Gcm(PacketCipher), + ChaCha20Poly1305(PacketCipher), +} + +impl PacketOps for PacketKey { + fn seal( + &self, + packet_number: u64, + header: &[u8], + payload: &mut [u8], + ) -> Result { + match self { + Self::Aes128Gcm(c) => c.seal(packet_number, header, payload), + Self::Aes256Gcm(c) => c.seal(packet_number, header, payload), + Self::ChaCha20Poly1305(c) => c.seal(packet_number, header, payload), + } + } + + fn seal_for_path( + &self, + path_id: u32, + packet_number: u64, + header: &[u8], + payload: &mut [u8], + ) -> Result { + match self { + Self::Aes128Gcm(c) => c.seal_for_path(path_id, packet_number, header, payload), + Self::Aes256Gcm(c) => c.seal_for_path(path_id, packet_number, header, payload), + Self::ChaCha20Poly1305(c) => c.seal_for_path(path_id, packet_number, header, payload), + } + } + + fn open<'a>( + &self, + packet_number: u64, + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], Error> { + match self { + Self::Aes128Gcm(c) => c.open(packet_number, header, payload), + Self::Aes256Gcm(c) => c.open(packet_number, header, payload), + Self::ChaCha20Poly1305(c) => c.open(packet_number, header, payload), + } + } + + fn open_for_path<'a>( + &self, + path_id: u32, + packet_number: u64, + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], Error> { + match self { + Self::Aes128Gcm(c) => c.open_for_path(path_id, packet_number, header, payload), + Self::Aes256Gcm(c) => c.open_for_path(path_id, packet_number, header, payload), + Self::ChaCha20Poly1305(c) => c.open_for_path(path_id, packet_number, header, payload), + } } - /// Tag length for the underlying AEAD algorithm - #[inline] fn tag_len(&self) -> usize { - ::TagSize::to_usize() + match self { + Self::Aes128Gcm(c) => c.tag_len(), + Self::Aes256Gcm(c) => c.tag_len(), + Self::ChaCha20Poly1305(c) => c.tag_len(), + } + } + + fn confidentiality_limit(&self) -> u64 { + match self { + Self::Aes128Gcm(c) => c.confidentiality_limit(), + Self::Aes256Gcm(c) => c.confidentiality_limit(), + Self::ChaCha20Poly1305(c) => c.confidentiality_limit(), + } } fn integrity_limit(&self) -> u64 { - 1 << 36 + match self { + Self::Aes128Gcm(c) => c.integrity_limit(), + Self::Aes256Gcm(c) => c.integrity_limit(), + Self::ChaCha20Poly1305(c) => c.integrity_limit(), + } + } +} + +impl quic::PacketKey for PacketKey { + fn encrypt_in_place( + &self, + packet_number: u64, + header: &[u8], + payload: &mut [u8], + ) -> Result { + PacketOps::seal(self, packet_number, header, payload) + } + + fn encrypt_in_place_for_path( + &self, + path_id: u32, + packet_number: u64, + header: &[u8], + payload: &mut [u8], + ) -> Result { + PacketOps::seal_for_path(self, path_id, packet_number, header, payload) + } + + fn decrypt_in_place<'a>( + &self, + packet_number: u64, + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], Error> { + PacketOps::open(self, packet_number, header, payload) + } + + fn decrypt_in_place_for_path<'a>( + &self, + path_id: u32, + packet_number: u64, + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], Error> { + PacketOps::open_for_path(self, path_id, packet_number, header, payload) + } + + #[inline] + fn tag_len(&self) -> usize { + PacketOps::tag_len(self) } fn confidentiality_limit(&self) -> u64 { - u64::MAX + PacketOps::confidentiality_limit(self) + } + + fn integrity_limit(&self) -> u64 { + PacketOps::integrity_limit(self) } } -#[allow(dead_code)] // TODO -pub struct KeyBuilder(AeadKey); +// --------------------------------------------------------------------------- +// Algorithm builders wired into TLS 1.3 suites +// --------------------------------------------------------------------------- + +/// Which AEAD + header-protection pair a [`KeyBuilder`] constructs. +#[derive(Clone, Copy)] +enum AlgorithmKind { + Aes128Gcm, + Aes256Gcm, + ChaCha20Poly1305, +} + +/// QUIC key algorithm for a single TLS 1.3 cipher suite. +pub struct KeyBuilder { + kind: AlgorithmKind, + confidentiality_limit: u64, + integrity_limit: u64, +} + +// Named statics so `&KeyBuilder` can be coerced to `&'static dyn Algorithm` +// without temporary-lifetime issues on MSRV (1.85). +static AES_128_GCM_BUILDER: KeyBuilder = KeyBuilder { + kind: AlgorithmKind::Aes128Gcm, + confidentiality_limit: 1 << 23, + integrity_limit: 1 << 52, +}; +static AES_256_GCM_BUILDER: KeyBuilder = KeyBuilder { + kind: AlgorithmKind::Aes256Gcm, + confidentiality_limit: 1 << 23, + integrity_limit: 1 << 52, +}; +static CHACHA20_POLY1305_BUILDER: KeyBuilder = KeyBuilder { + kind: AlgorithmKind::ChaCha20Poly1305, + confidentiality_limit: u64::MAX, + integrity_limit: 1 << 36, +}; + +/// AES-128-GCM packet protection with AES-128-ECB header protection. +/// +/// Limits match the ring provider (RFC 9001 §6.6). +pub static AES_128_GCM: &dyn quic::Algorithm = &AES_128_GCM_BUILDER; + +/// AES-256-GCM packet protection with AES-256-ECB header protection. +pub static AES_256_GCM: &dyn quic::Algorithm = &AES_256_GCM_BUILDER; + +/// ChaCha20-Poly1305 packet protection with ChaCha20 header protection. +pub static CHACHA20_POLY1305: &dyn quic::Algorithm = &CHACHA20_POLY1305_BUILDER; -impl rustls::quic::Algorithm for KeyBuilder { - fn packet_key(&self, _key: AeadKey, _iv: Iv) -> Box { - todo!() +impl quic::Algorithm for KeyBuilder { + fn packet_key(&self, key: AeadKey, iv: Iv) -> Box { + let conf = self.confidentiality_limit; + let integrity = self.integrity_limit; + let packet = match self.kind { + AlgorithmKind::Aes128Gcm => { + PacketKey::Aes128Gcm(PacketCipher::new(key, iv, conf, integrity)) + } + AlgorithmKind::Aes256Gcm => { + PacketKey::Aes256Gcm(PacketCipher::new(key, iv, conf, integrity)) + } + AlgorithmKind::ChaCha20Poly1305 => { + PacketKey::ChaCha20Poly1305(PacketCipher::new(key, iv, conf, integrity)) + } + }; + Box::new(packet) } fn header_protection_key(&self, key: AeadKey) -> Box { - Box::new(HeaderProtectionKey::new(key)) + let hpk = match self.kind { + AlgorithmKind::Aes128Gcm => { + HeaderProtectionKey::new_aes128(key).expect("AES-128 HP key") + } + AlgorithmKind::Aes256Gcm => { + HeaderProtectionKey::new_aes256(key).expect("AES-256 HP key") + } + AlgorithmKind::ChaCha20Poly1305 => { + HeaderProtectionKey::new_chacha20(key).expect("ChaCha20 HP key") + } + }; + Box::new(hpk) } fn aead_key_len(&self) -> usize { - chacha20poly1305::ChaCha20Poly1305::key_size() + match self.kind { + AlgorithmKind::Aes128Gcm => aes_gcm::Aes128Gcm::key_size(), + AlgorithmKind::Aes256Gcm => aes_gcm::Aes256Gcm::key_size(), + AlgorithmKind::ChaCha20Poly1305 => chacha20poly1305::ChaCha20Poly1305::key_size(), + } + } + + fn fips(&self) -> bool { + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rustls::quic::{Keys, Version}; + use rustls::Side; + + fn tls13_suite(suite: rustls::SupportedCipherSuite) -> &'static rustls::Tls13CipherSuite { + match suite { + rustls::SupportedCipherSuite::Tls13(s) => s, + _ => panic!("expected TLS 1.3 suite"), + } + } + + /// Encrypt/decrypt round-trip through initial keys derived like a real QUIC handshake. + fn packet_roundtrip(suite: rustls::SupportedCipherSuite) { + let suite = tls13_suite(suite); + let quic_alg = suite.quic.expect("suite should advertise QUIC support"); + let keys = Keys::initial( + Version::V1, + suite, + quic_alg, + b"\x00\x01\x02\x03\x04\x05\x06\x07", + Side::Client, + ); + + let header = b"quic-aad"; + let mut payload = b"hello from quic packet key".to_vec(); + let original = payload.clone(); + let tag = keys + .local + .packet + .encrypt_in_place(42, header, &mut payload) + .expect("encrypt"); + payload.extend_from_slice(tag.as_ref()); + + // Peer's remote key is our local key when roles are swapped; re-derive server view. + let server = Keys::initial( + Version::V1, + suite, + quic_alg, + b"\x00\x01\x02\x03\x04\x05\x06\x07", + Side::Server, + ); + let plain = server + .remote + .packet + .decrypt_in_place(42, header, &mut payload) + .expect("decrypt"); + assert_eq!(plain, original.as_slice()); + } + + #[test] + fn aes128_gcm_initial_packet_roundtrip() { + packet_roundtrip(crate::TLS13_AES_128_GCM_SHA256); + } + + #[test] + fn aes256_gcm_initial_packet_roundtrip() { + packet_roundtrip(crate::TLS13_AES_256_GCM_SHA384); + } + + #[test] + fn chacha20_initial_packet_roundtrip() { + packet_roundtrip(crate::TLS13_CHACHA20_POLY1305_SHA256); + } + + #[test] + fn header_protection_roundtrip_aes128() { + let suite = tls13_suite(crate::TLS13_AES_128_GCM_SHA256); + let quic_alg = suite.quic.unwrap(); + let keys = Keys::initial(Version::V1, suite, quic_alg, b"conn-id!", Side::Client); + let hpk = &keys.local.header; + let sample = [0xab_u8; 16]; + let mut first = 0x40; // short header + let mut pn = [0x00, 0x01, 0x02, 0x03]; + let first_orig = first; + let pn_orig = pn; + hpk.encrypt_in_place(&sample, &mut first, &mut pn) + .expect("hp encrypt"); + assert_ne!((first, pn), (first_orig, pn_orig)); + hpk.decrypt_in_place(&sample, &mut first, &mut pn) + .expect("hp decrypt"); + assert_eq!(first, first_orig); + assert_eq!(pn, pn_orig); + } + + #[test] + fn header_protection_roundtrip_chacha20() { + let suite = tls13_suite(crate::TLS13_CHACHA20_POLY1305_SHA256); + let quic_alg = suite.quic.unwrap(); + let keys = Keys::initial(Version::V1, suite, quic_alg, b"conn-id!", Side::Client); + let hpk = &keys.local.header; + let sample = [0xcd_u8; 16]; + let mut first = 0xc0; // long header form + let mut pn = [0x11, 0x22, 0x33, 0x44]; + let first_orig = first; + let pn_orig = pn; + hpk.encrypt_in_place(&sample, &mut first, &mut pn) + .expect("hp encrypt"); + hpk.decrypt_in_place(&sample, &mut first, &mut pn) + .expect("hp decrypt"); + assert_eq!(first, first_orig); + assert_eq!(pn, pn_orig); + } + + #[test] + fn aead_key_lens() { + assert_eq!(AES_128_GCM.aead_key_len(), 16); + assert_eq!(AES_256_GCM.aead_key_len(), 32); + assert_eq!(CHACHA20_POLY1305.aead_key_len(), 32); } }