diff --git a/Cargo.lock b/Cargo.lock index e2b09b6..a32ac3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -731,6 +731,7 @@ dependencies = [ "aes", "aes-gcm", "ccm", + "chacha20", "chacha20poly1305", "crypto-common", "der", diff --git a/Cargo.toml b/Cargo.toml index c6e0836..730bd01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ 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"] } ccm = { version = "0.6.0-rc.3", default-features = false } +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/hash.rs b/src/hash.rs index dfa58e1..4ff7486 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -4,7 +4,7 @@ use alloc::boxed::Box; use digest::{Digest, OutputSizeUser}; use paste::paste; use rustls::crypto::{self, hash}; -use sha2::{Sha256, Sha384}; +use sha2::{Sha224, Sha256, Sha384, Sha512}; macro_rules! impl_hash { ($name:ident, $ty:ty, $algo:ty) => { @@ -56,7 +56,23 @@ macro_rules! impl_hash { }; } -// impl_hash! {SHA224, Sha224, hash::HashAlgorithm::SHA224} +impl_hash! {SHA224, Sha224, hash::HashAlgorithm::SHA224} impl_hash! {SHA256, Sha256, hash::HashAlgorithm::SHA256} impl_hash! {SHA384, Sha384, hash::HashAlgorithm::SHA384} -// impl_hash! {SHA512, Sha512, hash::HashAlgorithm::SHA512} +impl_hash! {SHA512, Sha512, hash::HashAlgorithm::SHA512} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sha224_and_sha512_providers() { + assert_eq!(SHA224.output_len(), 28); + assert_eq!(SHA224.algorithm(), hash::HashAlgorithm::SHA224); + assert_eq!(SHA224.hash(b"abc").as_ref().len(), 28); + + assert_eq!(SHA512.output_len(), 64); + assert_eq!(SHA512.algorithm(), hash::HashAlgorithm::SHA512); + assert_eq!(SHA512.hash(b"abc").as_ref().len(), 64); + } +} diff --git a/src/hmac.rs b/src/hmac.rs index 2ad3b00..e4d5578 100644 --- a/src/hmac.rs +++ b/src/hmac.rs @@ -5,7 +5,7 @@ use crypto_common::OutputSizeUser; use hmac::{KeyInit, Mac}; use paste::paste; use rustls::crypto; -use sha2::{Sha256, Sha384}; +use sha2::{Sha256, Sha384, Sha512}; macro_rules! impl_hmac { ( @@ -53,4 +53,18 @@ macro_rules! impl_hmac { impl_hmac! {SHA256, Sha256} impl_hmac! {SHA384, Sha384} -// impl_hmac! {SHA512, Sha512} +impl_hmac! {SHA512, Sha512} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sha512_hmac_provider() { + assert_eq!(SHA512.hash_output_len(), 64); + let key = SHA512.with_key(b"key"); + assert_eq!(key.tag_len(), 64); + let tag = key.sign_concat(b"a", &[], b"b"); + assert_eq!(tag.as_ref().len(), 64); + } +} diff --git a/src/lib.rs b/src/lib.rs index 236b264..dd42ead 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -274,7 +274,7 @@ pub const TLS13_AES_128_GCM_SHA256: SupportedCipherSuite = }, hkdf_provider: &rustls::crypto::tls13::HkdfUsingHmac(hmac::SHA256), aead_alg: &aead::gcm::Tls13Aes128Gcm, - quic: None, + quic: Some(quic::AES_128_GCM), }); pub const TLS13_AES_256_GCM_SHA384: SupportedCipherSuite = @@ -286,7 +286,7 @@ pub const TLS13_AES_256_GCM_SHA384: SupportedCipherSuite = }, hkdf_provider: &rustls::crypto::tls13::HkdfUsingHmac(hmac::SHA384), aead_alg: &aead::gcm::Tls13Aes256Gcm, - quic: None, + quic: Some(quic::AES_256_GCM), }); pub const TLS13_AES_128_CCM_SHA256: SupportedCipherSuite = @@ -298,7 +298,7 @@ pub const TLS13_AES_128_CCM_SHA256: SupportedCipherSuite = }, hkdf_provider: &rustls::crypto::tls13::HkdfUsingHmac(hmac::SHA256), aead_alg: &aead::ccm::Tls13Aes128Ccm, - quic: None, + quic: Some(quic::AES_128_CCM), }); pub const TLS13_AES_128_CCM_8_SHA256: SupportedCipherSuite = @@ -330,7 +330,7 @@ pub const TLS13_CHACHA20_POLY1305_SHA256: SupportedCipherSuite = }, hkdf_provider: &rustls::crypto::tls13::HkdfUsingHmac(hmac::SHA256), aead_alg: &aead::chacha20::Chacha20Poly1305, - quic: None, + quic: Some(quic::CHACHA20_POLY1305), }); const TLS13_SUITES: &[SupportedCipherSuite] = misc::const_concat_slices!( @@ -350,8 +350,10 @@ static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = misc::const_concat_slices!( ); mod aead; -mod hash; -mod hmac; +/// Hash algorithm providers (SHA-224/256/384/512). +pub mod hash; +/// HMAC providers (SHA-256/384/512). +pub mod hmac; mod kx; mod misc; pub mod quic; diff --git a/src/quic.rs b/src/quic.rs index 08dd10d..95615c9 100644 --- a/src/quic.rs +++ b/src/quic.rs @@ -1,141 +1,536 @@ +//! QUIC packet and header protection (RFC 9001). +//! +//! Provides AEAD packet protection and header protection for the TLS 1.3 +//! cipher suites that QUIC uses: AES-128-GCM, AES-256-GCM, AES-128-CCM, and +//! ChaCha20-Poly1305. AES-128-CCM-8 is not supported for QUIC because header +//! protection requires a 16-byte authentication tag sample. + #![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; -#[allow(dead_code)] // TODO -pub struct HeaderProtectionKey(AeadKey); +/// Header-protection mask size: 1 byte for the first header byte + 4 for PN. +const MASK_LEN: usize = 5; + +// --------------------------------------------------------------------------- +// Header protection +// --------------------------------------------------------------------------- + +enum HeaderProtectionKey { + // AES expanded keys are large; box them so the enum stays compact. + Aes128(Box), + Aes256(Box), + 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(|cipher| Self::Aes128(Box::new(cipher))) + .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(|cipher| Self::Aes256(Box::new(cipher))) + .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)) + } + + /// Produce the 5-byte header-protection mask from a 16-byte sample. + 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())); + } + + match self { + // RFC 9001 §5.4.3 — AES-ECB over the 16-byte sample. + Self::Aes128(cipher) => { + 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())) + } + Self::Aes256(cipher) => { + 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. + Self::ChaCha20(key) => { + 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(key, &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) + } + } + } + + /// Apply or remove header protection (RFC 9001 §5.4.1). + /// + /// When `masked` is true the first byte is currently protected (decrypt + /// path); when false it is still plaintext (encrypt path). + 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 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 +// --------------------------------------------------------------------------- - crypto: chacha20poly1305::ChaCha20Poly1305, +struct PacketKey { + key: A, + iv: Iv, + confidentiality_limit: u64, + integrity_limit: u64, } -impl PacketKey { - pub fn new(suite: &'static Tls13CipherSuite, key: AeadKey, iv: Iv) -> Self { +impl PacketKey +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 { +impl quic::PacketKey for PacketKey +where + A: AeadKeyInit + AeadInOut + Send + Sync, +{ fn encrypt_in_place( &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 encrypt_in_place_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>( &self, 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); - - AeadInOut::decrypt_in_place(&self.crypto, &nonce, aad, &mut payload_) - .map_err(|_| rustls::Error::DecryptError)?; - - // Unfortunately the lifetime bound on decrypt_in_place sucks - payload.copy_from_slice(&payload_); + self.decrypt_with_nonce(&Nonce::new(&self.iv, packet_number).0, header, payload) + } - let plain_len = payload_len - self.tag_len(); - Ok(&payload[..plain_len]) + fn decrypt_in_place_for_path<'a>( + &self, + path_id: u32, + packet_number: u64, + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], Error> { + self.decrypt_with_nonce( + &Nonce::for_path(path_id, &self.iv, packet_number).0, + header, + payload, + ) } - /// Tag length for the underlying AEAD algorithm #[inline] fn tag_len(&self) -> usize { - ::TagSize::to_usize() + A::TagSize::to_usize() } - fn integrity_limit(&self) -> u64 { - 1 << 36 + fn confidentiality_limit(&self) -> u64 { + self.confidentiality_limit } - fn confidentiality_limit(&self) -> u64 { - u64::MAX + fn integrity_limit(&self) -> u64 { + self.integrity_limit } } -#[allow(dead_code)] // TODO -pub struct KeyBuilder(AeadKey); +// --------------------------------------------------------------------------- +// Algorithm builders wired into TLS 1.3 suites +// --------------------------------------------------------------------------- -impl rustls::quic::Algorithm for KeyBuilder { - fn packet_key(&self, _key: AeadKey, _iv: Iv) -> Box { - todo!() +/// Which AEAD + header-protection pair a [`KeyBuilder`] constructs. +#[derive(Clone, Copy)] +enum AlgorithmKind { + Aes128Gcm, + Aes256Gcm, + Aes128Ccm, + ChaCha20Poly1305, +} + +/// QUIC key algorithm for a single TLS 1.3 cipher suite. +pub struct KeyBuilder { + kind: AlgorithmKind, + confidentiality_limit: u64, + integrity_limit: u64, +} + +/// 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 = &KeyBuilder { + kind: AlgorithmKind::Aes128Gcm, + confidentiality_limit: 1 << 23, + integrity_limit: 1 << 52, +}; + +/// AES-256-GCM packet protection with AES-256-ECB header protection. +pub static AES_256_GCM: &dyn quic::Algorithm = &KeyBuilder { + kind: AlgorithmKind::Aes256Gcm, + confidentiality_limit: 1 << 23, + integrity_limit: 1 << 52, +}; + +/// AES-128-CCM packet protection with AES-128-ECB header protection. +/// +/// Same header-protection construction as AES-GCM (RFC 9001 §5.4.3). +pub static AES_128_CCM: &dyn quic::Algorithm = &KeyBuilder { + kind: AlgorithmKind::Aes128Ccm, + confidentiality_limit: 1 << 23, + integrity_limit: 1 << 52, +}; + +/// ChaCha20-Poly1305 packet protection with ChaCha20 header protection. +pub static CHACHA20_POLY1305: &dyn quic::Algorithm = &KeyBuilder { + kind: AlgorithmKind::ChaCha20Poly1305, + confidentiality_limit: u64::MAX, + integrity_limit: 1 << 36, +}; + +impl quic::Algorithm for KeyBuilder { + fn packet_key(&self, key: AeadKey, iv: Iv) -> Box { + match self.kind { + AlgorithmKind::Aes128Gcm => Box::new(PacketKey::::new( + key, + iv, + self.confidentiality_limit, + self.integrity_limit, + )), + AlgorithmKind::Aes256Gcm => Box::new(PacketKey::::new( + key, + iv, + self.confidentiality_limit, + self.integrity_limit, + )), + AlgorithmKind::Aes128Ccm => Box::new(PacketKey::::new( + key, + iv, + self.confidentiality_limit, + self.integrity_limit, + )), + AlgorithmKind::ChaCha20Poly1305 => { + Box::new(PacketKey::::new( + key, + iv, + self.confidentiality_limit, + self.integrity_limit, + )) + } + } } fn header_protection_key(&self, key: AeadKey) -> Box { - Box::new(HeaderProtectionKey::new(key)) + let hpk = match self.kind { + AlgorithmKind::Aes128Gcm | AlgorithmKind::Aes128Ccm => { + 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::Aes128Ccm => crate::aead::ccm::Aes128Ccm::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 aes128_ccm_initial_packet_roundtrip() { + packet_roundtrip(crate::TLS13_AES_128_CCM_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!(AES_128_CCM.aead_key_len(), 16); + assert_eq!(CHACHA20_POLY1305.aead_key_len(), 32); + } + + #[test] + fn ccm8_has_no_quic() { + let suite = tls13_suite(crate::TLS13_AES_128_CCM_8_SHA256); + assert!(suite.quic.is_none()); } }