use crate::ct::{Choice, ConstantTimeEq};
use crate::hash::{HmacSha256, Sha256};
pub(crate) const COOKIE_LEN: usize = 36;
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],
now_minutes: u32,
) -> [u8; COOKIE_LEN] {
let ts = now_minutes.to_be_bytes();
let tag = HmacSha256::new(&self.secret)
.chain(client_addr)
.chain(client_random)
.chain(&ts)
.finalize();
let mut out = [0u8; COOKIE_LEN];
out[..4].copy_from_slice(&ts);
out[4..].copy_from_slice(tag.as_ref());
out
}
pub(crate) fn validate(
&self,
client_addr: &[u8],
client_random: &[u8; 32],
now_minutes: u32,
cookie: &[u8],
) -> bool {
if cookie.len() != COOKIE_LEN {
return false;
}
let mut ts_bytes = [0u8; 4];
ts_bytes.copy_from_slice(&cookie[..4]);
let ts = u32::from_be_bytes(ts_bytes);
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 false;
}
let expected = self.generate(client_addr, client_random, ts);
let eq: Choice = expected.as_slice().ct_eq(cookie);
bool::from(eq)
}
}
#[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;
#[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, TS);
assert!(cg.validate(addr, &rand, TS, &cookie));
assert!(cg.validate(addr, &rand, 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, TS);
assert!(cg.validate(addr, &rand, TS + 5, &cookie));
assert!(!cg.validate(addr, &rand, TS + 6, &cookie));
assert!(!cg.validate(addr, &rand, 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, TS + 5);
assert!(!cg.validate(addr, &rand, 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, TS);
assert!(!cg.validate(addr_b, &rand, 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, TS);
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, TS);
assert!(!cg.validate(addr, &rand, TS, &cookie[..COOKIE_LEN - 1]));
assert!(!cg.validate(addr, &rand, TS, &[]));
let mut bad = cookie;
bad[COOKIE_LEN - 1] ^= 1;
assert!(!cg.validate(addr, &rand, 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, TS);
assert!(!cg_b.validate(addr, &rand, TS, &cookie_a));
}
}