use bytes::Bytes;
use super::{
TlsParser,
session::TlsMessage,
types::{TlsConfig, TlsVersion},
};
use crate::{Timestamp, session::SessionParser};
pub const PARSER_KIND: &str = "tls-handshake";
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum HandshakeOutcome {
Completed,
AlertedByServer { description: u8 },
AlertedByClient { description: u8 },
Truncated,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct TlsHandshake {
pub sni: Option<String>,
pub client_alpn: Vec<String>,
pub server_alpn: Option<String>,
pub ja3: Option<String>,
pub ja4: Option<String>,
#[cfg(feature = "ja4plus")]
pub ja4s: Option<String>,
pub version: Option<TlsVersion>,
pub cipher_suite: Option<u16>,
pub resumption_attempted: bool,
pub outcome: HandshakeOutcome,
pub ech_outcome: EchOutcome,
pub ech_config_id: Option<u8>,
pub certificate_chain: Vec<Bytes>,
#[cfg(feature = "ja4plus")]
pub ja4x: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum EchOutcome {
NotOffered,
Accepted,
Rejected,
Unknown,
}
impl TlsHandshake {
pub fn ech_state(&self) -> super::EchState {
match self.ech_outcome {
EchOutcome::NotOffered => super::EchState::NotPresent,
EchOutcome::Rejected => super::EchState::Rejected,
EchOutcome::Accepted | EchOutcome::Unknown => {
super::ech::classify_client_hello(true, self.sni.as_deref())
}
}
}
}
impl Default for TlsHandshake {
fn default() -> Self {
Self {
sni: None,
client_alpn: Vec::new(),
server_alpn: None,
ja3: None,
ja4: None,
#[cfg(feature = "ja4plus")]
ja4s: None,
version: None,
cipher_suite: None,
resumption_attempted: false,
outcome: HandshakeOutcome::Truncated,
ech_outcome: EchOutcome::NotOffered,
ech_config_id: None,
certificate_chain: Vec::new(),
#[cfg(feature = "ja4plus")]
ja4x: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
AwaitingClientHello,
AwaitingServerHello,
Completed,
}
#[derive(Debug, Clone)]
pub struct TlsHandshakeParser {
inner: TlsParser,
state: State,
accumulator: TlsHandshake,
}
impl Default for TlsHandshakeParser {
fn default() -> Self {
Self::with_config(TlsConfig {
ja3: cfg!(feature = "tls-fingerprints"),
ja4: cfg!(feature = "tls-fingerprints"),
..Default::default()
})
}
}
impl TlsHandshakeParser {
pub fn with_config(config: TlsConfig) -> Self {
Self {
inner: TlsParser::with_config(config),
state: State::AwaitingClientHello,
accumulator: TlsHandshake::default(),
}
}
fn process(&mut self, msgs: Vec<TlsMessage>, out: &mut Vec<TlsHandshake>) {
for msg in msgs {
match msg {
TlsMessage::ClientHello(ch) => {
self.accumulator.sni = ch.sni.clone();
self.accumulator.client_alpn = ch.alpn.clone();
self.accumulator.resumption_attempted =
ch.extension_types.iter().any(|&e| e == 41 || e == 35);
if ch.ech_present {
self.accumulator.ech_outcome = EchOutcome::Unknown;
self.accumulator.ech_config_id = ch.ech_config_id;
}
self.state = State::AwaitingServerHello;
}
TlsMessage::ServerHello(sh) => {
self.accumulator.server_alpn = sh.alpn.clone();
self.accumulator.cipher_suite = Some(sh.cipher_suite);
self.accumulator.version =
Some(sh.supported_version.unwrap_or(sh.legacy_version));
self.accumulator.outcome = HandshakeOutcome::Completed;
if matches!(self.accumulator.ech_outcome, EchOutcome::Unknown) {
self.accumulator.ech_outcome = if sh.ech_retry_configs {
EchOutcome::Rejected
} else {
EchOutcome::Accepted
};
}
let done = std::mem::take(&mut self.accumulator);
out.push(done);
self.state = State::Completed;
}
TlsMessage::Alert(a) => {
if a.level == super::types::TlsAlertLevel::Fatal {
let outcome = match self.state {
State::AwaitingServerHello => HandshakeOutcome::AlertedByServer {
description: a.description,
},
_ => HandshakeOutcome::AlertedByClient {
description: a.description,
},
};
self.accumulator.outcome = outcome;
let done = std::mem::take(&mut self.accumulator);
out.push(done);
self.state = State::AwaitingClientHello;
}
}
#[cfg(feature = "tls-fingerprints")]
TlsMessage::Ja3 { hash, .. } => {
self.accumulator.ja3 = Some(hash);
}
#[cfg(feature = "tls-fingerprints")]
TlsMessage::Ja4 { fingerprint } => {
self.accumulator.ja4 = Some(fingerprint);
}
#[cfg(feature = "ja4plus")]
TlsMessage::Ja4s { fingerprint } => {
self.accumulator.ja4s = Some(fingerprint);
}
TlsMessage::Certificate { chain } => {
#[cfg(feature = "ja4plus")]
if let Some(leaf) = chain.first() {
self.accumulator.ja4x = super::ja4x::ja4x_for_der(leaf);
}
self.accumulator.certificate_chain = chain;
}
}
}
}
}
impl SessionParser for TlsHandshakeParser {
type Message = TlsHandshake;
fn parser_kind(&self) -> &'static str {
PARSER_KIND
}
fn feed_initiator(&mut self, bytes: &[u8], ts: Timestamp, out: &mut Vec<Self::Message>) {
let mut inner_out = Vec::new();
self.inner.feed_initiator(bytes, ts, &mut inner_out);
self.process(inner_out, out);
}
fn feed_responder(&mut self, bytes: &[u8], ts: Timestamp, out: &mut Vec<Self::Message>) {
let mut inner_out = Vec::new();
self.inner.feed_responder(bytes, ts, &mut inner_out);
self.process(inner_out, out);
}
fn fin_initiator(&mut self, out: &mut Vec<Self::Message>) {
let mut inner_out = Vec::new();
self.inner.fin_initiator(&mut inner_out);
self.process(inner_out, out);
if matches!(self.state, State::AwaitingServerHello) {
self.accumulator.outcome = HandshakeOutcome::Truncated;
let done = std::mem::take(&mut self.accumulator);
out.push(done);
self.state = State::AwaitingClientHello;
}
}
fn fin_responder(&mut self, out: &mut Vec<Self::Message>) {
let mut inner_out = Vec::new();
self.inner.fin_responder(&mut inner_out);
self.process(inner_out, out);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_has_session_parser_shape() {
fn assert_impls<P: SessionParser>() {}
assert_impls::<TlsHandshakeParser>();
}
#[test]
fn parser_kind_label() {
let p = TlsHandshakeParser::default();
assert_eq!(p.parser_kind(), "tls-handshake");
}
#[cfg(feature = "ja4plus")]
#[test]
fn aggregator_captures_ja4s_onto_the_handshake() {
use super::super::{TlsMessage, types::TlsServerHello};
let mut p = TlsHandshakeParser::default();
let mut out = Vec::new();
let sh = TlsServerHello {
cipher_suite: 0x1301,
..Default::default()
};
p.process(
vec![
TlsMessage::Ja4s {
fingerprint: "t130100_1301_deadbeefcafe".to_string(),
},
TlsMessage::ServerHello(Box::new(sh)),
],
&mut out,
);
assert_eq!(out.len(), 1, "expected one completed handshake");
assert_eq!(out[0].ja4s.as_deref(), Some("t130100_1301_deadbeefcafe"));
}
}