use alloc::vec::Vec;
use crate::tls::{ClientHelloInfo, Error};
use super::crypto::{AeadAlg, aead_open, derive_dir_keys, derive_initial_secrets};
use super::crypto_buf::CryptoBuf;
use super::frame::Frame;
use super::pkt::{LongHeader, LongType, QUIC_V1, remove_header_protection};
use super::pn::decode_packet_number;
pub fn peek_initial_sni(datagram: &[u8]) -> Result<Option<ClientHelloInfo>, Error> {
let hdr = LongHeader::parse(datagram)?;
if hdr.version != QUIC_V1 {
return Err(Error::UnsupportedVersion);
}
if hdr.typ != LongType::Initial {
return Err(Error::Decode);
}
let pkt_total = hdr
.payload_off
.checked_add(hdr.length as usize)
.ok_or(Error::Decode)?;
if datagram.len() < pkt_total {
return Ok(None);
}
let (client_secret, _server_secret) = derive_initial_secrets(hdr.dcid);
let keys = derive_dir_keys(AeadAlg::Aes128Gcm, &client_secret);
let mut pkt = datagram[..pkt_total].to_vec();
let sample_start = hdr.pn_offset.checked_add(4).ok_or(Error::Decode)?;
let sample_end = sample_start.checked_add(16).ok_or(Error::Decode)?;
if sample_end > pkt.len() {
return Err(Error::Decode);
}
let sample: [u8; 16] = pkt[sample_start..sample_end]
.try_into()
.expect("16-byte sample slice");
let mask = keys.hp.mask(&sample)?;
let pn_len = remove_header_protection(&mut pkt, hdr.pn_offset, &mask, true)?;
let mut truncated_pn = 0u64;
for i in 0..pn_len as usize {
truncated_pn = (truncated_pn << 8) | pkt[hdr.pn_offset + i] as u64;
}
let pn = decode_packet_number(0, truncated_pn, (pn_len as u32) * 8);
let aad_end = hdr.pn_offset + pn_len as usize;
let aad = pkt[..aad_end].to_vec();
let ct_with_tag = &mut pkt[aad_end..];
if ct_with_tag.len() < 16 {
return Err(Error::Decode);
}
let tag_start = ct_with_tag.len() - 16;
let tag: [u8; 16] = ct_with_tag[tag_start..]
.try_into()
.expect("16-byte tag slice");
let payload = &mut ct_with_tag[..tag_start];
aead_open(&keys, pn, &aad, payload, &tag)?;
let mut crypto = CryptoBuf::new();
let mut handshake: Vec<u8> = Vec::new();
let mut p = 0usize;
while p < payload.len() {
let (frame, n) = Frame::decode(&payload[p..])?;
if n == 0 {
break; }
p += n;
if let Frame::Crypto { offset, data } = frame {
let fresh = crypto.on_crypto(offset, data)?;
handshake.extend_from_slice(&fresh);
}
}
crate::tls::peek::client_hello_info_from_handshake(&handshake)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quic::{QuicConfig, QuicConnection};
fn client_initial_datagram(server_name: &str, alpn: &[&[u8]]) -> Vec<u8> {
let tls = crate::tls::Config::builder()
.rng(alloc::sync::Arc::new(crate::rng::OsRng))
.tls_only()
.server_name(server_name)
.verify_certificates(false)
.alpn(alpn.iter().map(|p| p.to_vec()).collect())
.build();
let cfg = QuicConfig {
tls,
..QuicConfig::default()
};
let mut client = QuicConnection::client(cfg, server_name).expect("client");
let dg = client.pop_datagram();
assert!(!dg.is_empty(), "expected an Initial datagram");
dg
}
#[test]
fn peeks_sni_and_alpn_from_real_initial() {
let dg = client_initial_datagram("h3.example", &[b"h3"]);
let before = dg.clone();
let info = peek_initial_sni(&dg).expect("ok").expect("complete CH");
assert_eq!(info.server_name.as_deref(), Some("h3.example"));
assert_eq!(info.alpn_protocols, alloc::vec![b"h3".to_vec()]);
assert_eq!(dg, before, "peek must not mutate the datagram");
}
#[test]
fn unknown_version_is_rejected() {
let mut dg = client_initial_datagram("x.example", &[b"h3"]);
dg[1] = 0x6b;
dg[2] = 0x33;
dg[3] = 0x43;
dg[4] = 0xcf;
assert!(matches!(
peek_initial_sni(&dg),
Err(Error::UnsupportedVersion)
));
}
#[test]
fn tampered_ciphertext_fails_aead_no_panic() {
let mut dg = client_initial_datagram("x.example", &[b"h3"]);
let n = dg.len();
dg[n - 1] ^= 0xff;
assert!(peek_initial_sni(&dg).is_err());
}
#[test]
fn short_header_packet_is_rejected() {
let buf = alloc::vec![0x40u8, 0x00, 0x01, 0x02, 0x03];
assert!(peek_initial_sni(&buf).is_err());
}
#[test]
fn truncated_datagram_does_not_panic() {
let dg = client_initial_datagram("x.example", &[b"h3"]);
for n in 0..dg.len() {
let _ = peek_initial_sni(&dg[..n]);
}
}
}