From dd710cb3caa8b7c27c3522ae463a6b86b2eeec17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=BCth?= Date: Mon, 27 Nov 2023 14:06:24 +0100 Subject: [PATCH 1/3] Update boring and allow settings fips --- Cargo.toml | 4 ++-- boring-rustls-provider/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9dbfd7b..ef7a6ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,6 @@ default-members = [ resolver = "2" [workspace.dependencies] -boring = { version = "4.0", default-features = false } -boring-sys = { version = "4.0", default-features = false } +boring = { version = "4", default-features = false } +boring-sys = { version = "4", default-features = false } rustls = { version = "=0.22.0-alpha.5", default-features = false } diff --git a/boring-rustls-provider/Cargo.toml b/boring-rustls-provider/Cargo.toml index 50ffd8d..e27ca10 100644 --- a/boring-rustls-provider/Cargo.toml +++ b/boring-rustls-provider/Cargo.toml @@ -10,7 +10,7 @@ publish = false [features] default = ["tls12"] # Use a FIPS-validated version of boringssl. -#fips = ["boring/fips", "boring-sys/fips"] +fips = ["boring/fips", "boring-sys/fips"] logging = ["log"] fips-only = [] tls12 = ["rustls/tls12"] From 555d558034c7b749cb30e15587102b524a7e4f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=BCth?= Date: Thu, 30 Nov 2023 00:17:28 +0100 Subject: [PATCH 2/3] Consolidate rustls dependencies in workspace deps --- Cargo.toml | 5 +++++ boring-additions/Cargo.toml | 4 ---- boring-rustls-provider/Cargo.toml | 9 +++------ boring-sys-additions/Cargo.toml | 4 ---- examples/Cargo.toml | 4 ++-- 5 files changed, 10 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef7a6ca..3b3bf7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,8 @@ resolver = "2" boring = { version = "4", default-features = false } boring-sys = { version = "4", default-features = false } rustls = { version = "=0.22.0-alpha.5", default-features = false } +rustls-pemfile = { version = "=2.0.0-alpha.2" } +rustls-pki-types = { version = "=0.2.2" } +tokio-rustls = { version = "0.25.0-alpha.3" } +webpki = { package = "rustls-webpki", version = "0.102.0-alpha.7", default-features = false, features = ["alloc", "std"] } +webpki-roots = { version = "=0.26.0-alpha.2" } diff --git a/boring-additions/Cargo.toml b/boring-additions/Cargo.toml index 88d8c52..ef0d341 100644 --- a/boring-additions/Cargo.toml +++ b/boring-additions/Cargo.toml @@ -12,7 +12,3 @@ aead = { version = "0.5", default_features = false, features = ["alloc"] } boring = { workspace = true } boring-sys = { workspace = true } foreign-types = "0.5" - - - - diff --git a/boring-rustls-provider/Cargo.toml b/boring-rustls-provider/Cargo.toml index e27ca10..53ac217 100644 --- a/boring-rustls-provider/Cargo.toml +++ b/boring-rustls-provider/Cargo.toml @@ -27,15 +27,12 @@ lazy_static = "1.4" log = { version = "0.4.4", optional = true } once_cell = "1" rustls = { workspace = true } -rustls-pki-types = "0.2" +rustls-pki-types = { workspace = true } spki = "0.7" -webpki = { package = "rustls-webpki", version = "0.102.0-alpha.1", default-features = false, features = ["alloc", "std"] } +webpki = { workspace = true, features = ["alloc", "std"] } [dev-dependencies] hex-literal = "0.4" rcgen = "0.11.3" tokio = { version = "1.34", features = ["macros", "rt", "net", "io-util", "io-std"] } -tokio-rustls = "0.25.0-alpha.2" - - - +tokio-rustls = { workspace = true } diff --git a/boring-sys-additions/Cargo.toml b/boring-sys-additions/Cargo.toml index eab9833..3831620 100644 --- a/boring-sys-additions/Cargo.toml +++ b/boring-sys-additions/Cargo.toml @@ -9,7 +9,3 @@ publish = false [dependencies] boring-sys = { workspace = true } - - - - diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 09f7247..0b89373 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -15,7 +15,7 @@ pki-types = { package = "rustls-pki-types", version = "0.2" } rcgen = { version = "0.11.3", features = ["pem"], default-features = false } rustls = { workspace = true, features = [ "logging" ]} boring-rustls-provider = { path = "../boring-rustls-provider", features = ["logging"] } -rustls-pemfile = "=2.0.0-alpha.1" +rustls-pemfile = "=2.0.0-alpha.2" serde = "1.0" serde_derive = "1.0" -webpki-roots = "=0.26.0-alpha.1" \ No newline at end of file +webpki-roots = "=0.26.0-alpha.2" From 5b0c4665580117ffc050313ebfebf743ea5ffc2d 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 3/3] 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), };