From b24ac52817c06350d8c8fc08a8f7c1ce0702b679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=BCth?= Date: Wed, 29 Nov 2023 23:58:34 +0100 Subject: [PATCH] Implement QUIC traits (untested) --- Readme.md | 2 + boring-rustls-provider/src/aead.rs | 166 +++++++++++++++++++- boring-rustls-provider/src/aead/aes.rs | 49 +++++- boring-rustls-provider/src/aead/chacha20.rs | 31 +++- boring-rustls-provider/src/tls13.rs | 6 +- 5 files changed, 247 insertions(+), 7 deletions(-) diff --git a/Readme.md b/Readme.md index 10ec339..f44e007 100644 --- a/Readme.md +++ b/Readme.md @@ -1,5 +1,7 @@ # boring-rustls-provider +[![Build Status](https://github.com/janrueth/boring-rustls-provider/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/janrueth/boring-rustls-provider/actions/workflows/ci.yml?query=branch%3Amain) + This is supposed to be the start to a [boringssl](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider. ## Status diff --git a/boring-rustls-provider/src/aead.rs b/boring-rustls-provider/src/aead.rs index 9d6058d..772dddc 100644 --- a/boring-rustls-provider/src/aead.rs +++ b/boring-rustls-provider/src/aead.rs @@ -22,6 +22,9 @@ pub(crate) trait BoringCipher { /// The key size in bytes const KEY_SIZE: usize; + /// The length of the authentication tag + const TAG_LEN: usize; + /// Constructs a new instance of this cipher as an AEAD algorithm fn new_cipher() -> Algorithm; @@ -29,6 +32,16 @@ pub(crate) trait BoringCipher { fn extract_keys(key: cipher::AeadKey, iv: cipher::Iv) -> ConnectionTrafficSecrets; } +pub(crate) trait QuicCipher { + /// The key size in bytes + const KEY_SIZE: usize; + + /// the expected length of a sample + const SAMPLE_LEN: usize; + + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5]; +} + pub(crate) trait BoringAead: BoringCipher + AeadCore + Send + Sync {} pub(crate) struct BoringAeadCrypter { @@ -237,9 +250,51 @@ where } } -pub(crate) struct Aead(PhantomData); +impl rustls::quic::PacketKey for BoringAeadCrypter +where + T: QuicCipher + BoringAead, +{ + fn encrypt_in_place( + &self, + packet_number: u64, + header: &[u8], + payload: &mut [u8], + ) -> Result { + let associated_data = header; + let nonce = cipher::Nonce::new(&self.iv, packet_number); + let tag = self + .encrypt_in_place_detached(Nonce::::from_slice(&nonce.0), associated_data, payload) + .map_err(|_| rustls::Error::EncryptError)?; + + Ok(rustls::quic::Tag::from(tag.as_ref())) + } + + fn decrypt_in_place<'a>( + &self, + packet_number: u64, + header: &[u8], + payload: &'a mut [u8], + ) -> Result<&'a [u8], rustls::Error> { + let associated_data = header; + let nonce = cipher::Nonce::new(&self.iv, packet_number); -impl Aead { + let (buffer, tag) = payload.split_at_mut(payload.len() - self.crypter.max_overhead()); + + self.crypter + .open_in_place(&nonce.0, associated_data, buffer, tag) + .map_err(|_| rustls::Error::DecryptError)?; + + Ok(buffer) + } + + fn tag_len(&self) -> usize { + ::TAG_LEN + } +} + +pub(crate) struct Aead(PhantomData); + +impl Aead { pub const DEFAULT: Self = Self(PhantomData); } @@ -331,3 +386,110 @@ impl cipher::Tls12AeadAlgorithm for Aead { Ok(::extract_keys(key, Iv::copy(&nonce))) } } + +struct QuicHeaderProtector { + key: cipher::AeadKey, + phantom: PhantomData, +} + +impl QuicHeaderProtector { + const MAX_PN_LEN: usize = 4; + fn rfc9001_header_protection( + &self, + sample: &[u8], + first: &mut u8, + packet_number: &mut [u8], + remove: bool, + ) { + let mask = T::header_protection_mask(self.key.as_ref(), sample); + + const LONG_HEADER_FORMAT: u8 = 0x80; + let bits_to_mask = if (*first & LONG_HEADER_FORMAT) == LONG_HEADER_FORMAT { + // Long header: 4 bits masked + 0x0f + } else { + // Short header: 5 bits masked + 0x1f + }; + + let pn_length = if remove { + // remove the mask on the first byte + // then get length to get same as below + *first ^= mask[0] & bits_to_mask; + (*first & 0x03) as usize + 1 + } else { + // calculate length than mask + let pn_length = (*first & 0x03) as usize + 1; + *first ^= mask[0] & bits_to_mask; + pn_length + }; + + // mask the first `pn_length` bytes of the packet number with the mask + for (pn_byte, m) in packet_number.iter_mut().zip(&mask[1..]).take(pn_length) { + *pn_byte ^= m; + } + } +} + +impl rustls::quic::HeaderProtectionKey for QuicHeaderProtector { + fn encrypt_in_place( + &self, + sample: &[u8], + first: &mut u8, + packet_number: &mut [u8], + ) -> Result<(), rustls::Error> { + // We can only mask up to 4 bytes + if packet_number.len() > Self::MAX_PN_LEN { + return Err(rustls::Error::General("packet number too long".into())); + } + + self.rfc9001_header_protection(sample, first, packet_number, false); + + Ok(()) + } + + fn decrypt_in_place( + &self, + sample: &[u8], + first: &mut u8, + packet_number: &mut [u8], + ) -> Result<(), rustls::Error> { + if packet_number.len() > Self::MAX_PN_LEN { + return Err(rustls::Error::General("packet number too long".into())); + } + + self.rfc9001_header_protection(sample, first, packet_number, true); + + Ok(()) + } + + fn sample_len(&self) -> usize { + T::SAMPLE_LEN + } +} + +impl rustls::quic::Algorithm for Aead +where + T: QuicCipher + BoringAead + 'static, +{ + fn packet_key(&self, key: cipher::AeadKey, iv: Iv) -> Box { + Box::new( + BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) + .expect("failed to create AEAD crypter"), + ) + } + + fn header_protection_key( + &self, + key: cipher::AeadKey, + ) -> Box { + Box::new(QuicHeaderProtector { + key, + phantom: PhantomData::, + }) + } + + fn aead_key_len(&self) -> usize { + ::KEY_SIZE + } +} diff --git a/boring-rustls-provider/src/aead/aes.rs b/boring-rustls-provider/src/aead/aes.rs index a503194..712d6ce 100644 --- a/boring-rustls-provider/src/aead/aes.rs +++ b/boring-rustls-provider/src/aead/aes.rs @@ -1,4 +1,4 @@ -use super::{BoringAead, BoringCipher}; +use super::{BoringAead, BoringCipher, QuicCipher}; use aead::consts::{U12, U16}; use boring_additions::aead::Algorithm; use rustls::{crypto::cipher, ConnectionTrafficSecrets}; @@ -15,6 +15,8 @@ impl BoringCipher for Aes128 { const KEY_SIZE: usize = 16; + const TAG_LEN: usize = 16; + fn new_cipher() -> Algorithm { Algorithm::aes_128_gcm() } @@ -30,6 +32,18 @@ impl aead::AeadCore for Aes128 { type CiphertextOverhead = U16; } +impl QuicCipher for Aes128 { + const KEY_SIZE: usize = ::KEY_SIZE; + const SAMPLE_LEN: usize = 16; + + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] { + quic_header_protection_mask::< + { ::KEY_SIZE }, + { ::SAMPLE_LEN }, + >(boring::symm::Cipher::aes_128_ecb(), hp_key, sample) + } +} + /// Aes256 AEAD cipher pub struct Aes256 {} @@ -42,6 +56,8 @@ impl BoringCipher for Aes256 { const KEY_SIZE: usize = 32; + const TAG_LEN: usize = 16; + fn new_cipher() -> Algorithm { Algorithm::aes_256_gcm() } @@ -57,6 +73,37 @@ impl aead::AeadCore for Aes256 { type CiphertextOverhead = U16; } +impl QuicCipher for Aes256 { + const KEY_SIZE: usize = ::KEY_SIZE; + const SAMPLE_LEN: usize = 16; + + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] { + quic_header_protection_mask::< + { ::KEY_SIZE }, + { ::SAMPLE_LEN }, + >(boring::symm::Cipher::aes_256_ecb(), hp_key, sample) + } +} + +fn quic_header_protection_mask( + cipher: boring::symm::Cipher, + hp_key: &[u8], + sample: &[u8], +) -> [u8; 5] { + assert!(hp_key.len() == KEY_SIZE); + assert!(sample.len() >= SAMPLE_LEN); + + let mut output = [0u8; SAMPLE_LEN]; + + let mut crypter = boring::symm::Crypter::new(cipher, boring::symm::Mode::Encrypt, hp_key, None) + .expect("failed getting crypter"); + + let len = crypter.update(sample, &mut output).unwrap(); + let _ = len + crypter.finalize(&mut output[len..]).unwrap(); + + output[..5].try_into().unwrap() +} + #[cfg(test)] mod tests { use aead::{generic_array::GenericArray, AeadCore, Nonce, Tag}; diff --git a/boring-rustls-provider/src/aead/chacha20.rs b/boring-rustls-provider/src/aead/chacha20.rs index 8825449..b59e0b5 100644 --- a/boring-rustls-provider/src/aead/chacha20.rs +++ b/boring-rustls-provider/src/aead/chacha20.rs @@ -1,4 +1,4 @@ -use super::{BoringAead, BoringCipher}; +use super::{BoringAead, BoringCipher, QuicCipher}; use aead::{ consts::{U12, U16}, AeadCore, @@ -18,6 +18,8 @@ impl BoringCipher for ChaCha20Poly1305 { const KEY_SIZE: usize = 32; + const TAG_LEN: usize = 16; + fn new_cipher() -> Algorithm { Algorithm::chacha20_poly1305() } @@ -27,6 +29,33 @@ impl BoringCipher for ChaCha20Poly1305 { } } +impl QuicCipher for ChaCha20Poly1305 { + const KEY_SIZE: usize = 32; + const SAMPLE_LEN: usize = 16; + + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] { + assert!(hp_key.len() == ::KEY_SIZE); + assert!(sample.len() >= ::SAMPLE_LEN); + + let mut mask = [0u8; 5]; + // RFC9001 5.4.4: The first 4 bytes of the sampled ciphertext are the block counter. A ChaCha20 implementation could take a 32-bit integer in place of a byte sequence, in which case, the byte sequence is interpreted as a little-endian value. + let counter = u32::from_le_bytes(sample[0..4].try_into().unwrap()); + // RFC9001 5.4.4: The remaining 12 bytes are used as the nonce. + let nonce = &sample[4..16]; + unsafe { + boring_sys::CRYPTO_chacha_20( + mask.as_mut_ptr(), + mask.as_ptr(), + mask.len(), + hp_key.as_ptr(), + nonce.as_ptr(), + counter, + ); + }; + mask + } +} + impl AeadCore for ChaCha20Poly1305 { type NonceSize = U12; diff --git a/boring-rustls-provider/src/tls13.rs b/boring-rustls-provider/src/tls13.rs index c400bad..0769e5e 100644 --- a/boring-rustls-provider/src/tls13.rs +++ b/boring-rustls-provider/src/tls13.rs @@ -11,7 +11,7 @@ pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite { }, hkdf_provider: &hkdf::Hkdf::::DEFAULT, aead_alg: &aead::Aead::::DEFAULT, - quic: None, + quic: Some(&aead::Aead::::DEFAULT), }; pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite { @@ -23,7 +23,7 @@ pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite { }, hkdf_provider: &hkdf::Hkdf::::DEFAULT, aead_alg: &aead::Aead::::DEFAULT, - quic: None, + quic: Some(&aead::Aead::::DEFAULT), }; pub static CHACHA20_POLY1305_SHA256: Tls13CipherSuite = Tls13CipherSuite { @@ -36,5 +36,5 @@ pub static CHACHA20_POLY1305_SHA256: Tls13CipherSuite = Tls13CipherSuite { hkdf_provider: &hkdf::Hkdf::::DEFAULT, aead_alg: &aead::Aead::::DEFAULT, - quic: None, + quic: Some(&aead::Aead::::DEFAULT), };