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
* 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()``
events.
- The QUIC analyzer has been extended to support analyzing QUIC Version 2
INITIAL packets (RFC 9369).
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 {
const version_strings: table[count] of string = {
[0x00000001] = "1",
[0x6b3343cf] = "quicv2",
} &default=function(version: count): string { return fmt("unknown-%x", version); };
}

View file

@ -10,9 +10,6 @@ protocol analyzer QUIC over UDP:
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::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,
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
public function decrypt_crypto_payload(
version: uint32,
all_data: bytes,
connection_id: bytes,
encrypted_offset: uint64,
@ -23,11 +24,10 @@ public function decrypt_crypto_payload(
# Can we decrypt?
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;
# decrypt_crypto_payload() has known secrets for version 1, nothing else.
if ( long_header.version != 0x00000001 )
if ( long_header.version != Version1 && long_header.version != Version2 )
return False;
if ( is_client )
@ -80,14 +80,26 @@ type ConnectionIDInfo = struct {
##############
# Definitions
##############
const Version1: uint32 = 0x00000001;
const Version2: uint32 = 0x6b3343cf;
type LongPacketType = enum {
type LongPacketTypeV1 = enum {
INITIAL = 0,
ZERO_RTT = 1,
HANDSHAKE = 2,
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 {
SHORT = 0,
LONG = 1,
@ -154,17 +166,66 @@ type VariableLengthInteger = unit {
# Long packets
# 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 {
var encrypted_offset: uint64;
var payload_length: uint64;
var client_conn_id_length: uint8;
var server_conn_id_length: uint8;
var is_initial: bool;
var is_retry: bool;
first_byte: bitfield(8) {
header_form: 7 &convert=cast<HeaderForm>(cast<uint8>($$));
fixed_bit: 6;
packet_type: 4..5 &convert=cast<LongPacketType>(cast<uint8>($$));
packet_type: 4..5;
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: bytes &size=self.client_conn_id_length;
switch ( self.first_byte.packet_type ) {
LongPacketType::INITIAL -> initial_hdr : InitialPacket(self) {
self.encrypted_offset = self.offset() +
self.initial_hdr.length.bytes_to_parse +
self.initial_hdr.token_length.bytes_to_parse +
self.initial_hdr.token_length.result;
self.payload_length = self.initial_hdr.length.result;
switch ( self.version ) {
Version1 -> v1: LongHeaderPacketV1(self);
Version2 -> v2: LongHeaderPacketV2(self);
* -> unknown: UnhandledVersion(self) {
throw "unhandled QUIC version 0x%x" % self.version;
}
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
# 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.server_initial_processed = False;
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!
# All data is accessible via the `long_header` unit
self.decrypted_data = decrypt_crypto_payload(
self.long_header.version,
self.all_data,
self.long_header.dest_conn_id,
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
# received Initial from the client. So disable change of ConnectionID's afterwards
self.decrypted_data = decrypt_crypto_payload(
self.long_header.version,
self.all_data,
context.initial_destination_conn_id,
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
#
# 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.client_initial_processed = True;
}

View file

@ -2,15 +2,15 @@
// 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
* 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
lazily just once and re-used for performance reasons. Previously, every
decrypt operation allocated, initialized and freed each of the used context
resulting in a significant performance hit.
The involved contexts are EVP_CIPHER_CTX and EVP_PKEY_CTX. These are allocated
lazily and re-used for performance reasons. Previously, every decrypt operation
allocated, initialized and freed these individually, resulting in a significant
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 <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
@ -51,31 +52,6 @@ struct DecryptionInformation {
// 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()); }
/*
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
*/
@ -107,100 +83,6 @@ EVP_CIPHER_CTX* get_aes_128_gcm() {
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
@ -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
decrypted data
Function that calls the AEAD decryption routine, and returns the decrypted 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) {
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);
}
// 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
/*
Function that is called from Spicy. It's a wrapper around `process_data`;
it stores all the passed data in a global struct and then calls `process_data`,
which will eventually return the decrypted data and pass it back to Spicy.
Function that is called from Spicy, decrypting an INITIAL packet and returning
the decrypted payload back to the analyzer.
*/
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>& payload_length,
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(
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 ( from_client ) {
server_client_secret = hkdf_expand_client_initial_info(INITIAL_SECRET_LEN, initial_secret);
if ( version == 0x00000001 ) { // quicv1
QuicPacketProtectionV1::Initialize();
qpp = QuicPacketProtectionV1::instance.get();
}
else if ( version == 0x6b3343cf ) { // quicv2
QuicPacketProtectionV2::Initialize();
qpp = QuicPacketProtectionV2::instance.get();
}
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);
std::vector<uint8_t> iv = hkdf_expand_iv_info(AEAD_IV_LEN, server_client_secret);
std::vector<uint8_t> hp = hkdf_expand_hp_info(AEAD_HP_LEN, server_client_secret);
const auto& secret = qpp->GetSecret(from_client, connection_id);
std::vector<uint8_t> key = qpp->GetKey(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);

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.
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-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 cause analyzer_kind analyzer_name failure_reason < analyzer.log > analyzer.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
# 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
# @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
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;
}