use crate::aead::{CipherSuite, EncryptError, RecordKeys};
use crate::consts::{CT_HANDSHAKE, HS_CERTIFICATE, HS_CERTIFICATE_VERIFY, HS_FINISHED};
use crate::hkdf::{HkdfLabelError, TranscriptHash, finished_mac};
use crate::newtype::{Secret, TranscriptDigest, ZeroBuf};
use crate::traits::HkdfSha256;
use crate::traits::client_auth::{ClientAuth, ClientAuthError, MAX_CLIENT_SIG_LEN};
#[derive(Debug, PartialEq, Eq, Clone, Copy, thiserror::Error)]
pub enum ClientFinishedError {
#[error("record-layer encrypt step failed")]
Encrypt(#[from] EncryptError),
#[error("HKDF-Expand-Label rejected a key-schedule derivation")]
Hkdf(#[from] HkdfLabelError),
}
pub(crate) fn build_finished_plaintext<H: HkdfSha256>(
c_hs_traffic_secret: &Secret,
transcript_hash_through_server_finished: &TranscriptDigest,
) -> Result<ZeroBuf<{ 4 + 32 }>, ClientFinishedError> {
let verify_data =
finished_mac::<H>(c_hs_traffic_secret, transcript_hash_through_server_finished)?;
let mut finished_msg = ZeroBuf::<{ 4 + 32 }>::new([0; 4 + 32]);
finished_msg[0] = HS_FINISHED;
finished_msg[1..4].copy_from_slice(&[0x00, 0x00, 0x20]);
finished_msg[4..].copy_from_slice(&verify_data[..]);
Ok(finished_msg)
}
impl<S: CipherSuite> RecordKeys<S> {
pub fn build_client_finished<'a, H: HkdfSha256>(
c_hs_traffic_secret: &Secret,
transcript_hash_through_server_finished: &TranscriptDigest,
seq: u64,
out_buf: &'a mut [u8],
) -> Result<&'a [u8], ClientFinishedError> {
let finished_msg = build_finished_plaintext::<H>(
c_hs_traffic_secret,
transcript_hash_through_server_finished,
)?;
let keys = Self::derive::<H>(c_hs_traffic_secret)?;
let record = keys.encrypt_record(&finished_msg[..], CT_HANDSHAKE, seq, out_buf)?;
Ok(record)
}
}
pub const CLIENT_FINISHED_LEN: usize = 58;
#[derive(Debug, PartialEq, Eq, Clone, Copy, thiserror::Error)]
pub enum ClientAuthFlightError {
#[error("output buffer too small for the client auth handshake message")]
BufferTooSmall,
#[error("caller-supplied signer failed")]
Sign(#[from] ClientAuthError),
#[error("client Finished derivation failed")]
Finished(#[from] ClientFinishedError),
#[error("server requested a client certificate but none is configured")]
CertificateRequested,
#[error("server's CertificateRequest does not offer the signer's signature scheme")]
NoMutualSignatureAlgorithm,
#[error("client certificate DER is empty")]
EmptyCertificate,
#[error("client certificate DER exceeds MAX_CLIENT_CERT_DER")]
CertificateTooLong,
#[error("certificate_request_context exceeds 255 bytes")]
ContextTooLong,
}
pub const MAX_CLIENT_CERT_DER: usize = 1024;
const HS_HEADER: usize = 4;
const CERT_FRAMING: usize = 1 + 255 + 3 + 3 + 2;
const CV_FRAMING: usize = 2 + 2;
const FINISHED_MSG: usize = HS_HEADER + 32;
pub const MAX_CLIENT_AUTH_FLIGHT: usize = (HS_HEADER + CERT_FRAMING + MAX_CLIENT_CERT_DER)
+ (HS_HEADER + CV_FRAMING + MAX_CLIENT_SIG_LEN)
+ FINISHED_MSG;
const EMPTY_CERT_FRAMING: usize = 1 + 255 + 3;
pub const MAX_CLIENT_EMPTY_AUTH_FLIGHT: usize = (HS_HEADER + EMPTY_CERT_FRAMING) + FINISHED_MSG;
const CLIENT_CV_CTX: &[u8] = b"TLS 1.3, client CertificateVerify";
const CLIENT_CV_SIGNED_LEN: usize = 64 + CLIENT_CV_CTX.len() + 1 + 32;
fn certificate_verify_signed_content(
transcript_hash_through_client_cert: &TranscriptDigest,
) -> [u8; CLIENT_CV_SIGNED_LEN] {
let mut buf = [0u8; CLIENT_CV_SIGNED_LEN];
buf[..64].fill(0x20);
buf[64..64 + CLIENT_CV_CTX.len()].copy_from_slice(CLIENT_CV_CTX);
buf[64 + CLIENT_CV_CTX.len()] = 0x00; let hash_at = 64 + CLIENT_CV_CTX.len() + 1;
buf[hash_at..].copy_from_slice(transcript_hash_through_client_cert.as_bytes());
buf
}
pub fn build_client_certificate<'a>(
cert_der: &[u8],
cert_request_context: &[u8],
out: &'a mut [u8],
) -> Result<&'a [u8], ClientAuthFlightError> {
if cert_der.is_empty() {
return Err(ClientAuthFlightError::EmptyCertificate);
}
if cert_der.len() > MAX_CLIENT_CERT_DER {
return Err(ClientAuthFlightError::CertificateTooLong);
}
if cert_request_context.len() > 255 {
return Err(ClientAuthFlightError::ContextTooLong);
}
let entry_len = 3 + cert_der.len() + 2;
let list_len = 3 + entry_len;
let body_len = 1 + cert_request_context.len() + list_len;
let total = 4 + body_len;
let out = out
.get_mut(..total)
.ok_or(ClientAuthFlightError::BufferTooSmall)?;
out[0] = HS_CERTIFICATE;
out[1..4].copy_from_slice(&u24(body_len));
let mut p = 4;
out[p] = cert_request_context.len() as u8; p += 1;
out[p..p + cert_request_context.len()].copy_from_slice(cert_request_context);
p += cert_request_context.len();
out[p..p + 3].copy_from_slice(&u24(entry_len));
p += 3;
out[p..p + 3].copy_from_slice(&u24(cert_der.len()));
p += 3;
out[p..p + cert_der.len()].copy_from_slice(cert_der);
p += cert_der.len();
out[p..p + 2].copy_from_slice(&[0x00, 0x00]); Ok(out)
}
pub fn build_client_empty_certificate<'a>(
cert_request_context: &[u8],
out: &'a mut [u8],
) -> Result<&'a [u8], ClientAuthFlightError> {
if cert_request_context.len() > 255 {
return Err(ClientAuthFlightError::ContextTooLong);
}
let body_len = 1 + cert_request_context.len() + 3; let total = 4 + body_len;
let out = out
.get_mut(..total)
.ok_or(ClientAuthFlightError::BufferTooSmall)?;
out[0] = HS_CERTIFICATE;
out[1..4].copy_from_slice(&u24(body_len));
out[4] = cert_request_context.len() as u8; out[5..5 + cert_request_context.len()].copy_from_slice(cert_request_context);
let list_at = 5 + cert_request_context.len();
out[list_at..list_at + 3].copy_from_slice(&[0, 0, 0]); Ok(out)
}
pub fn build_client_certificate_verify<'a, A: ClientAuth + ?Sized>(
auth: &A,
transcript_hash_through_client_cert: &TranscriptDigest,
entropy: &[u8; 32],
out: &'a mut [u8],
) -> Result<&'a [u8], ClientAuthFlightError> {
let signed = certificate_verify_signed_content(transcript_hash_through_client_cert);
let sig = auth.sign(&signed, entropy)?;
let body_len = 2 + 2 + sig.len();
let total = 4 + body_len;
let out = out
.get_mut(..total)
.ok_or(ClientAuthFlightError::BufferTooSmall)?;
out[0] = HS_CERTIFICATE_VERIFY;
out[1..4].copy_from_slice(&u24(body_len));
out[4..6].copy_from_slice(&auth.scheme().to_be_bytes());
out[6..8].copy_from_slice(
&u16::try_from(sig.len())
.map_err(|_| ClientAuthFlightError::BufferTooSmall)?
.to_be_bytes(),
);
out[8..].copy_from_slice(&sig);
Ok(out)
}
pub trait ClientAuthPolicy {
const ACCEPT_CERT_REQUEST: bool;
const MAX_FLIGHT_LEN: usize;
fn needs_signing_entropy(&self) -> bool {
Self::ACCEPT_CERT_REQUEST
}
fn build_flight<'a, H: HkdfSha256>(
&self,
cert_request_context: &[u8],
cert_request_sig_algs: &[u8],
entropy: &[u8; 32],
c_hs_traffic_secret: &Secret,
transcript: &mut TranscriptHash<H>,
out: &'a mut [u8],
) -> Result<&'a [u8], ClientAuthFlightError>;
}
fn sig_algs_offer(list: &[u8], scheme: u16) -> bool {
if (list.len() & 1) != 0 {
return false;
}
list.chunks_exact(2)
.any(|c| u16::from_be_bytes([c[0], c[1]]) == scheme)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoClientAuth;
impl ClientAuthPolicy for NoClientAuth {
const ACCEPT_CERT_REQUEST: bool = false;
const MAX_FLIGHT_LEN: usize = 0;
#[inline]
fn build_flight<'a, H: HkdfSha256>(
&self,
_cert_request_context: &[u8],
_cert_request_sig_algs: &[u8],
_entropy: &[u8; 32],
_c_hs_traffic_secret: &Secret,
_transcript: &mut TranscriptHash<H>,
_out: &'a mut [u8],
) -> Result<&'a [u8], ClientAuthFlightError> {
Err(ClientAuthFlightError::CertificateRequested)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DeclineClientAuth;
impl ClientAuthPolicy for DeclineClientAuth {
const ACCEPT_CERT_REQUEST: bool = true;
const MAX_FLIGHT_LEN: usize = MAX_CLIENT_EMPTY_AUTH_FLIGHT;
fn needs_signing_entropy(&self) -> bool {
false
}
fn build_flight<'a, H: HkdfSha256>(
&self,
cert_request_context: &[u8],
_cert_request_sig_algs: &[u8],
_entropy: &[u8; 32],
c_hs_traffic_secret: &Secret,
transcript: &mut TranscriptHash<H>,
out: &'a mut [u8],
) -> Result<&'a [u8], ClientAuthFlightError> {
let cert_end = build_client_empty_certificate(cert_request_context, out)?.len();
transcript.update(&out[..cert_end]);
append_finished::<H>(c_hs_traffic_secret, transcript, out, cert_end)
}
}
pub struct WithClientAuth<'a, A: ClientAuth + ?Sized = dyn ClientAuth>(pub &'a A);
impl<A: ClientAuth + ?Sized> Clone for WithClientAuth<'_, A> {
#[inline]
fn clone(&self) -> Self {
*self
}
}
impl<A: ClientAuth + ?Sized> Copy for WithClientAuth<'_, A> {}
impl<A: ClientAuth + ?Sized> ClientAuthPolicy for WithClientAuth<'_, A> {
const ACCEPT_CERT_REQUEST: bool = true;
const MAX_FLIGHT_LEN: usize = MAX_CLIENT_AUTH_FLIGHT;
fn needs_signing_entropy(&self) -> bool {
self.0.needs_entropy()
}
fn build_flight<'a, H: HkdfSha256>(
&self,
cert_request_context: &[u8],
cert_request_sig_algs: &[u8],
entropy: &[u8; 32],
c_hs_traffic_secret: &Secret,
transcript: &mut TranscriptHash<H>,
out: &'a mut [u8],
) -> Result<&'a [u8], ClientAuthFlightError> {
if !sig_algs_offer(cert_request_sig_algs, self.0.scheme()) {
return Err(ClientAuthFlightError::NoMutualSignatureAlgorithm);
}
let cert_end =
build_client_certificate(self.0.cert_der(), cert_request_context, out)?.len();
transcript.update(&out[..cert_end]);
let th_through_cert = transcript.snapshot();
let cv_end = cert_end
+ build_client_certificate_verify(
self.0,
&th_through_cert,
entropy,
&mut out[cert_end..],
)?
.len();
transcript.update(&out[cert_end..cv_end]);
append_finished::<H>(c_hs_traffic_secret, transcript, out, cv_end)
}
}
fn append_finished<'a, H: HkdfSha256>(
c_hs_traffic_secret: &Secret,
transcript: &mut TranscriptHash<H>,
out: &'a mut [u8],
head_end: usize,
) -> Result<&'a [u8], ClientAuthFlightError> {
let th = transcript.snapshot();
let finished = build_finished_plaintext::<H>(c_hs_traffic_secret, &th)?;
let fin_end = head_end + finished.len();
out.get_mut(head_end..fin_end)
.ok_or(ClientAuthFlightError::BufferTooSmall)?
.copy_from_slice(&finished[..]);
transcript.update(&out[head_end..fin_end]);
Ok(&out[..fin_end])
}
fn u24(n: usize) -> [u8; 3] {
let b = (n as u32).to_be_bytes();
[b[1], b[2], b[3]]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backends::Ed25519ClientAuth;
use crate::consts::SIG_SCHEME_ED25519;
use crate::newtype::TranscriptDigest;
use crate::bigint::Curve25519VerifyBn as VerifyBn;
fn read_u24(b: &[u8]) -> usize {
u32::from_be_bytes([0, b[0], b[1], b[2]]) as usize
}
#[test]
fn certificate_message_framing() {
let der = [0xABu8; 5];
let mut out = [0u8; 64];
let msg = build_client_certificate(&der, &[], &mut out).unwrap();
assert_eq!(msg[0], HS_CERTIFICATE);
assert_eq!(read_u24(&msg[1..4]), msg.len() - 4);
assert_eq!(msg[4], 0, "empty certificate_request_context");
assert_eq!(read_u24(&msg[5..8]), 3 + der.len() + 2, "cert_list length");
assert_eq!(read_u24(&msg[8..11]), der.len(), "cert_data length");
assert_eq!(&msg[11..11 + der.len()], &der);
assert_eq!(
&msg[11 + der.len()..],
&[0x00, 0x00],
"empty entry extensions"
);
}
#[test]
fn certificate_message_echoes_request_context() {
let der = [0x11u8; 4];
let ctx = [0xCA, 0xFE];
let mut out = [0u8; 64];
let msg = build_client_certificate(&der, &ctx, &mut out).unwrap();
assert_eq!(msg[4], ctx.len() as u8);
assert_eq!(&msg[5..5 + ctx.len()], &ctx);
}
#[test]
fn empty_certificate_framing() {
let mut out = [0u8; 32];
let msg = build_client_empty_certificate(&[], &mut out).unwrap();
assert_eq!(msg[0], HS_CERTIFICATE);
assert_eq!(read_u24(&msg[1..4]), msg.len() - 4);
assert_eq!(msg[4], 0, "empty context");
assert_eq!(read_u24(&msg[5..8]), 0, "empty certificate_list");
assert_eq!(msg.len(), 8);
}
#[test]
fn with_client_auth_dyn_default_is_copy() {
fn assert_copy<T: Copy>() {}
assert_copy::<WithClientAuth<'static, dyn ClientAuth>>();
}
#[test]
fn sig_algs_offer_membership() {
assert!(sig_algs_offer(&[0x08, 0x07], SIG_SCHEME_ED25519));
assert!(sig_algs_offer(
&[0x04, 0x03, 0x08, 0x07],
SIG_SCHEME_ED25519
));
assert!(!sig_algs_offer(&[0x04, 0x03], SIG_SCHEME_ED25519));
assert!(!sig_algs_offer(&[], SIG_SCHEME_ED25519));
assert!(!sig_algs_offer(&[0x08], SIG_SCHEME_ED25519));
}
#[test]
fn with_client_auth_declines_when_scheme_not_offered() {
use crate::backends::RustCrypto;
use crate::hkdf::TranscriptHash;
use crate::newtype::{Secret, ZeroBuf};
let auth = Ed25519ClientAuth::from_seed(&[7u8; 32], &[0x55u8; 16]).unwrap();
let policy = WithClientAuth(&auth);
let secret = Secret::new(ZeroBuf::<32>::new([0x42; 32]));
let mut out = [0u8; MAX_CLIENT_AUTH_FLIGHT];
let mut t = TranscriptHash::<RustCrypto>::new();
assert_eq!(
policy.build_flight::<RustCrypto>(
&[],
&[0x04, 0x03],
&[0u8; 32],
&secret,
&mut t,
&mut out
),
Err(ClientAuthFlightError::NoMutualSignatureAlgorithm)
);
let mut t = TranscriptHash::<RustCrypto>::new();
assert!(
policy
.build_flight::<RustCrypto>(
&[],
&[0x08, 0x07],
&[0u8; 32],
&secret,
&mut t,
&mut out
)
.is_ok()
);
}
#[test]
fn certificate_message_rejects_empty_der() {
let mut out = [0u8; 32];
assert_eq!(
build_client_certificate(&[], &[], &mut out),
Err(ClientAuthFlightError::EmptyCertificate)
);
}
#[test]
fn certificate_message_rejects_oversized_inputs() {
let mut out = [0u8; 32];
let big_der = [0xABu8; MAX_CLIENT_CERT_DER + 1];
assert_eq!(
build_client_certificate(&big_der, &[], &mut out),
Err(ClientAuthFlightError::CertificateTooLong)
);
let big_ctx = [0u8; 256];
assert_eq!(
build_client_certificate(&[0xCD], &big_ctx, &mut out),
Err(ClientAuthFlightError::ContextTooLong)
);
assert_eq!(
build_client_empty_certificate(&big_ctx, &mut out),
Err(ClientAuthFlightError::ContextTooLong)
);
}
#[test]
fn certificate_message_buffer_too_small() {
let der = [0u8; 5];
let mut out = [0u8; 8];
assert_eq!(
build_client_certificate(&der, &[], &mut out),
Err(ClientAuthFlightError::BufferTooSmall)
);
}
#[test]
fn certificate_verify_signature_round_trips() {
let seed = [7u8; 32];
let der = [0x55u8; 16];
let auth = Ed25519ClientAuth::from_seed(&seed, &der).unwrap();
let pubkey = auth.public_key();
let th = TranscriptDigest::new([0x42u8; 32]);
let mut out = [0u8; 128];
let cv = build_client_certificate_verify(&auth, &th, &[0u8; 32], &mut out).unwrap();
assert_eq!(cv[0], HS_CERTIFICATE_VERIFY);
assert_eq!(read_u24(&cv[1..4]), cv.len() - 4);
assert_eq!(u16::from_be_bytes([cv[4], cv[5]]), SIG_SCHEME_ED25519);
let sig_len = u16::from_be_bytes([cv[6], cv[7]]) as usize;
assert_eq!(sig_len, 64);
let sig: [u8; 64] = cv[8..8 + 64].try_into().unwrap();
let signed = certificate_verify_signed_content(&th);
assert!(ed25519_heapless::verify::<VerifyBn>(pubkey, &signed, sig));
let other = certificate_verify_signed_content(&TranscriptDigest::new([0x43u8; 32]));
assert!(!ed25519_heapless::verify::<VerifyBn>(pubkey, &other, sig));
}
#[test]
fn signed_content_layout() {
let th = TranscriptDigest::new([0x99u8; 32]);
let signed = certificate_verify_signed_content(&th);
assert_eq!(signed.len(), CLIENT_CV_SIGNED_LEN);
assert!(signed[..64].iter().all(|&b| b == 0x20));
assert_eq!(&signed[64..64 + CLIENT_CV_CTX.len()], CLIENT_CV_CTX);
assert_eq!(signed[64 + CLIENT_CV_CTX.len()], 0x00);
assert_eq!(&signed[CLIENT_CV_SIGNED_LEN - 32..], th.as_bytes());
}
}