use crate::crypto::rng::RngProvider;
use crate::errors::CoreError;
const TLS_LEGACY_VERSION: u16 = 0x0303;
const HS_TYPE_CLIENT_HELLO: u8 = 0x01;
const HS_TYPE_SERVER_HELLO: u8 = 0x02;
const EXT_SUPPORTED_GROUPS: u16 = 0x000a;
const EXT_KEY_SHARE: u16 = 0x0033;
const EXT_SUPPORTED_VERSIONS: u16 = 0x002b;
const GROUP_X25519: u16 = 0x001d;
const HRR_RANDOM: [u8; 32] = [
0xCF, 0x21, 0xAD, 0x74, 0xE5, 0x9A, 0x61, 0x11, 0xBE, 0x1D, 0x8C, 0x02, 0x1E, 0x65, 0xB8, 0x91,
0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB, 0x8C, 0x5E, 0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C,
];
const DOWNGRADE_TLS12: [u8; 8] = [0x44, 0x4F, 0x57, 0x4E, 0x47, 0x52, 0x44, 0x01];
const DOWNGRADE_TLS11: [u8; 8] = [0x44, 0x4F, 0x57, 0x4E, 0x47, 0x52, 0x44, 0x00];
fn is_grease(v: u16) -> bool {
let hi = (v >> 8) as u8;
let lo = (v & 0xff) as u8;
hi == lo && (hi & 0x0f) == 0x0a
}
fn group_key_exchange_len(group: u16) -> usize {
match group {
GROUP_X25519 => 32,
0x11ec => 1216, 0x0017 => 65, 0x0018 => 97, _ => 32,
}
}
pub(crate) struct ParsedClientHello {
pub(crate) legacy_session_id: Vec<u8>,
pub(crate) cipher_suites: Vec<u16>,
pub(crate) supported_groups: Vec<u16>,
pub(crate) key_share_groups: Vec<u16>,
}
struct Cursor<'a> {
b: &'a [u8],
i: usize,
}
impl<'a> Cursor<'a> {
fn new(b: &'a [u8]) -> Self {
Self { b, i: 0 }
}
fn remaining(&self) -> usize {
self.b.len() - self.i
}
fn take(&mut self, n: usize) -> Result<&'a [u8], CoreError> {
let end = self
.i
.checked_add(n)
.ok_or_else(|| err("length overflow"))?;
let s = self.b.get(self.i..end).ok_or_else(|| err("short read"))?;
self.i = end;
Ok(s)
}
fn u8(&mut self) -> Result<u8, CoreError> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, CoreError> {
let s = self.take(2)?;
Ok(u16::from_be_bytes([s[0], s[1]]))
}
fn u24(&mut self) -> Result<u32, CoreError> {
let s = self.take(3)?;
Ok(u32::from_be_bytes([0, s[0], s[1], s[2]]))
}
fn u8_vec(&mut self) -> Result<&'a [u8], CoreError> {
let n = self.u8()? as usize;
self.take(n)
}
fn u16_vec(&mut self) -> Result<&'a [u8], CoreError> {
let n = self.u16()? as usize;
self.take(n)
}
}
fn err(msg: &str) -> CoreError {
CoreError::NetworkError(format!("malformed ClientHello: {msg}"))
}
fn parse_u16_list(b: &[u8]) -> Result<Vec<u16>, CoreError> {
if !b.len().is_multiple_of(2) {
return Err(err("odd-length u16 list"));
}
Ok(b.chunks(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect())
}
pub(crate) fn parse_client_hello(msg: &[u8]) -> Result<ParsedClientHello, CoreError> {
let mut c = Cursor::new(msg);
if c.u8()? != HS_TYPE_CLIENT_HELLO {
return Err(err("not a ClientHello"));
}
let body_len = c.u24()? as usize;
if c.remaining() != body_len {
return Err(err("handshake length mismatch"));
}
if c.u16()? != TLS_LEGACY_VERSION {
return Err(err("unexpected legacy_version"));
}
c.take(32)?; let legacy_session_id = c.u8_vec()?.to_vec();
let cipher_suites = parse_u16_list(c.u16_vec()?)?;
let _compression = c.u8_vec()?;
let mut supported_groups = Vec::new();
let mut key_share_groups = Vec::new();
if c.remaining() > 0 {
let ext_bytes = c.u16_vec()?;
let mut e = Cursor::new(ext_bytes);
while e.remaining() > 0 {
let ext_type = e.u16()?;
let ext_data = e.u16_vec()?;
if ext_type == EXT_SUPPORTED_GROUPS {
let mut g = Cursor::new(ext_data);
supported_groups = parse_u16_list(g.u16_vec()?)?;
} else if ext_type == EXT_KEY_SHARE {
let mut k = Cursor::new(ext_data);
let shares = k.u16_vec()?;
let mut s = Cursor::new(shares);
while s.remaining() > 0 {
key_share_groups.push(s.u16()?);
let _share = s.u16_vec()?; }
}
}
}
Ok(ParsedClientHello {
legacy_session_id,
cipher_suites,
supported_groups,
key_share_groups,
})
}
fn push_u16(out: &mut Vec<u8>, v: u16) {
out.extend_from_slice(&v.to_be_bytes());
}
fn push_u16_vec(out: &mut Vec<u8>, body: &[u8]) {
push_u16(out, body.len() as u16);
out.extend_from_slice(body);
}
fn fresh_server_random(rng: &dyn RngProvider) -> [u8; 32] {
loop {
let mut r = [0u8; 32];
rng.fill_bytes(&mut r);
let tail = &r[24..32];
if r != HRR_RANDOM && tail != DOWNGRADE_TLS12 && tail != DOWNGRADE_TLS11 {
return r;
}
}
}
pub(crate) fn build_server_hello(
client: &ParsedClientHello,
rng: &dyn RngProvider,
) -> Result<Vec<u8>, CoreError> {
let tls13 = [0x1301u16, 0x1302, 0x1303];
let cipher = client
.cipher_suites
.iter()
.copied()
.find(|c| tls13.contains(c))
.or_else(|| {
client
.cipher_suites
.iter()
.copied()
.find(|c| !is_grease(*c))
})
.ok_or_else(|| err("no usable cipher_suite offered"))?;
let usable = |g: u16| {
!is_grease(g)
&& client.supported_groups.contains(&g)
&& client.key_share_groups.contains(&g)
};
let group = if usable(GROUP_X25519) {
GROUP_X25519
} else {
client
.key_share_groups
.iter()
.copied()
.find(|g| usable(*g))
.ok_or_else(|| err("no key_share group common to supported_groups"))?
};
let mut body = Vec::new();
push_u16(&mut body, TLS_LEGACY_VERSION);
body.extend_from_slice(&fresh_server_random(rng));
body.push(client.legacy_session_id.len() as u8);
body.extend_from_slice(&client.legacy_session_id);
push_u16(&mut body, cipher);
body.push(0x00);
let mut exts = Vec::new();
{
let mut sv = Vec::new();
push_u16(&mut sv, 0x0304);
exts.extend_from_slice(&ext(EXT_SUPPORTED_VERSIONS, &sv));
}
{
let klen = group_key_exchange_len(group);
let mut key = vec![0u8; klen];
rng.fill_bytes(&mut key);
let mut ks = Vec::new();
push_u16(&mut ks, group);
push_u16_vec(&mut ks, &key);
exts.extend_from_slice(&ext(EXT_KEY_SHARE, &ks));
}
push_u16_vec(&mut body, &exts);
let mut msg = Vec::with_capacity(4 + body.len());
msg.push(HS_TYPE_SERVER_HELLO);
let len = body.len() as u32;
msg.push((len >> 16) as u8);
msg.push((len >> 8) as u8);
msg.push(len as u8);
msg.extend_from_slice(&body);
Ok(msg)
}
fn ext(ext_type: u16, data: &[u8]) -> Vec<u8> {
let mut e = Vec::with_capacity(4 + data.len());
push_u16(&mut e, ext_type);
push_u16_vec(&mut e, data);
e
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::rng::OsRng;
use crate::transport::legs::mimic_tls::client_hello::build_client_hello;
fn reparse_server_hello(msg: &[u8]) -> Result<(u16, Vec<u8>, u16, u16), CoreError> {
let mut c = Cursor::new(msg);
if c.u8()? != HS_TYPE_SERVER_HELLO {
return Err(err("not a ServerHello"));
}
let blen = c.u24()? as usize;
if c.remaining() != blen {
return Err(err("length mismatch"));
}
if c.u16()? != TLS_LEGACY_VERSION {
return Err(err("bad legacy_version"));
}
c.take(32)?;
let sid = c.u8_vec()?.to_vec();
let cipher = c.u16()?;
if c.u8()? != 0x00 {
return Err(err("bad compression"));
}
let ext_bytes = c.u16_vec()?;
let mut e = Cursor::new(ext_bytes);
let mut selected_version = 0;
let mut ks_group = 0;
while e.remaining() > 0 {
let et = e.u16()?;
let ed = e.u16_vec()?;
if et == EXT_SUPPORTED_VERSIONS {
let mut v = Cursor::new(ed);
selected_version = v.u16()?;
} else if et == EXT_KEY_SHARE {
let mut k = Cursor::new(ed);
ks_group = k.u16()?;
let _share = k.u16_vec()?;
}
}
Ok((cipher, sid, selected_version, ks_group))
}
#[test]
fn server_hello_is_consistent_with_the_client_hello() {
let ch = build_client_hello("consistency.test", &OsRng).expect("build");
let parsed = parse_client_hello(&ch).expect("parse our own CH");
let sh = build_server_hello(&parsed, &OsRng).expect("build SH");
let (cipher, sid, version, group) = reparse_server_hello(&sh).expect("reparse SH");
assert!(parsed.cipher_suites.contains(&cipher));
assert!(!is_grease(cipher));
assert_eq!(sid, parsed.legacy_session_id);
assert_eq!(version, 0x0304);
assert!(parsed.supported_groups.contains(&group));
assert!(parsed.key_share_groups.contains(&group));
assert!(!is_grease(group));
}
#[test]
fn server_random_avoids_hrr_and_downgrade_sentinels() {
let parsed = ParsedClientHello {
legacy_session_id: vec![7u8; 32],
cipher_suites: vec![0x1301],
supported_groups: vec![GROUP_X25519],
key_share_groups: vec![GROUP_X25519],
};
let sh = build_server_hello(&parsed, &OsRng).expect("build");
let random = &sh[6..38];
assert_ne!(random, &HRR_RANDOM[..]);
assert_ne!(&random[24..32], &DOWNGRADE_TLS12[..]);
assert_ne!(&random[24..32], &DOWNGRADE_TLS11[..]);
}
#[test]
fn key_share_length_matches_selected_group() {
let parsed = ParsedClientHello {
legacy_session_id: vec![0u8; 32],
cipher_suites: vec![0x1301],
supported_groups: vec![GROUP_X25519],
key_share_groups: vec![GROUP_X25519],
};
let sh = build_server_hello(&parsed, &OsRng).expect("build");
let (_c, _s, _v, group) = reparse_server_hello(&sh).expect("reparse");
assert_eq!(group, GROUP_X25519);
}
#[test]
fn rejects_a_truncated_client_hello() {
let ch = build_client_hello("x.test", &OsRng).expect("build");
let truncated = &ch[..ch.len() - 10];
assert!(parse_client_hello(truncated).is_err());
}
#[test]
fn rejects_garbage() {
assert!(parse_client_hello(b"not a tls clienthello at all").is_err());
assert!(parse_client_hello(&[]).is_err());
assert!(parse_client_hello(&[HS_TYPE_SERVER_HELLO, 0, 0, 0]).is_err());
}
#[test]
fn no_common_key_share_group_is_an_error_not_a_panic() {
let parsed = ParsedClientHello {
legacy_session_id: vec![0u8; 32],
cipher_suites: vec![0x1301],
supported_groups: vec![GROUP_X25519],
key_share_groups: vec![], };
assert!(build_server_hello(&parsed, &OsRng).is_err());
}
fn ch_with_raw_extensions(ext_block: &[u8]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0x03, 0x03]); body.extend_from_slice(&[0u8; 32]); body.push(0x00); body.extend_from_slice(&[0x00, 0x02, 0x13, 0x01]); body.extend_from_slice(&[0x01, 0x00]); body.extend_from_slice(&(ext_block.len() as u16).to_be_bytes());
body.extend_from_slice(ext_block);
let mut msg = vec![HS_TYPE_CLIENT_HELLO];
let len = body.len() as u32;
msg.extend_from_slice(&[(len >> 16) as u8, (len >> 8) as u8, len as u8]);
msg.extend_from_slice(&body);
msg
}
#[test]
fn rejects_inner_keyshare_length_overrun() {
let ext_block = [
0x00, 0x33, 0x00, 0x06, 0x00, 0x04, 0x00, 0x1d, 0xFF, 0xFF, ];
let ch = ch_with_raw_extensions(&ext_block);
assert!(
parse_client_hello(&ch).is_err(),
"an inner key_share length overrun must be a typed error, not an OOB"
);
}
#[test]
fn rejects_maximal_junk_in_bounded_time() {
let junk = vec![0x01u8; 64 * 1024];
assert!(parse_client_hello(&junk).is_err());
}
}