diff --git a/core/Cargo.toml b/core/Cargo.toml index c1e32141..71a42790 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -46,6 +46,8 @@ thiserror = "1.0" tls-parser = { git = "https://github.com/thegwan/tls-parser.git" } toml = "0.5.11" x509-parser = "0.13.2" +rust-crypto="0.2.36" +ring = "0.17.8" [features] timing = [] diff --git a/core/src/protocols/stream/mod.rs b/core/src/protocols/stream/mod.rs index 676c7dea..14464d60 100644 --- a/core/src/protocols/stream/mod.rs +++ b/core/src/protocols/stream/mod.rs @@ -11,7 +11,7 @@ pub mod tls; use self::dns::{parser::DnsParser, Dns}; use self::http::{parser::HttpParser, Http}; -use self::quic::{parser::QuicParser, QuicPacket}; +use self::quic::parser::QuicParser; use self::tls::{parser::TlsParser, Tls}; use crate::conntrack::conn::conn_info::ConnState; use crate::conntrack::conn_id::FiveTuple; @@ -22,6 +22,7 @@ use crate::subscription::*; use std::str::FromStr; use anyhow::{bail, Result}; +use quic::QuicConn; use strum_macros::EnumString; /// Represents the result of parsing one packet as a protocol message. @@ -195,7 +196,7 @@ pub enum SessionData { Tls(Box), Dns(Box), Http(Box), - Quic(Box), + Quic(Box), Null, } diff --git a/core/src/protocols/stream/quic/crypto.rs b/core/src/protocols/stream/quic/crypto.rs new file mode 100644 index 00000000..fd465186 --- /dev/null +++ b/core/src/protocols/stream/quic/crypto.rs @@ -0,0 +1,328 @@ +// crypto.rs contains the cryptograpic functions needed to derive QUIC +// initial keys. These keys can be used to remove header protection and +// decrypt QUIC initial packets. This file is heavily based on Cloudflare's +// crypto module in their Rust implementation of QUIC, known as Quiche. +// Therefore, the original license from https://github.com/cloudflare/quiche/blob/master/quiche/src/crypto/mod.rs is below: + +// Copyright (C) 2018-2019, Cloudflare, Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::iter::repeat; + +use crypto::aead::AeadDecryptor; +use crypto::aes::KeySize; +use crypto::aes_gcm::AesGcm; +use ring::aead; +use ring::hkdf; +use serde::Serialize; + +use crate::protocols::stream::quic::parser::QuicVersion; +use crate::protocols::stream::quic::QuicError; + +// The algorithm enum defines the available +// cryptographic algorithms used to secure +// QUIC packets. +#[derive(Copy, Clone, Debug, Serialize)] +pub enum Algorithm { + AES128GCM, +} + +impl Algorithm { + fn get_ring_hp(self) -> &'static aead::quic::Algorithm { + match self { + Algorithm::AES128GCM => &aead::quic::AES_128, + } + } + + fn get_ring_digest(self) -> hkdf::Algorithm { + match self { + Algorithm::AES128GCM => hkdf::HKDF_SHA256, + } + } + + pub fn key_len(self) -> usize { + match self { + Algorithm::AES128GCM => 16, + } + } + + pub fn tag_len(self) -> usize { + match self { + Algorithm::AES128GCM => 16, + } + } + + pub fn nonce_len(self) -> usize { + match self { + Algorithm::AES128GCM => 12, + } + } + + pub fn get_key_len(self) -> Option { + match self { + Algorithm::AES128GCM => Some(KeySize::KeySize128), + } + } +} + +// The Open struct gives a return value +// that contains all of the components +// needed for HP removal and decryption +#[derive(Serialize)] +pub struct Open { + alg: Algorithm, + + #[serde(skip_serializing)] + key_len: Option, + + initial_key: Vec, + + #[serde(skip_serializing)] + hp_key: aead::quic::HeaderProtectionKey, + + iv: Vec, +} + +impl Open { + pub fn new(alg: Algorithm, key: &[u8], iv: &[u8], hp_key: &[u8]) -> Result { + Ok(Open { + alg, + + key_len: alg.get_key_len(), + + initial_key: key.to_vec(), + + hp_key: aead::quic::HeaderProtectionKey::new(alg.get_ring_hp(), hp_key) + .map_err(|_| QuicError::CryptoFail)?, + + iv: iv.to_vec(), + }) + } + + pub fn open_with_u64_counter( + &self, + counter: u64, + ad: &[u8], + buf: &mut [u8], + tag: &[u8], + ) -> Result, QuicError> { + let nonce = make_nonce(&self.iv, counter); + let mut cipher = match self.alg { + Algorithm::AES128GCM => { + AesGcm::new(self.key_len.unwrap(), &self.initial_key, &nonce, ad) + } + }; + + let mut out: Vec = repeat(0).take(buf.len()).collect(); + + let rc = cipher.decrypt(buf, &mut out, tag); + + if !rc { + return Err(QuicError::CryptoFail); + } + + Ok(out) + } + + pub fn new_mask(&self, sample: &[u8]) -> Result<[u8; 5], QuicError> { + let mask = self + .hp_key + .new_mask(sample) + .map_err(|_| QuicError::CryptoFail)?; + + Ok(mask) + } + + pub fn alg(&self) -> Algorithm { + self.alg + } + + pub fn sample_len(&self) -> usize { + self.hp_key.algorithm().sample_len() + } +} +impl std::fmt::Debug for Open { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Point") + .field("alg", &self.alg) + .field("iv", &self.iv) + .finish() + } +} + +pub fn calc_init_keys(cid: &[u8], version: u32) -> Result<[Open; 2], QuicError> { + let aead = Algorithm::AES128GCM; + let key_len = aead.key_len(); + let nonce_len = aead.nonce_len(); + let initial_secret = derive_initial_secret(cid, version); + + let mut secret = [0; 32]; + let mut client_key = vec![0; key_len]; + let mut client_iv = vec![0; nonce_len]; + let mut client_hp_key = vec![0; key_len]; + + derive_client_initial_secret(&initial_secret, &mut secret)?; + derive_pkt_key(aead, &secret, &mut client_key)?; + derive_pkt_iv(aead, &secret, &mut client_iv)?; + derive_hdr_key(aead, &secret, &mut client_hp_key)?; + + // Server. + let mut server_key = vec![0; key_len]; + let mut server_iv = vec![0; nonce_len]; + let mut server_hp_key = vec![0; key_len]; + + derive_server_initial_secret(&initial_secret, &mut secret)?; + derive_pkt_key(aead, &secret, &mut server_key)?; + derive_pkt_iv(aead, &secret, &mut server_iv)?; + derive_hdr_key(aead, &secret, &mut server_hp_key)?; + + Ok([ + Open::new(aead, &client_key, &client_iv, &client_hp_key)?, + Open::new(aead, &server_key, &server_iv, &server_hp_key)?, + ]) +} + +fn derive_initial_secret(secret: &[u8], version: u32) -> hkdf::Prk { + const INITIAL_SALT_RFC9000: [u8; 20] = [ + 0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, + 0xad, 0xcc, 0xbb, 0x7f, 0x0a, + ]; + + const INITIAL_SALT_RFC9369: [u8; 20] = [ + 0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, + 0xcb, 0xf9, 0xbd, 0x2e, 0xd9, + ]; + + const INITIAL_SALT_DRAFT29: [u8; 20] = [ + 0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, + 0xe0, 0x43, 0x90, 0xa8, 0x99, + ]; + + const INITIAL_SALT_DRAFT27: [u8; 20] = [ + 0xc3, 0xee, 0xf7, 0x12, 0xc7, 0x2e, 0xbb, 0x5a, 0x11, 0xa7, 0xd2, 0x43, 0x2b, 0xb4, 0x63, + 0x65, 0xbe, 0xf9, 0xf5, 0x02, + ]; + + let salt = match QuicVersion::from_u32(version) { + QuicVersion::Rfc9000 => &INITIAL_SALT_RFC9000, + QuicVersion::Rfc9369 => &INITIAL_SALT_RFC9369, + QuicVersion::Draft29 => &INITIAL_SALT_DRAFT29, + QuicVersion::Draft27 | QuicVersion::Draft28 | QuicVersion::Mvfst27 => &INITIAL_SALT_DRAFT27, + _ => &INITIAL_SALT_RFC9000, + }; + + let salt = hkdf::Salt::new(hkdf::HKDF_SHA256, salt); + salt.extract(secret) +} + +fn derive_client_initial_secret(prk: &hkdf::Prk, out: &mut [u8]) -> Result<(), QuicError> { + const LABEL: &[u8] = b"client in"; + hkdf_expand_label(prk, LABEL, out) +} + +fn derive_server_initial_secret(prk: &hkdf::Prk, out: &mut [u8]) -> Result<(), QuicError> { + const LABEL: &[u8] = b"server in"; + hkdf_expand_label(prk, LABEL, out) +} + +pub fn derive_hdr_key(aead: Algorithm, secret: &[u8], out: &mut [u8]) -> Result<(), QuicError> { + const LABEL: &[u8] = b"quic hp"; + + let key_len = aead.key_len(); + + if key_len > out.len() { + return Err(QuicError::CryptoFail); + } + + let secret = hkdf::Prk::new_less_safe(aead.get_ring_digest(), secret); + hkdf_expand_label(&secret, LABEL, &mut out[..key_len]) +} + +pub fn derive_pkt_key(aead: Algorithm, secret: &[u8], out: &mut [u8]) -> Result<(), QuicError> { + const LABEL: &[u8] = b"quic key"; + + let key_len = aead.key_len(); + + if key_len > out.len() { + return Err(QuicError::CryptoFail); + } + + let secret = hkdf::Prk::new_less_safe(aead.get_ring_digest(), secret); + hkdf_expand_label(&secret, LABEL, &mut out[..key_len]) +} + +pub fn derive_pkt_iv(aead: Algorithm, secret: &[u8], out: &mut [u8]) -> Result<(), QuicError> { + const LABEL: &[u8] = b"quic iv"; + + let nonce_len = aead.nonce_len(); + + if nonce_len > out.len() { + return Err(QuicError::CryptoFail); + } + + let secret = hkdf::Prk::new_less_safe(aead.get_ring_digest(), secret); + hkdf_expand_label(&secret, LABEL, &mut out[..nonce_len]) +} + +fn hkdf_expand_label(prk: &hkdf::Prk, label: &[u8], out: &mut [u8]) -> Result<(), QuicError> { + const LABEL_PREFIX: &[u8] = b"tls13 "; + + let out_len = (out.len() as u16).to_be_bytes(); + let label_len = (LABEL_PREFIX.len() + label.len()) as u8; + + let info = [&out_len, &[label_len][..], LABEL_PREFIX, label, &[0][..]]; + + prk.expand(&info, ArbitraryOutputLen(out.len())) + .map_err(|_| QuicError::CryptoFail)? + .fill(out) + .map_err(|_| QuicError::CryptoFail)?; + + Ok(()) +} + +fn make_nonce(iv: &[u8], counter: u64) -> [u8; aead::NONCE_LEN] { + let mut nonce = [0; aead::NONCE_LEN]; + nonce.copy_from_slice(iv); + + // XOR the last bytes of the IV with the counter. This is equivalent to + // left-padding the counter with zero bytes. + for (a, b) in nonce[4..].iter_mut().zip(counter.to_be_bytes().iter()) { + *a ^= b; + } + + nonce +} + +// The ring HKDF expand() API does not accept an arbitrary output length, so we +// need to hide the `usize` length as part of a type that implements the trait +// `ring::hkdf::KeyType` in order to trick ring into accepting it. +struct ArbitraryOutputLen(usize); + +impl hkdf::KeyType for ArbitraryOutputLen { + fn len(&self) -> usize { + self.0 + } +} diff --git a/core/src/protocols/stream/quic/frame.rs b/core/src/protocols/stream/quic/frame.rs new file mode 100644 index 00000000..5825ab2c --- /dev/null +++ b/core/src/protocols/stream/quic/frame.rs @@ -0,0 +1,241 @@ +// QUIC Frame types and parsing +// Implemented per RFC 9000: https://datatracker.ietf.org/doc/html/rfc9000#name-frame-types-and-formats + +use serde::Serialize; +use std::collections::BTreeMap; + +use crate::protocols::stream::quic::QuicError; +use crate::protocols::stream::quic::QuicPacket; + +// Types of supported QUIC frames +// Currently only includes those seen in the Init and Handshake packets +#[derive(Debug, Serialize, Clone)] +pub enum QuicFrame { + Padding { + length: usize, + }, + Ping, + Ack { + largest_acknowledged: u64, + ack_delay: u64, + first_ack_range: u64, + ack_ranges: Vec, + ecn_counts: Option, + }, + Crypto { + offset: u64, + }, +} + +// ACK Range field, part of ACK frame +// https://datatracker.ietf.org/doc/html/rfc9000#ack-range-format +#[derive(Debug, Serialize, Clone)] +pub struct AckRange { + gap: u64, + ack_range_len: u64, +} + +// ECN Counts field, part of some ACK frames +// https://datatracker.ietf.org/doc/html/rfc9000#ecn-count-format +#[derive(Debug, Serialize, Clone)] +pub struct EcnCounts { + ect0_count: u64, + ect1_count: u64, + ecn_ce_count: u64, +} + +impl QuicFrame { + // parse_frames takes the plaintext QUIC packet payload and parses the frame list + // it also returns the reassembled CRYPTO frame bytes as a Vec + pub fn parse_frames( + data: &[u8], + mut expected_offset: usize, + ) -> Result<(Vec, Vec), QuicError> { + let mut frames: Vec = Vec::new(); + let mut crypto_map: BTreeMap> = BTreeMap::new(); + let mut offset = 0; + // Iterate over plaintext payload bytes, this is a list of frames + while offset < data.len() { + // Parse frame type + let frame_type_len = + QuicPacket::get_var_len(QuicPacket::access_data(data, offset, offset + 1)?[0])?; + let frame_type = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + frame_type_len, + )?)?; + offset += frame_type_len; + match frame_type { + 0x00 => { + // Handle PADDING + let mut length = 0; + while offset + length + 1 < data.len() + && QuicPacket::access_data(data, offset + length, offset + length + 1)?[0] + == 0 + { + length += 1; + } + offset += length; + length += frame_type_len; // Add the original frame type bytes to length. Wireshark also does this + frames.push(QuicFrame::Padding { length }); + } + 0x01 => { + // Handle PING + frames.push(QuicFrame::Ping); + } + 0x02 | 0x03 => { + // Handle ACK + // Parse Largest Acknowledged + let largest_acknowledged_len = QuicPacket::get_var_len( + QuicPacket::access_data(data, offset, offset + 1)?[0], + )?; + let largest_acknowledged = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + largest_acknowledged_len, + )?)?; + offset += largest_acknowledged_len; + // Parse ACK Delay + let ack_delay_len = QuicPacket::get_var_len( + QuicPacket::access_data(data, offset, offset + 1)?[0], + )?; + let ack_delay = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + ack_delay_len, + )?)?; + offset += ack_delay_len; + // Parse ACK Range Count + let ack_range_count_len = QuicPacket::get_var_len( + QuicPacket::access_data(data, offset, offset + 1)?[0], + )?; + let ack_range_count = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + ack_range_count_len, + )?)?; + offset += ack_range_count_len; + // Parse First ACK Range + let first_ack_range_len = QuicPacket::get_var_len( + QuicPacket::access_data(data, offset, offset + 1)?[0], + )?; + let first_ack_range = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + first_ack_range_len, + )?)?; + offset += first_ack_range_len; + // Parse ACK Range list field + let mut ack_ranges = Vec::new(); + for _ in 0..ack_range_count { + let gap_len = QuicPacket::get_var_len( + QuicPacket::access_data(data, offset, offset + 1)?[0], + )?; + let gap = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + gap_len, + )?)?; + offset += gap_len; + let ack_range_len_len = QuicPacket::get_var_len( + QuicPacket::access_data(data, offset, offset + 1)?[0], + )?; + let ack_range_len = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + ack_range_len_len, + )?)?; + offset += ack_range_len_len; + ack_ranges.push(AckRange { gap, ack_range_len }) + } + // Parse ECN Counts, if the ACK frame contains them + let ecn_counts: Option = if frame_type == 0x03 { + let ect0_count_len = QuicPacket::get_var_len( + QuicPacket::access_data(data, offset, offset + 1)?[0], + )?; + let ect0_count = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + ect0_count_len, + )?)?; + offset += ect0_count_len; + let ect1_count_len = QuicPacket::get_var_len( + QuicPacket::access_data(data, offset, offset + 1)?[0], + )?; + let ect1_count = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + ect1_count_len, + )?)?; + offset += ect1_count_len; + let ecn_ce_count_len = QuicPacket::get_var_len( + QuicPacket::access_data(data, offset, offset + 1)?[0], + )?; + let ecn_ce_count = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + ecn_ce_count_len, + )?)?; + Some(EcnCounts { + ect0_count, + ect1_count, + ecn_ce_count, + }) + } else { + None + }; + frames.push(QuicFrame::Ack { + largest_acknowledged, + ack_delay, + first_ack_range, + ack_ranges, + ecn_counts, + }) + } + 0x06 => { + // Handle CRYPTO frame + // Parse offset + let crypto_offset_len = QuicPacket::get_var_len( + QuicPacket::access_data(data, offset, offset + 1)?[0], + )?; + let crypto_offset = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + crypto_offset_len, + )?)?; + offset += crypto_offset_len; + // Parse length + let crypto_len_len = QuicPacket::get_var_len( + QuicPacket::access_data(data, offset, offset + 1)?[0], + )?; + let crypto_len = QuicPacket::slice_to_u64(QuicPacket::access_data( + data, + offset, + offset + crypto_len_len, + )?)? as usize; + offset += crypto_len_len; + // Parse data + let crypto_data = + QuicPacket::access_data(data, offset, offset + crypto_len)?.to_vec(); + crypto_map + .entry(crypto_offset as usize) + .or_insert(crypto_data); + frames.push(QuicFrame::Crypto { + offset: crypto_offset, + }); + offset += crypto_len; + } + _ => return Err(QuicError::UnknownFrameType), + } + } + let mut reassembled_crypto: Vec = Vec::new(); + for (crypto_offset, crypto_data) in crypto_map { + if crypto_offset != expected_offset { + return Err(QuicError::MissingCryptoFrames); + } + expected_offset += crypto_data.len(); + reassembled_crypto.extend(crypto_data); + } + Ok((frames, reassembled_crypto)) + } +} diff --git a/core/src/protocols/stream/quic/header.rs b/core/src/protocols/stream/quic/header.rs index f9386c3a..b53e0d57 100644 --- a/core/src/protocols/stream/quic/header.rs +++ b/core/src/protocols/stream/quic/header.rs @@ -2,7 +2,7 @@ use serde::Serialize; -use crate::protocols::stream::quic::parser::QuicError; +use crate::protocols::stream::quic::QuicError; /// Quic Long Header #[derive(Debug, Serialize, Clone)] @@ -23,9 +23,6 @@ pub struct QuicLongHeader { #[derive(Debug, Serialize, Clone)] pub struct QuicShortHeader { pub dcid: Option, // optional. If not pre-existing cid then none. - - #[serde(skip)] - pub dcid_bytes: Vec, } // Long Header Packet Types from RFC 9000 Table 5 diff --git a/core/src/protocols/stream/quic/mod.rs b/core/src/protocols/stream/quic/mod.rs index 0bb2067a..38bf254a 100644 --- a/core/src/protocols/stream/quic/mod.rs +++ b/core/src/protocols/stream/quic/mod.rs @@ -22,14 +22,66 @@ TODO: support HTTP/3 */ pub(crate) mod parser; +use std::collections::HashSet; + pub use self::header::{QuicLongHeader, QuicShortHeader}; +use crypto::Open; +use frame::QuicFrame; use header::LongHeaderPacketType; -use parser::QuicError; use serde::Serialize; + +use super::tls::Tls; +pub(crate) mod crypto; +pub(crate) mod frame; pub(crate) mod header; +/// Errors Thrown throughout QUIC parsing. These are handled by retina and used to skip packets. +#[derive(Debug)] +pub enum QuicError { + FixedBitNotSet, + PacketTooShort, + UnknownVersion, + ShortHeader, + UnknowLongHeaderPacketType, + NoLongHeader, + UnsupportedVarLen, + InvalidDataIndices, + CryptoFail, + FailedHeaderProtection, + UnknownFrameType, + TlsParseFail, + MissingCryptoFrames, +} + +/// Parsed Quic connections +#[derive(Debug, Serialize)] +pub struct QuicConn { + // All packets associated with the connection + pub packets: Vec, + + // All cids, both src and destination, seen in Long Header packets + pub cids: HashSet, + + // Parsed TLS messsages + pub tls: Tls, + + // Crypto needed to decrypt initial packets sent by client + pub client_opener: Option, + + // Crypto needed to decrypt initial packets sent by server + pub server_opener: Option, + + // Client buffer for multi-packet TLS messages + #[serde(skip_serializing)] + pub client_buffer: Vec, + + // Server buffer for multi-packet TLS messages + #[serde(skip_serializing)] + pub server_buffer: Vec, +} + /// Parsed Quic Packet contents -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize)] pub struct QuicPacket { /// Quic Short header pub short_header: Option, @@ -39,6 +91,8 @@ pub struct QuicPacket { /// The number of bytes contained in the estimated payload pub payload_bytes_count: Option, + + pub frames: Option>, } impl QuicPacket { diff --git a/core/src/protocols/stream/quic/parser.rs b/core/src/protocols/stream/quic/parser.rs index ab327cbe..73334acc 100644 --- a/core/src/protocols/stream/quic/parser.rs +++ b/core/src/protocols/stream/quic/parser.rs @@ -2,21 +2,37 @@ //! Custom Quic Parser with many design choices borrowed from //! [Wireshark Quic Disector](https://gitlab.com/wireshark/wireshark/-/blob/master/epan/dissectors/packet-quic.c) //! +use crate::protocols::stream::quic::crypto::calc_init_keys; +use crate::protocols::stream::quic::frame::QuicFrame; use crate::protocols::stream::quic::header::{ LongHeaderPacketType, QuicLongHeader, QuicShortHeader, }; -use crate::protocols::stream::quic::QuicPacket; +use crate::protocols::stream::quic::{QuicError, QuicPacket}; +use crate::protocols::stream::tls::Tls; use crate::protocols::stream::{ ConnParsable, ConnState, L4Pdu, ParseResult, ProbeResult, Session, SessionData, }; -use std::collections::HashMap; +use byteorder::{BigEndian, ByteOrder}; +use std::collections::HashSet; +use tls_parser::parse_tls_message_handshake; -#[derive(Default, Debug)] +use super::QuicConn; + +#[derive(Debug)] pub struct QuicParser { - /// Maps session ID to Quic transaction - sessions: HashMap, - /// Total sessions ever seen (Running session ID) - cnt: usize, + // /// Maps session ID to Quic transaction + // sessions: HashMap, + // /// Total sessions ever seen (Running session ID) + // cnt: usize, + sessions: Vec, +} + +impl Default for QuicParser { + fn default() -> Self { + QuicParser { + sessions: vec![QuicConn::new()], + } + } } impl ConnParsable for QuicParser { @@ -28,7 +44,10 @@ impl ConnParsable for QuicParser { } if let Ok(data) = (pdu.mbuf_ref()).get_data_slice(offset, length) { - self.process(data) + if !self.sessions.is_empty() { + return self.sessions[0].parse_packet(data, pdu.dir); + } + ParseResult::Skipped } else { log::warn!("Malformed packet on parse"); ParseResult::Skipped @@ -74,7 +93,7 @@ impl ConnParsable for QuicParser { } fn remove_session(&mut self, session_id: usize) -> Option { - self.sessions.remove(&session_id).map(|quic| Session { + self.sessions.pop().map(|quic| Session { data: SessionData::Quic(Box::new(quic)), id: session_id, }) @@ -82,10 +101,10 @@ impl ConnParsable for QuicParser { fn drain_sessions(&mut self) -> Vec { self.sessions - .drain() - .map(|(session_id, quic)| Session { + .drain(..) + .map(|quic| Session { data: SessionData::Quic(Box::new(quic)), - id: session_id, + id: 0, }) .collect() } @@ -100,37 +119,33 @@ impl ConnParsable for QuicParser { /// Supported Quic Versions #[derive(Debug, PartialEq, Eq, Hash)] -enum QuicVersion { +#[repr(u32)] +pub enum QuicVersion { ReservedNegotiation = 0x00000000, Rfc9000 = 0x00000001, // Quic V1 Rfc9369 = 0x6b3343cf, // Quic V2 + Draft27 = 0xff00001b, // Quic draft 27 + Draft28 = 0xff00001c, // Quic draft 28 + Draft29 = 0xff00001d, // Quic draft 29 + Mvfst27 = 0xfaceb002, // Facebook Implementation of draft 27 Unknown, } impl QuicVersion { - fn from_u32(version: u32) -> Self { + pub fn from_u32(version: u32) -> Self { match version { 0x00000000 => QuicVersion::ReservedNegotiation, 0x00000001 => QuicVersion::Rfc9000, 0x6b3343cf => QuicVersion::Rfc9369, + 0xff00001b => QuicVersion::Draft27, + 0xff00001c => QuicVersion::Draft28, + 0xff00001d => QuicVersion::Draft29, + 0xfaceb002 => QuicVersion::Mvfst27, _ => QuicVersion::Unknown, } } } -/// Errors Thrown by Quic Parser. These are handled by retina and used to skip packets. -#[derive(Debug)] -pub enum QuicError { - FixedBitNotSet, - PacketTooShort, - UnknownVersion, - ShortHeader, - UnknowLongHeaderPacketType, - NoLongHeader, - UnsupportedVarLen, - InvalidDataIndices, -} - impl QuicPacket { /// Processes the connection ID bytes array to a hex string pub fn vec_u8_to_hex_string(vec: &[u8]) -> String { @@ -154,7 +169,7 @@ impl QuicPacket { } // Masks variable length encoding and returns u64 value for remainder of field - fn slice_to_u64(data: &[u8]) -> Result { + pub fn slice_to_u64(data: &[u8]) -> Result { if data.len() > 8 { return Err(QuicError::UnsupportedVarLen); } @@ -167,7 +182,7 @@ impl QuicPacket { Ok(result) } - fn access_data(data: &[u8], start: usize, end: usize) -> Result<&[u8], QuicError> { + pub fn access_data(data: &[u8], start: usize, end: usize) -> Result<&[u8], QuicError> { if end < start { return Err(QuicError::InvalidDataIndices); } @@ -178,8 +193,12 @@ impl QuicPacket { } /// Parses Quic packet from bytes - pub fn parse_from(data: &[u8]) -> Result { - let mut offset = 0; + pub fn parse_from( + conn: &mut QuicConn, + data: &[u8], + mut offset: usize, + dir: bool, + ) -> Result<(QuicPacket, usize), QuicError> { let packet_header_byte = QuicPacket::access_data(data, offset, offset + 1)?[0]; offset += 1; // Check the fixed bit @@ -207,18 +226,25 @@ impl QuicPacket { offset += 1; let dcid_bytes = QuicPacket::access_data(data, offset, offset + dcid_len as usize)?; let dcid = QuicPacket::vec_u8_to_hex_string(dcid_bytes); + if dcid_len > 0 && !conn.cids.contains(&dcid) { + conn.cids.insert(dcid.clone()); + } offset += dcid_len as usize; // Parse SCID let scid_len = QuicPacket::access_data(data, offset, offset + 1)?[0]; offset += 1; let scid_bytes = QuicPacket::access_data(data, offset, offset + scid_len as usize)?; let scid = QuicPacket::vec_u8_to_hex_string(scid_bytes); + if scid_len > 0 && !conn.cids.contains(&scid) { + conn.cids.insert(scid.clone()); + } offset += scid_len as usize; let token_len; let token; let packet_len; let retry_tag; + let decrypted_payload; // Parse packet type specific fields match packet_type { LongHeaderPacketType::Initial => { @@ -227,11 +253,9 @@ impl QuicPacket { let token_len_len = QuicPacket::get_var_len( QuicPacket::access_data(data, offset, offset + 1)?[0], )?; - token_len = Some(QuicPacket::slice_to_u64(QuicPacket::access_data( - data, - offset, - offset + token_len_len, - )?)?); + let token_len_bytes = + QuicPacket::access_data(data, offset, offset + token_len_len)?; + token_len = Some(QuicPacket::slice_to_u64(token_len_bytes)?); offset += token_len_len; let token_bytes = QuicPacket::access_data( data, @@ -244,16 +268,90 @@ impl QuicPacket { let packet_len_len = QuicPacket::get_var_len( QuicPacket::access_data(data, offset, offset + 1)?[0], )?; - packet_len = Some(QuicPacket::slice_to_u64(QuicPacket::access_data( - data, - offset, - offset + packet_len_len, - )?)?); + let packet_len_bytes = + QuicPacket::access_data(data, offset, offset + packet_len_len)?; + packet_len = Some(QuicPacket::slice_to_u64(packet_len_bytes)?); + offset += packet_len_len; + if conn.client_opener.is_none() { + // Derive initial keys + let [client_opener, server_opener] = calc_init_keys(dcid_bytes, version)?; + conn.client_opener = Some(client_opener); + conn.server_opener = Some(server_opener); + } + // Calculate HP + let sample_len = conn.client_opener.as_ref().unwrap().sample_len(); + let hp_sample = + QuicPacket::access_data(data, offset + 4, offset + 4 + sample_len)?; + let mask = if dir { + conn.client_opener.as_ref().unwrap().new_mask(hp_sample)? + } else { + conn.server_opener.as_ref().unwrap().new_mask(hp_sample)? + }; + // Remove HP from packet header byte + let unprotected_header = packet_header_byte ^ (mask[0] & 0b00001111); + if (unprotected_header >> 2) & 0b00000011 != 0 { + return Err(QuicError::FailedHeaderProtection); + } + // Parse packet number + let packet_num_len = ((unprotected_header & 0b00000011) + 1) as usize; + let packet_number_bytes = + QuicPacket::access_data(data, offset, offset + packet_num_len)?; + let mut packet_number = vec![0; 4 - packet_num_len]; + for i in 0..packet_num_len { + packet_number.push(packet_number_bytes[i] ^ mask[i + 1]); + } + + let initial_packet_number_bytes = &packet_number[4 - packet_num_len..]; + let packet_number_int = BigEndian::read_i32(&packet_number); + offset += packet_num_len; + // Parse the encrypted payload + let tag_len = conn.client_opener.as_ref().unwrap().alg().tag_len(); + if (packet_len.unwrap() as usize) < (tag_len + packet_num_len) { + return Err(QuicError::PacketTooShort); + } + let cipher_text_len = packet_len.unwrap() as usize - tag_len - packet_num_len; + let mut encrypted_payload = + QuicPacket::access_data(data, offset, offset + cipher_text_len)?.to_vec(); + offset += cipher_text_len; + // Parse auth tag + let tag = QuicPacket::access_data(data, offset, offset + tag_len)?; + offset += tag_len; + // Reconstruct authenticated data + let mut ad = Vec::new(); + ad.append(&mut [unprotected_header].to_vec()); + ad.append(&mut version_bytes.to_vec()); + ad.append(&mut [dcid_len].to_vec()); + ad.append(&mut dcid_bytes.to_vec()); + ad.append(&mut [scid_len].to_vec()); + ad.append(&mut scid_bytes.to_vec()); + ad.append(&mut token_len_bytes.to_vec()); + ad.append(&mut token_bytes.to_vec()); + ad.append(&mut packet_len_bytes.to_vec()); + ad.append(&mut initial_packet_number_bytes.to_vec()); + // Decrypt payload with proper keys based on traffic direction + if dir { + decrypted_payload = + Some(conn.client_opener.as_ref().unwrap().open_with_u64_counter( + packet_number_int as u64, + &ad, + &mut encrypted_payload, + tag, + )?); + } else { + decrypted_payload = + Some(conn.server_opener.as_ref().unwrap().open_with_u64_counter( + packet_number_int as u64, + &ad, + &mut encrypted_payload, + tag, + )?); + } } LongHeaderPacketType::ZeroRTT | LongHeaderPacketType::Handshake => { token_len = None; token = None; retry_tag = None; + decrypted_payload = None; // Parse payload length let packet_len_len = QuicPacket::get_var_len( QuicPacket::access_data(data, offset, offset + 1)?[0], @@ -263,10 +361,17 @@ impl QuicPacket { offset, offset + packet_len_len, )?)?); + offset += packet_len_len; + offset += packet_len.unwrap() as usize; } LongHeaderPacketType::Retry => { packet_len = None; - token_len = Some((data.len() - offset - 16) as u64); + decrypted_payload = None; + if data.len() > (offset + 16) { + token_len = Some((data.len() - offset - 16) as u64); + } else { + return Err(QuicError::PacketTooShort); + } // Parse retry token let token_bytes = QuicPacket::access_data( data, @@ -278,57 +383,129 @@ impl QuicPacket { // Parse retry tag let retry_tag_bytes = QuicPacket::access_data(data, offset, offset + 16)?; retry_tag = Some(QuicPacket::vec_u8_to_hex_string(retry_tag_bytes)); + offset += 16; } } - Ok(QuicPacket { - payload_bytes_count: packet_len, - short_header: None, - long_header: Some(QuicLongHeader { - packet_type, - type_specific, - version, - dcid_len, - dcid, - scid_len, - scid, - token_len, - token, - retry_tag, - }), - }) + let mut frames: Option> = None; + // Grab the proper buffer for CRYPTO frame data + let crypto_buffer: &mut Vec = if dir { + conn.client_buffer.as_mut() + } else { + conn.server_buffer.as_mut() + }; + // If decrypted payload is not None, parse the frames + if let Some(frame_bytes) = decrypted_payload { + // Get frames and reassembled CRYPTO data + // Pass the buffer's current length as starting offset for CRYPTO frames + let (q_frames, mut crypto_bytes) = + QuicFrame::parse_frames(&frame_bytes, crypto_buffer.len())?; + frames = Some(q_frames); + if !crypto_bytes.is_empty() { + crypto_buffer.append(&mut crypto_bytes); + // Attempt to parse CRYPTO buffer + // clear on success + // TODO: This naive buffer will not work for out of order frames + // across packets or multiple messages in the same buffer + match parse_tls_message_handshake(crypto_buffer) { + Ok((_, msg)) => { + conn.tls.parse_message_level(&msg, dir); + crypto_buffer.clear(); + } + Err(_) => return Err(QuicError::TlsParseFail), + } + } + } + + Ok(( + QuicPacket { + payload_bytes_count: packet_len, + short_header: None, + long_header: Some(QuicLongHeader { + packet_type, + type_specific, + version, + dcid_len, + dcid, + scid_len, + scid, + token_len, + token, + retry_tag, + }), + frames, + }, + offset, + )) } else { // Short Header - let mut max_dcid_len = 20; - if data.len() < 1 + max_dcid_len { - max_dcid_len = data.len() - 1; + let mut dcid_len = 20; + if data.len() < 1 + dcid_len { + dcid_len = data.len() - 1; } // Parse DCID - let dcid_bytes = QuicPacket::access_data(data, offset, offset + max_dcid_len)?.to_vec(); - offset += max_dcid_len; + let dcid_hex = QuicPacket::vec_u8_to_hex_string(QuicPacket::access_data( + data, + offset, + offset + dcid_len, + )?); + let mut dcid = None; + for cid in &conn.cids { + if dcid_hex.starts_with(cid) { + dcid_len = cid.chars().count() / 2; + dcid = Some(cid.clone()); + } + } + offset += dcid_len; // Counts all bytes remaining - let payload_bytes_count = Some((data.len() - offset) as u64); - Ok(QuicPacket { - short_header: Some(QuicShortHeader { - dcid: None, - dcid_bytes, - }), - long_header: None, - payload_bytes_count, - }) + let payload_bytes_count = (data.len() - offset) as u64; + offset += payload_bytes_count as usize; + Ok(( + QuicPacket { + short_header: Some(QuicShortHeader { dcid }), + long_header: None, + payload_bytes_count: Some(payload_bytes_count), + frames: None, + }, + offset, + )) } } } -impl QuicParser { - fn process(&mut self, data: &[u8]) -> ParseResult { - if let Ok(quic) = QuicPacket::parse_from(data) { - let session_id = self.cnt; - self.sessions.insert(session_id, quic); - self.cnt += 1; - ParseResult::Done(session_id) - } else { - ParseResult::Skipped +impl QuicConn { + pub(crate) fn new() -> QuicConn { + QuicConn { + packets: Vec::new(), + cids: HashSet::new(), + tls: Tls::new(), + client_opener: None, + server_opener: None, + client_buffer: Vec::new(), + server_buffer: Vec::new(), + } + } + + fn parse_packet(&mut self, data: &[u8], direction: bool) -> ParseResult { + let mut offset = 0; + // Iterate over all of the data in the datagram + // Parse as many QUIC packets as possible + // TODO: identify padding appended to datagram + while data.len() > offset { + if let Ok((quic, off)) = QuicPacket::parse_from(self, data, offset, direction) { + self.packets.push(quic); + offset = off; + } else { + return ParseResult::Skipped; + } + } + if self + .packets + .last() + .is_some_and(|p| p.short_header.is_some()) + { + return ParseResult::Done(0); } + ParseResult::Continue(0) } } diff --git a/core/src/subscription/quic_stream.rs b/core/src/subscription/quic_stream.rs index 84762e8f..41e44d1e 100644 --- a/core/src/subscription/quic_stream.rs +++ b/core/src/subscription/quic_stream.rs @@ -3,25 +3,14 @@ //! This is a session-level subscription that delivers parsed QUIC stream records and associated //! connection metadata. //! -//! ## Example -//! Prints QUIC connections that use long headers: -//! ``` -//! #[filter("quic.header_type = 'long'")] -//! fn main() { -//! let config = default_config(); -//! let cb = |quic: QuicStream| { -//! println!("{}", quic.data); -//! }; -//! let mut runtime = Runtime::new(config, filter, cb).unwrap(); -//! runtime.run(); -//! } use crate::conntrack::conn_id::FiveTuple; use crate::conntrack::pdu::{L4Context, L4Pdu}; use crate::conntrack::ConnTracker; use crate::filter::FilterResult; use crate::memory::mbuf::Mbuf; -use crate::protocols::stream::quic::{parser::QuicParser, QuicPacket}; +use crate::protocols::stream::quic::parser::QuicParser; +use crate::protocols::stream::quic::QuicConn; use crate::protocols::stream::{ConnParser, Session, SessionData}; use crate::subscription::{Level, Subscribable, Subscription, Trackable}; use std::collections::HashSet; @@ -34,7 +23,7 @@ use std::net::SocketAddr; #[derive(Debug, Serialize)] pub struct QuicStream { pub five_tuple: FiveTuple, - pub data: QuicPacket, + pub data: QuicConn, } impl QuicStream { @@ -95,18 +84,6 @@ pub struct TrackedQuic { connection_id: HashSet, } -impl TrackedQuic { - fn get_connection_id(&self, dcid_bytes: &[u8]) -> Option { - let dcid_hex = QuicPacket::vec_u8_to_hex_string(dcid_bytes); - for dcid in &self.connection_id { - if dcid_hex.starts_with(dcid) { - return Some(dcid.clone()); - } - } - None - } -} - impl Trackable for TrackedQuic { type Subscribed = QuicStream; @@ -121,24 +98,9 @@ impl Trackable for TrackedQuic { fn on_match(&mut self, session: Session, subscription: &Subscription) { if let SessionData::Quic(quic) = session.data { - let mut quic_clone = (*quic).clone(); - - if let Some(long_header) = &quic_clone.long_header { - if long_header.dcid_len > 0 { - self.connection_id.insert(long_header.dcid.clone()); - } - if long_header.scid_len > 0 { - self.connection_id.insert(long_header.scid.clone()); - } - } else { - if let Some(ref mut short_header_value) = quic_clone.short_header { - short_header_value.dcid = - self.get_connection_id(&short_header_value.dcid_bytes); - } - return subscription.invoke(QuicStream { - five_tuple: self.five_tuple, - data: quic_clone, - }); + let quic_clone = *quic; + for cid in &quic_clone.cids { + self.connection_id.insert(cid.to_string()); } subscription.invoke(QuicStream { diff --git a/traces/README.md b/traces/README.md index e5c94e44..99f3f07f 100644 --- a/traces/README.md +++ b/traces/README.md @@ -2,8 +2,10 @@ A collection of sample packet captures pulled from a variety of sources. -| Trace | Source | Description | -|--------------------|-------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| -| `small_flows.pcap` | [Tcpreplay Sample Captures](https://tcpreplay.appneta.com/wiki/captures.html) | A synthetic combination of a few different applications and protocols at a relatively low network traffic rate. | -| `tls_ciphers.pcap` | [Wireshark Sample Captures](https://wiki.wireshark.org/SampleCaptures) | OpenSSL client/server GET requests over TLS 1.2 with 73 different cipher suites. | -| `quic_retry.pcapng`| [Wireshark Issue](https://gitlab.com/wireshark/wireshark/-/issues/18757) | An example of a QUIC Retry Packet. Original Pcap modified to remove CookedLinux and add Ether | \ No newline at end of file +| Trace | Source | Description | +|--------------------|-------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| `small_flows.pcap` | [Tcpreplay Sample Captures](https://tcpreplay.appneta.com/wiki/captures.html) | A synthetic combination of a few different applications and protocols at a relatively low network traffic rate. | +| `tls_ciphers.pcap` | [Wireshark Sample Captures](https://wiki.wireshark.org/SampleCaptures) | OpenSSL client/server GET requests over TLS 1.2 with 73 different cipher suites. | +| `quic_retry.pcapng`| [Wireshark Issue](https://gitlab.com/wireshark/wireshark/-/issues/18757) | An example of a QUIC Retry Packet. Original Pcap modified to remove CookedLinux and add Ether | +| `quic_xargs.pcap` | [illustrated-quic GitHub](https://github.com/syncsynchalt/illustrated-quic/blob/main/captures/capture.pcap) | The pcap used in the creation of [The Illustrated QUIC Connection](https://quic.xargs.org). | +| `quic_kyber.pcap` | Captured from Chrome 124 | A QUIC packet demonstrating the use of the Kyber keyshare, exceeding MTU, and requiring CRYPTO buffers. | \ No newline at end of file diff --git a/traces/quic_kyber.pcapng b/traces/quic_kyber.pcapng new file mode 100644 index 00000000..476ca356 Binary files /dev/null and b/traces/quic_kyber.pcapng differ diff --git a/traces/quic_xargs.pcap b/traces/quic_xargs.pcap new file mode 100644 index 00000000..dbd17fdc Binary files /dev/null and b/traces/quic_xargs.pcap differ