// See the file "COPYING" in the main distribution directory for copyright. // Copyright (c) 2023, NCC Group / Fox-IT. See COPYING for details. /* WARNING: THIS CODE IS NOT SAFE IN MULTI-THREADED ENVIRONMENTS: * Initializations of static OpenSSL contexts without locking * Use of SSL contexts is not protected by locks 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. */ /* WORK-IN-PROGRESS Initial working version of decrypting the INITIAL packets from both client & server to be used by the Spicy parser. Might need a few more refactors as C++ development is not our main profession. */ // Default imports #include #include #include #include #include // OpenSSL imports #include #include #include // Import HILTI #include 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; }; // 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 by the different functions */ const size_t INITIAL_SECRET_LEN = 32; const size_t AEAD_KEY_LEN = 16; const size_t AEAD_IV_LEN = 12; const size_t AEAD_HP_LEN = 16; const size_t AEAD_SAMPLE_LENGTH = 16; const size_t AEAD_TAG_LENGTH = 16; 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; } /* 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; } /* 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); return client_iv; } /* 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; 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)); // Bail on large payloads, somewhat arbitrarily. 10k allows for Jumbo frames // and sometimes the fuzzer produces packets up to that size as well. if ( payload_length > 10000 ) throw hilti::rt::RuntimeError(hilti::rt::fmt("payload_length too large %ld", payload_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; // Allocate memory for decryption. std::vector decrypt_buffer(encrypted_payload_size); // 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); EVP_CIPHER_CTX_set_key_length(ctx, client_key.size()); // Set the KEY and IV EVP_CipherInit_ex(ctx, NULL, NULL, client_key.data(), decryptInfo.nonce.data(), 0); // 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)); // 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()); // 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); // Validate whether the decryption was successful or not if ( EVP_CipherFinal_ex(ctx, NULL, &out2) == 0 ) throw hilti::rt::RuntimeError("decryption failed"); // 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 { 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, uint32_t version, const hilti::rt::Bytes& connection_id) { const auto& ctxs = GetHkdfCtxs(); const auto initial_secret = hkdf_extract(GetInitialSalt(version), 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 bool Supports(uint32_t version) const = 0; virtual const std::vector& GetInitialSalt(uint32_t version) const = 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 class QuicPacketProtectionV1 : public QuicPacketProtection { public: virtual bool Supports(uint32_t version) const override { // Quic V1 if ( version == 0x00000001 ) return true; // Draft 22 through 34 if ( version >= 0xff000016 && version <= 0xff000022 ) return true; // mvfst from facebook if ( version == 0xfaceb001 || (version >= 0xfaceb002 && version <= 0xfaceb013) ) return true; return false; }; virtual const std::vector& GetInitialSalt(uint32_t version) const override { static 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}; // https://insights.sei.cmu.edu/documents/4499/2023_017_001_890985.pdf static std::vector INITIAL_SALT_D22 = {0x7f, 0xbc, 0xdb, 0x0e, 0x7c, 0x66, 0xbb, 0xe9, 0x19, 0x3a, 0x96, 0xcd, 0x21, 0x51, 0x9e, 0xbd, 0x7a, 0x02, 0x64, 0x4a}; static std::vector INITIAL_SALT_D23_D28 = {0xc3, 0xee, 0xf7, 0x12, 0xc7, 0x2e, 0xbb, 0x5a, 0x11, 0xa7, 0xd2, 0x43, 0x2b, 0xb4, 0x63, 0x65, 0xbe, 0xf9, 0xf5, 0x02}; static std::vector INITIAL_SALT_D29_D32 = {0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}; if ( version == 0xff000016 ) return INITIAL_SALT_D22; if ( version >= 0xff000017 && version <= 0xff00001c ) return INITIAL_SALT_D23_D28; if ( version >= 0xff00001d && version <= 0xff000020 ) return INITIAL_SALT_D29_D32; if ( version == 0xfaceb001 ) return INITIAL_SALT_D22; if ( version >= 0xfaceb002 && version <= 0xfaceb013 ) return INITIAL_SALT_D23_D28; 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() { 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}; std::vector hkdf_ctx_params = { {&hkdf_ctxs.client_in_ctx, std::move(CLIENT_INITIAL_INFO)}, {&hkdf_ctxs.server_in_ctx, std::move(SERVER_INITIAL_INFO)}, {&hkdf_ctxs.key_info_ctx, std::move(KEY_INFO)}, {&hkdf_ctxs.iv_info_ctx, std::move(IV_INFO)}, {&hkdf_ctxs.hp_info_ctx, std::move(HP_INFO)}, }; QuicPacketProtection::Initialize(hkdf_ctx_params); instance = std::make_unique(); } 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/ class QuicPacketProtectionV2 : public QuicPacketProtection { public: virtual bool Supports(uint32_t version) const override { return version == 0x6b3343cf; } virtual const std::vector& GetInitialSalt(uint32_t version) const override { static 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}; return INITIAL_SALT_V2; } virtual HkdfCtx& GetHkdfCtxs() override { return hkdf_ctxs; } static void Initialize() { 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}; 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}; 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}; std::vector IV_INFO_V2 = {0x00, 0x0c, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x71, 0x75, 0x69, 0x63, 0x76, 0x32, 0x20, 0x69, 0x76, 0x00}; std::vector HP_INFO_V2 = {0x00, 0x10, 0x0f, 0x74, 0x6c, 0x73, 0x31, 0x33, 0x20, 0x71, 0x75, 0x69, 0x63, 0x76, 0x32, 0x20, 0x68, 0x70, 0x00}; std::vector hkdf_ctx_params = { {&hkdf_ctxs.client_in_ctx, std::move(CLIENT_INITIAL_INFO_V2)}, {&hkdf_ctxs.server_in_ctx, std::move(SERVER_INITIAL_INFO_V2)}, {&hkdf_ctxs.key_info_ctx, std::move(KEY_INFO_V2)}, {&hkdf_ctxs.iv_info_ctx, std::move(IV_INFO_V2)}, {&hkdf_ctxs.hp_info_ctx, std::move(HP_INFO_V2)}, }; QuicPacketProtection::Initialize(hkdf_ctx_params); instance = std::make_unique(); } static HkdfCtx hkdf_ctxs; static std::unique_ptr instance; }; HkdfCtx QuicPacketProtectionV2::hkdf_ctxs = {0}; std::unique_ptr QuicPacketProtectionV2::instance = nullptr; } // namespace /* 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::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) { static bool initialized = false; if ( ! initialized ) { QuicPacketProtectionV1::Initialize(); QuicPacketProtectionV2::Initialize(); initialized = true; } 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)); uint32_t v = version; QuicPacketProtection* qpp = nullptr; if ( QuicPacketProtectionV1::instance->Supports(v) ) { qpp = QuicPacketProtectionV1::instance.get(); } else if ( QuicPacketProtectionV2::instance->Supports(v) ) { qpp = QuicPacketProtectionV2::instance.get(); } else { throw hilti::rt::RuntimeError(hilti::rt::fmt("unable to decrypt QUIC version 0x%lx", version)); } const auto& secret = qpp->GetSecret(from_client, v, 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(std::move(iv), decryptInfo.packet_number); return decrypt(key, all_data, payload_length, decryptInfo); }