use super::{CompressionMethod, Dtls13CipherSuite, ProtocolVersion};
use super::{Cookie, Extension, Random, SessionId};
use arrayvec::ArrayVec;
use nom::bytes::complete::take;
use nom::error::{Error, ErrorKind};
use nom::number::complete::{be_u8, be_u16};
use nom::{Err, IResult};
use crate::buffer::Buf;
use crate::util::{many0, many1};
#[derive(Debug, PartialEq, Eq)]
pub struct ClientHello {
pub legacy_version: ProtocolVersion,
pub random: Random,
pub legacy_session_id: SessionId,
pub legacy_cookie: Cookie,
pub cipher_suites: ArrayVec<Dtls13CipherSuite, 3>,
pub legacy_compression_methods: ArrayVec<CompressionMethod, 1>,
pub extensions: ArrayVec<Extension, 8>,
}
impl ClientHello {
pub fn new(
legacy_version: ProtocolVersion,
random: Random,
legacy_session_id: SessionId,
legacy_cookie: Cookie,
cipher_suites: ArrayVec<Dtls13CipherSuite, 3>,
legacy_compression_methods: ArrayVec<CompressionMethod, 1>,
) -> Self {
ClientHello {
legacy_version,
random,
legacy_session_id,
legacy_cookie,
cipher_suites,
legacy_compression_methods,
extensions: ArrayVec::new(),
}
}
pub fn parse(input: &[u8], base_offset: usize) -> IResult<&[u8], ClientHello> {
Self::parse_with_options(input, base_offset, false)
}
pub(crate) fn parse_allow_unknown_suites(
input: &[u8],
base_offset: usize,
) -> IResult<&[u8], ClientHello> {
Self::parse_with_options(input, base_offset, true)
}
fn parse_with_options(
input: &[u8],
base_offset: usize,
allow_unknown_suites: bool,
) -> IResult<&[u8], ClientHello> {
let original_input = input;
let (input, legacy_version) = ProtocolVersion::parse(input)?;
let (input, random) = Random::parse(input)?;
let (input, legacy_session_id) = SessionId::parse(input)?;
let (input, legacy_cookie) = Cookie::parse(input)?;
let (input, cipher_suites_len) = be_u16(input)?;
if cipher_suites_len == 0 || cipher_suites_len % 2 != 0 {
return Err(Err::Failure(Error::new(input, ErrorKind::LengthValue)));
}
let (input, input_cipher) = take(cipher_suites_len)(input)?;
let (rest, cipher_suites) = if allow_unknown_suites {
many0(Dtls13CipherSuite::parse, Dtls13CipherSuite::is_supported)(input_cipher)?
} else {
many1(Dtls13CipherSuite::parse, Dtls13CipherSuite::is_supported)(input_cipher)?
};
if !rest.is_empty() {
return Err(Err::Failure(Error::new(rest, ErrorKind::LengthValue)));
}
let (input, compression_methods_len) = be_u8(input)?;
let (input, input_compression) = take(compression_methods_len)(input)?;
let (rest, legacy_compression_methods) =
many1(CompressionMethod::parse, CompressionMethod::is_supported)(input_compression)?;
if !rest.is_empty() {
return Err(Err::Failure(Error::new(rest, ErrorKind::LengthValue)));
}
let consumed = input.as_ptr() as usize - original_input.as_ptr() as usize;
let extensions_base_offset = base_offset + consumed;
let (remaining_input, extensions) = Self::parse_extensions(input, extensions_base_offset)?;
Ok((
remaining_input,
ClientHello {
legacy_version,
random,
legacy_session_id,
legacy_cookie,
cipher_suites,
legacy_compression_methods,
extensions,
},
))
}
fn parse_extensions(
input: &[u8],
base_offset: usize,
) -> IResult<&[u8], ArrayVec<Extension, 8>> {
let mut extensions = ArrayVec::new();
if input.is_empty() {
return Ok((input, extensions));
}
let original_input = input;
let (remaining, extensions_len) = be_u16(input)?;
let (remaining, extensions_data) = take(extensions_len)(remaining)?;
if !remaining.is_empty() {
return Err(Err::Failure(Error::new(remaining, ErrorKind::LengthValue)));
}
let consumed = extensions_data.as_ptr() as usize - original_input.as_ptr() as usize;
let data_base_offset = base_offset + consumed;
let mut extensions_rest = extensions_data;
let mut current_offset = data_base_offset;
while !extensions_rest.is_empty() {
let before_len = extensions_rest.len();
let (rest, extension) = Extension::parse(extensions_rest, current_offset)?;
let parsed_len = before_len - rest.len();
current_offset += parsed_len;
if extension.extension_type.is_supported() {
if extensions
.iter()
.any(|existing| existing.extension_type == extension.extension_type)
{
return Err(Err::Failure(Error::new(
extensions_rest,
ErrorKind::LengthValue,
)));
}
extensions.try_push(extension).map_err(|_| {
Err::Failure(Error::new(extensions_rest, ErrorKind::LengthValue))
})?;
}
extensions_rest = rest;
}
Ok((remaining, extensions))
}
pub fn serialize(&self, source_buf: &[u8], output: &mut Buf) {
output.extend_from_slice(&self.legacy_version.as_u16().to_be_bytes());
self.random.serialize(output);
output.push(self.legacy_session_id.len() as u8);
output.extend_from_slice(&self.legacy_session_id);
output.push(self.legacy_cookie.len() as u8);
output.extend_from_slice(&self.legacy_cookie);
output.extend_from_slice(&(self.cipher_suites.len() as u16 * 2).to_be_bytes());
for suite in &self.cipher_suites {
output.extend_from_slice(&suite.as_u16().to_be_bytes());
}
output.push(self.legacy_compression_methods.len() as u8);
for method in &self.legacy_compression_methods {
output.push(method.as_u8());
}
if !self.extensions.is_empty() {
let mut extensions_len = 0;
for ext in &self.extensions {
let ext_data = ext.extension_data(source_buf);
extensions_len += 4 + ext_data.len();
}
output.extend_from_slice(&(extensions_len as u16).to_be_bytes());
for ext in &self.extensions {
ext.serialize(source_buf, output);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::buffer::Buf;
use crate::dtls13::message::ExtensionType;
const MESSAGE: &[u8] = &[
0xFE, 0xFD, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E,
0x1F, 0x20, 0x01, 0xAA, 0x01, 0xBB, 0x00, 0x04, 0x13, 0x01, 0x13, 0x02, 0x01, 0x00, ];
#[test]
fn roundtrip() {
let random = Random::parse(&MESSAGE[2..34]).unwrap().1;
let session_id = SessionId::try_new(&[0xAA]).unwrap();
let cookie = Cookie::try_new(&[0xBB]).unwrap();
let mut cipher_suites = ArrayVec::new();
cipher_suites.push(Dtls13CipherSuite::AES_128_GCM_SHA256);
cipher_suites.push(Dtls13CipherSuite::AES_256_GCM_SHA384);
let mut compression_methods = ArrayVec::new();
compression_methods.push(CompressionMethod::Null);
let client_hello = ClientHello::new(
ProtocolVersion::DTLS1_2,
random,
session_id,
cookie,
cipher_suites,
compression_methods,
);
let mut serialized = Buf::new();
client_hello.serialize(&[], &mut serialized);
assert_eq!(&*serialized, MESSAGE);
let (rest, parsed) = ClientHello::parse(&serialized, 0).unwrap();
assert_eq!(parsed, client_hello);
assert!(rest.is_empty());
}
#[test]
fn dtls12_only_cipher_suites_parse_for_auto_detection() {
let mut message = MESSAGE.to_vec();
message[40..44].copy_from_slice(&[0xC0, 0x2B, 0xC0, 0x2C]);
let (rest, parsed) = ClientHello::parse_allow_unknown_suites(&message, 0).unwrap();
assert!(rest.is_empty());
assert!(parsed.cipher_suites.is_empty());
}
#[test]
fn dtls12_only_cipher_suites_are_rejected_by_normal_parse() {
let mut message = MESSAGE.to_vec();
message[40..44].copy_from_slice(&[0xC0, 0x2B, 0xC0, 0x2C]);
assert!(ClientHello::parse(&message, 0).is_err());
}
#[test]
fn empty_cipher_suites_is_malformed_in_both_modes() {
let mut message = Vec::new();
message.extend_from_slice(&MESSAGE[..38]);
message.extend_from_slice(&[0x00, 0x00]);
message.extend_from_slice(&MESSAGE[44..]);
assert!(ClientHello::parse(&message, 0).is_err());
assert!(ClientHello::parse_allow_unknown_suites(&message, 0).is_err());
}
#[test]
fn odd_cipher_suites_is_malformed_in_both_modes() {
let mut message = Vec::new();
message.extend_from_slice(&MESSAGE[..38]);
message.extend_from_slice(&[0x00, 0x03]);
message.extend_from_slice(&[0x13, 0x01, 0x13]);
message.extend_from_slice(&MESSAGE[44..]);
assert!(ClientHello::parse(&message, 0).is_err());
assert!(ClientHello::parse_allow_unknown_suites(&message, 0).is_err());
}
#[test]
fn session_id_too_long() {
let mut message = MESSAGE.to_vec();
message[34] = 0x21;
let result = ClientHello::parse(&message, 0);
assert!(result.is_err());
}
#[test]
fn cookie_too_long() {
let mut message = MESSAGE.to_vec();
message[36] = 0xFF;
let result = ClientHello::parse(&message, 0);
assert!(result.is_err());
}
#[test]
fn cipher_suites_capacity_matches_known() {
let client_hello = ClientHello {
legacy_version: ProtocolVersion::DTLS1_2,
random: Random::parse(&MESSAGE[2..34]).unwrap().1,
legacy_session_id: SessionId::empty(),
legacy_cookie: Cookie::empty(),
cipher_suites: ArrayVec::new(),
legacy_compression_methods: ArrayVec::new(),
extensions: ArrayVec::new(),
};
assert_eq!(
client_hello.cipher_suites.capacity(),
Dtls13CipherSuite::supported().len(),
"cipher_suites ArrayVec capacity must match supported Dtls13CipherSuites"
);
}
#[test]
fn extensions_capacity_fits_supported() {
let client_hello = ClientHello {
legacy_version: ProtocolVersion::DTLS1_2,
random: Random::parse(&MESSAGE[2..34]).unwrap().1,
legacy_session_id: SessionId::empty(),
legacy_cookie: Cookie::empty(),
cipher_suites: ArrayVec::new(),
legacy_compression_methods: ArrayVec::new(),
extensions: ArrayVec::new(),
};
assert!(
client_hello.extensions.capacity() >= ExtensionType::supported().len(),
"extensions ArrayVec capacity must fit all supported ExtensionTypes"
);
}
#[test]
fn duplicate_supported_extensions_are_rejected() {
for count in [2, 9] {
let mut message = MESSAGE.to_vec();
message.extend_from_slice(&(count as u16 * 4).to_be_bytes());
for _ in 0..count {
message.extend_from_slice(&ExtensionType::Cookie.as_u16().to_be_bytes());
message.extend_from_slice(&0u16.to_be_bytes());
}
let result = ClientHello::parse(&message, 0);
assert!(
matches!(
result,
Err(nom::Err::Failure(error))
if error.code == nom::error::ErrorKind::LengthValue
),
"duplicate supported extensions should fail with LengthValue"
);
}
}
#[test]
fn zero_length_extension_vector_rejects_trailing_bytes() {
let mut message = MESSAGE.to_vec();
message.extend_from_slice(&0u16.to_be_bytes());
message.extend_from_slice(&ExtensionType::Cookie.as_u16().to_be_bytes());
message.extend_from_slice(&0u16.to_be_bytes());
assert!(
ClientHello::parse(&message, 0).is_err(),
"extension vector length 0 must consume the remaining ClientHello body"
);
}
#[test]
fn underdeclared_extension_vector_rejects_trailing_bytes() {
let mut message = MESSAGE.to_vec();
message.extend_from_slice(&4u16.to_be_bytes());
message.extend_from_slice(&ExtensionType::Cookie.as_u16().to_be_bytes());
message.extend_from_slice(&0u16.to_be_bytes());
message.push(0);
assert!(
ClientHello::parse(&message, 0).is_err(),
"declared extension vector length must consume the remaining ClientHello body"
);
}
}