use crate::newtype::{AeadIv, Secret, TranscriptDigest, ZeroBuf};
use crate::traits::{HkdfExpandError, HkdfSha256};
use digest::Digest;
const HKDF_LABEL_MAX: usize = 64;
#[derive(Debug, PartialEq, Eq, Clone, Copy, thiserror::Error)]
pub enum HkdfLabelError {
#[error("HKDF-Label length field overflowed")]
OutputTooLong,
#[error("HKDF label exceeds the u8 length field")]
LabelTooLong,
#[error("HKDF context exceeds the u8 length field")]
ContextTooLong,
#[error("encoded HKDF label exceeds the scratch buffer")]
EncodedTooLong,
#[error("HKDF backend rejected the requested output length")]
Expand(#[from] HkdfExpandError),
}
impl From<heapless::CapacityError> for HkdfLabelError {
fn from(_: heapless::CapacityError) -> Self {
HkdfLabelError::EncodedTooLong
}
}
pub(crate) fn hkdf_expand_label<H: HkdfSha256>(
secret: &[u8; 32],
label: &[u8],
context: &[u8],
out: &mut [u8],
) -> Result<(), HkdfLabelError> {
const PREFIX: &[u8] = b"tls13 ";
if out.len() > u16::MAX as usize {
return Err(HkdfLabelError::OutputTooLong);
}
let label_total = PREFIX.len() + label.len();
if label_total > u8::MAX as usize {
return Err(HkdfLabelError::LabelTooLong);
}
if context.len() > u8::MAX as usize {
return Err(HkdfLabelError::ContextTooLong);
}
let mut info: heapless::Vec<u8, HKDF_LABEL_MAX> = heapless::Vec::new();
info.extend_from_slice(&(out.len() as u16).to_be_bytes())?;
info.extend_from_slice(&[label_total as u8])?;
info.extend_from_slice(PREFIX)?;
info.extend_from_slice(label)?;
info.extend_from_slice(&[context.len() as u8])?;
info.extend_from_slice(context)?;
H::expand(secret, &info, out)?;
Ok(())
}
pub(crate) fn derive_secret<H: HkdfSha256>(
secret: &Secret,
label: &[u8],
transcript_hash: &TranscriptDigest,
) -> Result<Secret, HkdfLabelError> {
let mut out = ZeroBuf::<32>::new([0; 32]);
hkdf_expand_label::<H>(
secret.as_bytes(),
label,
transcript_hash.as_bytes(),
&mut out[..],
)?;
Ok(Secret::new(out))
}
pub(crate) const EMPTY_TRANSCRIPT_HASH: TranscriptDigest = TranscriptDigest::new([
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
]);
pub struct TranscriptHash<H: HkdfSha256> {
hasher: H::Hasher,
}
impl<H: HkdfSha256> TranscriptHash<H> {
pub fn new() -> Self {
Self {
hasher: H::hasher(),
}
}
pub fn update_record(&mut self, record: &[u8]) -> Result<(), TranscriptError> {
if record.len() < 5 {
return Err(TranscriptError::RecordTooShort);
}
let body_len = u16::from_be_bytes([record[3], record[4]]) as usize;
let end = 5usize
.checked_add(body_len)
.ok_or(TranscriptError::RecordTooShort)?;
if record.len() < end {
return Err(TranscriptError::RecordTooShort);
}
self.hasher.update(&record[5..end]);
Ok(())
}
pub fn update(&mut self, msg: &[u8]) {
self.hasher.update(msg);
}
pub fn snapshot(&self) -> TranscriptDigest {
TranscriptDigest::new(self.hasher.clone().finalize().into())
}
}
impl<H: HkdfSha256> Default for TranscriptHash<H> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, thiserror::Error)]
pub enum TranscriptError {
#[error("record slice is shorter than the 5-byte TLS record header")]
RecordTooShort,
}
pub(crate) fn early_secret<H: HkdfSha256>() -> Secret {
let zeros = [0u8; 32];
Secret::new(H::extract(&zeros, &zeros))
}
pub(crate) fn handshake_secret<H: HkdfSha256>(dhe: &[u8]) -> Result<Secret, HkdfLabelError> {
debug_assert!(
dhe.len() == 32 || dhe.len() == 64,
"dhe must be the 32-byte X25519 secret or the 64-byte ML-KEM||X25519 concatenation"
);
let salt = derive_secret::<H>(&early_secret::<H>(), b"derived", &EMPTY_TRANSCRIPT_HASH)?;
Ok(Secret::new(H::extract(salt.as_bytes(), dhe)))
}
pub(crate) fn handshake_traffic_secrets<H: HkdfSha256>(
hs: &Secret,
transcript_hash_ch_sh: &TranscriptDigest,
) -> Result<(Secret, Secret), HkdfLabelError> {
Ok((
derive_secret::<H>(hs, b"c hs traffic", transcript_hash_ch_sh)?,
derive_secret::<H>(hs, b"s hs traffic", transcript_hash_ch_sh)?,
))
}
pub(crate) fn traffic_keys<H: HkdfSha256, const N: usize>(
traffic_secret: &Secret,
) -> Result<(ZeroBuf<N>, AeadIv), HkdfLabelError> {
let mut key = ZeroBuf::<N>::new([0; N]);
let mut iv = ZeroBuf::<12>::new([0; 12]);
hkdf_expand_label::<H>(traffic_secret.as_bytes(), b"key", &[], &mut key[..])?;
hkdf_expand_label::<H>(traffic_secret.as_bytes(), b"iv", &[], &mut iv[..])?;
Ok((key, AeadIv::new(iv)))
}
pub(crate) fn master_secret<H: HkdfSha256>(
handshake_secret: &Secret,
) -> Result<Secret, HkdfLabelError> {
let salt = derive_secret::<H>(handshake_secret, b"derived", &EMPTY_TRANSCRIPT_HASH)?;
Ok(Secret::new(H::extract(salt.as_bytes(), &[0u8; 32])))
}
pub(crate) fn application_traffic_secrets<H: HkdfSha256>(
master_secret: &Secret,
transcript_hash_through_server_finished: &TranscriptDigest,
) -> Result<(Secret, Secret), HkdfLabelError> {
Ok((
derive_secret::<H>(
master_secret,
b"c ap traffic",
transcript_hash_through_server_finished,
)?,
derive_secret::<H>(
master_secret,
b"s ap traffic",
transcript_hash_through_server_finished,
)?,
))
}
pub(crate) fn next_application_traffic_secret<H: HkdfSha256>(
secret: &Secret,
) -> Result<Secret, HkdfLabelError> {
let mut out = ZeroBuf::<32>::new([0; 32]);
hkdf_expand_label::<H>(secret.as_bytes(), b"traffic upd", &[], &mut out[..])?;
Ok(Secret::new(out))
}
pub(crate) fn finished_mac<H: HkdfSha256>(
traffic_secret: &Secret,
transcript_hash: &TranscriptDigest,
) -> Result<ZeroBuf<32>, HkdfLabelError> {
let mut finished_key = ZeroBuf::<32>::new([0; 32]);
hkdf_expand_label::<H>(
traffic_secret.as_bytes(),
b"finished",
&[],
&mut finished_key[..],
)?;
Ok(H::extract(&finished_key[..], transcript_hash.as_bytes()))
}