diff --git a/.clang-format b/.clang-format index 4c628b3..b652ea6 100644 --- a/.clang-format +++ b/.clang-format @@ -1,74 +1,66 @@ -# Clang-format configuration for Zeek. This configuration requires -# at least clang-format 12.0.1 to format correctly. - -Language: Cpp -Standard: c++17 - -BreakBeforeBraces: Whitesmiths - -# BraceWrapping: -# AfterCaseLabel: true -# AfterClass: false -# AfterControlStatement: Always -# AfterEnum: false -# AfterFunction: true -# AfterNamespace: false -# AfterStruct: false -# AfterUnion: false -# AfterExternBlock: false -# BeforeCatch: true -# BeforeElse: true -# BeforeWhile: false -# IndentBraces: true -# SplitEmptyFunction: false -# SplitEmptyRecord: false -# SplitEmptyNamespace: false +# Copyright (c) 2020-2023 by the Zeek Project. See LICENSE for details. +--- +Language: Cpp AccessModifierOffset: -4 AlignAfterOpenBracket: Align -AlignTrailingComments: false -AllowShortBlocksOnASingleLine: Empty -AllowShortEnumsOnASingleLine: true -AllowShortFunctionsOnASingleLine: Inline +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: true AllowShortIfStatementsOnASingleLine: false -AllowShortLambdasOnASingleLine: Empty AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes BinPackArguments: true BinPackParameters: true -BreakConstructorInitializers: BeforeColon +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false BreakInheritanceList: BeforeColon -ColumnLimit: 100 -ConstructorInitializerAllOnOneLineOrOnePerLine: false -FixNamespaceComments: false -IndentCaseLabels: true -IndentCaseBlocks: false -IndentExternBlock: NoIndent -IndentPPDirectives: None -IndentWidth: 4 -NamespaceIndentation: None -PointerAlignment: Left -SpaceAfterCStyleCast: false -SpaceAfterLogicalNot: true -SpaceBeforeAssignmentOperators: true -SpaceBeforeCpp11BracedList: false -SpaceBeforeCtorInitializerColon: true -SpaceBeforeInheritanceColon: true -SpaceBeforeParens: ControlStatements -SpaceBeforeRangeBasedForLoopColon: true -SpaceInEmptyBlock: true -SpaceInEmptyParentheses: false -SpacesInAngles: false -SpacesInConditionalStatement: true -SpacesInContainerLiterals: false -SpacesInParentheses: false -TabWidth: 4 -UseTab: AlignWithSpaces - -# Setting this to a high number causes clang-format to prefer breaking somewhere else -# over breaking after the assignment operator in a line that's over the column limit -PenaltyBreakAssignment: 100 - +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: 'NOLINT' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH IncludeBlocks: Regroup # Include categories go like this: @@ -98,3 +90,57 @@ IncludeCategories: Priority: 4 - Regex: '.*' Priority: 5 + +IncludeIsMainRegex: '$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '^BEGIN_' +MacroBlockEnd: '^END_' +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 500 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 1000 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: false +SpaceAfterLogicalNot: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpacesInConditionalStatement: true +Standard: Cpp11 +StatementMacros: + - STANDARD_OPERATOR_1 +TabWidth: 4 +UseTab: Never +--- +Language: Json +... diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ba2206..f2b909a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: exclude: ^testing/Baseline/ repos: - repo: https://github.com/pre-commit/mirrors-clang-format - rev: 'v13.0.0' + rev: 'v17.0.3' hooks: - id: clang-format - repo: https://github.com/crate-ci/typos diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..d15e801 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,3 @@ +[default.extend-words] +# `inout` is used as a keyword in Spicy, but looks like a typo of `input`. +inout = "inout" diff --git a/analyzer/QUIC.evt b/analyzer/QUIC.evt index 6a35ce2..3bef66a 100644 --- a/analyzer/QUIC.evt +++ b/analyzer/QUIC.evt @@ -12,9 +12,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); @@ -25,3 +22,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); diff --git a/analyzer/QUIC.spicy b/analyzer/QUIC.spicy index 56dc0c9..df5e89d 100644 --- a/analyzer/QUIC.spicy +++ b/analyzer/QUIC.spicy @@ -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(cast($$)); fixed_bit: 6; - packet_type: 4..5 &convert=cast(cast($$)); + packet_type: 4..5; type_specific_bits: 0..3 &convert=cast($$); }; @@ -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; } diff --git a/analyzer/decrypt_crypto.cc b/analyzer/decrypt_crypto.cc index bcbb20e..4f2fc38 100644 --- a/analyzer/decrypt_crypto.cc +++ b/analyzer/decrypt_crypto.cc @@ -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 #include #include +#include #include #include @@ -36,52 +37,20 @@ refactors as C++ development is not our main profession. // Import HILTI #include -namespace - { +namespace { // Struct to store decryption info for this specific connection -struct DecryptionInformation - { - std::vector unprotected_header; - uint64_t packet_number; - std::vector nonce; - uint8_t packet_number_length; - }; +struct DecryptionInformation { + std::vector unprotected_header; + uint64_t packet_number; + std::vector nonce; + uint8_t packet_number_length; +}; // Return rt::hilti::Bytes::data() value as const uint8_t* // // This should be alright: https://stackoverflow.com/a/15172304 -inline const uint8_t* data_as_uint8(const hilti::rt::Bytes& b) - { - return reinterpret_cast(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 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 CLIENT_INITIAL_INFO = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, - 0x33, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x20, 0x69, 0x6e, 0x00}; - -std::vector SERVER_INITIAL_INFO = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, - 0x33, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x20, 0x69, 0x6e, 0x00}; - -std::vector KEY_INFO = {0x00, 0x10, 0x0e, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, - 0x71, 0x75, 0x69, 0x63, 0x20, 0x6b, 0x65, 0x79, 0x00}; - -std::vector IV_INFO = {0x00, 0x0c, 0x0d, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, - 0x71, 0x75, 0x69, 0x63, 0x20, 0x69, 0x76, 0x00}; - -std::vector HP_INFO = {0x00, 0x10, 0x0d, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, - 0x71, 0x75, 0x69, 0x63, 0x20, 0x68, 0x70, 0x00}; +inline const uint8_t* data_as_uint8(const hilti::rt::Bytes& b) { return reinterpret_cast(b.data()); } /* Constants used by the different functions @@ -95,323 +64,376 @@ const size_t AEAD_TAG_LENGTH = 16; const size_t MAXIMUM_PACKET_LENGTH = 1500; const size_t MAXIMUM_PACKET_NUMBER_LENGTH = 4; -EVP_CIPHER_CTX* get_aes_128_ecb() - { - static EVP_CIPHER_CTX* ctx = nullptr; - if ( ! ctx ) - { - ctx = EVP_CIPHER_CTX_new(); - EVP_CipherInit_ex(ctx, EVP_aes_128_ecb(), NULL, NULL, NULL, 1); - } - - return ctx; - } - -EVP_CIPHER_CTX* get_aes_128_gcm() - { - static EVP_CIPHER_CTX* ctx = nullptr; - if ( ! ctx ) - { - ctx = EVP_CIPHER_CTX_new(); - EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, NULL, NULL, 1); - } - - return ctx; - } -/* -HKDF-Extract as described in https://www.rfc-editor.org/rfc/rfc8446.html#section-7.1 -*/ -std::vector hkdf_extract(const hilti::rt::Bytes& connection_id) - { - std::vector 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; - } +EVP_CIPHER_CTX* get_aes_128_ecb() { + static EVP_CIPHER_CTX* ctx = nullptr; + if ( ! ctx ) { + ctx = EVP_CIPHER_CTX_new(); + EVP_CipherInit_ex(ctx, EVP_aes_128_ecb(), NULL, NULL, NULL, 1); + } -/* -HKDF-Expand-Label as described in https://www.rfc-editor.org/rfc/rfc8446.html#section-7.1 -*/ -std::vector hkdf_expand(EVP_PKEY_CTX* ctx, size_t out_len, const std::vector& key) - { - std::vector 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 hkdf_expand_client_initial_info(size_t out_len, - const std::vector& 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 hkdf_expand_server_initial_info(size_t out_len, - const std::vector& 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 hkdf_expand_key_info(size_t out_len, const std::vector& 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 hkdf_expand_iv_info(size_t out_len, const std::vector& 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 hkdf_expand_hp_info(size_t out_len, const std::vector& 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); - } + return ctx; +} + +EVP_CIPHER_CTX* get_aes_128_gcm() { + static EVP_CIPHER_CTX* ctx = nullptr; + if ( ! ctx ) { + ctx = EVP_CIPHER_CTX_new(); + EVP_CipherInit_ex(ctx, EVP_aes_128_gcm(), NULL, NULL, NULL, 1); + } + + return ctx; +} /* Removes the header protection from the INITIAL packet and returns a DecryptionInformation struct that is partially filled */ -DecryptionInformation remove_header_protection(const std::vector& client_hp, - uint64_t encrypted_offset, - const hilti::rt::Bytes& all_data) - { - DecryptionInformation decryptInfo; - int outlen; - auto* ctx = get_aes_128_ecb(); - EVP_CIPHER_CTX_set_key_length(ctx, client_hp.size()); - // Passing an 1 means ENCRYPT - EVP_CipherInit_ex(ctx, NULL, NULL, client_hp.data(), NULL, 1); - - static_assert(AEAD_SAMPLE_LENGTH > 0); - assert(all_data.size() >= encrypted_offset + MAXIMUM_PACKET_NUMBER_LENGTH + AEAD_SAMPLE_LENGTH); - - const uint8_t* sample = data_as_uint8(all_data) + encrypted_offset + - MAXIMUM_PACKET_NUMBER_LENGTH; - - std::array mask; - EVP_CipherUpdate(ctx, mask.data(), &outlen, sample, AEAD_SAMPLE_LENGTH); - - // To determine the actual packet number length, - // we have to remove the mask from the first byte - uint8_t first_byte = data_as_uint8(all_data)[0]; - - if ( first_byte & 0x80 ) - { - first_byte ^= mask[0] & 0x0F; - } - else - { - first_byte ^= first_byte & 0x1F; - } - - // And now we can fully recover the correct packet number length... - int recovered_packet_number_length = (first_byte & 0x03) + 1; - - // .. and use this to reconstruct the (partially) unprotected header - std::vector unprotected_header(data_as_uint8(all_data), - data_as_uint8(all_data) + encrypted_offset + - recovered_packet_number_length); - - uint32_t decoded_packet_number = 0; - - unprotected_header[0] = first_byte; - for ( int i = 0; i < recovered_packet_number_length; ++i ) - { - unprotected_header[encrypted_offset + i] ^= mask[1 + i]; - decoded_packet_number = unprotected_header[encrypted_offset + i] | - (decoded_packet_number << 8); - } - - // Store the information back in the struct - decryptInfo.packet_number = decoded_packet_number; - decryptInfo.packet_number_length = recovered_packet_number_length; - decryptInfo.unprotected_header = std::move(unprotected_header); - return decryptInfo; - } +DecryptionInformation remove_header_protection(const std::vector& client_hp, uint64_t encrypted_offset, + const hilti::rt::Bytes& all_data) { + DecryptionInformation decryptInfo; + int outlen; + auto* ctx = get_aes_128_ecb(); + EVP_CIPHER_CTX_set_key_length(ctx, client_hp.size()); + // Passing an 1 means ENCRYPT + EVP_CipherInit_ex(ctx, NULL, NULL, client_hp.data(), NULL, 1); + + static_assert(AEAD_SAMPLE_LENGTH > 0); + assert(all_data.size() >= encrypted_offset + MAXIMUM_PACKET_NUMBER_LENGTH + AEAD_SAMPLE_LENGTH); + + const uint8_t* sample = data_as_uint8(all_data) + encrypted_offset + MAXIMUM_PACKET_NUMBER_LENGTH; + + std::array mask; + EVP_CipherUpdate(ctx, mask.data(), &outlen, sample, AEAD_SAMPLE_LENGTH); + + // To determine the actual packet number length, + // we have to remove the mask from the first byte + uint8_t first_byte = data_as_uint8(all_data)[0]; + + if ( first_byte & 0x80 ) { + first_byte ^= mask[0] & 0x0F; + } + else { + first_byte ^= first_byte & 0x1F; + } + + // And now we can fully recover the correct packet number length... + int recovered_packet_number_length = (first_byte & 0x03) + 1; + + // .. and use this to reconstruct the (partially) unprotected header + std::vector unprotected_header(data_as_uint8(all_data), data_as_uint8(all_data) + encrypted_offset + + recovered_packet_number_length); + + uint32_t decoded_packet_number = 0; + + unprotected_header[0] = first_byte; + for ( int i = 0; i < recovered_packet_number_length; ++i ) { + unprotected_header[encrypted_offset + i] ^= mask[1 + i]; + decoded_packet_number = unprotected_header[encrypted_offset + i] | (decoded_packet_number << 8); + } + + // Store the information back in the struct + decryptInfo.packet_number = decoded_packet_number; + decryptInfo.packet_number_length = recovered_packet_number_length; + decryptInfo.unprotected_header = std::move(unprotected_header); + return decryptInfo; +} /* Calculate the nonce for the AEAD by XOR'ing the CLIENT_IV and the decoded packet number, and returns the nonce */ -std::vector calculate_nonce(std::vector client_iv, uint64_t packet_number) - { - for ( int i = 0; i < 8; ++i ) - client_iv[AEAD_IV_LEN - 1 - i] ^= (uint8_t)(packet_number >> 8 * i); +std::vector calculate_nonce(std::vector client_iv, uint64_t packet_number) { + for ( int i = 0; i < 8; ++i ) + client_iv[AEAD_IV_LEN - 1 - i] ^= (uint8_t)(packet_number >> 8 * i); - return client_iv; - } + return client_iv; +} /* -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& client_key, const hilti::rt::Bytes& all_data, - uint64_t payload_length, const DecryptionInformation& decryptInfo) - { - int out, out2, res; + uint64_t payload_length, const DecryptionInformation& decryptInfo) { + int out, out2, res; + + if ( payload_length < decryptInfo.packet_number_length + AEAD_TAG_LENGTH ) + throw hilti::rt::RuntimeError(hilti::rt::fmt("payload too small %ld < %ld", payload_length, + decryptInfo.packet_number_length + AEAD_TAG_LENGTH)); + + const uint8_t* encrypted_payload = data_as_uint8(all_data) + decryptInfo.unprotected_header.size(); + + int encrypted_payload_size = payload_length - decryptInfo.packet_number_length - AEAD_TAG_LENGTH; + + if ( encrypted_payload_size < 0 ) + throw hilti::rt::RuntimeError(hilti::rt::fmt("encrypted_payload_size underflow %ld", encrypted_payload_size)); + + if ( all_data.size() < decryptInfo.unprotected_header.size() + encrypted_payload_size + AEAD_TAG_LENGTH ) + throw hilti::rt::RuntimeError(hilti::rt::fmt("all_data too short %ld < %ld", all_data.size(), + decryptInfo.unprotected_header.size() + encrypted_payload_size)); + + const void* tag_to_check = all_data.data() + decryptInfo.unprotected_header.size() + encrypted_payload_size; + int tag_to_check_length = AEAD_TAG_LENGTH; + + std::array decrypt_buffer; + + // Setup context + auto* ctx = get_aes_128_gcm(); + + // Set the sizes for the IV and KEY + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_IVLEN, decryptInfo.nonce.size(), NULL); - if ( payload_length < decryptInfo.packet_number_length + AEAD_TAG_LENGTH ) - throw hilti::rt::RuntimeError( - hilti::rt::fmt("payload too small %ld < %ld", payload_length, - decryptInfo.packet_number_length + AEAD_TAG_LENGTH)); + EVP_CIPHER_CTX_set_key_length(ctx, client_key.size()); - const uint8_t* encrypted_payload = data_as_uint8(all_data) + - decryptInfo.unprotected_header.size(); + // Set the KEY and IV + EVP_CipherInit_ex(ctx, NULL, NULL, client_key.data(), decryptInfo.nonce.data(), 0); - int encrypted_payload_size = payload_length - decryptInfo.packet_number_length - - AEAD_TAG_LENGTH; + // Set the tag to be validated after decryption + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_TAG, tag_to_check_length, const_cast(tag_to_check)); - if ( encrypted_payload_size < 0 ) - throw hilti::rt::RuntimeError( - hilti::rt::fmt("encrypted_payload_size underflow %ld", encrypted_payload_size)); + // Setting the second parameter to NULL will pass it as Associated Data + EVP_CipherUpdate(ctx, NULL, &out, decryptInfo.unprotected_header.data(), decryptInfo.unprotected_header.size()); - if ( all_data.size() < - decryptInfo.unprotected_header.size() + encrypted_payload_size + AEAD_TAG_LENGTH ) - throw hilti::rt::RuntimeError( - hilti::rt::fmt("all_data too short %ld < %ld", all_data.size(), - decryptInfo.unprotected_header.size() + encrypted_payload_size)); + // Set the actual data to decrypt data into the decrypt_buffer. The amount of + // byte decrypted is stored into `out` + EVP_CipherUpdate(ctx, decrypt_buffer.data(), &out, encrypted_payload, encrypted_payload_size); - const void* tag_to_check = all_data.data() + decryptInfo.unprotected_header.size() + - encrypted_payload_size; - int tag_to_check_length = AEAD_TAG_LENGTH; + // Validate whether the decryption was successful or not + EVP_CipherFinal_ex(ctx, NULL, &out2); + + // Copy the decrypted data from the decrypted buffer into a Bytes instance. + 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 info; +}; + +/* +HKDF-Extract as described in https://www.rfc-editor.org/rfc/rfc8446.html#section-7.1 +*/ +std::vector hkdf_extract(const std::vector& salt, const hilti::rt::Bytes& connection_id) { + std::vector 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 hkdf_expand(EVP_PKEY_CTX* ctx, size_t out_len, const std::vector& key) { + std::vector 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 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 GetKey(const std::vector& secret) { + const auto& ctxs = GetHkdfCtxs(); + return hkdf_expand(ctxs.key_info_ctx, AEAD_KEY_LEN, secret); + } + + std::vector GetIv(const std::vector& secret) { + const auto& ctxs = GetHkdfCtxs(); + return hkdf_expand(ctxs.iv_info_ctx, AEAD_IV_LEN, secret); + } + + std::vector GetHp(const std::vector& secret) { + const auto& ctxs = GetHkdfCtxs(); + return hkdf_expand(ctxs.hp_info_ctx, AEAD_HP_LEN, secret); + } + + virtual const std::vector& GetInitialSalt() = 0; + virtual HkdfCtx& GetHkdfCtxs() = 0; + + virtual ~QuicPacketProtection() = default; + + // Helper to initialize HKDF expand only contexts. + static void Initialize(std::vector& 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 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 CLIENT_INITIAL_INFO = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x00}; + +std::vector SERVER_INITIAL_INFO = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x69, 0x6e, 0x00}; + +std::vector KEY_INFO = {0x00, 0x10, 0x0e, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, + 0x71, 0x75, 0x69, 0x63, 0x20, 0x6b, 0x65, 0x79, 0x00}; + +std::vector IV_INFO = {0x00, 0x0c, 0x0d, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, + 0x71, 0x75, 0x69, 0x63, 0x20, 0x69, 0x76, 0x00}; + +std::vector 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& 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 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(); + hkdf_ctxs.initialized = true; + } + + static HkdfCtx hkdf_ctxs; + static std::unique_ptr instance; +}; + +HkdfCtx QuicPacketProtectionV1::hkdf_ctxs = {0}; +std::unique_ptr QuicPacketProtectionV1::instance = nullptr; + + +// QUIC v2 +// +// https://datatracker.ietf.org/doc/rfc9369/ +std::vector INITIAL_SALT_V2 = {0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, + 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9}; - std::array decrypt_buffer; +std::vector CLIENT_INITIAL_INFO_V2 = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x00}; - // Setup context - auto* ctx = get_aes_128_gcm(); +std::vector SERVER_INITIAL_INFO_V2 = {0x00, 0x20, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x69, 0x6e, 0x00}; - // Set the sizes for the IV and KEY - EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_IVLEN, decryptInfo.nonce.size(), NULL); +std::vector KEY_INFO_V2 = {0x00, 0x10, 0x10, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x71, + 0x75, 0x69, 0x63, 0x76, 0x32, 0x20, 0x6b, 0x65, 0x79, 0x00}; - EVP_CIPHER_CTX_set_key_length(ctx, client_key.size()); +std::vector IV_INFO_V2 = {0x00, 0x0c, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x71, + 0x75, 0x69, 0x63, 0x76, 0x32, 0x20, 0x69, 0x76, 0x00}; - // Set the KEY and IV - EVP_CipherInit_ex(ctx, NULL, NULL, client_key.data(), decryptInfo.nonce.data(), 0); +std::vector HP_INFO_V2 = {0x00, 0x10, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x71, + 0x75, 0x69, 0x63, 0x76, 0x32, 0x20, 0x68, 0x70, 0x00}; - // Set the tag to be validated after decryption - EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_TAG, tag_to_check_length, - const_cast(tag_to_check)); +class QuicPacketProtectionV2 : public QuicPacketProtection { +public: + virtual std::vector& GetInitialSalt() override { return INITIAL_SALT_V2; } + virtual HkdfCtx& GetHkdfCtxs() override { return hkdf_ctxs; } - // Setting the second parameter to NULL will pass it as Associated Data - EVP_CipherUpdate(ctx, NULL, &out, decryptInfo.unprotected_header.data(), - decryptInfo.unprotected_header.size()); + static void Initialize() { + if ( hkdf_ctxs.initialized ) + return; + std::vector 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}, + }; - // Set the actual data to decrypt data into the decrypt_buffer. The amount of - // byte decrypted is stored into `out` - EVP_CipherUpdate(ctx, decrypt_buffer.data(), &out, encrypted_payload, encrypted_payload_size); + QuicPacketProtection::Initialize(hkdf_ctx_params); + instance = std::make_unique(); + hkdf_ctxs.initialized = true; + } - // Validate whether the decryption was successful or not - EVP_CipherFinal_ex(ctx, NULL, &out2); + static HkdfCtx hkdf_ctxs; + static std::unique_ptr instance; +}; - // Copy the decrypted data from the decrypted buffer into a Bytes instance. - return hilti::rt::Bytes(decrypt_buffer.data(), decrypt_buffer.data() + out); - } +HkdfCtx QuicPacketProtectionV2::hkdf_ctxs = {0}; +std::unique_ptr 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, - const hilti::rt::integer::safe& encrypted_offset, - const hilti::rt::integer::safe& payload_length, - const hilti::rt::Bool& from_client) - { - - if ( payload_length < 20 ) - throw hilti::rt::RuntimeError(hilti::rt::fmt("payload too small %ld < 20", payload_length)); - - if ( (all_data.size() < encrypted_offset + payload_length) ) - throw hilti::rt::RuntimeError(hilti::rt::fmt("packet too small %ld %ld", all_data.size(), - encrypted_offset + payload_length)); - - std::vector initial_secret = hkdf_extract(connection_id); - - std::vector server_client_secret; - if ( from_client ) - { - server_client_secret = hkdf_expand_client_initial_info(INITIAL_SECRET_LEN, initial_secret); - } - else - { - server_client_secret = hkdf_expand_server_initial_info(INITIAL_SECRET_LEN, initial_secret); - } - - std::vector key = hkdf_expand_key_info(AEAD_KEY_LEN, server_client_secret); - std::vector iv = hkdf_expand_iv_info(AEAD_IV_LEN, server_client_secret); - std::vector hp = hkdf_expand_hp_info(AEAD_HP_LEN, server_client_secret); - - DecryptionInformation decryptInfo = remove_header_protection(hp, encrypted_offset, all_data); - - // Calculate the correct nonce for the decryption - decryptInfo.nonce = calculate_nonce(iv, decryptInfo.packet_number); - - return decrypt(key, all_data, payload_length, decryptInfo); - } +hilti::rt::Bytes QUIC_decrypt_crypto_payload(const hilti::rt::integer::safe& version, + const hilti::rt::Bytes& all_data, const hilti::rt::Bytes& connection_id, + const hilti::rt::integer::safe& encrypted_offset, + const hilti::rt::integer::safe& payload_length, + const hilti::rt::Bool& from_client) { + if ( payload_length < 20 ) + throw hilti::rt::RuntimeError(hilti::rt::fmt("payload too small %ld < 20", payload_length)); + + if ( (all_data.size() < encrypted_offset + payload_length) ) + throw hilti::rt::RuntimeError( + hilti::rt::fmt("packet too small %ld %ld", all_data.size(), encrypted_offset + payload_length)); + + QuicPacketProtection* qpp = nullptr; + + if ( version == 0x00000001 ) { // quicv1 + QuicPacketProtectionV1::Initialize(); + qpp = QuicPacketProtectionV1::instance.get(); + } + else if ( version == 0x6b3343cf ) { // quicv2 + QuicPacketProtectionV2::Initialize(); + qpp = QuicPacketProtectionV2::instance.get(); + } + else { + throw hilti::rt::RuntimeError(hilti::rt::fmt("unable to handle version %lx", version)); + } + + const auto& secret = qpp->GetSecret(from_client, connection_id); + std::vector key = qpp->GetKey(secret); + std::vector iv = qpp->GetIv(secret); + std::vector hp = qpp->GetHp(secret); + + DecryptionInformation decryptInfo = remove_header_protection(hp, encrypted_offset, all_data); + + // Calculate the correct nonce for the decryption + decryptInfo.nonce = calculate_nonce(iv, decryptInfo.packet_number); + + return decrypt(key, all_data, payload_length, decryptInfo); +} diff --git a/scripts/consts.zeek b/scripts/consts.zeek index b502b04..477c8ab 100644 --- a/scripts/consts.zeek +++ b/scripts/consts.zeek @@ -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); }; } diff --git a/testing/Baseline/tests.quicv2-echo-443/conn.log.cut b/testing/Baseline/tests.quicv2-echo-443/conn.log.cut new file mode 100644 index 0000000..46d72b1 --- /dev/null +++ b/testing/Baseline/tests.quicv2-echo-443/conn.log.cut @@ -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 diff --git a/testing/Baseline/tests.quicv2-echo-443/quic.log b/testing/Baseline/tests.quicv2-echo-443/quic.log new file mode 100644 index 0000000..2680a6b --- /dev/null +++ b/testing/Baseline/tests.quicv2-echo-443/quic.log @@ -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 diff --git a/testing/Baseline/tests.quicv2-echo-443/ssl.log b/testing/Baseline/tests.quicv2-echo-443/ssl.log new file mode 100644 index 0000000..0167629 --- /dev/null +++ b/testing/Baseline/tests.quicv2-echo-443/ssl.log @@ -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 diff --git a/testing/Baseline/tests.quicv2-http3-443/conn.log.cut b/testing/Baseline/tests.quicv2-http3-443/conn.log.cut new file mode 100644 index 0000000..46d72b1 --- /dev/null +++ b/testing/Baseline/tests.quicv2-http3-443/conn.log.cut @@ -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 diff --git a/testing/Baseline/tests.quicv2-http3-443/quic.log b/testing/Baseline/tests.quicv2-http3-443/quic.log new file mode 100644 index 0000000..c02fd62 --- /dev/null +++ b/testing/Baseline/tests.quicv2-http3-443/quic.log @@ -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 diff --git a/testing/Baseline/tests.quicv2-http3-443/ssl.log b/testing/Baseline/tests.quicv2-http3-443/ssl.log new file mode 100644 index 0000000..1f7c430 --- /dev/null +++ b/testing/Baseline/tests.quicv2-http3-443/ssl.log @@ -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 diff --git a/testing/Baseline/tests.vector-max-size-crash/analyzer.log.cut b/testing/Baseline/tests.vector-max-size-crash/analyzer.log.cut index 2b3de83..a8d2f38 100644 --- a/testing/Baseline/tests.vector-max-size-crash/analyzer.log.cut +++ b/testing/Baseline/tests.vector-max-size-crash/analyzer.log.cut @@ -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::) +1693925959.000001 CHhAvVGS1DHFjwGM9 violation protocol QUIC unhandled QUIC version 0x10010000 (<...>/QUIC.spicy::) diff --git a/testing/Baseline/tests.vector-max-size-crash/out b/testing/Baseline/tests.vector-max-size-crash/out new file mode 100644 index 0000000..3f16ba0 --- /dev/null +++ b/testing/Baseline/tests.vector-max-size-crash/out @@ -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, diff --git a/testing/Traces/quicv2-echo-443.pcap b/testing/Traces/quicv2-echo-443.pcap new file mode 100644 index 0000000..7b26d0d Binary files /dev/null and b/testing/Traces/quicv2-echo-443.pcap differ diff --git a/testing/Traces/quicv2-http3-443.pcap b/testing/Traces/quicv2-http3-443.pcap new file mode 100644 index 0000000..d35ded4 Binary files /dev/null and b/testing/Traces/quicv2-http3-443.pcap differ diff --git a/testing/tests/quicv2-echo-443.zeek b/testing/tests/quicv2-echo-443.zeek new file mode 100644 index 0000000..bff7602 --- /dev/null +++ b/testing/tests/quicv2-echo-443.zeek @@ -0,0 +1,7 @@ +# @TEST-DOC: Pcap with quicv2 echo traffic produced with https://raw.githubusercontent.com/quic-go/quic-go/master/example/echo/echo.go +# +# @TEST-EXEC: zeek -Cr $TRACES/quicv2-echo-443.pcap $PACKAGE +# @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 diff --git a/testing/tests/quicv2-http3-443.zeek b/testing/tests/quicv2-http3-443.zeek new file mode 100644 index 0000000..75e70e7 --- /dev/null +++ b/testing/tests/quicv2-http3-443.zeek @@ -0,0 +1,7 @@ +# @TEST-DOC: Pcap with quicv2 http3 traffic produced with https://raw.githubusercontent.com/quic-go/quic-go/master/example/main.go +# +# @TEST-EXEC: zeek -Cr $TRACES/quicv2-http3-443.pcap $PACKAGE +# @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 diff --git a/testing/tests/vector-max-size-crash.zeek b/testing/tests/vector-max-size-crash.zeek index deae025..83ca82f 100644 --- a/testing/tests/vector-max-size-crash.zeek +++ b/testing/tests/vector-max-size-crash.zeek @@ -1,9 +1,13 @@ # @TEST-DOC: Test that runs the pcap -# @TEST-EXEC: zeek -Cr $TRACES/vector-max-size-crash.pcap $PACKAGE +# @TEST-EXEC: zeek -Cr $TRACES/vector-max-size-crash.pcap $PACKAGE %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 base/misc/version -e 'exit(Version::info$version_number < 60100 ? 0 : 1)' || TEST_DIFF_CANONIFIER='sed -r "s/\((.+)\.spicy:[0-9]+:[0-9]+\)/(\1.spicy::)/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::)/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; + }