Generate Concatenated Keys as per OHTTP Key Configuration encoding defined in RFC https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#name-key-configuration-encoding PiperOrigin-RevId: 501684207
diff --git a/quiche/oblivious_http/common/oblivious_http_header_key_config.cc b/quiche/oblivious_http/common/oblivious_http_header_key_config.cc index 2963ca2..b0103c2 100644 --- a/quiche/oblivious_http/common/oblivious_http_header_key_config.cc +++ b/quiche/oblivious_http/common/oblivious_http_header_key_config.cc
@@ -1,13 +1,18 @@ #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" +#include <algorithm> #include <cstdint> +#include <string> +#include <vector> #include "absl/memory/memory.h" #include "absl/status/status.h" +#include "absl/strings/escaping.h" #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "openssl/base.h" #include "openssl/hpke.h" +#include "quiche/common/platform/api/quiche_bug_tracker.h" #include "quiche/common/platform/api/quiche_logging.h" #include "quiche/common/quiche_data_writer.h" #include "quiche/common/quiche_endian.h" @@ -15,6 +20,16 @@ namespace quiche { namespace { +// Size of KEM ID is 2 bytes. Refer to OHTTP Key Config in the spec, +// https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-06.html#name-a-single-key-configuration +constexpr size_t kSizeOfHpkeKemId = 2; + +// Size of Symmetric algorithms is 2 bytes(16 bits) each. +// Refer to HPKE Symmetric Algorithms configuration in the spec, +// https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-06.html#name-a-single-key-configuration +constexpr size_t kSizeOfSymmetricAlgorithmHpkeKdfId = 2; +constexpr size_t kSizeOfSymmetricAlgorithmHpkeAeadId = 2; + absl::StatusOr<const EVP_HPKE_KEM*> CheckKemId(uint16_t kem_id) { switch (kem_id) { case EVP_HPKE_DHKEM_X25519_HKDF_SHA256: @@ -194,6 +209,130 @@ return hdr; } +namespace { +// https://www.rfc-editor.org/rfc/rfc9180#section-7.1 +absl::StatusOr<uint16_t> KeyLength(uint16_t kem_id) { + auto supported_kem = CheckKemId(kem_id); + if (!supported_kem.ok()) { + return absl::InvalidArgumentError(absl::StrCat( + "Unsupported KEM ID:", kem_id, ". public key length is unknown.")); + } + return EVP_HPKE_KEM_public_key_len(supported_kem.value()); +} + +absl::StatusOr<std::string> SerializeOhttpKeyWithPublicKey( + uint8_t key_id, absl::string_view public_key, + const std::vector<ObliviousHttpHeaderKeyConfig>& ohttp_configs) { + auto ohttp_config = ohttp_configs[0]; + // Check if `ohttp_config` match spec's encoding guidelines. + static_assert(sizeof(ohttp_config.GetHpkeKemId()) == kSizeOfHpkeKemId && + sizeof(ohttp_config.GetHpkeKdfId()) == + kSizeOfSymmetricAlgorithmHpkeKdfId && + sizeof(ohttp_config.GetHpkeAeadId()) == + kSizeOfSymmetricAlgorithmHpkeAeadId, + "Size of HPKE IDs should match RFC specification."); + + uint16_t symmetric_algs_length = + ohttp_configs.size() * (kSizeOfSymmetricAlgorithmHpkeKdfId + + kSizeOfSymmetricAlgorithmHpkeAeadId); + int buf_len = sizeof(key_id) + kSizeOfHpkeKemId + public_key.size() + + sizeof(symmetric_algs_length) + symmetric_algs_length; + std::string ohttp_key_configuration(buf_len, '\0'); + QuicheDataWriter writer(ohttp_key_configuration.size(), + ohttp_key_configuration.data()); + if (!writer.WriteUInt8(key_id)) { + return absl::InternalError("Failed to serialize OHTTP key.[key_id]"); + } + if (!writer.WriteUInt16(ohttp_config.GetHpkeKemId())) { + return absl::InternalError( + "Failed to serialize OHTTP key.[kem_id]"); // kemID. + } + if (!writer.WriteStringPiece(public_key)) { + return absl::InternalError( + "Failed to serialize OHTTP key.[public_key]"); // Raw public key. + } + if (!writer.WriteUInt16(symmetric_algs_length)) { + return absl::InternalError( + "Failed to serialize OHTTP key.[symmetric_algs_length]"); + } + for (const auto& item : ohttp_configs) { + // Check if KEM ID is the same for all the configs stored in `this` for + // given `key_id`. + if (item.GetHpkeKemId() != ohttp_config.GetHpkeKemId()) { + QUICHE_BUG(ohttp_key_configs_builder_parser) + << "ObliviousHttpKeyConfigs object cannot hold ConfigMap of " + "different KEM IDs:[ " + << item.GetHpkeKemId() << "," << ohttp_config.GetHpkeKemId() + << " ]for a given key_id:" << static_cast<uint16_t>(key_id); + } + if (!writer.WriteUInt16(item.GetHpkeKdfId())) { + return absl::InternalError( + "Failed to serialize OHTTP key.[kdf_id]"); // kdfID. + } + if (!writer.WriteUInt16(item.GetHpkeAeadId())) { + return absl::InternalError( + "Failed to serialize OHTTP key.[aead_id]"); // aeadID. + } + } + QUICHE_DCHECK_EQ(writer.remaining(), 0u); + return ohttp_key_configuration; +} + +std::string GetDebugStringForFailedKeyConfig( + const ObliviousHttpKeyConfigs::OhttpKeyConfig& failed_key_config) { + std::string debug_string = "[ "; + absl::StrAppend(&debug_string, + "key_id:", static_cast<uint16_t>(failed_key_config.key_id), + " , kem_id:", failed_key_config.kem_id, + ". Printing HEX formatted public_key:", + absl::BytesToHexString(failed_key_config.public_key)); + absl::StrAppend(&debug_string, ", symmetric_algorithms: { "); + for (const auto& symmetric_config : failed_key_config.symmetric_algorithms) { + absl::StrAppend(&debug_string, "{kdf_id: ", symmetric_config.kdf_id, + ", aead_id:", symmetric_config.aead_id, " }"); + } + absl::StrAppend(&debug_string, " } ]"); + return debug_string; +} + +// Verifies if the `key_config` contains all valid combinations of [kem_id, +// kdf_id, aead_id] that comprises Single Key configuration encoding as +// specified in +// https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#name-a-single-key-configuration. +absl::Status StoreKeyConfigIfValid( + ObliviousHttpKeyConfigs::OhttpKeyConfig key_config, + absl::btree_map<uint8_t, std::vector<ObliviousHttpHeaderKeyConfig>, + std::greater<uint8_t>>& configs, + absl::flat_hash_map<uint8_t, std::string>& keys) { + if (!CheckKemId(key_config.kem_id).ok() || + key_config.public_key.size() != KeyLength(key_config.kem_id).value()) { + QUICHE_LOG(ERROR) << "Failed to process: " + << GetDebugStringForFailedKeyConfig(key_config); + return absl::InvalidArgumentError( + absl::StrCat("Invalid key_config! [KEM ID:", key_config.kem_id, "]")); + } + for (const auto& symmetric_config : key_config.symmetric_algorithms) { + if (!CheckKdfId(symmetric_config.kdf_id).ok() || + !CheckAeadId(symmetric_config.aead_id).ok()) { + QUICHE_LOG(ERROR) << "Failed to process: " + << GetDebugStringForFailedKeyConfig(key_config); + return absl::InvalidArgumentError( + absl::StrCat("Invalid key_config! [KDF ID:", symmetric_config.kdf_id, + ", AEAD ID:", symmetric_config.aead_id, "]")); + } + auto ohttp_config = ObliviousHttpHeaderKeyConfig::Create( + key_config.key_id, key_config.kem_id, symmetric_config.kdf_id, + symmetric_config.aead_id); + if (ohttp_config.ok()) { + configs[key_config.key_id].emplace_back(std::move(ohttp_config.value())); + } + } + keys.emplace(key_config.key_id, std::move(key_config.public_key)); + return absl::OkStatus(); +} + +} // namespace + absl::StatusOr<ObliviousHttpKeyConfigs> ObliviousHttpKeyConfigs::ParseConcatenatedKeys(absl::string_view key_config) { ConfigMap configs; @@ -206,6 +345,67 @@ return ObliviousHttpKeyConfigs(std::move(configs), std::move(keys)); } +absl::StatusOr<ObliviousHttpKeyConfigs> ObliviousHttpKeyConfigs::Create( + absl::flat_hash_set<ObliviousHttpKeyConfigs::OhttpKeyConfig> + ohttp_key_configs) { + if (ohttp_key_configs.empty()) { + return absl::InvalidArgumentError("Empty input."); + } + ConfigMap configs_map; + PublicKeyMap keys_map; + for (auto& ohttp_key_config : ohttp_key_configs) { + auto result = StoreKeyConfigIfValid(std::move(ohttp_key_config), + configs_map, keys_map); + if (!result.ok()) { + return result; + } + } + auto oblivious_configs = + ObliviousHttpKeyConfigs(std::move(configs_map), std::move(keys_map)); + return oblivious_configs; +} + +absl::StatusOr<ObliviousHttpKeyConfigs> ObliviousHttpKeyConfigs::Create( + const ObliviousHttpHeaderKeyConfig& single_key_config, + absl::string_view public_key) { + if (public_key.empty()) { + return absl::InvalidArgumentError("Empty input."); + } + + if (auto key_length = KeyLength(single_key_config.GetHpkeKemId()); + public_key.size() != key_length.value()) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid key. Key size mismatch. Expected:", key_length.value(), + " Actual:", public_key.size())); + } + + ConfigMap configs; + PublicKeyMap keys; + uint8_t key_id = single_key_config.GetKeyId(); + keys.emplace(key_id, public_key); + configs[key_id].emplace_back(std::move(single_key_config)); + return ObliviousHttpKeyConfigs(std::move(configs), std::move(keys)); +} + +absl::StatusOr<std::string> ObliviousHttpKeyConfigs::GenerateConcatenatedKeys() + const { + std::string concatenated_keys; + for (const auto& [key_id, ohttp_configs] : configs_) { + auto key = public_keys_.find(key_id); + if (key == public_keys_.end()) { + return absl::InternalError( + "Failed to serialize. No public key found for key_id"); + } + auto serialized = + SerializeOhttpKeyWithPublicKey(key_id, key->second, ohttp_configs); + if (!serialized.ok()) { + return absl::InternalError("Failed to serialize OHTTP key configs."); + } + absl::StrAppend(&concatenated_keys, serialized.value()); + } + return concatenated_keys; +} + ObliviousHttpHeaderKeyConfig ObliviousHttpKeyConfigs::PreferredConfig() const { // configs_ is forced to have at least one object during construction. return configs_.begin()->second.front(); @@ -220,21 +420,6 @@ return key->second; } -namespace { -// https://www.rfc-editor.org/rfc/rfc9180#section-7.1 -// TODO(kmg): Switch to BoringSSL's EVP_HPKE_KEM_public_key_len() -// https://boringssl-review.googlesource.com/c/boringssl/+/54605 -absl::StatusOr<uint16_t> KeyLength(uint16_t kem_id) { - switch (kem_id) { - case EVP_HPKE_DHKEM_X25519_HKDF_SHA256: - return 32; - default: - return absl::InvalidArgumentError( - "Unsupported kem_id; public key length is unknown."); - } -} -} // namespace - absl::Status ObliviousHttpKeyConfigs::ReadSingleKeyConfig( QuicheDataReader& reader, ConfigMap& configs, PublicKeyMap& keys) { uint8_t key_id;
diff --git a/quiche/oblivious_http/common/oblivious_http_header_key_config.h b/quiche/oblivious_http/common/oblivious_http_header_key_config.h index 9c46f81..488561b 100644 --- a/quiche/oblivious_http/common/oblivious_http_header_key_config.h +++ b/quiche/oblivious_http/common/oblivious_http_header_key_config.h
@@ -5,6 +5,7 @@ #include "absl/container/btree_map.h" #include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" @@ -111,12 +112,74 @@ // ObliviousHttpKeyConfigs objects are immutable after construction. class QUICHE_EXPORT ObliviousHttpKeyConfigs { public: + // Below two structures follow the Single key configuration spec in OHTTP RFC. + // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-06.html#name-a-single-key-configuration + struct SymmetricAlgorithmsConfig { + uint16_t kdf_id; + uint16_t aead_id; + + bool operator==(const SymmetricAlgorithmsConfig& other) const { + return kdf_id == other.kdf_id && aead_id == other.aead_id; + } + + template <typename H> + friend H AbslHashValue(H h, const SymmetricAlgorithmsConfig& sym_alg_cfg) { + return H::combine(std::move(h), sym_alg_cfg.kdf_id, sym_alg_cfg.aead_id); + } + }; + + struct OhttpKeyConfig { + uint8_t key_id; + uint16_t kem_id; + std::string public_key; // Raw byte string. + absl::flat_hash_set<SymmetricAlgorithmsConfig> symmetric_algorithms; + + bool operator==(const OhttpKeyConfig& other) const { + return key_id == other.key_id && kem_id == other.kem_id && + public_key == other.public_key && + symmetric_algorithms == other.symmetric_algorithms; + } + + template <typename H> + friend H AbslHashValue(H h, const OhttpKeyConfig& ohttp_key_cfg) { + return H::combine(std::move(h), ohttp_key_cfg.key_id, + ohttp_key_cfg.kem_id, ohttp_key_cfg.public_key, + ohttp_key_cfg.symmetric_algorithms); + } + }; + // Parses the "application/ohttp-keys" media type, which is a byte string // formatted according to the spec: // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-04.html#section-3 static absl::StatusOr<ObliviousHttpKeyConfigs> ParseConcatenatedKeys( absl::string_view key_configs); + // Builds `ObliviousHttpKeyConfigs` with multiple key configurations, each + // made up of Single Key Configuration([{key_id, kem_id, public key}, + // Set<SymmetricAlgos>]) encoding specified in section 3. + // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#name-key-configuration-encoding + // @params: Set<{key_id, kem_id, public key, Set<HPKE Symmetric Algorithms>>. + // @return: When given all valid configs supported by BoringSSL, builds and + // returns `ObliviousHttpKeyConfigs`. If any one of the input configs are + // invalid or unsupported by BSSL, returns an error. + // @note: Subsequently, To get concatenated keys[contiguous byte string of + // keys], use `GenerateConcatenatedKeys()`. This output can inturn be parsed + // by `ObliviousHttpKeyConfigs::ParseConcatenatedKeys` on client side. + static absl::StatusOr<ObliviousHttpKeyConfigs> Create( + absl::flat_hash_set<OhttpKeyConfig> ohttp_key_configs); + + // Builds `ObliviousHttpKeyConfigs` with given public_key and Single key + // configuration specified in `ObliviousHttpHeaderKeyConfig` object. After + // successful `Create`, clients can call `GenerateConcatenatedKeys()` to build + // the Single key config. + static absl::StatusOr<ObliviousHttpKeyConfigs> Create( + const ObliviousHttpHeaderKeyConfig& single_key_config, + absl::string_view public_key); + + // Generates byte string corresponding to "application/ohttp-keys" media type. + // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-04.html#section-3 + absl::StatusOr<std::string> GenerateConcatenatedKeys() const; + int NumKeys() const { return public_keys_.size(); } // Returns a preferred config to use. The preferred key is the key with
diff --git a/quiche/oblivious_http/common/oblivious_http_header_key_config_test.cc b/quiche/oblivious_http/common/oblivious_http_header_key_config_test.cc index 0227ee4..73e4c07 100644 --- a/quiche/oblivious_http/common/oblivious_http_header_key_config_test.cc +++ b/quiche/oblivious_http/common/oblivious_http_header_key_config_test.cc
@@ -3,6 +3,7 @@ #include <cstdint> #include "absl/strings/escaping.h" +#include "absl/strings/str_cat.h" #include "openssl/hpke.h" #include "quiche/common/platform/api/quiche_logging.h" #include "quiche/common/platform/api/quiche_test.h" @@ -13,6 +14,8 @@ using ::testing::AllOf; using ::testing::Property; using ::testing::StrEq; +using ::testing::UnorderedElementsAre; +using ::testing::UnorderedElementsAreArray; /** * Build Request header. @@ -30,6 +33,28 @@ return hdr; } +std::string GetSerializedKeyConfig( + ObliviousHttpKeyConfigs::OhttpKeyConfig& key_config) { + uint16_t symmetric_algs_length = + key_config.symmetric_algorithms.size() * + (sizeof(key_config.symmetric_algorithms.cbegin()->kdf_id) + + sizeof(key_config.symmetric_algorithms.cbegin()->aead_id)); + int buf_len = sizeof(key_config.key_id) + sizeof(key_config.kem_id) + + key_config.public_key.size() + sizeof(symmetric_algs_length) + + symmetric_algs_length; + std::string ohttp_key(buf_len, '\0'); + QuicheDataWriter writer(ohttp_key.size(), ohttp_key.data()); + EXPECT_TRUE(writer.WriteUInt8(key_config.key_id)); + EXPECT_TRUE(writer.WriteUInt16(key_config.kem_id)); + EXPECT_TRUE(writer.WriteStringPiece(key_config.public_key)); + EXPECT_TRUE(writer.WriteUInt16(symmetric_algs_length)); + for (const auto& symmetric_alg : key_config.symmetric_algorithms) { + EXPECT_TRUE(writer.WriteUInt16(symmetric_alg.kdf_id)); + EXPECT_TRUE(writer.WriteUInt16(symmetric_alg.aead_id)); + } + return ohttp_key; +} + TEST(ObliviousHttpHeaderKeyConfig, TestSerializeRecipientContextInfo) { uint8_t key_id = 3; uint16_t kem_id = EVP_HPKE_DHKEM_X25519_HKDF_SHA256; @@ -198,5 +223,134 @@ EXPECT_FALSE(ObliviousHttpKeyConfigs::ParseConcatenatedKeys(key).ok()); } +TEST(ObliviousHttpHeaderKeyConfigs, TestCreateWithSingleKeyConfig) { + auto instance = ObliviousHttpHeaderKeyConfig::Create( + 123, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256, + EVP_HPKE_CHACHA20_POLY1305); + EXPECT_TRUE(instance.ok()); + std::string test_public_key( + EVP_HPKE_KEM_public_key_len(instance->GetHpkeKem()), 'a'); + auto configs = + ObliviousHttpKeyConfigs::Create(instance.value(), test_public_key); + EXPECT_TRUE(configs.ok()); + auto serialized_key = configs->GenerateConcatenatedKeys(); + EXPECT_TRUE(serialized_key.ok()); + auto ohttp_configs = + ObliviousHttpKeyConfigs::ParseConcatenatedKeys(serialized_key.value()); + EXPECT_TRUE(ohttp_configs.ok()); + ASSERT_EQ(ohttp_configs->PreferredConfig().GetKeyId(), 123); + auto parsed_public_key = ohttp_configs->GetPublicKeyForId(123); + EXPECT_TRUE(parsed_public_key.ok()); + EXPECT_EQ(parsed_public_key.value(), test_public_key); +} + +TEST(ObliviousHttpHeaderKeyConfigs, TestCreateWithWithMultipleKeys) { + std::string expected_preferred_public_key(32, 'b'); + ObliviousHttpKeyConfigs::OhttpKeyConfig config1 = { + 100, + EVP_HPKE_DHKEM_X25519_HKDF_SHA256, + std::string(32, 'a'), + {{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM}}}; + ObliviousHttpKeyConfigs::OhttpKeyConfig config2 = { + 200, + EVP_HPKE_DHKEM_X25519_HKDF_SHA256, + expected_preferred_public_key, + {{EVP_HPKE_HKDF_SHA256, EVP_HPKE_CHACHA20_POLY1305}}}; + auto configs = ObliviousHttpKeyConfigs::Create({config1, config2}); + EXPECT_TRUE(configs.ok()); + auto serialized_key = configs->GenerateConcatenatedKeys(); + EXPECT_TRUE(serialized_key.ok()); + ASSERT_EQ(serialized_key.value(), + absl::StrCat(GetSerializedKeyConfig(config2), + GetSerializedKeyConfig(config1))); + auto ohttp_configs = + ObliviousHttpKeyConfigs::ParseConcatenatedKeys(serialized_key.value()); + EXPECT_TRUE(ohttp_configs.ok()); + ASSERT_EQ(ohttp_configs->NumKeys(), 2); + EXPECT_THAT(configs->PreferredConfig(), + AllOf(HasKeyId(200), HasKemId(EVP_HPKE_DHKEM_X25519_HKDF_SHA256), + HasKdfId(EVP_HPKE_HKDF_SHA256), + HasAeadId(EVP_HPKE_CHACHA20_POLY1305))); + auto parsed_preferred_public_key = ohttp_configs->GetPublicKeyForId( + ohttp_configs->PreferredConfig().GetKeyId()); + EXPECT_TRUE(parsed_preferred_public_key.ok()); + EXPECT_EQ(parsed_preferred_public_key.value(), expected_preferred_public_key); +} + +TEST(ObliviousHttpHeaderKeyConfigs, TestCreateWithInvalidConfigs) { + ASSERT_EQ(ObliviousHttpKeyConfigs::Create({}).status().code(), + absl::StatusCode::kInvalidArgument); + ASSERT_EQ(ObliviousHttpKeyConfigs::Create( + {{100, 2, std::string(32, 'a'), {{2, 3}, {4, 5}}}, + {200, 6, std::string(32, 'b'), {{7, 8}, {9, 10}}}}) + .status() + .code(), + absl::StatusCode::kInvalidArgument); + + EXPECT_EQ( + ObliviousHttpKeyConfigs::Create( + {{123, + EVP_HPKE_DHKEM_X25519_HKDF_SHA256, + "invalid key length" /*expected length for given kem_id is 32*/, + {{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM}}}}) + .status() + .code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ObliviousHttpHeaderKeyConfigs, + TestCreateSingleKeyConfigWithInvalidConfig) { + const auto sample_ohttp_hdr_config = ObliviousHttpHeaderKeyConfig::Create( + 123, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256, + EVP_HPKE_AES_128_GCM); + ASSERT_TRUE(sample_ohttp_hdr_config.ok()); + ASSERT_EQ(ObliviousHttpKeyConfigs::Create(sample_ohttp_hdr_config.value(), + "" /*empty public_key*/) + .status() + .code(), + absl::StatusCode::kInvalidArgument); + EXPECT_EQ(ObliviousHttpKeyConfigs::Create( + sample_ohttp_hdr_config.value(), + "invalid key length" /*expected length for given kem_id is 32*/) + .status() + .code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ObliviousHttpHeaderKeyConfigs, TestHashImplWithObliviousStruct) { + // Insert different symmetric algorithms 50 times. + absl::flat_hash_set<ObliviousHttpKeyConfigs::SymmetricAlgorithmsConfig> + symmetric_algs_set; + for (int i = 0; i < 50; ++i) { + symmetric_algs_set.insert({EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM}); + symmetric_algs_set.insert({EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM}); + symmetric_algs_set.insert( + {EVP_HPKE_HKDF_SHA256, EVP_HPKE_CHACHA20_POLY1305}); + } + ASSERT_EQ(symmetric_algs_set.size(), 3); + EXPECT_THAT(symmetric_algs_set, + UnorderedElementsAreArray< + ObliviousHttpKeyConfigs::SymmetricAlgorithmsConfig>({ + {EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM}, + {EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM}, + {EVP_HPKE_HKDF_SHA256, EVP_HPKE_CHACHA20_POLY1305}, + })); + + // Insert different Key configs 50 times. + absl::flat_hash_set<ObliviousHttpKeyConfigs::OhttpKeyConfig> + ohttp_key_configs_set; + ObliviousHttpKeyConfigs::OhttpKeyConfig expected_key_config{ + 100, + EVP_HPKE_DHKEM_X25519_HKDF_SHA256, + std::string(32, 'c'), + {{EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM}, + {EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM}}}; + for (int i = 0; i < 50; ++i) { + ohttp_key_configs_set.insert(expected_key_config); + } + ASSERT_EQ(ohttp_key_configs_set.size(), 1); + EXPECT_THAT(ohttp_key_configs_set, UnorderedElementsAre(expected_key_config)); +} + } // namespace } // namespace quiche