Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quic #7

Merged
merged 3 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ 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 }
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" }
2 changes: 2 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 0 additions & 4 deletions boring-additions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,3 @@ aead = { version = "0.5", default_features = false, features = ["alloc"] }
boring = { workspace = true }
boring-sys = { workspace = true }
foreign-types = "0.5"




11 changes: 4 additions & 7 deletions boring-rustls-provider/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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 }
166 changes: 164 additions & 2 deletions boring-rustls-provider/src/aead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,26 @@ 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;

/// Extract keys
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<T: BoringAead> {
Expand Down Expand Up @@ -237,9 +250,51 @@ where
}
}

pub(crate) struct Aead<T: BoringCipher>(PhantomData<T>);
impl<T> rustls::quic::PacketKey for BoringAeadCrypter<T>
where
T: QuicCipher + BoringAead,
{
fn encrypt_in_place(
&self,
packet_number: u64,
header: &[u8],
payload: &mut [u8],
) -> Result<rustls::quic::Tag, rustls::Error> {
let associated_data = header;
let nonce = cipher::Nonce::new(&self.iv, packet_number);
let tag = self
.encrypt_in_place_detached(Nonce::<T>::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<T: BoringCipher> Aead<T> {
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 {
<T as BoringCipher>::TAG_LEN
}
}

pub(crate) struct Aead<T>(PhantomData<T>);

impl<T> Aead<T> {
pub const DEFAULT: Self = Self(PhantomData);
}

Expand Down Expand Up @@ -331,3 +386,110 @@ impl<T: BoringAead + 'static> cipher::Tls12AeadAlgorithm for Aead<T> {
Ok(<T as BoringCipher>::extract_keys(key, Iv::copy(&nonce)))
}
}

struct QuicHeaderProtector<T: QuicCipher> {
key: cipher::AeadKey,
phantom: PhantomData<T>,
}

impl<T: QuicCipher> QuicHeaderProtector<T> {
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<T: QuicCipher> rustls::quic::HeaderProtectionKey for QuicHeaderProtector<T> {
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<T> rustls::quic::Algorithm for Aead<T>
where
T: QuicCipher + BoringAead + 'static,
{
fn packet_key(&self, key: cipher::AeadKey, iv: Iv) -> Box<dyn rustls::quic::PacketKey> {
Box::new(
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3)
.expect("failed to create AEAD crypter"),
)
}

fn header_protection_key(
&self,
key: cipher::AeadKey,
) -> Box<dyn rustls::quic::HeaderProtectionKey> {
Box::new(QuicHeaderProtector {
key,
phantom: PhantomData::<T>,
})
}

fn aead_key_len(&self) -> usize {
<T as QuicCipher>::KEY_SIZE
}
}
49 changes: 48 additions & 1 deletion boring-rustls-provider/src/aead/aes.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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()
}
Expand All @@ -30,6 +32,18 @@ impl aead::AeadCore for Aes128 {
type CiphertextOverhead = U16;
}

impl QuicCipher for Aes128 {
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
const SAMPLE_LEN: usize = 16;

fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] {
quic_header_protection_mask::<
{ <Self as QuicCipher>::KEY_SIZE },
{ <Self as QuicCipher>::SAMPLE_LEN },
>(boring::symm::Cipher::aes_128_ecb(), hp_key, sample)
}
}

/// Aes256 AEAD cipher
pub struct Aes256 {}

Expand All @@ -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()
}
Expand All @@ -57,6 +73,37 @@ impl aead::AeadCore for Aes256 {
type CiphertextOverhead = U16;
}

impl QuicCipher for Aes256 {
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
const SAMPLE_LEN: usize = 16;

fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] {
quic_header_protection_mask::<
{ <Self as QuicCipher>::KEY_SIZE },
{ <Self as QuicCipher>::SAMPLE_LEN },
>(boring::symm::Cipher::aes_256_ecb(), hp_key, sample)
}
}

fn quic_header_protection_mask<const KEY_SIZE: usize, const SAMPLE_LEN: usize>(
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};
Expand Down
31 changes: 30 additions & 1 deletion boring-rustls-provider/src/aead/chacha20.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{BoringAead, BoringCipher};
use super::{BoringAead, BoringCipher, QuicCipher};
use aead::{
consts::{U12, U16},
AeadCore,
Expand All @@ -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()
}
Expand All @@ -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() == <Self as QuicCipher>::KEY_SIZE);
assert!(sample.len() >= <Self as QuicCipher>::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;

Expand Down
Loading