use alloc::boxed::Box;
use ic_core::traits::BlockCipher as _;
use rustls::crypto::cipher::{AeadKey, Iv, Nonce};
use rustls::quic::{Algorithm, HeaderProtectionKey, PacketKey, Tag};
use rustls::Error;
use crate::aead::{Cipher, Suite};
const TAG_LEN: usize = 16;
const SAMPLE_LEN: usize = 16;
const MASK_LEN: usize = 5;
pub(crate) static AES_128_GCM: IcQuic = IcQuic {
suite: Suite::Aes128,
confidentiality_limit: 1 << 23,
integrity_limit: 1 << 52,
};
pub(crate) static AES_256_GCM: IcQuic = IcQuic {
suite: Suite::Aes256,
confidentiality_limit: 1 << 23,
integrity_limit: 1 << 52,
};
pub(crate) static CHACHA20_POLY1305: IcQuic = IcQuic {
suite: Suite::ChaCha20,
confidentiality_limit: u64::MAX,
integrity_limit: 1 << 36,
};
#[derive(Debug)]
pub(crate) struct IcQuic {
suite: Suite,
confidentiality_limit: u64,
integrity_limit: u64,
}
impl Algorithm for IcQuic {
fn packet_key(&self, key: AeadKey, iv: Iv) -> Box<dyn PacketKey> {
Box::new(IcPacketKey {
cipher: self
.suite
.cipher(key.as_ref())
.expect("rustls supplied a key this suite does not use"),
iv,
confidentiality_limit: self.confidentiality_limit,
integrity_limit: self.integrity_limit,
})
}
fn header_protection_key(&self, key: AeadKey) -> Box<dyn HeaderProtectionKey> {
Box::new(IcHeaderKey::new(self.suite, key.as_ref()))
}
fn aead_key_len(&self) -> usize {
self.suite.key_len()
}
fn fips(&self) -> bool {
false
}
}
struct IcPacketKey {
cipher: Cipher,
iv: Iv,
confidentiality_limit: u64,
integrity_limit: u64,
}
impl PacketKey for IcPacketKey {
fn encrypt_in_place(
&self,
packet_number: u64,
header: &[u8],
payload: &mut [u8],
) -> Result<Tag, Error> {
let nonce = Nonce::new(&self.iv, packet_number).0;
let mut tag = [0u8; TAG_LEN];
self.cipher
.seal(&nonce, header, payload, &mut tag)
.map_err(|_| Error::EncryptError)?;
Ok(Tag::from(&tag[..]))
}
fn decrypt_in_place<'a>(
&self,
packet_number: u64,
header: &[u8],
payload: &'a mut [u8],
) -> Result<&'a [u8], Error> {
if payload.len() < TAG_LEN {
return Err(Error::DecryptError);
}
let nonce = Nonce::new(&self.iv, packet_number).0;
let plain_len = payload.len() - TAG_LEN;
let (body, tag) = payload.split_at_mut(plain_len);
let tag: [u8; TAG_LEN] = tag.try_into().expect("split at exactly the tag length");
self.cipher
.open(&nonce, header, body, &tag)
.map_err(|_| Error::DecryptError)?;
Ok(&payload[..plain_len])
}
fn tag_len(&self) -> usize {
TAG_LEN
}
fn confidentiality_limit(&self) -> u64 {
self.confidentiality_limit
}
fn integrity_limit(&self) -> u64 {
self.integrity_limit
}
}
enum IcHeaderKey {
Aes128(ic_cipher::Aes128),
Aes256(ic_cipher::Aes256),
ChaCha20([u8; 32]),
}
impl IcHeaderKey {
fn new(suite: Suite, key: &[u8]) -> Self {
match suite {
Suite::Aes128 => Self::Aes128(
ic_cipher::Aes128::new(key).expect("rustls supplied a key this suite does not use"),
),
Suite::Aes256 => Self::Aes256(
ic_cipher::Aes256::new(key).expect("rustls supplied a key this suite does not use"),
),
Suite::ChaCha20 => {
let mut k = [0u8; 32];
k.copy_from_slice(key);
Self::ChaCha20(k)
}
}
}
fn mask(&self, sample: &[u8]) -> Result<[u8; MASK_LEN], Error> {
if sample.len() != SAMPLE_LEN {
return Err(Error::General(alloc::format!(
"a header protection sample is {SAMPLE_LEN} bytes, not {}",
sample.len()
)));
}
let mut mask = [0u8; MASK_LEN];
match self {
Self::Aes128(_) | Self::Aes256(_) => {
let mut block = [0u8; 16];
block.copy_from_slice(sample);
match self {
Self::Aes128(c) => c.encrypt_block(&mut block),
Self::Aes256(c) => c.encrypt_block(&mut block),
Self::ChaCha20(_) => unreachable!("matched on AES above"),
}
.map_err(|_| Error::General("header protection block failed".into()))?;
mask.copy_from_slice(&block[..MASK_LEN]);
}
Self::ChaCha20(key) => {
let counter = u32::from_le_bytes([sample[0], sample[1], sample[2], sample[3]]);
ic_cipher::chacha20_xor(key, &sample[4..], counter, &mut mask)
.map_err(|_| Error::General("header protection keystream failed".into()))?;
}
}
Ok(mask)
}
fn apply(&self, sample: &[u8], first: &mut u8, packet_number: &mut [u8]) -> Result<(), Error> {
if packet_number.len() > 4 {
return Err(Error::General(alloc::format!(
"a QUIC packet number is at most 4 bytes, not {}",
packet_number.len()
)));
}
let mask = self.mask(sample)?;
let long = *first & 0x80 == 0x80;
*first ^= mask[0] & if long { 0x0f } else { 0x1f };
for (byte, m) in packet_number.iter_mut().zip(&mask[1..]) {
*byte ^= m;
}
Ok(())
}
}
impl HeaderProtectionKey for IcHeaderKey {
fn encrypt_in_place(
&self,
sample: &[u8],
first: &mut u8,
packet_number: &mut [u8],
) -> Result<(), Error> {
self.apply(sample, first, packet_number)
}
fn decrypt_in_place(
&self,
sample: &[u8],
first: &mut u8,
packet_number: &mut [u8],
) -> Result<(), Error> {
self.apply(sample, first, packet_number)
}
fn sample_len(&self) -> usize {
SAMPLE_LEN
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hex(s: &str) -> alloc::vec::Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
#[test]
fn the_client_initial_header_matches_rfc9001() {
let key = hex("9f50449e04a0e810283a1e9933adedd2");
let sample = hex("d1b1c98dd7689fb8ec11d242b123dc9b");
let hp = IcHeaderKey::new(Suite::Aes128, &key);
let mut first = 0xc3u8;
let mut pn = hex("00000002");
hp.encrypt_in_place(&sample, &mut first, &mut pn).unwrap();
assert_eq!(first, 0xc0, "protected first byte");
assert_eq!(pn, hex("7b9aec34"), "protected packet number");
hp.decrypt_in_place(&sample, &mut first, &mut pn).unwrap();
assert_eq!(first, 0xc3);
assert_eq!(pn, hex("00000002"));
}
#[test]
fn the_server_initial_header_matches_rfc9001() {
let key = hex("c206b8d9b9f0f37644430b490eeaa314");
let sample = hex("2cd0991cd25b0aac406a5816b6394100");
let hp = IcHeaderKey::new(Suite::Aes128, &key);
let mut first = 0xc1u8;
let mut pn = hex("0001");
hp.encrypt_in_place(&sample, &mut first, &mut pn).unwrap();
assert_eq!(first, 0xcf);
assert_eq!(pn, hex("c0d9"));
}
#[test]
fn the_chacha20_short_header_matches_rfc9001() {
let key = hex("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4");
let sample = hex("5e5cd55c41f69080575d7999c25a5bfb");
let hp = IcHeaderKey::new(Suite::ChaCha20, &key);
let mut first = 0x42u8;
let mut pn = hex("00bff4");
hp.encrypt_in_place(&sample, &mut first, &mut pn).unwrap();
assert_eq!(first, 0x4c, "protected first byte");
assert_eq!(pn, hex("fe4189"), "protected packet number");
hp.decrypt_in_place(&sample, &mut first, &mut pn).unwrap();
assert_eq!(first, 0x42);
assert_eq!(pn, hex("00bff4"));
}
#[test]
fn the_header_form_decides_how_many_bits_are_masked() {
let hp = IcHeaderKey::new(Suite::Aes128, &[0x11u8; 16]);
let sample = (0u8..=255)
.map(|i| alloc::vec![i; SAMPLE_LEN])
.find(|s| hp.mask(s).unwrap()[0] & 0x10 != 0)
.expect("no sample set the fifth mask bit");
let mask0 = hp.mask(&sample).unwrap()[0];
let mut first = 0xc3u8;
let mut pn = alloc::vec![0u8; 1];
hp.encrypt_in_place(&sample, &mut first, &mut pn).unwrap();
assert_eq!(
first & 0x10,
0xc3 & 0x10,
"a long header masked the fifth bit, which belongs to the packet type"
);
assert_eq!(first ^ 0xc3, mask0 & 0x0f);
let mut first = 0x42u8;
let mut pn = alloc::vec![0u8; 1];
hp.encrypt_in_place(&sample, &mut first, &mut pn).unwrap();
assert_ne!(
first & 0x10,
0x42 & 0x10,
"a short header left the fifth bit alone, so it protects too little"
);
assert_eq!(first ^ 0x42, mask0 & 0x1f);
}
#[test]
fn a_bad_sample_is_refused_without_touching_the_header() {
let hp = IcHeaderKey::new(Suite::Aes128, &[0x11u8; 16]);
for bad in [alloc::vec![], alloc::vec![0u8; 15], alloc::vec![0u8; 17]] {
let mut first = 0xc3u8;
let mut pn = alloc::vec![1u8, 2, 3, 4];
assert!(hp.encrypt_in_place(&bad, &mut first, &mut pn).is_err());
assert_eq!(first, 0xc3, "the first byte was modified anyway");
assert_eq!(pn, [1, 2, 3, 4], "the packet number was modified anyway");
}
let mut first = 0xc3u8;
let mut pn = alloc::vec![1u8, 2, 3, 4, 5];
assert!(hp
.encrypt_in_place(&[0u8; SAMPLE_LEN], &mut first, &mut pn)
.is_err());
assert_eq!(first, 0xc3);
}
#[test]
fn a_packet_round_trips_and_is_bound_to_its_context() {
let mut checked = 0;
for alg in [&AES_256_GCM, &CHACHA20_POLY1305] {
assert_eq!(alg.aead_key_len(), 32, "only the 32-byte suites fit here");
let pk = alg.packet_key(AeadKey::from([0x3au8; 32]), Iv::copy(&[0x5cu8; 12]));
let header = b"@";
let mut buf = alloc::vec![0u8; 32 + TAG_LEN];
buf[..32].copy_from_slice(&[0x7eu8; 32]);
let tag = pk.encrypt_in_place(7, header, &mut buf[..32]).unwrap();
buf[32..].copy_from_slice(tag.as_ref());
assert_ne!(&buf[..32], &[0x7eu8; 32][..], "the payload was not sealed");
let mut wrong = buf.clone();
assert!(pk.decrypt_in_place(8, header, &mut wrong).is_err());
let mut wrong = buf.clone();
assert!(pk.decrypt_in_place(7, b"@", &mut wrong).is_err());
let mut short = alloc::vec![0u8; TAG_LEN - 1];
assert!(pk.decrypt_in_place(7, header, &mut short).is_err());
let opened = pk.decrypt_in_place(7, header, &mut buf).unwrap();
assert_eq!(opened, &[0x7eu8; 32][..]);
checked += 1;
}
assert_eq!(checked, 2);
}
#[test]
fn the_key_lengths_match_the_suites() {
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);
for suite in [Suite::Aes128, Suite::Aes256, Suite::ChaCha20] {
let _ = IcHeaderKey::new(suite, &alloc::vec![0u8; suite.key_len()]);
}
for alg in [&AES_128_GCM, &AES_256_GCM, &CHACHA20_POLY1305] {
assert!(!alg.fips());
}
}
#[test]
fn the_aead_limits_are_the_published_ones() {
let pk = AES_256_GCM.packet_key(AeadKey::from([0u8; 32]), Iv::copy(&[0u8; 12]));
assert_eq!(pk.confidentiality_limit(), 1 << 23, "RFC 9001 B.1.1");
assert_eq!(pk.integrity_limit(), 1 << 52, "RFC 9001 B.1.2");
assert_eq!(pk.tag_len(), 16);
assert_eq!(AES_128_GCM.confidentiality_limit, 1 << 23);
assert_eq!(AES_128_GCM.integrity_limit, 1 << 52);
let pk = CHACHA20_POLY1305.packet_key(AeadKey::from([0u8; 32]), Iv::copy(&[0u8; 12]));
assert_eq!(pk.confidentiality_limit(), u64::MAX, "RFC 9001 6.6");
assert_eq!(pk.integrity_limit(), 1 << 36, "RFC 9001 6.6");
assert_ne!(
AES_256_GCM.integrity_limit,
CHACHA20_POLY1305.integrity_limit
);
}
}