extern crate alloc;
use crate::ct::{Choice, ConstantTimeEq};
use crate::hash::{HmacSha256, Sha256};
pub(crate) const COOKIE_LEN: usize = 38;
pub(crate) const COOKIE_OVERHEAD: usize = 4 + 2 + 32;
pub(crate) const DEFAULT_MAX_AGE_MIN: u32 = 10;
pub(crate) struct CookieGenerator {
secret: [u8; 32],
max_age_minutes: u32,
}
impl CookieGenerator {
pub(crate) fn new(secret: [u8; 32]) -> Self {
Self {
secret,
max_age_minutes: DEFAULT_MAX_AGE_MIN,
}
}
#[allow(dead_code)]
pub(crate) fn with_max_age_minutes(mut self, minutes: u32) -> Self {
self.max_age_minutes = minutes;
self
}
pub(crate) fn generate(
&self,
client_addr: &[u8],
client_random: &[u8; 32],
ch_fingerprint: &[u8],
now_minutes: u32,
) -> [u8; COOKIE_LEN] {
let v =
self.generate_with_aux(client_addr, client_random, ch_fingerprint, &[], now_minutes);
let mut out = [0u8; COOKIE_LEN];
debug_assert_eq!(v.len(), COOKIE_LEN);
out.copy_from_slice(&v);
out
}
pub(crate) fn generate_with_aux(
&self,
client_addr: &[u8],
client_random: &[u8; 32],
ch_fingerprint: &[u8],
aux: &[u8],
now_minutes: u32,
) -> alloc::vec::Vec<u8> {
let ts = now_minutes.to_be_bytes();
let aux_len = (aux.len() as u16).to_be_bytes();
let tag = HmacSha256::new(&self.secret)
.chain(client_addr)
.chain(client_random)
.chain(ch_fingerprint)
.chain(&ts)
.chain(&aux_len)
.chain(aux)
.finalize();
let mut out = alloc::vec::Vec::with_capacity(COOKIE_OVERHEAD + aux.len());
out.extend_from_slice(&ts);
out.extend_from_slice(&aux_len);
out.extend_from_slice(aux);
out.extend_from_slice(tag.as_ref());
out
}
pub(crate) fn validate(
&self,
client_addr: &[u8],
client_random: &[u8; 32],
ch_fingerprint: &[u8],
now_minutes: u32,
cookie: &[u8],
) -> bool {
self.validate_with_aux(
client_addr,
client_random,
ch_fingerprint,
now_minutes,
cookie,
)
.map(|aux| aux.is_empty())
.unwrap_or(false)
}
pub(crate) fn validate_with_aux(
&self,
client_addr: &[u8],
client_random: &[u8; 32],
ch_fingerprint: &[u8],
now_minutes: u32,
cookie: &[u8],
) -> Option<alloc::vec::Vec<u8>> {
if cookie.len() < COOKIE_OVERHEAD {
return None;
}
let mut ts_bytes = [0u8; 4];
ts_bytes.copy_from_slice(&cookie[..4]);
let ts = u32::from_be_bytes(ts_bytes);
let aux_len = u16::from_be_bytes([cookie[4], cookie[5]]) as usize;
if cookie.len() != COOKIE_OVERHEAD + aux_len {
return None;
}
let age = now_minutes.saturating_sub(ts);
let future_skew = ts.saturating_sub(now_minutes);
if age > self.max_age_minutes || future_skew > 1 {
return None;
}
let aux = &cookie[6..6 + aux_len];
let expected = self.generate_with_aux(client_addr, client_random, ch_fingerprint, aux, ts);
let eq: Choice = expected.as_slice().ct_eq(cookie);
if bool::from(eq) {
Some(aux.to_vec())
} else {
None
}
}
}
pub(crate) fn build_ch_fingerprint(
cipher_suites_be: &[u8],
supported_groups_ext: Option<&[u8]>,
supported_versions_ext: Option<&[u8]>,
key_share_groups_be: &[u8],
) -> alloc::vec::Vec<u8> {
use alloc::vec::Vec;
fn push_field(out: &mut Vec<u8>, body: &[u8]) {
let len = body.len() as u32;
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(body);
}
let mut out = Vec::with_capacity(
16 + cipher_suites_be.len()
+ supported_groups_ext.map(|b| b.len()).unwrap_or(0)
+ supported_versions_ext.map(|b| b.len()).unwrap_or(0)
+ key_share_groups_be.len(),
);
push_field(&mut out, cipher_suites_be);
push_field(&mut out, supported_groups_ext.unwrap_or(&[]));
push_field(&mut out, supported_versions_ext.unwrap_or(&[]));
push_field(&mut out, key_share_groups_be);
out
}
#[allow(dead_code)]
type _Sha256ForHmac = Sha256;
#[cfg(test)]
mod tests {
use super::*;
fn fixed_secret() -> [u8; 32] {
let mut s = [0u8; 32];
for (i, b) in s.iter_mut().enumerate() {
*b = i as u8;
}
s
}
fn fixed_random() -> [u8; 32] {
let mut r = [0u8; 32];
for (i, b) in r.iter_mut().enumerate() {
*b = (0xa0 + i) as u8;
}
r
}
const TS: u32 = 1_000_000;
const FP: &[u8] = b"fingerprint-bytes";
#[test]
fn generate_then_validate_succeeds() {
let cg = CookieGenerator::new(fixed_secret());
let addr = b"203.0.113.5:50000";
let rand = fixed_random();
let cookie = cg.generate(addr, &rand, FP, TS);
assert!(cg.validate(addr, &rand, FP, TS, &cookie));
assert!(cg.validate(addr, &rand, FP, TS + 1, &cookie));
}
#[test]
fn expired_cookie_fails() {
let cg = CookieGenerator::new(fixed_secret()).with_max_age_minutes(5);
let addr = b"client";
let rand = fixed_random();
let cookie = cg.generate(addr, &rand, FP, TS);
assert!(cg.validate(addr, &rand, FP, TS + 5, &cookie));
assert!(!cg.validate(addr, &rand, FP, TS + 6, &cookie));
assert!(!cg.validate(addr, &rand, FP, TS + 1_000_000, &cookie));
}
#[test]
fn future_cookie_fails() {
let cg = CookieGenerator::new(fixed_secret());
let addr = b"client";
let rand = fixed_random();
let cookie = cg.generate(addr, &rand, FP, TS + 5);
assert!(!cg.validate(addr, &rand, FP, TS, &cookie));
}
#[test]
fn wrong_address_fails() {
let cg = CookieGenerator::new(fixed_secret());
let addr_a = b"203.0.113.5:50000";
let addr_b = b"203.0.113.5:50001";
let rand = fixed_random();
let cookie = cg.generate(addr_a, &rand, FP, TS);
assert!(!cg.validate(addr_b, &rand, FP, TS, &cookie));
}
#[test]
fn wrong_random_fails() {
let cg = CookieGenerator::new(fixed_secret());
let addr = b"203.0.113.5:50000";
let rand_a = fixed_random();
let mut rand_b = rand_a;
rand_b[0] ^= 1;
let cookie = cg.generate(addr, &rand_a, FP, TS);
assert!(!cg.validate(addr, &rand_b, FP, TS, &cookie));
}
#[test]
fn wrong_fingerprint_fails() {
let cg = CookieGenerator::new(fixed_secret());
let addr = b"203.0.113.5:50000";
let rand = fixed_random();
let fp_a = b"cipher=A,groups=X25519,versions=1.3";
let fp_b = b"cipher=B,groups=X25519,versions=1.3"; let cookie = cg.generate(addr, &rand, fp_a, TS);
assert!(cg.validate(addr, &rand, fp_a, TS, &cookie));
assert!(!cg.validate(addr, &rand, fp_b, TS, &cookie));
assert!(!cg.validate(addr, &rand, b"", TS, &cookie));
}
#[test]
fn truncated_cookie_fails() {
let cg = CookieGenerator::new(fixed_secret());
let addr = b"203.0.113.5:50000";
let rand = fixed_random();
let cookie = cg.generate(addr, &rand, FP, TS);
assert!(!cg.validate(addr, &rand, FP, TS, &cookie[..COOKIE_LEN - 1]));
assert!(!cg.validate(addr, &rand, FP, TS, &[]));
let mut bad = cookie;
bad[COOKIE_LEN - 1] ^= 1;
assert!(!cg.validate(addr, &rand, FP, TS, &bad));
}
#[test]
fn distinct_secrets_disagree() {
let cg_a = CookieGenerator::new([0xaa; 32]);
let cg_b = CookieGenerator::new([0xbb; 32]);
let addr = b"client";
let rand = fixed_random();
let cookie_a = cg_a.generate(addr, &rand, FP, TS);
assert!(!cg_b.validate(addr, &rand, FP, TS, &cookie_a));
}
#[test]
fn aux_roundtrip() {
let cg = CookieGenerator::new(fixed_secret());
let addr = b"client";
let rand = fixed_random();
let aux = b"\x13\x01\x00\x1d\x04hash-of-ch1-32-bytes-........";
let cookie = cg.generate_with_aux(addr, &rand, FP, aux, TS);
let recovered = cg.validate_with_aux(addr, &rand, FP, TS, &cookie);
assert_eq!(recovered.as_deref(), Some(aux.as_slice()));
assert!(!cg.validate(addr, &rand, FP, TS, &cookie));
}
#[test]
fn aux_tamper_fails() {
let cg = CookieGenerator::new(fixed_secret());
let addr = b"client";
let rand = fixed_random();
let aux = b"abcdef";
let mut cookie = cg.generate_with_aux(addr, &rand, FP, aux, TS);
cookie[6] ^= 1;
assert!(cg.validate_with_aux(addr, &rand, FP, TS, &cookie).is_none());
}
#[test]
fn aux_length_field_lie_rejected() {
let cg = CookieGenerator::new(fixed_secret());
let addr = b"client";
let rand = fixed_random();
let aux = b"abcdef";
let mut cookie = cg.generate_with_aux(addr, &rand, FP, aux, TS);
cookie[4] = 0xff;
cookie[5] = 0xff;
assert!(cg.validate_with_aux(addr, &rand, FP, TS, &cookie).is_none());
let mut cookie2 = cg.generate_with_aux(addr, &rand, FP, aux, TS);
cookie2[4] = 0;
cookie2[5] = (aux.len() as u8) + 1;
assert!(
cg.validate_with_aux(addr, &rand, FP, TS, &cookie2)
.is_none()
);
}
#[test]
fn fingerprint_length_prefix_is_unambiguous() {
let a = build_ch_fingerprint(b"AA", Some(b"BB"), Some(b""), b"");
let b = build_ch_fingerprint(b"A", Some(b"ABB"), Some(b""), b"");
assert_ne!(a, b);
let c = build_ch_fingerprint(b"AA", Some(b"BB"), Some(b""), b"");
assert_eq!(a, c);
}
}