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