use crate::Timestamp;
use crate::session::SessionParser;
use super::TlsParser;
use super::session::TlsMessage;
use super::types::{TlsConfig, TlsVersion};
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>,
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>,
}
#[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 Default for TlsHandshake {
fn default() -> Self {
Self {
sni: None,
client_alpn: Vec::new(),
server_alpn: None,
ja3: None,
ja4: None,
version: None,
cipher_suite: None,
resumption_attempted: false,
outcome: HandshakeOutcome::Truncated,
ech_outcome: EchOutcome::NotOffered,
ech_config_id: 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);
}
}
}
}
}
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");
}
}