Skip to content

Commit 67004f6

Browse files
committed
Add ClientHello parsing and CRYPTO frame reassembly
1 parent f243f89 commit 67004f6

File tree

7 files changed

+100
-28
lines changed

7 files changed

+100
-28
lines changed

core/src/protocols/stream/quic/crypto.rs

+33-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
// This is heavily based on Cloudflare's Rust implementation of QUIC, known as Quiche.
1+
// crypto.rs contains the cryptograpic functions needed to derive QUIC
2+
// initial keys. These keys can be used to remove header protection and
3+
// decrypt QUIC initial packets. This file is heavily based on Cloudflare's
4+
// crypto module in their Rust implementation of QUIC, known as Quiche.
25
// Therefore, the original license from https://github.com/cloudflare/quiche/blob/master/quiche/src/crypto/mod.rs is below:
36

47
// Copyright (C) 2018-2019, Cloudflare, Inc.
@@ -35,8 +38,12 @@ use crypto::aes_gcm::AesGcm;
3538
use ring::aead;
3639
use ring::hkdf;
3740

41+
use crate::protocols::stream::quic::parser::QuicVersion;
3842
use crate::protocols::stream::quic::QuicError;
3943

44+
// The algorithm enum defines the available
45+
// cryptographic algorithms used to secure
46+
// QUIC packets.
4047
#[derive(Copy, Clone, Debug)]
4148
pub enum Algorithm {
4249
AES128GCM,
@@ -80,6 +87,9 @@ impl Algorithm {
8087
}
8188
}
8289

90+
// The Open struct gives a return value
91+
// that contains all of the components
92+
// needed for HP removal and decryption
8393
pub struct Open {
8494
alg: Algorithm,
8595

@@ -192,13 +202,32 @@ pub fn calc_init_keys(cid: &[u8], version: u32) -> Result<[Open; 2], QuicError>
192202
}
193203

194204
fn derive_initial_secret(secret: &[u8], version: u32) -> hkdf::Prk {
195-
const INITIAL_SALT: [u8; 20] = [
205+
const INITIAL_SALT_RFC9000: [u8; 20] = [
196206
0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c,
197207
0xad, 0xcc, 0xbb, 0x7f, 0x0a,
198208
];
199209

200-
let salt = match version {
201-
_ => &INITIAL_SALT,
210+
const INITIAL_SALT_RFC9369: [u8; 20] = [
211+
0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d,
212+
0xcb, 0xf9, 0xbd, 0x2e, 0xd9,
213+
];
214+
215+
const INITIAL_SALT_DRAFT29: [u8; 20] = [
216+
0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11,
217+
0xe0, 0x43, 0x90, 0xa8, 0x99,
218+
];
219+
220+
const INITIAL_SALT_DRAFT27: [u8; 20] = [
221+
0xc3, 0xee, 0xf7, 0x12, 0xc7, 0x2e, 0xbb, 0x5a, 0x11, 0xa7, 0xd2, 0x43, 0x2b, 0xb4, 0x63,
222+
0x65, 0xbe, 0xf9, 0xf5, 0x02,
223+
];
224+
225+
let salt = match QuicVersion::from_u32(version) {
226+
QuicVersion::Rfc9000 => &INITIAL_SALT_RFC9000,
227+
QuicVersion::Rfc9369 => &INITIAL_SALT_RFC9369,
228+
QuicVersion::Draft29 => &INITIAL_SALT_DRAFT29,
229+
QuicVersion::Draft27 | QuicVersion::Draft28 | QuicVersion::Mvfst27 => &INITIAL_SALT_DRAFT27,
230+
_ => &INITIAL_SALT_RFC9000,
202231
};
203232

204233
let salt = hkdf::Salt::new(hkdf::HKDF_SHA256, salt);

core/src/protocols/stream/quic/frame.rs

+16-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Implemented per RFC 9000: https://datatracker.ietf.org/doc/html/rfc9000#name-frame-types-and-formats
33

44
use serde::Serialize;
5+
use std::collections::BTreeMap;
56

67
use crate::protocols::stream::quic::QuicError;
78
use crate::protocols::stream::quic::QuicPacket;
@@ -23,7 +24,6 @@ pub enum QuicFrame {
2324
},
2425
Crypto {
2526
offset: u64,
26-
data: Vec<u8>,
2727
},
2828
}
2929

@@ -46,8 +46,9 @@ pub struct EcnCounts {
4646

4747
impl QuicFrame {
4848
// parse_frames takes the plaintext QUIC packet payload and parses the frame list
49-
pub fn parse_frames(data: &[u8]) -> Result<Vec<QuicFrame>, QuicError> {
49+
pub fn parse_frames(data: &[u8]) -> Result<(Vec<QuicFrame>, Vec<u8>), QuicError> {
5050
let mut frames: Vec<QuicFrame> = Vec::new();
51+
let mut crypto_map: BTreeMap<u64, Vec<u8>> = BTreeMap::new();
5152
let mut offset = 0;
5253
// Iterate over plaintext payload bytes, this is a list of frames
5354
while offset < data.len() {
@@ -209,16 +210,26 @@ impl QuicFrame {
209210
)?)? as usize;
210211
offset += crypto_len_len;
211212
// Parse data
212-
let crypto_data = QuicPacket::access_data(data, offset, offset + crypto_len)?;
213+
let crypto_data =
214+
QuicPacket::access_data(data, offset, offset + crypto_len)?.to_vec();
215+
crypto_map.entry(crypto_offset).or_insert(crypto_data);
213216
frames.push(QuicFrame::Crypto {
214217
offset: crypto_offset,
215-
data: crypto_data.to_vec(),
216218
});
217219
offset += crypto_len;
218220
}
219221
_ => return Err(QuicError::UnknownFrameType),
220222
}
221223
}
222-
Ok(frames)
224+
let mut reassembled_crypto: Vec<u8> = Vec::new();
225+
let mut expected_offset: u64 = 0;
226+
for (crypto_offset, crypto_data) in crypto_map {
227+
if crypto_offset != expected_offset {
228+
return Err(QuicError::MissingCryptoFrames);
229+
}
230+
reassembled_crypto.extend(crypto_data);
231+
expected_offset = reassembled_crypto.len() as u64;
232+
}
233+
Ok((frames, reassembled_crypto))
223234
}
224235
}

core/src/protocols/stream/quic/mod.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub use self::header::{QuicLongHeader, QuicShortHeader};
2626
use frame::QuicFrame;
2727
use header::LongHeaderPacketType;
2828
use serde::Serialize;
29+
30+
use super::tls::ClientHello;
2931
pub(crate) mod crypto;
3032
pub(crate) mod frame;
3133
pub(crate) mod header;
@@ -44,10 +46,12 @@ pub enum QuicError {
4446
CryptoFail,
4547
FailedHeaderProtection,
4648
UnknownFrameType,
49+
TlsParseFail,
50+
MissingCryptoFrames,
4751
}
4852

4953
/// Parsed Quic Packet contents
50-
#[derive(Debug, Serialize, Clone)]
54+
#[derive(Debug, Serialize)]
5155
pub struct QuicPacket {
5256
/// Quic Short header
5357
pub short_header: Option<QuicShortHeader>,
@@ -59,6 +63,8 @@ pub struct QuicPacket {
5963
pub payload_bytes_count: Option<u64>,
6064

6165
pub frames: Option<Vec<QuicFrame>>,
66+
67+
pub tls: Option<ClientHello>,
6268
}
6369

6470
impl QuicPacket {

core/src/protocols/stream/quic/parser.rs

+37-12
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ use crate::protocols::stream::quic::header::{
88
LongHeaderPacketType, QuicLongHeader, QuicShortHeader,
99
};
1010
use crate::protocols::stream::quic::{QuicError, QuicPacket};
11+
use crate::protocols::stream::tls::{ClientHello, Tls};
1112
use crate::protocols::stream::{
1213
ConnParsable, ConnState, L4Pdu, ParseResult, ProbeResult, Session, SessionData,
1314
};
1415
use byteorder::{BigEndian, ByteOrder};
1516
use std::collections::HashMap;
17+
use tls_parser::parse_tls_message_handshake;
1618

1719
#[derive(Default, Debug)]
1820
pub struct QuicParser {
@@ -31,7 +33,7 @@ impl ConnParsable for QuicParser {
3133
}
3234

3335
if let Ok(data) = (pdu.mbuf_ref()).get_data_slice(offset, length) {
34-
self.process(data)
36+
self.process(data, pdu)
3537
} else {
3638
log::warn!("Malformed packet on parse");
3739
ParseResult::Skipped
@@ -103,19 +105,28 @@ impl ConnParsable for QuicParser {
103105

104106
/// Supported Quic Versions
105107
#[derive(Debug, PartialEq, Eq, Hash)]
106-
enum QuicVersion {
108+
#[repr(u32)]
109+
pub enum QuicVersion {
107110
ReservedNegotiation = 0x00000000,
108111
Rfc9000 = 0x00000001, // Quic V1
109112
Rfc9369 = 0x6b3343cf, // Quic V2
113+
Draft27 = 0xff00001b, // Quic draft 27
114+
Draft28 = 0xff00001c, // Quic draft 28
115+
Draft29 = 0xff00001d, // Quic draft 29
116+
Mvfst27 = 0xfaceb002, // Facebook Implementation of draft 27
110117
Unknown,
111118
}
112119

113120
impl QuicVersion {
114-
fn from_u32(version: u32) -> Self {
121+
pub fn from_u32(version: u32) -> Self {
115122
match version {
116123
0x00000000 => QuicVersion::ReservedNegotiation,
117124
0x00000001 => QuicVersion::Rfc9000,
118125
0x6b3343cf => QuicVersion::Rfc9369,
126+
0xff00001b => QuicVersion::Draft27,
127+
0xff00001c => QuicVersion::Draft28,
128+
0xff00001d => QuicVersion::Draft29,
129+
0xfaceb002 => QuicVersion::Mvfst27,
119130
_ => QuicVersion::Unknown,
120131
}
121132
}
@@ -168,7 +179,7 @@ impl QuicPacket {
168179
}
169180

170181
/// Parses Quic packet from bytes
171-
pub fn parse_from(data: &[u8], cnt: usize) -> Result<QuicPacket, QuicError> {
182+
pub fn parse_from(data: &[u8], cnt: usize, dir: bool) -> Result<QuicPacket, QuicError> {
172183
let mut offset = 0;
173184
let packet_header_byte = QuicPacket::access_data(data, offset, offset + 1)?[0];
174185
offset += 1;
@@ -239,7 +250,7 @@ impl QuicPacket {
239250
offset += packet_len_len;
240251
if cnt == 0 {
241252
// Derive initial keys
242-
let [client_opener, server_opener] = calc_init_keys(dcid_bytes, version)?;
253+
let [client_opener, _server_opener] = calc_init_keys(dcid_bytes, version)?;
243254
// Calculate HP
244255
let sample_len = client_opener.sample_len();
245256
let hp_sample =
@@ -274,7 +285,9 @@ impl QuicPacket {
274285
offset += cipher_text_len;
275286
// Parse auth tag
276287
let tag = QuicPacket::access_data(data, offset, offset + tag_len)?;
277-
offset += tag_len;
288+
// Commenting out the offset increase for tag_len to make clippy happy
289+
// Will be needed when handling multiple QUIC packets in single datagram
290+
// offset += tag_len;
278291
// Reconstruct authenticated data
279292
let mut ad = Vec::new();
280293
ad.append(&mut [unprotected_header].to_vec());
@@ -334,12 +347,22 @@ impl QuicPacket {
334347
}
335348
}
336349

337-
let frames: Option<Vec<QuicFrame>>;
350+
let mut frames: Option<Vec<QuicFrame>> = None;
351+
let mut ch: Option<ClientHello> = None;
338352
// If decrypted payload is not None, parse the frames
339353
if let Some(frame_bytes) = decrypted_payload {
340-
frames = Some(QuicFrame::parse_frames(&frame_bytes)?);
341-
} else {
342-
frames = None;
354+
let (q_frames, ch_bytes) = QuicFrame::parse_frames(&frame_bytes)?;
355+
frames = Some(q_frames);
356+
match parse_tls_message_handshake(&ch_bytes) {
357+
Ok((_, msg)) => {
358+
let mut tls = Tls::new();
359+
tls.parse_message_level(&msg, dir);
360+
if let Some(client_hello) = tls.client_hello {
361+
ch = Some(client_hello);
362+
}
363+
}
364+
Err(_) => return Err(QuicError::TlsParseFail),
365+
}
343366
}
344367

345368
Ok(QuicPacket {
@@ -358,6 +381,7 @@ impl QuicPacket {
358381
retry_tag,
359382
}),
360383
frames,
384+
tls: ch,
361385
})
362386
} else {
363387
// Short Header
@@ -378,14 +402,15 @@ impl QuicPacket {
378402
long_header: None,
379403
payload_bytes_count,
380404
frames: None,
405+
tls: None,
381406
})
382407
}
383408
}
384409
}
385410

386411
impl QuicParser {
387-
fn process(&mut self, data: &[u8]) -> ParseResult {
388-
if let Ok(quic) = QuicPacket::parse_from(data, self.cnt) {
412+
fn process(&mut self, data: &[u8], pdu: &L4Pdu) -> ParseResult {
413+
if let Ok(quic) = QuicPacket::parse_from(data, self.cnt, pdu.dir) {
389414
let session_id = self.cnt;
390415
self.sessions.insert(session_id, quic);
391416
self.cnt += 1;

core/src/subscription/quic_stream.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ impl Trackable for TrackedQuic {
121121

122122
fn on_match(&mut self, session: Session, subscription: &Subscription<Self::Subscribed>) {
123123
if let SessionData::Quic(quic) = session.data {
124-
let mut quic_clone = (*quic).clone();
124+
let mut quic_clone = *quic;
125125

126126
if let Some(long_header) = &quic_clone.long_header {
127127
if long_header.dcid_len > 0 {

traces/README.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
A collection of sample packet captures pulled from a variety of sources.
44

5-
| Trace | Source | Description |
6-
|--------------------|-------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
7-
| `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. |
8-
| `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. |
9-
| `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 |
5+
| Trace | Source | Description |
6+
|--------------------|-------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
7+
| `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. |
8+
| `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. |
9+
| `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 |
10+
| `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). |

traces/quic_xargs.pcap

4.47 KB
Binary file not shown.

0 commit comments

Comments
 (0)