Merge remote-tracking branch 'origin/topic/awelzel/3503-quic-v2'

* origin/topic/awelzel/3503-quic-v2:
  quic: tests: Require have-spicy
  quic: analyzer: Recognize and report unknown versions better
  quic: tests: Add QUIC v2 test cases
  quic: analyzer: Support QUIC v2
  quic: decrypt_crypto: Support QUIC v2
This commit is contained in:
Arne Welzel 2024-01-05 14:43:59 +01:00
commit fe0f981f87
20 changed files with 398 additions and 167 deletions

25
CHANGES
View file

@ -1,3 +1,28 @@
6.2.0-dev.320 | 2024-01-05 14:43:59 +0100
* quic: tests: Require have-spicy (Arne Welzel, Corelight)
* quic: analyzer: Recognize and report unknown versions better (Arne Welzel, Corelight)
This makes the analyzer.log entry more informative by including the
actual version and also allows to handle this scenario in script land
if needed.
* quic: tests: Add QUIC v2 test cases (Arne Welzel, Corelight)
Produced using examples from the go-quic project, patching the clients
to force QUIC v2.
* quic: analyzer: Support QUIC v2 (Arne Welzel, Corelight)
QUIC v2 changed the version *and* the packet type enumeration to prevent
protocol ossification. Use an intermediary unit to handle the difference.
* quic: decrypt_crypto: Support QUIC v2 (Arne Welzel, Corelight)
Attempt to refactor in order to re-use common code between the two
versions.
6.2.0-dev.314 | 2024-01-04 16:40:00 +0100 6.2.0-dev.314 | 2024-01-04 16:40:00 +0100
* Bump auxil/spicy to latest development snapshot (Benjamin Bannier, Corelight) * Bump auxil/spicy to latest development snapshot (Benjamin Bannier, Corelight)

3
NEWS
View file

@ -132,6 +132,9 @@ New Functionality
``signatures.log``. This log is based on the generation of ``signature_match()`` ``signatures.log``. This log is based on the generation of ``signature_match()``
events. events.
- The QUIC analyzer has been extended to support analyzing QUIC Version 2
INITIAL packets (RFC 9369).
Changed Functionality Changed Functionality
--------------------- ---------------------

View file

@ -1 +1 @@
6.2.0-dev.314 6.2.0-dev.320

View file

@ -3,5 +3,6 @@ module QUIC;
export { export {
const version_strings: table[count] of string = { const version_strings: table[count] of string = {
[0x00000001] = "1", [0x00000001] = "1",
[0x6b3343cf] = "quicv2",
} &default=function(version: count): string { return fmt("unknown-%x", version); }; } &default=function(version: count): string { return fmt("unknown-%x", version); };
} }

View file

@ -10,9 +10,6 @@ protocol analyzer QUIC over UDP:
import QUIC; import QUIC;
# Make the enum available.
export QUIC::LongPacketType;
on QUIC::InitialPacket -> event QUIC::initial_packet($conn, $is_orig, self.header.version, self.header.dest_conn_id, self.header.src_conn_id); on QUIC::InitialPacket -> event QUIC::initial_packet($conn, $is_orig, self.header.version, self.header.dest_conn_id, self.header.src_conn_id);
on QUIC::RetryPacket -> event QUIC::retry_packet($conn, $is_orig, self.header.version, self.header.dest_conn_id, self.header.src_conn_id, self.retry_token, self.integrity_tag); on QUIC::RetryPacket -> event QUIC::retry_packet($conn, $is_orig, self.header.version, self.header.dest_conn_id, self.header.src_conn_id, self.retry_token, self.integrity_tag);
@ -23,3 +20,5 @@ on QUIC::ZeroRTTPacket -> event QUIC::zero_rtt_packet($conn, $is_orig, self.head
on QUIC::ConnectionClosePayload -> event QUIC::connection_close_frame($conn, $is_orig, self.header.version, self.header.dest_conn_id, self.header.src_conn_id, on QUIC::ConnectionClosePayload -> event QUIC::connection_close_frame($conn, $is_orig, self.header.version, self.header.dest_conn_id, self.header.src_conn_id,
self.error_code.result, self.reason_phrase); self.error_code.result, self.reason_phrase);
on QUIC::UnhandledVersion -> event QUIC::unhandled_version($conn, $is_orig, self.header.version, self.header.dest_conn_id, self.header.src_conn_id);

View file

@ -8,6 +8,7 @@ import zeek;
# The interface to the C++ code that handles the decryption of the INITIAL packet payload using well-known keys # The interface to the C++ code that handles the decryption of the INITIAL packet payload using well-known keys
public function decrypt_crypto_payload( public function decrypt_crypto_payload(
version: uint32,
all_data: bytes, all_data: bytes,
connection_id: bytes, connection_id: bytes,
encrypted_offset: uint64, encrypted_offset: uint64,
@ -23,11 +24,10 @@ public function decrypt_crypto_payload(
# Can we decrypt? # Can we decrypt?
function can_decrypt(long_header: LongHeaderPacket, context: ConnectionIDInfo, is_client: bool): bool { function can_decrypt(long_header: LongHeaderPacket, context: ConnectionIDInfo, is_client: bool): bool {
if ( long_header.first_byte.packet_type != LongPacketType::INITIAL ) if ( ! long_header.is_initial )
return False; return False;
# decrypt_crypto_payload() has known secrets for version 1, nothing else. if ( long_header.version != Version1 && long_header.version != Version2 )
if ( long_header.version != 0x00000001 )
return False; return False;
if ( is_client ) if ( is_client )
@ -80,14 +80,26 @@ type ConnectionIDInfo = struct {
############## ##############
# Definitions # Definitions
############## ##############
const Version1: uint32 = 0x00000001;
const Version2: uint32 = 0x6b3343cf;
type LongPacketType = enum { type LongPacketTypeV1 = enum {
INITIAL = 0, INITIAL = 0,
ZERO_RTT = 1, ZERO_RTT = 1,
HANDSHAKE = 2, HANDSHAKE = 2,
RETRY = 3, RETRY = 3,
}; };
# V2 changed packet types to avoid ossification.
#
# https://www.rfc-editor.org/rfc/rfc9369.html#name-long-header-packet-types
type LongPacketTypeV2 = enum {
INITIAL = 1,
ZERO_RTT = 2,
HANDSHAKE = 3,
RETRY = 0,
};
type HeaderForm = enum { type HeaderForm = enum {
SHORT = 0, SHORT = 0,
LONG = 1, LONG = 1,
@ -154,17 +166,66 @@ type VariableLengthInteger = unit {
# Long packets # Long packets
# Generic units # Generic units
############## ##############
public type LongHeaderPacketV1 = unit(inout outer: LongHeaderPacket) {
switch ( LongPacketTypeV1(outer.first_byte.packet_type) ) {
LongPacketTypeV1::INITIAL -> initial_hdr : InitialPacket(outer) {
outer.is_initial = True;
outer.encrypted_offset = outer.offset() +
self.initial_hdr.length.bytes_to_parse +
self.initial_hdr.token_length.bytes_to_parse +
self.initial_hdr.token_length.result;
outer.payload_length = self.initial_hdr.length.result;
}
LongPacketTypeV1::ZERO_RTT -> zerortt_hdr : ZeroRTTPacket(outer);
LongPacketTypeV1::HANDSHAKE -> handshake_hdr : HandshakePacket(outer);
LongPacketTypeV1::RETRY -> retry_hdr : RetryPacket(outer) {
outer.is_retry = True;
}
};
};
public type LongHeaderPacketV2 = unit(inout outer: LongHeaderPacket) {
switch ( LongPacketTypeV2(outer.first_byte.packet_type) ) {
LongPacketTypeV2::INITIAL -> initial_hdr : InitialPacket(outer) {
outer.is_initial = True;
outer.encrypted_offset = outer.offset() +
self.initial_hdr.length.bytes_to_parse +
self.initial_hdr.token_length.bytes_to_parse +
self.initial_hdr.token_length.result;
outer.payload_length = self.initial_hdr.length.result;
}
LongPacketTypeV2::ZERO_RTT -> zerortt_hdr : ZeroRTTPacket(outer);
LongPacketTypeV2::HANDSHAKE -> handshake_hdr : HandshakePacket(outer);
LongPacketTypeV2::RETRY -> retry_hdr : RetryPacket(outer) {
outer.is_retry = True;
}
};
};
# Just eat the data for event raising.
public type UnhandledVersion = unit(header: LongHeaderPacket) {
var header: LongHeaderPacket = header;
@if SPICY_VERSION >= 10800
payload: skip bytes &eod;
@else
payload: bytes &eod;
@endif
};
public type LongHeaderPacket = unit { public type LongHeaderPacket = unit {
var encrypted_offset: uint64; var encrypted_offset: uint64;
var payload_length: uint64; var payload_length: uint64;
var client_conn_id_length: uint8; var client_conn_id_length: uint8;
var server_conn_id_length: uint8; var server_conn_id_length: uint8;
var is_initial: bool;
var is_retry: bool;
first_byte: bitfield(8) { first_byte: bitfield(8) {
header_form: 7 &convert=cast<HeaderForm>(cast<uint8>($$)); header_form: 7 &convert=cast<HeaderForm>(cast<uint8>($$));
fixed_bit: 6; fixed_bit: 6;
packet_type: 4..5 &convert=cast<LongPacketType>(cast<uint8>($$)); packet_type: 4..5;
type_specific_bits: 0..3 &convert=cast<uint8>($$); type_specific_bits: 0..3 &convert=cast<uint8>($$);
}; };
@ -174,18 +235,12 @@ public type LongHeaderPacket = unit {
src_conn_id_len: uint8 { self.client_conn_id_length = $$; } src_conn_id_len: uint8 { self.client_conn_id_length = $$; }
src_conn_id: bytes &size=self.client_conn_id_length; src_conn_id: bytes &size=self.client_conn_id_length;
switch ( self.first_byte.packet_type ) { switch ( self.version ) {
LongPacketType::INITIAL -> initial_hdr : InitialPacket(self) { Version1 -> v1: LongHeaderPacketV1(self);
self.encrypted_offset = self.offset() + Version2 -> v2: LongHeaderPacketV2(self);
self.initial_hdr.length.bytes_to_parse + * -> unknown: UnhandledVersion(self) {
self.initial_hdr.token_length.bytes_to_parse + throw "unhandled QUIC version 0x%x" % self.version;
self.initial_hdr.token_length.result;
self.payload_length = self.initial_hdr.length.result;
} }
LongPacketType::ZERO_RTT -> zerortt_hdr : ZeroRTTPacket(self);
LongPacketType::HANDSHAKE -> handshake_hdr : HandshakePacket(self);
LongPacketType::RETRY -> retry_hdr : RetryPacket(self);
}; };
}; };
@ -401,7 +456,7 @@ type Packet = unit(from_client: bool, context: ConnectionIDInfo&) {
# If we see a retry packet from the responder, reset the decryption # If we see a retry packet from the responder, reset the decryption
# context such that the next DCID from the client is used for decryption. # context such that the next DCID from the client is used for decryption.
if ( self.long_header.first_byte.packet_type == LongPacketType::RETRY ) { if ( self.long_header.is_retry ) {
context.client_initial_processed = False; context.client_initial_processed = False;
context.server_initial_processed = False; context.server_initial_processed = False;
context.initial_destination_conn_id = b""; context.initial_destination_conn_id = b"";
@ -430,6 +485,7 @@ type Packet = unit(from_client: bool, context: ConnectionIDInfo&) {
# This means that here, we can try to decrypt the initial packet! # This means that here, we can try to decrypt the initial packet!
# All data is accessible via the `long_header` unit # All data is accessible via the `long_header` unit
self.decrypted_data = decrypt_crypto_payload( self.decrypted_data = decrypt_crypto_payload(
self.long_header.version,
self.all_data, self.all_data,
self.long_header.dest_conn_id, self.long_header.dest_conn_id,
self.long_header.encrypted_offset, self.long_header.encrypted_offset,
@ -449,6 +505,7 @@ type Packet = unit(from_client: bool, context: ConnectionIDInfo&) {
# Assuming that the client set up the connection, this can be considered the first # Assuming that the client set up the connection, this can be considered the first
# received Initial from the client. So disable change of ConnectionID's afterwards # received Initial from the client. So disable change of ConnectionID's afterwards
self.decrypted_data = decrypt_crypto_payload( self.decrypted_data = decrypt_crypto_payload(
self.long_header.version,
self.all_data, self.all_data,
context.initial_destination_conn_id, context.initial_destination_conn_id,
self.long_header.encrypted_offset, self.long_header.encrypted_offset,
@ -467,7 +524,7 @@ type Packet = unit(from_client: bool, context: ConnectionIDInfo&) {
# are restablished and decryption is no longer possible # are restablished and decryption is no longer possible
# #
# TODO: verify if this is actually correct per RFC # TODO: verify if this is actually correct per RFC
if ( self.long_header.first_byte.packet_type != LongPacketType::RETRY && ! from_client ) { if ( ! self.long_header.is_retry && ! from_client ) {
context.server_initial_processed = True; context.server_initial_processed = True;
context.client_initial_processed = True; context.client_initial_processed = True;
} }

View file

@ -2,15 +2,15 @@
// Copyright (c) 2023 by the Zeek Project. See COPYING for details. // Copyright (c) 2023 by the Zeek Project. See COPYING for details.
/* /*
WARNING: THIS CODE IS NOT SAFE IN MULTI-THREADED WARNING: THIS CODE IS NOT SAFE IN MULTI-THREADED ENVIRONMENTS:
* Initializations of static OpenSSL contexts without locking * Initializations of static OpenSSL contexts without locking
* Use of contexts is not protected by locks. * Use of SSL contexts is not protected by locks
The involved contexts are EVP_CIPHER_CTX and EVP_PKEY_CTX and are allocated The involved contexts are EVP_CIPHER_CTX and EVP_PKEY_CTX. These are allocated
lazily just once and re-used for performance reasons. Previously, every lazily and re-used for performance reasons. Previously, every decrypt operation
decrypt operation allocated, initialized and freed each of the used context allocated, initialized and freed these individually, resulting in a significant
resulting in a significant performance hit. performance hit. Given Zeek's single threaded nature, this is fine.
*/ */
/* /*
@ -25,6 +25,7 @@ refactors as C++ development is not our main profession.
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
@ -51,31 +52,6 @@ struct DecryptionInformation {
// This should be alright: https://stackoverflow.com/a/15172304 // This should be alright: https://stackoverflow.com/a/15172304
inline const uint8_t* data_as_uint8(const hilti::rt::Bytes& b) { return reinterpret_cast<const uint8_t*>(b.data()); } inline const uint8_t* data_as_uint8(const hilti::rt::Bytes& b) { return reinterpret_cast<const uint8_t*>(b.data()); }
/*
Constants used in the HKDF functions. HKDF-Expand-Label uses labels
such as 'quic key' and 'quic hp'. These labels can obviously be
calculated dynamically, but are incluced statically for now, as the
goal of this analyser is only to analyze the INITIAL packets.
*/
std::vector<uint8_t> INITIAL_SALT_V1 = {0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17,
0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a};
std::vector<uint8_t> CLIENT_INITIAL_INFO = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x63,
0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x00};
std::vector<uint8_t> SERVER_INITIAL_INFO = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x73,
0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x69, 0x6e, 0x00};
std::vector<uint8_t> KEY_INFO = {0x00, 0x10, 0x0e, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20,
0x71, 0x75, 0x69, 0x63, 0x20, 0x6b, 0x65, 0x79, 0x00};
std::vector<uint8_t> IV_INFO = {0x00, 0x0c, 0x0d, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20,
0x71, 0x75, 0x69, 0x63, 0x20, 0x69, 0x76, 0x00};
std::vector<uint8_t> HP_INFO = {0x00, 0x10, 0x0d, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20,
0x71, 0x75, 0x69, 0x63, 0x20, 0x68, 0x70, 0x00};
/* /*
Constants used by the different functions Constants used by the different functions
*/ */
@ -107,100 +83,6 @@ EVP_CIPHER_CTX* get_aes_128_gcm() {
return ctx; return ctx;
} }
/*
HKDF-Extract as described in https://www.rfc-editor.org/rfc/rfc8446.html#section-7.1
*/
std::vector<uint8_t> hkdf_extract(const hilti::rt::Bytes& connection_id) {
std::vector<uint8_t> out_temp(INITIAL_SECRET_LEN);
size_t initial_secret_len = out_temp.size();
static EVP_PKEY_CTX* ctx = nullptr;
if ( ! ctx ) {
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
EVP_PKEY_derive_init(ctx);
EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256());
EVP_PKEY_CTX_hkdf_mode(ctx, EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY);
}
EVP_PKEY_CTX_set1_hkdf_key(ctx, data_as_uint8(connection_id), connection_id.size());
EVP_PKEY_CTX_set1_hkdf_salt(ctx, INITIAL_SALT_V1.data(), INITIAL_SALT_V1.size());
EVP_PKEY_derive(ctx, out_temp.data(), &initial_secret_len);
return out_temp;
}
/*
HKDF-Expand-Label as described in https://www.rfc-editor.org/rfc/rfc8446.html#section-7.1
*/
std::vector<uint8_t> hkdf_expand(EVP_PKEY_CTX* ctx, size_t out_len, const std::vector<uint8_t>& key) {
std::vector<uint8_t> out_temp(out_len);
EVP_PKEY_CTX_set1_hkdf_key(ctx, key.data(), key.size());
EVP_PKEY_derive(ctx, out_temp.data(), &out_len);
return out_temp;
}
std::vector<uint8_t> hkdf_expand_client_initial_info(size_t out_len, const std::vector<uint8_t>& key) {
static EVP_PKEY_CTX* ctx = nullptr;
if ( ! ctx ) {
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
EVP_PKEY_derive_init(ctx);
EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256());
EVP_PKEY_CTX_hkdf_mode(ctx, EVP_PKEY_HKDEF_MODE_EXPAND_ONLY);
EVP_PKEY_CTX_add1_hkdf_info(ctx, CLIENT_INITIAL_INFO.data(), CLIENT_INITIAL_INFO.size());
}
return hkdf_expand(ctx, out_len, key);
}
std::vector<uint8_t> hkdf_expand_server_initial_info(size_t out_len, const std::vector<uint8_t>& key) {
static EVP_PKEY_CTX* ctx = nullptr;
if ( ! ctx ) {
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
EVP_PKEY_derive_init(ctx);
EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256());
EVP_PKEY_CTX_hkdf_mode(ctx, EVP_PKEY_HKDEF_MODE_EXPAND_ONLY);
EVP_PKEY_CTX_add1_hkdf_info(ctx, SERVER_INITIAL_INFO.data(), SERVER_INITIAL_INFO.size());
}
return hkdf_expand(ctx, out_len, key);
}
std::vector<uint8_t> hkdf_expand_key_info(size_t out_len, const std::vector<uint8_t>& key) {
static EVP_PKEY_CTX* ctx = nullptr;
if ( ! ctx ) {
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
EVP_PKEY_derive_init(ctx);
EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256());
EVP_PKEY_CTX_hkdf_mode(ctx, EVP_PKEY_HKDEF_MODE_EXPAND_ONLY);
EVP_PKEY_CTX_add1_hkdf_info(ctx, KEY_INFO.data(), KEY_INFO.size());
}
return hkdf_expand(ctx, out_len, key);
}
std::vector<uint8_t> hkdf_expand_iv_info(size_t out_len, const std::vector<uint8_t>& key) {
static EVP_PKEY_CTX* ctx = nullptr;
if ( ! ctx ) {
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
EVP_PKEY_derive_init(ctx);
EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256());
EVP_PKEY_CTX_hkdf_mode(ctx, EVP_PKEY_HKDEF_MODE_EXPAND_ONLY);
EVP_PKEY_CTX_add1_hkdf_info(ctx, IV_INFO.data(), IV_INFO.size());
}
return hkdf_expand(ctx, out_len, key);
}
std::vector<uint8_t> hkdf_expand_hp_info(size_t out_len, const std::vector<uint8_t>& key) {
static EVP_PKEY_CTX* ctx = nullptr;
if ( ! ctx ) {
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
EVP_PKEY_derive_init(ctx);
EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256());
EVP_PKEY_CTX_hkdf_mode(ctx, EVP_PKEY_HKDEF_MODE_EXPAND_ONLY);
EVP_PKEY_CTX_add1_hkdf_info(ctx, HP_INFO.data(), HP_INFO.size());
}
return hkdf_expand(ctx, out_len, key);
}
/* /*
Removes the header protection from the INITIAL packet and returns a DecryptionInformation struct Removes the header protection from the INITIAL packet and returns a DecryptionInformation struct
@ -268,10 +150,8 @@ std::vector<uint8_t> calculate_nonce(std::vector<uint8_t> client_iv, uint64_t pa
} }
/* /*
Function that calls the AEAD decryption routine, and returns the Function that calls the AEAD decryption routine, and returns the decrypted data.
decrypted data
*/ */
hilti::rt::Bytes decrypt(const std::vector<uint8_t>& client_key, const hilti::rt::Bytes& all_data, hilti::rt::Bytes decrypt(const std::vector<uint8_t>& client_key, const hilti::rt::Bytes& all_data,
uint64_t payload_length, const DecryptionInformation& decryptInfo) { uint64_t payload_length, const DecryptionInformation& decryptInfo) {
int out, out2, res; int out, out2, res;
@ -324,14 +204,203 @@ hilti::rt::Bytes decrypt(const std::vector<uint8_t>& client_key, const hilti::rt
return hilti::rt::Bytes(decrypt_buffer.data(), decrypt_buffer.data() + out); return hilti::rt::Bytes(decrypt_buffer.data(), decrypt_buffer.data() + out);
} }
// Pre-initialized SSL contexts for re-use. Not thread-safe. These are only used in expand-only mode
// and have a fixed HKDF info set.
struct HkdfCtx {
bool initialized = false;
EVP_PKEY_CTX* client_in_ctx = nullptr;
EVP_PKEY_CTX* server_in_ctx = nullptr;
EVP_PKEY_CTX* key_info_ctx = nullptr;
EVP_PKEY_CTX* iv_info_ctx = nullptr;
EVP_PKEY_CTX* hp_info_ctx = nullptr;
};
struct HkdfCtxParam {
EVP_PKEY_CTX** ctx;
std::vector<uint8_t> info;
};
/*
HKDF-Extract as described in https://www.rfc-editor.org/rfc/rfc8446.html#section-7.1
*/
std::vector<uint8_t> hkdf_extract(const std::vector<uint8_t>& salt, const hilti::rt::Bytes& connection_id) {
std::vector<uint8_t> out_temp(INITIAL_SECRET_LEN);
size_t initial_secret_len = out_temp.size();
static EVP_PKEY_CTX* ctx = nullptr;
if ( ! ctx ) {
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
EVP_PKEY_derive_init(ctx);
EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256());
EVP_PKEY_CTX_hkdf_mode(ctx, EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY);
}
EVP_PKEY_CTX_set1_hkdf_key(ctx, data_as_uint8(connection_id), connection_id.size());
EVP_PKEY_CTX_set1_hkdf_salt(ctx, salt.data(), salt.size());
EVP_PKEY_derive(ctx, out_temp.data(), &initial_secret_len);
return out_temp;
}
std::vector<uint8_t> hkdf_expand(EVP_PKEY_CTX* ctx, size_t out_len, const std::vector<uint8_t>& key) {
std::vector<uint8_t> out_temp(out_len);
EVP_PKEY_CTX_set1_hkdf_key(ctx, key.data(), key.size());
EVP_PKEY_derive(ctx, out_temp.data(), &out_len);
return out_temp;
}
class QuicPacketProtection {
public:
std::vector<uint8_t> GetSecret(bool is_orig, const hilti::rt::Bytes& connection_id) {
const auto& ctxs = GetHkdfCtxs();
const auto initial_secret = hkdf_extract(GetInitialSalt(), connection_id);
EVP_PKEY_CTX* ctx = is_orig ? ctxs.client_in_ctx : ctxs.server_in_ctx;
return hkdf_expand(ctx, INITIAL_SECRET_LEN, initial_secret);
}
std::vector<uint8_t> GetKey(const std::vector<uint8_t>& secret) {
const auto& ctxs = GetHkdfCtxs();
return hkdf_expand(ctxs.key_info_ctx, AEAD_KEY_LEN, secret);
}
std::vector<uint8_t> GetIv(const std::vector<uint8_t>& secret) {
const auto& ctxs = GetHkdfCtxs();
return hkdf_expand(ctxs.iv_info_ctx, AEAD_IV_LEN, secret);
}
std::vector<uint8_t> GetHp(const std::vector<uint8_t>& secret) {
const auto& ctxs = GetHkdfCtxs();
return hkdf_expand(ctxs.hp_info_ctx, AEAD_HP_LEN, secret);
}
virtual const std::vector<uint8_t>& GetInitialSalt() = 0;
virtual HkdfCtx& GetHkdfCtxs() = 0;
virtual ~QuicPacketProtection() = default;
// Helper to initialize HKDF expand only contexts.
static void Initialize(std::vector<HkdfCtxParam>& params) {
for ( const auto& p : params ) {
*p.ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
EVP_PKEY_derive_init(*p.ctx);
EVP_PKEY_CTX_set_hkdf_md(*p.ctx, EVP_sha256());
EVP_PKEY_CTX_hkdf_mode(*p.ctx, EVP_PKEY_HKDEF_MODE_EXPAND_ONLY);
EVP_PKEY_CTX_add1_hkdf_info(*p.ctx, p.info.data(), p.info.size());
}
}
};
// QUIC v1
//
// https://datatracker.ietf.org/doc/html/rfc9001
std::vector<uint8_t> INITIAL_SALT_V1 = {0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17,
0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a};
std::vector<uint8_t> CLIENT_INITIAL_INFO = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x63,
0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x00};
std::vector<uint8_t> SERVER_INITIAL_INFO = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x73,
0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x69, 0x6e, 0x00};
std::vector<uint8_t> KEY_INFO = {0x00, 0x10, 0x0e, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20,
0x71, 0x75, 0x69, 0x63, 0x20, 0x6b, 0x65, 0x79, 0x00};
std::vector<uint8_t> IV_INFO = {0x00, 0x0c, 0x0d, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20,
0x71, 0x75, 0x69, 0x63, 0x20, 0x69, 0x76, 0x00};
std::vector<uint8_t> HP_INFO = {0x00, 0x10, 0x0d, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20,
0x71, 0x75, 0x69, 0x63, 0x20, 0x68, 0x70, 0x00};
class QuicPacketProtectionV1 : public QuicPacketProtection {
public:
virtual std::vector<uint8_t>& GetInitialSalt() override { return INITIAL_SALT_V1; }
virtual HkdfCtx& GetHkdfCtxs() override { return hkdf_ctxs; }
// Pre-initialize SSL context for reuse with HKDF info set to version specific values.
static void Initialize() {
if ( hkdf_ctxs.initialized )
return;
std::vector<HkdfCtxParam> hkdf_ctx_params = {
{&hkdf_ctxs.client_in_ctx, CLIENT_INITIAL_INFO},
{&hkdf_ctxs.server_in_ctx, SERVER_INITIAL_INFO},
{&hkdf_ctxs.key_info_ctx, KEY_INFO},
{&hkdf_ctxs.iv_info_ctx, IV_INFO},
{&hkdf_ctxs.hp_info_ctx, HP_INFO},
};
QuicPacketProtection::Initialize(hkdf_ctx_params);
instance = std::make_unique<QuicPacketProtectionV1>();
hkdf_ctxs.initialized = true;
}
static HkdfCtx hkdf_ctxs;
static std::unique_ptr<QuicPacketProtectionV1> instance;
};
HkdfCtx QuicPacketProtectionV1::hkdf_ctxs = {0};
std::unique_ptr<QuicPacketProtectionV1> QuicPacketProtectionV1::instance = nullptr;
// QUIC v2
//
// https://datatracker.ietf.org/doc/rfc9369/
std::vector<uint8_t> INITIAL_SALT_V2 = {0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93,
0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9};
std::vector<uint8_t> CLIENT_INITIAL_INFO_V2 = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x63,
0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x00};
std::vector<uint8_t> SERVER_INITIAL_INFO_V2 = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x73,
0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x69, 0x6e, 0x00};
std::vector<uint8_t> KEY_INFO_V2 = {0x00, 0x10, 0x10, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x71,
0x75, 0x69, 0x63, 0x76, 0x32, 0x20, 0x6b, 0x65, 0x79, 0x00};
std::vector<uint8_t> IV_INFO_V2 = {0x00, 0x0c, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x71,
0x75, 0x69, 0x63, 0x76, 0x32, 0x20, 0x69, 0x76, 0x00};
std::vector<uint8_t> HP_INFO_V2 = {0x00, 0x10, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x71,
0x75, 0x69, 0x63, 0x76, 0x32, 0x20, 0x68, 0x70, 0x00};
class QuicPacketProtectionV2 : public QuicPacketProtection {
public:
virtual std::vector<uint8_t>& GetInitialSalt() override { return INITIAL_SALT_V2; }
virtual HkdfCtx& GetHkdfCtxs() override { return hkdf_ctxs; }
static void Initialize() {
if ( hkdf_ctxs.initialized )
return;
std::vector<HkdfCtxParam> hkdf_ctx_params = {
{&hkdf_ctxs.client_in_ctx, CLIENT_INITIAL_INFO_V2},
{&hkdf_ctxs.server_in_ctx, SERVER_INITIAL_INFO_V2},
{&hkdf_ctxs.key_info_ctx, KEY_INFO_V2},
{&hkdf_ctxs.iv_info_ctx, IV_INFO_V2},
{&hkdf_ctxs.hp_info_ctx, HP_INFO_V2},
};
QuicPacketProtection::Initialize(hkdf_ctx_params);
instance = std::make_unique<QuicPacketProtectionV2>();
hkdf_ctxs.initialized = true;
}
static HkdfCtx hkdf_ctxs;
static std::unique_ptr<QuicPacketProtectionV2> instance;
};
HkdfCtx QuicPacketProtectionV2::hkdf_ctxs = {0};
std::unique_ptr<QuicPacketProtectionV2> QuicPacketProtectionV2::instance = nullptr;
} // namespace } // namespace
/* /*
Function that is called from Spicy. It's a wrapper around `process_data`; Function that is called from Spicy, decrypting an INITIAL packet and returning
it stores all the passed data in a global struct and then calls `process_data`, the decrypted payload back to the analyzer.
which will eventually return the decrypted data and pass it back to Spicy.
*/ */
hilti::rt::Bytes QUIC_decrypt_crypto_payload(const hilti::rt::Bytes& all_data, const hilti::rt::Bytes& connection_id, hilti::rt::Bytes QUIC_decrypt_crypto_payload(const hilti::rt::integer::safe<uint32_t>& version,
const hilti::rt::Bytes& all_data, const hilti::rt::Bytes& connection_id,
const hilti::rt::integer::safe<uint64_t>& encrypted_offset, const hilti::rt::integer::safe<uint64_t>& encrypted_offset,
const hilti::rt::integer::safe<uint64_t>& payload_length, const hilti::rt::integer::safe<uint64_t>& payload_length,
const hilti::rt::Bool& from_client) { const hilti::rt::Bool& from_client) {
@ -342,19 +411,24 @@ hilti::rt::Bytes QUIC_decrypt_crypto_payload(const hilti::rt::Bytes& all_data, c
throw hilti::rt::RuntimeError( throw hilti::rt::RuntimeError(
hilti::rt::fmt("packet too small %ld %ld", all_data.size(), encrypted_offset + payload_length)); hilti::rt::fmt("packet too small %ld %ld", all_data.size(), encrypted_offset + payload_length));
std::vector<uint8_t> initial_secret = hkdf_extract(connection_id); QuicPacketProtection* qpp = nullptr;
std::vector<uint8_t> server_client_secret; if ( version == 0x00000001 ) { // quicv1
if ( from_client ) { QuicPacketProtectionV1::Initialize();
server_client_secret = hkdf_expand_client_initial_info(INITIAL_SECRET_LEN, initial_secret); qpp = QuicPacketProtectionV1::instance.get();
}
else if ( version == 0x6b3343cf ) { // quicv2
QuicPacketProtectionV2::Initialize();
qpp = QuicPacketProtectionV2::instance.get();
} }
else { else {
server_client_secret = hkdf_expand_server_initial_info(INITIAL_SECRET_LEN, initial_secret); throw hilti::rt::RuntimeError(hilti::rt::fmt("unable to handle version %lx", version));
} }
std::vector<uint8_t> key = hkdf_expand_key_info(AEAD_KEY_LEN, server_client_secret); const auto& secret = qpp->GetSecret(from_client, connection_id);
std::vector<uint8_t> iv = hkdf_expand_iv_info(AEAD_IV_LEN, server_client_secret); std::vector<uint8_t> key = qpp->GetKey(secret);
std::vector<uint8_t> hp = hkdf_expand_hp_info(AEAD_HP_LEN, server_client_secret); std::vector<uint8_t> iv = qpp->GetIv(secret);
std::vector<uint8_t> hp = qpp->GetHp(secret);
DecryptionInformation decryptInfo = remove_header_protection(hp, encrypted_offset, all_data); DecryptionInformation decryptInfo = remove_header_protection(hp, encrypted_offset, all_data);

View file

@ -0,0 +1,3 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
ts uid history service
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 Dd quic,ssl

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path quic
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p version client_initial_dcid server_scid server_name client_protocol history
#types time string addr port addr port string string string string string string
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 49320 127.0.0.1 443 quicv2 fa603212c8688817af3d3238735bc7 b168b5cc localhost quic-echo-example ISIIisIH
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ssl
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p version cipher curve server_name resumed last_alert next_protocol established ssl_history cert_chain_fps client_cert_chain_fps sni_matches_cert
#types time string addr port addr port string string string string bool string string bool string vector[string] vector[string] bool
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 49320 127.0.0.1 443 TLSv13 TLS_AES_128_GCM_SHA256 x25519 localhost F - - F Cs - - -
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,3 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
ts uid history service
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 Dd quic,ssl

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path quic
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p version client_initial_dcid server_scid server_name client_protocol history
#types time string addr port addr port string string string string string string
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 50841 127.0.0.1 443 quicv2 bdf0c5b27927cc667e58d95b cdc8b6e6 - h3 ISishIHH
#close XXXX-XX-XX-XX-XX-XX

View file

@ -0,0 +1,11 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path ssl
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p version cipher curve server_name resumed last_alert next_protocol established ssl_history cert_chain_fps client_cert_chain_fps sni_matches_cert
#types time string addr port addr port string string string string bool string string bool string vector[string] vector[string] bool
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 50841 127.0.0.1 443 TLSv13 TLS_AES_128_GCM_SHA256 x25519 - F - - F Cs - - -
#close XXXX-XX-XX-XX-XX-XX

View file

@ -1,3 +1,3 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
ts uid cause analyzer_kind analyzer_name failure_reason ts uid cause analyzer_kind analyzer_name failure_reason
1693925959.000001 CHhAvVGS1DHFjwGM9 violation protocol QUIC &requires failed: self.length.result >= 20 (<...>/QUIC.spicy:<line>:<column>) 1693925959.000001 CHhAvVGS1DHFjwGM9 violation protocol QUIC unhandled QUIC version 0x10010000 (<...>/QUIC.spicy:<line>:<column>)

View file

@ -0,0 +1,2 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
QUIC::unhandled_version, CHhAvVGS1DHFjwGM9, T, 268500992, \x00,

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,8 @@
# @TEST-DOC: Pcap with quicv2 echo traffic produced with https://raw.githubusercontent.com/quic-go/quic-go/master/example/echo/echo.go
#
# @TEST-REQUIRES: ${SCRIPTS}/have-spicy
# @TEST-EXEC: zeek -Cr $TRACES/quic/quicv2-echo-443.pcap base/protocols/quic
# @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut
# @TEST-EXEC: btest-diff conn.log.cut
# @TEST-EXEC: btest-diff ssl.log
# @TEST-EXEC: btest-diff quic.log

View file

@ -0,0 +1,8 @@
# @TEST-DOC: Pcap with quicv2 http3 traffic produced with https://raw.githubusercontent.com/quic-go/quic-go/master/example/main.go
#
# @TEST-REQUIRES: ${SCRIPTS}/have-spicy
# @TEST-EXEC: zeek -Cr $TRACES/quic/quicv2-http3-443.pcap base/protocols/quic
# @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut
# @TEST-EXEC: btest-diff conn.log.cut
# @TEST-EXEC: btest-diff ssl.log
# @TEST-EXEC: btest-diff quic.log

View file

@ -1,11 +1,15 @@
# @TEST-DOC: Test that runs the pcap # @TEST-DOC: Test that runs the pcap
# @TEST-REQUIRES: ${SCRIPTS}/have-spicy # @TEST-REQUIRES: ${SCRIPTS}/have-spicy
# @TEST-EXEC: zeek -Cr $TRACES/quic/vector-max-size-crash.pcap base/protocols/quic # @TEST-EXEC: zeek -Cr $TRACES/quic/vector-max-size-crash.pcap base/protocols/quic %INPUT > out
# @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut # @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut
# @TEST-EXEC: zeek-cut -m ts uid cause analyzer_kind analyzer_name failure_reason < analyzer.log > analyzer.log.cut # @TEST-EXEC: zeek-cut -m ts uid cause analyzer_kind analyzer_name failure_reason < analyzer.log > analyzer.log.cut
# @TEST-EXEC: btest-diff conn.log.cut # @TEST-EXEC: btest-diff conn.log.cut
# @TEST-EXEC: btest-diff out
# Only run btest-ddiff on analyzer.log with 6.1-dev or later. The violation # @TEST-EXEC: TEST_DIFF_CANONIFIER='sed -r "s/\((.+)\.spicy:[0-9]+:[0-9]+\)/(\1.spicy:<line>:<column>)/g" | $SCRIPTS/diff-remove-abspath' btest-diff analyzer.log.cut
# reporting has more detail in later versions.
# @TEST-EXEC: zeek -b -e 'exit(Version::info$version_number < 60100 ? 0 : 1)' || TEST_DIFF_CANONIFIER='sed -r "s/\((.+)\.spicy:[0-9]+:[0-9]+\)/(\1.spicy:<line>:<column>)/g" | $SCRIPTS/diff-remove-abspath' btest-diff analyzer.log.cut event QUIC::unhandled_version(c: connection, is_orig: bool, version: count, dcid: string, scid: string)
{
print "QUIC::unhandled_version", c$uid, is_orig, version, dcid, scid;
}