#![cfg_attr(not(test), no_std)]
pub(crate) mod aead;
pub mod backends;
pub(crate) mod bigint;
pub mod client;
pub(crate) mod client_flight;
pub(crate) mod connection;
pub(crate) mod errors;
pub(crate) mod hkdf;
pub(crate) mod identity;
pub(crate) mod newtype;
pub(crate) mod reassembler;
pub(crate) mod server_flight;
pub(crate) mod traits;
use errors::{ClientHelloError, ParseError, Write24Error};
#[cfg(all(test, feature = "cipher-aes"))]
pub(crate) use {
aead::RecordKeys,
hkdf::{application_traffic_secrets, master_secret},
server_flight::{tests::verify_self_signed_cert, verify_server_flight},
};
#[cfg(test)]
pub(crate) use {
aead::{aead_nonce, split_inner_plaintext},
hkdf::{derive_secret, handshake_secret, handshake_traffic_secrets, hkdf_expand_label},
};
use embedded_io::Write;
#[cfg(any(test, feature = "dev-utils"))]
pub const fn hex_decode<const N: usize>(s: &str) -> [u8; N] {
let bytes = s.as_bytes();
let mut out = [0u8; N];
let mut i = 0;
let mut o = 0;
while i < bytes.len() {
let c = bytes[i];
if c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' {
i += 1;
continue;
}
if c == b'#' {
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
continue;
}
let hi = hex_nibble(bytes[i]);
if i + 1 >= bytes.len() {
panic!("hex_decode: dangling nibble at end of input");
}
let lo = hex_nibble(bytes[i + 1]);
if o >= N {
panic!("hex_decode: more bytes in input than the declared N");
}
out[o] = (hi << 4) | lo;
i += 2;
o += 1;
}
if o != N {
panic!("hex_decode: fewer bytes in input than the declared N");
}
out
}
#[cfg(any(test, feature = "dev-utils"))]
const fn hex_nibble(c: u8) -> u8 {
match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
b'A'..=b'F' => c - b'A' + 10,
_ => panic!("hex_decode: non-hex byte in input"),
}
}
pub(crate) mod consts {
pub const CT_HANDSHAKE: u8 = 22;
pub const CT_APPLICATION_DATA: u8 = 23;
pub const CT_ALERT: u8 = 21;
pub const CT_CHANGE_CIPHER_SPEC: u8 = 0x14;
pub const CLOSE_NOTIFY_ALERT: [u8; 2] = [0x01, 0x00];
pub const LEGACY_VERSION: u16 = 0x0303;
pub const TLS_1_3: u16 = 0x0304;
pub const HS_CLIENT_HELLO: u8 = 1;
pub const HS_SERVER_HELLO: u8 = 2;
pub const HS_NEW_SESSION_TICKET: u8 = 4;
pub const HS_ENCRYPTED_EXTENSIONS: u8 = 8;
pub const HS_CERTIFICATE_REQUEST: u8 = 13;
pub const HS_CERTIFICATE: u8 = 11;
pub const HS_CERTIFICATE_VERIFY: u8 = 15;
pub const HS_FINISHED: u8 = 20;
pub const HS_KEY_UPDATE: u8 = 24;
pub const CONTENT_TYPE_LEN: usize = 1;
pub const CIPHER_AES_128_GCM_SHA256: u16 = 0x1301;
pub const CIPHER_CHACHA20_POLY1305_SHA256: u16 = 0x1303;
pub const NAMED_GROUP_X25519: u16 = 0x001D;
pub const NAMED_GROUP_X25519MLKEM768: u16 = 0x11EC;
pub const SIG_SCHEME_ED25519: u16 = 0x0807;
pub const SIG_SCHEME_RSA_PSS_RSAE_SHA256: u16 = 0x0804;
pub const SIG_SCHEME_MLDSA44: u16 = 0x0904;
pub const SIG_SCHEME_MLDSA65: u16 = 0x0905;
pub const SIG_SCHEME_MLDSA87: u16 = 0x0906;
pub const EXT_SERVER_NAME: u16 = 0;
pub const EXT_SUPPORTED_GROUPS: u16 = 10;
pub const EXT_SIGNATURE_ALGORITHMS: u16 = 13;
pub const EXT_RECORD_SIZE_LIMIT: u16 = 28;
pub const EXT_SUPPORTED_VERSIONS: u16 = 43;
pub const EXT_KEY_SHARE: u16 = 51;
pub const SNI_NAME_TYPE_HOST_NAME: u8 = 0;
pub 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,
];
pub const DOWNGRADE_TLS12: [u8; 8] = *b"DOWNGRD\x01";
pub const DOWNGRADE_TLS11_OR_BELOW: [u8; 8] = *b"DOWNGRD\x00";
}
use consts::*;
const EXT_SUPPORTED_VERSIONS_TOTAL: u16 = 4 + 3;
const EXT_SUPPORTED_GROUPS_TOTAL: u16 = 4 + 4;
const SIG_SCHEME_COUNT: u16 = 1 + cfg!(feature = "rsa") as u16 + 3 * cfg!(feature = "mldsa") as u16;
const EXT_SIGNATURE_ALGORITHMS_TOTAL: u16 = 4 + 2 + 2 * SIG_SCHEME_COUNT;
const KEY_SHARE_GROUP: u16 = if cfg!(feature = "mlkem") {
NAMED_GROUP_X25519MLKEM768
} else {
NAMED_GROUP_X25519
};
#[cfg(feature = "mlkem")]
const KEY_SHARE_KEY_LEN: usize = backends::mlkem::MLKEM768_EK_BYTES + 32;
#[cfg(not(feature = "mlkem"))]
const KEY_SHARE_KEY_LEN: usize = 32;
const _: () = assert!(KEY_SHARE_KEY_LEN <= u16::MAX as usize);
const KEY_SHARE_KEY_LEN_U16: u16 = KEY_SHARE_KEY_LEN as u16;
const KEY_SHARE_LIST_LEN: u16 = 4 + KEY_SHARE_KEY_LEN_U16;
const KEY_SHARE_EXT_DATA_LEN: u16 = 2 + KEY_SHARE_LIST_LEN;
const EXT_KEY_SHARE_TOTAL: u16 = 4 + KEY_SHARE_EXT_DATA_LEN;
#[cfg(not(any(feature = "cipher-aes", feature = "chacha20")))]
compile_error!(
"krabitls requires at least one of `cipher-aes` (default) or `chacha20` to provide a cipher suite"
);
const CH_CIPHER_SUITES_COUNT: usize =
cfg!(feature = "cipher-aes") as usize + cfg!(feature = "chacha20") as usize;
const EXT_RECORD_SIZE_LIMIT_TOTAL: u16 = 4 + 2;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SuiteList {
#[default]
Default,
#[cfg(feature = "cipher-aes")]
AesOnly,
#[cfg(feature = "chacha20")]
ChaChaOnly,
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct ClientHelloOptions<'a> {
pub hostname: Option<&'a [u8]>,
pub record_size_limit: Option<u16>,
pub suites: SuiteList,
#[cfg(feature = "mlkem")]
pub mlkem_ek: Option<&'a [u8; backends::mlkem::MLKEM768_EK_BYTES]>,
}
const CH_EXTENSIONS_FIXED_TOTAL: u16 = EXT_SUPPORTED_VERSIONS_TOTAL
+ EXT_SUPPORTED_GROUPS_TOTAL
+ EXT_SIGNATURE_ALGORITHMS_TOTAL
+ EXT_KEY_SHARE_TOTAL;
const fn sni_ext_total(hostname_len: usize) -> usize {
9 + hostname_len
}
const fn ch_n_suites(suites: SuiteList) -> usize {
match suites {
SuiteList::Default => CH_CIPHER_SUITES_COUNT,
#[cfg(feature = "cipher-aes")]
SuiteList::AesOnly => 1,
#[cfg(feature = "chacha20")]
SuiteList::ChaChaOnly => 1,
}
}
const fn ch_cipher_suites_field_len(suites: SuiteList) -> usize {
2 + 2 * ch_n_suites(suites)
}
const fn ch_extensions_total(sni_host_len: Option<usize>, has_record_size_limit: bool) -> usize {
let sni = match sni_host_len {
None => 0,
Some(n) => sni_ext_total(n),
};
let rsl = if has_record_size_limit {
EXT_RECORD_SIZE_LIMIT_TOTAL as usize
} else {
0
};
CH_EXTENSIONS_FIXED_TOTAL as usize + sni + rsl
}
const fn ch_body_len(
suites: SuiteList,
sni_host_len: Option<usize>,
has_record_size_limit: bool,
) -> usize {
2 + 32
+ 1
+ ch_cipher_suites_field_len(suites)
+ (1 + 1)
+ 2
+ ch_extensions_total(sni_host_len, has_record_size_limit)
}
const fn ch_total_len(
suites: SuiteList,
sni_host_len: Option<usize>,
has_record_size_limit: bool,
) -> usize {
5 + 4 + ch_body_len(suites, sni_host_len, has_record_size_limit)
}
pub(crate) const fn client_hello_len(hostname_len: Option<usize>) -> usize {
ch_total_len(SuiteList::Default, hostname_len, false)
}
pub(crate) const fn client_hello_len_with(opts: &ClientHelloOptions<'_>) -> usize {
let sni_host_len = match opts.hostname {
None => None,
Some(h) => Some(h.len()),
};
ch_total_len(opts.suites, sni_host_len, opts.record_size_limit.is_some())
}
pub(crate) const CLIENT_HELLO_LEN: usize = client_hello_len(None);
const _: () = assert!(
CLIENT_HELLO_LEN
== 117
+ 2 * CH_CIPHER_SUITES_COUNT.saturating_sub(1)
+ 2 * (SIG_SCHEME_COUNT as usize - 1)
+ (KEY_SHARE_KEY_LEN - 32)
);
trait WriteExt: Write {
fn write_u8(&mut self, n: u8) -> Result<(), Self::Error> {
self.write_all(&[n])
}
fn write_u16(&mut self, n: u16) -> Result<(), Self::Error> {
self.write_all(&n.to_be_bytes())
}
fn write_u24(&mut self, n: u32) -> Result<(), Write24Error<Self::Error>> {
if n > 0xFF_FFFF {
return Err(Write24Error::Overflow);
}
let bytes = n.to_be_bytes();
self.write_all(&bytes[1..])?;
Ok(())
}
}
impl<W: Write + ?Sized> WriteExt for W {}
const TLS_PLAINTEXT_MAX: usize = 1 << 14;
pub(crate) fn write_client_hello_with<W: Write>(
out: &mut W,
random: &[u8; 32],
x25519_pub: &[u8; 32],
opts: &ClientHelloOptions<'_>,
) -> Result<usize, ClientHelloError<W::Error>> {
let hostname = opts.hostname;
let host_len = hostname.map(|h| h.len()).unwrap_or(0);
if host_len > u16::MAX as usize {
return Err(ClientHelloError::HostnameTooLong);
}
if let Some(rsl) = opts.record_size_limit
&& !(64..=16385).contains(&rsl)
{
return Err(ClientHelloError::RecordSizeLimitOutOfRange);
}
let total_len = client_hello_len_with(opts);
if total_len > 5 + TLS_PLAINTEXT_MAX {
return Err(ClientHelloError::MessageTooLong);
}
let sni_host_len = hostname.map(|h| h.len());
let has_rsl = opts.record_size_limit.is_some();
let n_suites = ch_n_suites(opts.suites);
let extensions_total = ch_extensions_total(sni_host_len, has_rsl);
let body_len = ch_body_len(opts.suites, sni_host_len, has_rsl);
let hs_len = 4 + body_len;
#[cfg(feature = "chacha20")]
let advertise_chacha = matches!(opts.suites, SuiteList::Default | SuiteList::ChaChaOnly);
out.write_u8(CT_HANDSHAKE)?;
out.write_u16(LEGACY_VERSION)?;
out.write_u16(hs_len as u16)?;
out.write_u8(HS_CLIENT_HELLO)?;
out.write_u24(body_len as u32)?;
out.write_u16(LEGACY_VERSION)?;
out.write_all(random)?;
out.write_u8(0)?;
out.write_u16((2 * n_suites) as u16)?;
#[cfg(feature = "chacha20")]
if advertise_chacha {
out.write_u16(CIPHER_CHACHA20_POLY1305_SHA256)?;
}
#[cfg(all(feature = "cipher-aes", feature = "chacha20"))]
let advertise_aes = !matches!(opts.suites, SuiteList::ChaChaOnly);
#[cfg(all(feature = "cipher-aes", not(feature = "chacha20")))]
let advertise_aes = true;
#[cfg(feature = "cipher-aes")]
if advertise_aes {
out.write_u16(CIPHER_AES_128_GCM_SHA256)?;
}
out.write_u8(1)?;
out.write_u8(0)?;
out.write_u16(extensions_total as u16)?;
out.write_u16(EXT_SUPPORTED_VERSIONS)?;
out.write_u16(3)?;
out.write_u8(2)?;
out.write_u16(TLS_1_3)?;
out.write_u16(EXT_SUPPORTED_GROUPS)?;
out.write_u16(4)?;
out.write_u16(2)?;
out.write_u16(KEY_SHARE_GROUP)?;
out.write_u16(EXT_SIGNATURE_ALGORITHMS)?;
out.write_u16(2 + 2 * SIG_SCHEME_COUNT)?; out.write_u16(2 * SIG_SCHEME_COUNT)?; out.write_u16(SIG_SCHEME_ED25519)?;
if cfg!(feature = "rsa") {
out.write_u16(SIG_SCHEME_RSA_PSS_RSAE_SHA256)?;
}
if cfg!(feature = "mldsa") {
out.write_u16(SIG_SCHEME_MLDSA44)?;
out.write_u16(SIG_SCHEME_MLDSA65)?;
out.write_u16(SIG_SCHEME_MLDSA87)?;
}
if let Some(h) = hostname {
let host_len = h.len() as u16;
let list_len: u16 = 1 + 2 + host_len;
let ext_data_len: u16 = 2 + list_len;
out.write_u16(EXT_SERVER_NAME)?;
out.write_u16(ext_data_len)?;
out.write_u16(list_len)?;
out.write_u8(SNI_NAME_TYPE_HOST_NAME)?;
out.write_u16(host_len)?;
out.write_all(h)?;
}
if let Some(value) = opts.record_size_limit {
out.write_u16(EXT_RECORD_SIZE_LIMIT)?;
out.write_u16(2)?;
out.write_u16(value)?;
}
out.write_u16(EXT_KEY_SHARE)?;
out.write_u16(KEY_SHARE_EXT_DATA_LEN)?;
out.write_u16(KEY_SHARE_LIST_LEN)?;
out.write_u16(KEY_SHARE_GROUP)?;
out.write_u16(KEY_SHARE_KEY_LEN_U16)?;
#[cfg(feature = "mlkem")]
out.write_all(
opts.mlkem_ek
.ok_or(ClientHelloError::MissingMlKemKeyShare)?,
)?;
out.write_all(x25519_pub)?;
Ok(total_len)
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) struct ServerHelloView<'a> {
pub random: &'a [u8; 32],
pub session_id_echo: &'a [u8],
pub cipher_suite: u16,
pub selected_version: u16,
pub x25519_share: &'a [u8; 32],
#[cfg(feature = "mlkem")]
pub mlkem_ct: &'a [u8; backends::mlkem::MLKEM768_CT_BYTES],
}
pub(crate) fn parse_server_hello(input: &[u8]) -> Result<ServerHelloView<'_>, ParseError> {
let mut r = Reader::new(input);
let content_type = r.u8()?;
if content_type != CT_HANDSHAKE {
return Err(ParseError::UnexpectedContentType(content_type));
}
let record_version = r.u16()?;
if record_version != LEGACY_VERSION {
return Err(ParseError::UnexpectedLegacyVersion(record_version));
}
let record_body = r.vec_u16()?;
if !r.at_end() {
return Err(ParseError::TrailingBytes);
}
let mut hr = Reader::new(record_body);
let hs_type = hr.u8()?;
if hs_type != HS_SERVER_HELLO {
return Err(ParseError::UnexpectedHandshakeType(hs_type));
}
let hs_body = hr.vec_u24()?;
if !hr.at_end() {
return Err(ParseError::LengthMismatch);
}
let mut b = Reader::new(hs_body);
let legacy_version = b.u16()?;
if legacy_version != LEGACY_VERSION {
return Err(ParseError::UnexpectedLegacyVersion(legacy_version));
}
let random: &[u8; 32] = b.take_array()?;
if random == &HRR_RANDOM {
return Err(ParseError::HelloRetryRequested);
}
let suffix = &random[24..];
if suffix == DOWNGRADE_TLS12 || suffix == DOWNGRADE_TLS11_OR_BELOW {
return Err(ParseError::DowngradeDetected);
}
let session_id_echo = b.vec_u8()?;
if !session_id_echo.is_empty() {
return Err(ParseError::UnexpectedSessionIdEcho);
}
let cipher_suite = b.u16()?;
let suite_accepted = cipher_suite == CIPHER_AES_128_GCM_SHA256
|| (cfg!(feature = "chacha20") && cipher_suite == CIPHER_CHACHA20_POLY1305_SHA256);
if !suite_accepted {
return Err(ParseError::UnsupportedCipherSuite(cipher_suite));
}
let compression = b.u8()?;
if compression != 0 {
return Err(ParseError::UnexpectedCompressionMethod(compression));
}
let ext_body = b.vec_u16()?;
if !b.at_end() {
return Err(ParseError::TrailingBytes);
}
let mut selected_version: Option<u16> = None;
let mut x25519_share: Option<&[u8; 32]> = None;
#[cfg(feature = "mlkem")]
let mut mlkem_ct: Option<&[u8; backends::mlkem::MLKEM768_CT_BYTES]> = None;
let mut e = Reader::new(ext_body);
while !e.at_end() {
let ext_type = e.u16()?;
let ext_data = e.vec_u16()?;
match ext_type {
EXT_SUPPORTED_VERSIONS => {
if selected_version.is_some() {
return Err(ParseError::DuplicateExtension(ext_type));
}
if ext_data.len() != 2 {
return Err(ParseError::BadSupportedVersions);
}
let v = u16::from_be_bytes([ext_data[0], ext_data[1]]);
if v != TLS_1_3 {
return Err(ParseError::BadSupportedVersions);
}
selected_version = Some(v);
}
EXT_KEY_SHARE => {
if x25519_share.is_some() {
return Err(ParseError::DuplicateExtension(ext_type));
}
let mut kr = Reader::new(ext_data);
let group = kr.u16()?;
if group != KEY_SHARE_GROUP {
return Err(ParseError::BadKeyShare);
}
let key = kr.vec_u16()?;
if !kr.at_end() {
return Err(ParseError::BadKeyShare);
}
#[cfg(feature = "mlkem")]
{
let (ct, x) = key
.split_at_checked(backends::mlkem::MLKEM768_CT_BYTES)
.ok_or(ParseError::BadKeyShare)?;
mlkem_ct = Some(ct.try_into().map_err(|_| ParseError::BadKeyShare)?);
x25519_share = Some(x.try_into().map_err(|_| ParseError::BadKeyShare)?);
}
#[cfg(not(feature = "mlkem"))]
{
x25519_share = Some(key.try_into().map_err(|_| ParseError::BadKeyShare)?);
}
}
_ => return Err(ParseError::UnknownExtension(ext_type)),
}
}
Ok(ServerHelloView {
random,
session_id_echo,
cipher_suite,
selected_version: selected_version.ok_or(ParseError::BadSupportedVersions)?,
x25519_share: x25519_share.ok_or(ParseError::BadKeyShare)?,
#[cfg(feature = "mlkem")]
mlkem_ct: mlkem_ct.ok_or(ParseError::BadKeyShare)?,
})
}
struct Reader<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
fn remaining(&self) -> usize {
self.buf.len() - self.pos
}
fn at_end(&self) -> bool {
self.pos == self.buf.len()
}
fn take(&mut self, n: usize) -> Result<&'a [u8], ParseError> {
if self.remaining() < n {
return Err(ParseError::Truncated);
}
let slice = &self.buf[self.pos..self.pos + n];
self.pos += n;
Ok(slice)
}
fn take_array<const N: usize>(&mut self) -> Result<&'a [u8; N], ParseError> {
let slice = self.take(N)?;
<&[u8; N]>::try_from(slice).map_err(|_| ParseError::Truncated)
}
fn u8(&mut self) -> Result<u8, ParseError> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, ParseError> {
let bytes = self.take(2)?;
Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
}
fn u24(&mut self) -> Result<u32, ParseError> {
let bytes = self.take(3)?;
Ok(u32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]))
}
fn vec_u8(&mut self) -> Result<&'a [u8], ParseError> {
let n = self.u8()? as usize;
self.take(n)
}
fn vec_u16(&mut self) -> Result<&'a [u8], ParseError> {
let n = self.u16()? as usize;
self.take(n)
}
fn vec_u24(&mut self) -> Result<&'a [u8], ParseError> {
let n: usize = self.u24()?.try_into().map_err(|_| ParseError::Truncated)?;
self.take(n)
}
}
#[cfg(test)]
mod tests;