oblivious_http:Update comments to indicate that Client & Gateway are thread-safe.

Additional Context : Adding this as per @ericorth's suggestions on cl/479424618 comments section (Apologies for not linking the actual comment here, as Critique is not giving us a link to that comment)

PiperOrigin-RevId: 481960460
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 110f0da..afc80c7 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -1093,6 +1093,12 @@
     "http2/test_tools/http2_frame_builder_test.cc",
     "http2/test_tools/http2_random_test.cc",
     "http2/test_tools/random_decoder_test_base_test.cc",
+    "oblivious_http/buffers/oblivious_http_integration_test.cc",
+    "oblivious_http/buffers/oblivious_http_request_test.cc",
+    "oblivious_http/buffers/oblivious_http_response_test.cc",
+    "oblivious_http/common/oblivious_http_header_key_config_test.cc",
+    "oblivious_http/oblivious_http_client_test.cc",
+    "oblivious_http/oblivious_http_gateway_test.cc",
     "quic/core/congestion_control/bandwidth_sampler_test.cc",
     "quic/core/congestion_control/bbr2_simulator_test.cc",
     "quic/core/congestion_control/bbr_sender_test.cc",
@@ -1438,6 +1444,20 @@
 binary_http_srcs = [
     "binary_http/binary_http_message.cc",
 ]
+oblivious_http_hdrs = [
+    "oblivious_http/buffers/oblivious_http_request.h",
+    "oblivious_http/buffers/oblivious_http_response.h",
+    "oblivious_http/common/oblivious_http_header_key_config.h",
+    "oblivious_http/oblivious_http_client.h",
+    "oblivious_http/oblivious_http_gateway.h",
+]
+oblivious_http_srcs = [
+    "oblivious_http/buffers/oblivious_http_request.cc",
+    "oblivious_http/buffers/oblivious_http_response.cc",
+    "oblivious_http/common/oblivious_http_header_key_config.cc",
+    "oblivious_http/oblivious_http_client.cc",
+    "oblivious_http/oblivious_http_gateway.cc",
+]
 qbone_hdrs = [
     "quic/qbone/bonnet/icmp_reachable.h",
     "quic/qbone/bonnet/icmp_reachable_interface.h",
diff --git a/build/source_list.gni b/build/source_list.gni
index 94401df..53814a2 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -1093,6 +1093,12 @@
     "src/quiche/http2/test_tools/http2_frame_builder_test.cc",
     "src/quiche/http2/test_tools/http2_random_test.cc",
     "src/quiche/http2/test_tools/random_decoder_test_base_test.cc",
+    "src/quiche/oblivious_http/buffers/oblivious_http_integration_test.cc",
+    "src/quiche/oblivious_http/buffers/oblivious_http_request_test.cc",
+    "src/quiche/oblivious_http/buffers/oblivious_http_response_test.cc",
+    "src/quiche/oblivious_http/common/oblivious_http_header_key_config_test.cc",
+    "src/quiche/oblivious_http/oblivious_http_client_test.cc",
+    "src/quiche/oblivious_http/oblivious_http_gateway_test.cc",
     "src/quiche/quic/core/congestion_control/bandwidth_sampler_test.cc",
     "src/quiche/quic/core/congestion_control/bbr2_simulator_test.cc",
     "src/quiche/quic/core/congestion_control/bbr_sender_test.cc",
@@ -1438,6 +1444,20 @@
 binary_http_srcs = [
     "src/quiche/binary_http/binary_http_message.cc",
 ]
+oblivious_http_hdrs = [
+    "src/quiche/oblivious_http/buffers/oblivious_http_request.h",
+    "src/quiche/oblivious_http/buffers/oblivious_http_response.h",
+    "src/quiche/oblivious_http/common/oblivious_http_header_key_config.h",
+    "src/quiche/oblivious_http/oblivious_http_client.h",
+    "src/quiche/oblivious_http/oblivious_http_gateway.h",
+]
+oblivious_http_srcs = [
+    "src/quiche/oblivious_http/buffers/oblivious_http_request.cc",
+    "src/quiche/oblivious_http/buffers/oblivious_http_response.cc",
+    "src/quiche/oblivious_http/common/oblivious_http_header_key_config.cc",
+    "src/quiche/oblivious_http/oblivious_http_client.cc",
+    "src/quiche/oblivious_http/oblivious_http_gateway.cc",
+]
 qbone_hdrs = [
     "src/quiche/quic/qbone/bonnet/icmp_reachable.h",
     "src/quiche/quic/qbone/bonnet/icmp_reachable_interface.h",
diff --git a/build/source_list.json b/build/source_list.json
index 599ccdd..a877e81 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -1092,6 +1092,12 @@
     "quiche/http2/test_tools/http2_frame_builder_test.cc",
     "quiche/http2/test_tools/http2_random_test.cc",
     "quiche/http2/test_tools/random_decoder_test_base_test.cc",
+    "quiche/oblivious_http/buffers/oblivious_http_integration_test.cc",
+    "quiche/oblivious_http/buffers/oblivious_http_request_test.cc",
+    "quiche/oblivious_http/buffers/oblivious_http_response_test.cc",
+    "quiche/oblivious_http/common/oblivious_http_header_key_config_test.cc",
+    "quiche/oblivious_http/oblivious_http_client_test.cc",
+    "quiche/oblivious_http/oblivious_http_gateway_test.cc",
     "quiche/quic/core/congestion_control/bandwidth_sampler_test.cc",
     "quiche/quic/core/congestion_control/bbr2_simulator_test.cc",
     "quiche/quic/core/congestion_control/bbr_sender_test.cc",
@@ -1437,6 +1443,20 @@
   "binary_http_srcs": [
     "quiche/binary_http/binary_http_message.cc"
   ],
+  "oblivious_http_hdrs": [
+    "quiche/oblivious_http/buffers/oblivious_http_request.h",
+    "quiche/oblivious_http/buffers/oblivious_http_response.h",
+    "quiche/oblivious_http/common/oblivious_http_header_key_config.h",
+    "quiche/oblivious_http/oblivious_http_client.h",
+    "quiche/oblivious_http/oblivious_http_gateway.h"
+  ],
+  "oblivious_http_srcs": [
+    "quiche/oblivious_http/buffers/oblivious_http_request.cc",
+    "quiche/oblivious_http/buffers/oblivious_http_response.cc",
+    "quiche/oblivious_http/common/oblivious_http_header_key_config.cc",
+    "quiche/oblivious_http/oblivious_http_client.cc",
+    "quiche/oblivious_http/oblivious_http_gateway.cc"
+  ],
   "qbone_hdrs": [
     "quiche/quic/qbone/bonnet/icmp_reachable.h",
     "quiche/quic/qbone/bonnet/icmp_reachable_interface.h",
diff --git a/quiche/oblivious_http/buffers/oblivious_http_integration_test.cc b/quiche/oblivious_http/buffers/oblivious_http_integration_test.cc
new file mode 100644
index 0000000..7ed02c1
--- /dev/null
+++ b/quiche/oblivious_http/buffers/oblivious_http_integration_test.cc
@@ -0,0 +1,115 @@
+#include <stdint.h>
+
+#include <string>
+
+#include "absl/strings/escaping.h"
+#include "openssl/hpke.h"
+#include "quiche/common/platform/api/quiche_test.h"
+#include "quiche/oblivious_http/buffers/oblivious_http_response.h"
+
+namespace quiche {
+namespace {
+
+struct ObliviousHttpResponseTestStrings {
+  std::string test_case_name;
+  uint8_t key_id;
+  std::string request_plaintext;
+  std::string response_plaintext;
+};
+
+std::string GetHpkePrivateKey() {
+  absl::string_view hpke_key_hex =
+      "b77431ecfa8f4cfc30d6e467aafa06944dffe28cb9dd1409e33a3045f5adc8a1";
+  return absl::HexStringToBytes(hpke_key_hex);
+}
+
+std::string GetHpkePublicKey() {
+  absl::string_view public_key =
+      "6d21cfe09fbea5122f9ebc2eb2a69fcc4f06408cd54aac934f012e76fcdcef62";
+  return absl::HexStringToBytes(public_key);
+}
+
+const ObliviousHttpHeaderKeyConfig GetOhttpKeyConfig(uint8_t key_id,
+                                                     uint16_t kem_id,
+                                                     uint16_t kdf_id,
+                                                     uint16_t aead_id) {
+  auto ohttp_key_config =
+      ObliviousHttpHeaderKeyConfig::Create(key_id, kem_id, kdf_id, aead_id);
+  EXPECT_TRUE(ohttp_key_config.ok());
+  return std::move(ohttp_key_config.value());
+}
+
+bssl::UniquePtr<EVP_HPKE_KEY> ConstructHpkeKey(
+    absl::string_view hpke_key,
+    const ObliviousHttpHeaderKeyConfig &ohttp_key_config) {
+  bssl::UniquePtr<EVP_HPKE_KEY> bssl_hpke_key(EVP_HPKE_KEY_new());
+  EXPECT_NE(bssl_hpke_key, nullptr);
+  EXPECT_TRUE(EVP_HPKE_KEY_init(
+      bssl_hpke_key.get(), ohttp_key_config.GetHpkeKem(),
+      reinterpret_cast<const uint8_t *>(hpke_key.data()), hpke_key.size()));
+  return bssl_hpke_key;
+}
+}  // namespace
+
+absl::StatusOr<ObliviousHttpRequest>
+CreateClientObliviousRequestWithSeedForTesting(
+    absl::string_view plaintext_payload, absl::string_view hpke_public_key,
+    const ObliviousHttpHeaderKeyConfig &ohttp_key_config,
+    absl::string_view seed) {
+  return ObliviousHttpRequest::EncapsulateWithSeed(
+      plaintext_payload, hpke_public_key, ohttp_key_config, seed);
+}
+
+using ObliviousHttpParameterizedTest =
+    test::QuicheTestWithParam<ObliviousHttpResponseTestStrings>;
+
+TEST_P(ObliviousHttpParameterizedTest, TestEndToEndWithOfflineStrings) {
+  // For each test case, verify end to end request-handling and
+  // response-handling.
+  const ObliviousHttpResponseTestStrings &test = GetParam();
+
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(test.key_id, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  // Round-trip request flow.
+  auto client_req_encap = ObliviousHttpRequest::CreateClientObliviousRequest(
+      test.request_plaintext, GetHpkePublicKey(), ohttp_key_config);
+  EXPECT_TRUE(client_req_encap.ok());
+  ASSERT_FALSE(client_req_encap->EncapsulateAndSerialize().empty());
+  auto server_req_decap = ObliviousHttpRequest::CreateServerObliviousRequest(
+      client_req_encap->EncapsulateAndSerialize(),
+      *(ConstructHpkeKey(GetHpkePrivateKey(), ohttp_key_config)),
+      ohttp_key_config);
+  EXPECT_TRUE(server_req_decap.ok());
+  EXPECT_EQ(server_req_decap->GetPlaintextData(), test.request_plaintext);
+
+  // Round-trip response flow.
+  auto server_resp_encap = ObliviousHttpResponse::CreateServerObliviousResponse(
+      test.response_plaintext,
+      *(server_req_decap->oblivious_http_request_context()));
+  EXPECT_TRUE(server_resp_encap.ok());
+  ASSERT_FALSE(server_resp_encap->EncapsulateAndSerialize().empty());
+  auto client_resp_decap = ObliviousHttpResponse::CreateClientObliviousResponse(
+      server_resp_encap->EncapsulateAndSerialize(),
+      *(client_req_encap->oblivious_http_request_context()));
+  EXPECT_TRUE(client_resp_decap.ok());
+  EXPECT_EQ(client_resp_decap->GetPlaintextData(), test.response_plaintext);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ObliviousHttpParameterizedTests, ObliviousHttpParameterizedTest,
+    testing::ValuesIn<ObliviousHttpResponseTestStrings>(
+        {{"test_case_1", 4, "test request 1", "test response 1"},
+         {"test_case_2", 6, "test request 2", "test response 2"},
+         {"test_case_3", 7, "test request 3", "test response 3"},
+         {"test_case_4", 2, "test request 4", "test response 4"},
+         {"test_case_5", 1, "test request 5", "test response 5"},
+         {"test_case_6", 7, "test request 6", "test response 6"},
+         {"test_case_7", 3, "test request 7", "test response 7"},
+         {"test_case_8", 9, "test request 8", "test response 8"},
+         {"test_case_9", 3, "test request 9", "test response 9"},
+         {"test_case_10", 4, "test request 10", "test response 10"}}),
+    [](const testing::TestParamInfo<ObliviousHttpParameterizedTest::ParamType>
+           &info) { return info.param.test_case_name; });
+
+}  // namespace quiche
diff --git a/quiche/oblivious_http/buffers/oblivious_http_request.cc b/quiche/oblivious_http/buffers/oblivious_http_request.cc
new file mode 100644
index 0000000..ca26e4e
--- /dev/null
+++ b/quiche/oblivious_http/buffers/oblivious_http_request.cc
@@ -0,0 +1,194 @@
+#include "quiche/oblivious_http/buffers/oblivious_http_request.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "openssl/hpke.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/quiche_crypto_logging.h"
+
+namespace quiche {
+// Ctor.
+ObliviousHttpRequest::Context::Context(
+    bssl::UniquePtr<EVP_HPKE_CTX> hpke_context, std::string encapsulated_key)
+    : hpke_context_(std::move(hpke_context)),
+      encapsulated_key_(std::move(encapsulated_key)) {}
+
+// Ctor.
+ObliviousHttpRequest::ObliviousHttpRequest(
+    bssl::UniquePtr<EVP_HPKE_CTX> hpke_context, std::string encapsulated_key,
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
+    std::string req_ciphertext, std::string req_plaintext)
+    : oblivious_http_request_context_(
+          new Context(std::move(hpke_context), std::move(encapsulated_key))),
+      key_config_(ohttp_key_config),
+      request_ciphertext_(std::move(req_ciphertext)),
+      request_plaintext_(std::move(req_plaintext)) {}
+
+// Request Decapsulation.
+absl::StatusOr<ObliviousHttpRequest>
+ObliviousHttpRequest::CreateServerObliviousRequest(
+    absl::string_view encrypted_data, const EVP_HPKE_KEY& gateway_key,
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config) {
+  if (EVP_HPKE_KEY_kem(&gateway_key) == nullptr) {
+    return absl::InvalidArgumentError(
+        "Invalid input param. Failed to import gateway_key.");
+  }
+  bssl::UniquePtr<EVP_HPKE_CTX> gateway_ctx(EVP_HPKE_CTX_new());
+  if (gateway_ctx == nullptr) {
+    return SslErrorAsStatus("Failed to initialize Gateway/Server's Context.");
+  }
+  // TODO(anov) Add ParseOhttpPayloadHeader(QuicheDataReader) to read fields out
+  // of payload, and eliminate sub-stringing.
+  auto is_hdr_ok = ohttp_key_config.ParseOhttpPayloadHeader(encrypted_data);
+  if (!is_hdr_ok.ok()) {
+    return is_hdr_ok;
+  }
+  absl::string_view enc_plus_ciphertext =
+      encrypted_data.substr(ObliviousHttpHeaderKeyConfig::kHeaderLength);
+
+  size_t enc_key_len = EVP_HPKE_KEM_enc_len(EVP_HPKE_KEY_kem(&gateway_key));
+  if (enc_plus_ciphertext.size() < enc_key_len) {
+    return absl::FailedPreconditionError(absl::StrCat(
+        "Failed to extract encapsulation key of expected len=", enc_key_len,
+        "from payload."));
+  }
+  absl::string_view enc_key_received =
+      enc_plus_ciphertext.substr(0, enc_key_len);
+  std::string info = ohttp_key_config.SerializeRecipientContextInfo();
+  if (!EVP_HPKE_CTX_setup_recipient(
+          gateway_ctx.get(), &gateway_key, ohttp_key_config.GetHpkeKdf(),
+          ohttp_key_config.GetHpkeAead(),
+          reinterpret_cast<const uint8_t*>(enc_key_received.data()),
+          enc_key_received.size(),
+          reinterpret_cast<const uint8_t*>(info.data()), info.size())) {
+    return SslErrorAsStatus("Failed to setup recipient context");
+  }
+
+  absl::string_view ciphertext_received =
+      enc_plus_ciphertext.substr(enc_key_len);
+
+  // Decrypt the message.
+  std::string decrypted(ciphertext_received.size(), '\0');
+  size_t decrypted_len;
+  if (!EVP_HPKE_CTX_open(
+          gateway_ctx.get(), reinterpret_cast<uint8_t*>(decrypted.data()),
+          &decrypted_len, decrypted.size(),
+          reinterpret_cast<const uint8_t*>(ciphertext_received.data()),
+          ciphertext_received.size(), nullptr, 0)) {
+    return SslErrorAsStatus("Failed to decrypt.");
+  }
+  decrypted.resize(decrypted_len);
+  return ObliviousHttpRequest(
+      std::move(gateway_ctx), std::string(enc_key_received), ohttp_key_config,
+      std::string(ciphertext_received), std::move(decrypted));
+}
+
+// Request Encapsulation.
+absl::StatusOr<ObliviousHttpRequest>
+ObliviousHttpRequest::CreateClientObliviousRequest(
+    absl::string_view plaintext_payload, absl::string_view hpke_public_key,
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config) {
+  return EncapsulateWithSeed(plaintext_payload, hpke_public_key,
+                             ohttp_key_config, "");
+}
+
+absl::StatusOr<ObliviousHttpRequest> ObliviousHttpRequest::EncapsulateWithSeed(
+    absl::string_view plaintext_payload, absl::string_view hpke_public_key,
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
+    absl::string_view seed) {
+  if (plaintext_payload.empty() || hpke_public_key.empty()) {
+    return absl::InvalidArgumentError("Invalid input.");
+  }
+  // Initialize HPKE key and context.
+  bssl::UniquePtr<EVP_HPKE_KEY> client_key(EVP_HPKE_KEY_new());
+  if (client_key == nullptr) {
+    return SslErrorAsStatus("Failed to initialize HPKE Client Key.");
+  }
+  bssl::UniquePtr<EVP_HPKE_CTX> client_ctx(EVP_HPKE_CTX_new());
+  if (client_ctx == nullptr) {
+    return SslErrorAsStatus("Failed to initialize HPKE Client Context.");
+  }
+  // Setup the sender (client)
+  std::string encapsulated_key(EVP_HPKE_MAX_ENC_LENGTH, '\0');
+  size_t enc_len;
+  std::string info = ohttp_key_config.SerializeRecipientContextInfo();
+  if (seed.empty()) {
+    if (!EVP_HPKE_CTX_setup_sender(
+            client_ctx.get(),
+            reinterpret_cast<uint8_t*>(encapsulated_key.data()), &enc_len,
+            encapsulated_key.size(), ohttp_key_config.GetHpkeKem(),
+            ohttp_key_config.GetHpkeKdf(), ohttp_key_config.GetHpkeAead(),
+            reinterpret_cast<const uint8_t*>(hpke_public_key.data()),
+            hpke_public_key.size(),
+            reinterpret_cast<const uint8_t*>(info.data()), info.size())) {
+      return SslErrorAsStatus(
+          "Failed to setup HPKE context with given public key param "
+          "hpke_public_key.");
+    }
+  } else {
+    if (!EVP_HPKE_CTX_setup_sender_with_seed_for_testing(
+            client_ctx.get(),
+            reinterpret_cast<uint8_t*>(encapsulated_key.data()), &enc_len,
+            encapsulated_key.size(), ohttp_key_config.GetHpkeKem(),
+            ohttp_key_config.GetHpkeKdf(), ohttp_key_config.GetHpkeAead(),
+            reinterpret_cast<const uint8_t*>(hpke_public_key.data()),
+            hpke_public_key.size(),
+            reinterpret_cast<const uint8_t*>(info.data()), info.size(),
+            reinterpret_cast<const uint8_t*>(seed.data()), seed.size())) {
+      return SslErrorAsStatus(
+          "Failed to setup HPKE context with given public key param "
+          "hpke_public_key and seed.");
+    }
+  }
+  encapsulated_key.resize(enc_len);
+  std::string ciphertext(
+      plaintext_payload.size() + EVP_HPKE_CTX_max_overhead(client_ctx.get()),
+      '\0');
+  size_t ciphertext_len;
+  if (!EVP_HPKE_CTX_seal(
+          client_ctx.get(), reinterpret_cast<uint8_t*>(ciphertext.data()),
+          &ciphertext_len, ciphertext.size(),
+          reinterpret_cast<const uint8_t*>(plaintext_payload.data()),
+          plaintext_payload.size(), nullptr, 0)) {
+    return SslErrorAsStatus(
+        "Failed to encrypt plaintext_payload with given public key param "
+        "hpke_public_key.");
+  }
+  ciphertext.resize(ciphertext_len);
+  if (encapsulated_key.empty() || ciphertext.empty()) {
+    return absl::InternalError(absl::StrCat(
+        "Failed to generate required data: ",
+        (encapsulated_key.empty() ? "encapsulated key is empty" : ""),
+        (ciphertext.empty() ? "encrypted data is empty" : ""), "."));
+  }
+
+  return ObliviousHttpRequest(
+      std::move(client_ctx), std::move(encapsulated_key), ohttp_key_config,
+      std::move(ciphertext), std::string(plaintext_payload));
+}
+
+// Request Serialize.
+// Builds request=[hdr, enc, ct].
+// https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.1-4.5
+std::string ObliviousHttpRequest::EncapsulateAndSerialize() const {
+  return absl::StrCat(key_config_.SerializeOhttpPayloadHeader(),
+                      oblivious_http_request_context_->encapsulated_key_,
+                      request_ciphertext_);
+}
+
+// Returns Decrypted blob in the case of server, and returns plaintext used by
+// the client while `CreateClientObliviousRequest`.
+absl::string_view ObliviousHttpRequest::GetPlaintextData() const {
+  return request_plaintext_;
+}
+
+}  // namespace quiche
diff --git a/quiche/oblivious_http/buffers/oblivious_http_request.h b/quiche/oblivious_http/buffers/oblivious_http_request.h
new file mode 100644
index 0000000..0807fc8
--- /dev/null
+++ b/quiche/oblivious_http/buffers/oblivious_http_request.h
@@ -0,0 +1,105 @@
+#ifndef QUICHE_OBLIVIOUS_HTTP_BUFFERS_OBLIVIOUS_HTTP_REQUEST_H_
+#define QUICHE_OBLIVIOUS_HTTP_BUFFERS_OBLIVIOUS_HTTP_REQUEST_H_
+
+#include <memory>
+#include <string>
+
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "quiche/oblivious_http/common/oblivious_http_header_key_config.h"
+
+namespace quiche {
+// 1. Handles client side encryption of the payload that will subsequently be
+// added to HTTP POST body and passed on to Relay.
+// 2. Handles server side decryption of the payload received in HTTP POST body
+// from Relay.
+// https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#name-encapsulation-of-requests
+class QUICHE_EXPORT_PRIVATE ObliviousHttpRequest {
+ public:
+  // Holds the HPKE related data received from request. This context is created
+  // during request processing, and subsequently passed into response handling
+  // in `ObliviousHttpResponse`.
+  class Context {
+   public:
+    ~Context() = default;
+
+   private:
+    explicit Context(bssl::UniquePtr<EVP_HPKE_CTX> hpke_context,
+                     std::string encapsulated_key);
+
+    // All accessors must be friends to read `Context`.
+    friend class ObliviousHttpRequest;
+    friend class ObliviousHttpResponse;
+    // Tests which need access.
+    friend class
+        ObliviousHttpRequest_TestDecapsulateWithSpecAppendixAExample_Test;
+    friend class ObliviousHttpRequest_TestEncapsulatedRequestStructure_Test;
+    friend class
+        ObliviousHttpRequest_TestEncapsulatedOhttpEncryptedPayload_Test;
+    friend class ObliviousHttpRequest_TestDeterministicSeededOhttpRequest_Test;
+    friend class ObliviousHttpResponse_EndToEndTestForResponse_Test;
+    friend class ObliviousHttpResponse_TestEncapsulateWithQuicheRandom_Test;
+
+    bssl::UniquePtr<EVP_HPKE_CTX> hpke_context_;
+    const std::string encapsulated_key_;
+  };
+  // Parse the OHTTP request from the given `encrypted_data`.
+  // On success, returns obj that callers will use to `GetPlaintextData`.
+  // Generic Usecase : server-side calls this method in the context of Request.
+  static absl::StatusOr<ObliviousHttpRequest> CreateServerObliviousRequest(
+      absl::string_view encrypted_data, const EVP_HPKE_KEY& gateway_key,
+      const ObliviousHttpHeaderKeyConfig& ohttp_key_config);
+
+  // Constructs an OHTTP request for the given `plaintext_payload`.
+  // On success, returns obj that callers will use to `EncapsulateAndSerialize`
+  // OHttp request.
+  static absl::StatusOr<ObliviousHttpRequest> CreateClientObliviousRequest(
+      absl::string_view plaintext_payload, absl::string_view hpke_public_key,
+      const ObliviousHttpHeaderKeyConfig& ohttp_key_config);
+
+  // Movable.
+  ObliviousHttpRequest(ObliviousHttpRequest&& other) = default;
+  ObliviousHttpRequest& operator=(ObliviousHttpRequest&& other) = default;
+
+  ~ObliviousHttpRequest() = default;
+
+  // Returns serialized OHTTP request bytestring.
+  std::string EncapsulateAndSerialize() const;
+
+  // Generic Usecase : server-side calls this method after Decapsulation using
+  // `CreateServerObliviousRequest`.
+  absl::string_view GetPlaintextData() const;
+
+  // Oblivious HTTP request context is created after successful creation of
+  // `this` object, and subsequently passed into the `ObliviousHttpResponse` for
+  // followup response handling.
+  std::shared_ptr<Context> oblivious_http_request_context() const {
+    return oblivious_http_request_context_;
+  }
+
+ private:
+  friend absl::StatusOr<ObliviousHttpRequest>
+  CreateClientObliviousRequestWithSeedForTesting(
+      absl::string_view plaintext_payload, absl::string_view hpke_public_key,
+      const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
+      absl::string_view seed);
+
+  explicit ObliviousHttpRequest(
+      bssl::UniquePtr<EVP_HPKE_CTX> hpke_context, std::string encapsulated_key,
+      const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
+      std::string req_ciphertext, std::string req_plaintext);
+
+  static absl::StatusOr<ObliviousHttpRequest> EncapsulateWithSeed(
+      absl::string_view plaintext_payload, absl::string_view hpke_public_key,
+      const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
+      absl::string_view seed);
+
+  std::shared_ptr<Context> oblivious_http_request_context_;
+  ObliviousHttpHeaderKeyConfig key_config_;
+  std::string request_ciphertext_;
+  std::string request_plaintext_;
+};
+
+}  // namespace quiche
+
+#endif  // QUICHE_OBLIVIOUS_HTTP_BUFFERS_OBLIVIOUS_HTTP_REQUEST_H_
diff --git a/quiche/oblivious_http/buffers/oblivious_http_request_test.cc b/quiche/oblivious_http/buffers/oblivious_http_request_test.cc
new file mode 100644
index 0000000..91ca8e9
--- /dev/null
+++ b/quiche/oblivious_http/buffers/oblivious_http_request_test.cc
@@ -0,0 +1,275 @@
+#include "quiche/oblivious_http/buffers/oblivious_http_request.h"
+
+#include <stddef.h>
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "openssl/hkdf.h"
+#include "openssl/hpke.h"
+#include "quiche/common/platform/api/quiche_test.h"
+#include "quiche/common/quiche_data_reader.h"
+#include "quiche/oblivious_http/common/oblivious_http_header_key_config.h"
+
+namespace quiche {
+
+absl::StatusOr<ObliviousHttpRequest>
+CreateClientObliviousRequestWithSeedForTesting(
+    absl::string_view plaintext_payload, absl::string_view hpke_public_key,
+    const ObliviousHttpHeaderKeyConfig &ohttp_key_config,
+    absl::string_view seed) {
+  return ObliviousHttpRequest::EncapsulateWithSeed(
+      plaintext_payload, hpke_public_key, ohttp_key_config, seed);
+}
+
+namespace {
+const uint32_t kHeaderLength = ObliviousHttpHeaderKeyConfig::kHeaderLength;
+std::string GetHpkePrivateKey() {
+  absl::string_view hpke_key_hex =
+      "b77431ecfa8f4cfc30d6e467aafa06944dffe28cb9dd1409e33a3045f5adc8a1";
+  return absl::HexStringToBytes(hpke_key_hex);
+}
+
+std::string GetHpkePublicKey() {
+  absl::string_view public_key =
+      "6d21cfe09fbea5122f9ebc2eb2a69fcc4f06408cd54aac934f012e76fcdcef62";
+  return absl::HexStringToBytes(public_key);
+}
+
+std::string GetSeed() {
+  absl::string_view seed =
+      "52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736";
+  return absl::HexStringToBytes(seed);
+}
+
+std::string GetSeededEncapsulatedKey() {
+  absl::string_view encapsulated_key =
+      "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431";
+  return absl::HexStringToBytes(encapsulated_key);
+}
+
+bssl::UniquePtr<EVP_HPKE_KEY> ConstructHpkeKey(
+    absl::string_view hpke_key,
+    const ObliviousHttpHeaderKeyConfig &ohttp_key_config) {
+  bssl::UniquePtr<EVP_HPKE_KEY> bssl_hpke_key(EVP_HPKE_KEY_new());
+  EXPECT_NE(bssl_hpke_key, nullptr);
+  EXPECT_TRUE(EVP_HPKE_KEY_init(
+      bssl_hpke_key.get(), ohttp_key_config.GetHpkeKem(),
+      reinterpret_cast<const uint8_t *>(hpke_key.data()), hpke_key.size()));
+  return bssl_hpke_key;
+}
+
+const ObliviousHttpHeaderKeyConfig GetOhttpKeyConfig(uint8_t key_id,
+                                                     uint16_t kem_id,
+                                                     uint16_t kdf_id,
+                                                     uint16_t aead_id) {
+  auto ohttp_key_config =
+      ObliviousHttpHeaderKeyConfig::Create(key_id, kem_id, kdf_id, aead_id);
+  EXPECT_TRUE(ohttp_key_config.ok());
+  return std::move(ohttp_key_config.value());
+}
+}  // namespace
+
+// Direct test example from OHttp spec.
+// https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#appendix-A
+TEST(ObliviousHttpRequest, TestDecapsulateWithSpecAppendixAExample) {
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(/*key_id=*/1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM);
+
+  // X25519 Secret key (priv key).
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#appendix-A-2
+  constexpr absl::string_view kX25519SecretKey =
+      "3c168975674b2fa8e465970b79c8dcf09f1c741626480bd4c6162fc5b6a98e1a";
+
+  // Encapsulated request.
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#appendix-A-14
+  constexpr absl::string_view kEncapsulatedRequest =
+      "010020000100014b28f881333e7c164ffc499ad9796f877f4e1051ee6d31bad19dec96c2"
+      "08b4726374e469135906992e1268c594d2a10c695d858c40a026e7965e7d86b83dd440b2"
+      "c0185204b4d63525";
+
+  // Initialize Request obj to Decapsulate (decrypt).
+  auto instance = ObliviousHttpRequest::CreateServerObliviousRequest(
+      absl::HexStringToBytes(kEncapsulatedRequest),
+      *(ConstructHpkeKey(absl::HexStringToBytes(kX25519SecretKey),
+                         ohttp_key_config)),
+      ohttp_key_config);
+  ASSERT_TRUE(instance.ok());
+  auto decrypted = instance->GetPlaintextData();
+
+  // Encapsulated/Ephemeral public key.
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#appendix-A-10
+  constexpr absl::string_view kExpectedEphemeralPublicKey =
+      "4b28f881333e7c164ffc499ad9796f877f4e1051ee6d31bad19dec96c208b472";
+  EXPECT_EQ(instance->oblivious_http_request_context()->encapsulated_key_,
+            absl::HexStringToBytes(kExpectedEphemeralPublicKey));
+
+  // Binary HTTP message.
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#appendix-A-6
+  constexpr absl::string_view kExpectedBinaryHTTPMessage =
+      "00034745540568747470730b6578616d706c652e636f6d012f";
+  EXPECT_EQ(decrypted, absl::HexStringToBytes(kExpectedBinaryHTTPMessage));
+}
+
+TEST(ObliviousHttpRequest, TestEncapsulatedRequestStructure) {
+  uint8_t test_key_id = 7;
+  uint16_t test_kem_id = EVP_HPKE_DHKEM_X25519_HKDF_SHA256;
+  uint16_t test_kdf_id = EVP_HPKE_HKDF_SHA256;
+  uint16_t test_aead_id = EVP_HPKE_AES_256_GCM;
+  absl::string_view plaintext = "test";
+  auto instance = ObliviousHttpRequest::CreateClientObliviousRequest(
+      plaintext, GetHpkePublicKey(),
+      GetOhttpKeyConfig(test_key_id, test_kem_id, test_kdf_id, test_aead_id));
+  ASSERT_TRUE(instance.ok());
+  auto payload_bytes = instance->EncapsulateAndSerialize();
+  EXPECT_GE(payload_bytes.size(), kHeaderLength);
+  // Parse header.
+  QuicheDataReader reader(payload_bytes);
+  uint8_t key_id;
+  EXPECT_TRUE(reader.ReadUInt8(&key_id));
+  EXPECT_EQ(key_id, test_key_id);
+  uint16_t kem_id;
+  EXPECT_TRUE(reader.ReadUInt16(&kem_id));
+  EXPECT_EQ(kem_id, test_kem_id);
+  uint16_t kdf_id;
+  EXPECT_TRUE(reader.ReadUInt16(&kdf_id));
+  EXPECT_EQ(kdf_id, test_kdf_id);
+  uint16_t aead_id;
+  EXPECT_TRUE(reader.ReadUInt16(&aead_id));
+  EXPECT_EQ(aead_id, test_aead_id);
+  auto client_encapsulated_key =
+      instance->oblivious_http_request_context()->encapsulated_key_;
+  EXPECT_EQ(client_encapsulated_key.size(), X25519_PUBLIC_VALUE_LEN);
+  auto enc_key_plus_ciphertext = payload_bytes.substr(kHeaderLength);
+  auto packed_encapsulated_key =
+      enc_key_plus_ciphertext.substr(0, X25519_PUBLIC_VALUE_LEN);
+  EXPECT_EQ(packed_encapsulated_key, client_encapsulated_key);
+  auto ciphertext = enc_key_plus_ciphertext.substr(X25519_PUBLIC_VALUE_LEN);
+  EXPECT_GE(ciphertext.size(), plaintext.size());
+}
+
+TEST(ObliviousHttpRequest, TestDeterministicSeededOhttpRequest) {
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(4, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  auto encapsulated = CreateClientObliviousRequestWithSeedForTesting(
+      "test", GetHpkePublicKey(), ohttp_key_config, GetSeed());
+  ASSERT_TRUE(encapsulated.ok());
+  EXPECT_EQ(encapsulated->oblivious_http_request_context()->encapsulated_key_,
+            GetSeededEncapsulatedKey());
+  absl::string_view expected_encrypted_request =
+      "9f37cfed07d0111ecd2c34f794671759bcbd922a";
+  EXPECT_NE(encapsulated->oblivious_http_request_context()->hpke_context_,
+            nullptr);
+  size_t encapsulated_key_len = EVP_HPKE_KEM_enc_len(EVP_HPKE_CTX_kem(
+      encapsulated->oblivious_http_request_context()->hpke_context_.get()));
+  int encrypted_payload_offset = kHeaderLength + encapsulated_key_len;
+  EXPECT_EQ(
+      encapsulated->EncapsulateAndSerialize().substr(encrypted_payload_offset),
+      absl::HexStringToBytes(expected_encrypted_request));
+}
+
+TEST(ObliviousHttpRequest,
+     TestSeededEncapsulatedKeySamePlaintextsSameCiphertexts) {
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(8, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  auto req_with_same_plaintext_1 =
+      CreateClientObliviousRequestWithSeedForTesting(
+          "same plaintext", GetHpkePublicKey(), ohttp_key_config, GetSeed());
+  ASSERT_TRUE(req_with_same_plaintext_1.ok());
+  auto ciphertext_1 = req_with_same_plaintext_1->EncapsulateAndSerialize();
+  auto req_with_same_plaintext_2 =
+      CreateClientObliviousRequestWithSeedForTesting(
+          "same plaintext", GetHpkePublicKey(), ohttp_key_config, GetSeed());
+  ASSERT_TRUE(req_with_same_plaintext_2.ok());
+  auto ciphertext_2 = req_with_same_plaintext_2->EncapsulateAndSerialize();
+  EXPECT_EQ(ciphertext_1, ciphertext_2);
+}
+
+TEST(ObliviousHttpRequest,
+     TestSeededEncapsulatedKeyDifferentPlaintextsDifferentCiphertexts) {
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(8, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  auto req_with_different_plaintext_1 =
+      CreateClientObliviousRequestWithSeedForTesting(
+          "different 1", GetHpkePublicKey(), ohttp_key_config, GetSeed());
+  ASSERT_TRUE(req_with_different_plaintext_1.ok());
+  auto ciphertext_1 = req_with_different_plaintext_1->EncapsulateAndSerialize();
+  auto req_with_different_plaintext_2 =
+      CreateClientObliviousRequestWithSeedForTesting(
+          "different 2", GetHpkePublicKey(), ohttp_key_config, GetSeed());
+  ASSERT_TRUE(req_with_different_plaintext_2.ok());
+  auto ciphertext_2 = req_with_different_plaintext_2->EncapsulateAndSerialize();
+  EXPECT_NE(ciphertext_1, ciphertext_2);
+}
+
+TEST(ObliviousHttpRequest, TestInvalidInputsOnClientSide) {
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(30, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  // Empty plaintext.
+  EXPECT_EQ(ObliviousHttpRequest::CreateClientObliviousRequest(
+                /*plaintext_payload*/ "", GetHpkePublicKey(), ohttp_key_config)
+                .status()
+                .code(),
+            absl::StatusCode::kInvalidArgument);
+  // Empty HPKE public key.
+  EXPECT_EQ(ObliviousHttpRequest::CreateClientObliviousRequest(
+                "some plaintext",
+                /*hpke_public_key*/ "", ohttp_key_config)
+                .status()
+                .code(),
+            absl::StatusCode::kInvalidArgument);
+}
+
+TEST(ObliviousHttpRequest, TestInvalidInputsOnServerSide) {
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(4, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  // Empty encrypted payload.
+  EXPECT_EQ(ObliviousHttpRequest::CreateServerObliviousRequest(
+                /*encrypted_data*/ "",
+                *(ConstructHpkeKey(GetHpkePrivateKey(), ohttp_key_config)),
+                ohttp_key_config)
+                .status()
+                .code(),
+            absl::StatusCode::kInvalidArgument);
+  // Empty EVP_HPKE_KEY struct.
+  EXPECT_EQ(ObliviousHttpRequest::CreateServerObliviousRequest(
+                absl::StrCat(ohttp_key_config.SerializeOhttpPayloadHeader(),
+                             GetSeededEncapsulatedKey(),
+                             "9f37cfed07d0111ecd2c34f794671759bcbd922a"),
+                /*gateway_key*/ {}, ohttp_key_config)
+                .status()
+                .code(),
+            absl::StatusCode::kInvalidArgument);
+}
+
+TEST(ObliviousHttpRequest, EndToEndTestForRequest) {
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(5, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  auto encapsulate = ObliviousHttpRequest::CreateClientObliviousRequest(
+      "test", GetHpkePublicKey(), ohttp_key_config);
+  ASSERT_TRUE(encapsulate.ok());
+  auto oblivious_request = encapsulate->EncapsulateAndSerialize();
+  auto decapsulate = ObliviousHttpRequest::CreateServerObliviousRequest(
+      oblivious_request,
+      *(ConstructHpkeKey(GetHpkePrivateKey(), ohttp_key_config)),
+      ohttp_key_config);
+  ASSERT_TRUE(decapsulate.ok());
+  auto decrypted = decapsulate->GetPlaintextData();
+  EXPECT_EQ(decrypted, "test");
+}
+
+}  // namespace quiche
diff --git a/quiche/oblivious_http/buffers/oblivious_http_response.cc b/quiche/oblivious_http/buffers/oblivious_http_response.cc
new file mode 100644
index 0000000..4ab7092
--- /dev/null
+++ b/quiche/oblivious_http/buffers/oblivious_http_response.cc
@@ -0,0 +1,352 @@
+#include "quiche/oblivious_http/buffers/oblivious_http_response.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "openssl/aead.h"
+#include "openssl/hkdf.h"
+#include "openssl/hpke.h"
+#include "quiche/common/platform/api/quiche_bug_tracker.h"
+#include "quiche/common/quiche_crypto_logging.h"
+#include "quiche/common/quiche_random.h"
+#include "quiche/oblivious_http/common/oblivious_http_header_key_config.h"
+
+namespace quiche {
+namespace {
+// Generate a random string.
+std::string random(QuicheRandom* quiche_random, size_t len) {
+  std::string token(len, '\0');
+  if (quiche_random == nullptr) {
+    quiche_random = QuicheRandom::GetInstance();
+  }
+  quiche_random->RandBytes(token.data(), token.size());
+  return token;
+}
+}  // namespace
+
+// Ctor.
+ObliviousHttpResponse::ObliviousHttpResponse(std::string resp_nonce,
+                                             std::string resp_ciphertext,
+                                             std::string resp_plaintext)
+    : response_nonce_(resp_nonce),
+      response_ciphertext_(resp_ciphertext),
+      response_plaintext_(resp_plaintext) {}
+
+// Response Decapsulation.
+// 1. Extract resp_nonce
+// 2. Build prk (pseudorandom key) using HKDF_Extract
+// 3. Derive aead_key using HKDF_Labeled_Expand
+// 4. Derive aead_nonce using HKDF_Labeled_Expand
+// 5. Setup AEAD context and Decrypt.
+// https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-4
+absl::StatusOr<ObliviousHttpResponse>
+ObliviousHttpResponse::CreateClientObliviousResponse(
+    absl::string_view encrypted_data,
+    ObliviousHttpRequest::Context& oblivious_http_request_context) {
+  if (oblivious_http_request_context.hpke_context_ == nullptr) {
+    return absl::FailedPreconditionError(
+        "HPKE context wasn't initialized before proceeding with this Response "
+        "Decapsulation on Client-side.");
+  }
+  size_t expected_key_len = EVP_HPKE_KEM_enc_len(
+      EVP_HPKE_CTX_kem(oblivious_http_request_context.hpke_context_.get()));
+  if (oblivious_http_request_context.encapsulated_key_.size() !=
+      expected_key_len) {
+    return absl::InvalidArgumentError(absl::StrCat(
+        "Invalid len for encapsulated_key arg. Expected:", expected_key_len,
+        " Actual:", oblivious_http_request_context.encapsulated_key_.size()));
+  }
+  if (encrypted_data.empty()) {
+    return absl::InvalidArgumentError("Empty encrypted_data input param.");
+  }
+
+  absl::StatusOr<CommonAeadParamsResult> aead_params_st =
+      GetCommonAeadParams(oblivious_http_request_context);
+  if (!aead_params_st.ok()) {
+    return aead_params_st.status();
+  }
+
+  // secret_len = [max(Nn, Nk)] where Nk and Nn are the length of AEAD
+  // key and nonce associated with HPKE context.
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-2.1
+  size_t secret_len = aead_params_st.value().secret_len;
+  if (encrypted_data.size() < secret_len) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid input response. Failed to parse required minimum "
+                     "expected_len=",
+                     secret_len, " bytes."));
+  }
+  // Extract response_nonce. Step 2
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-2.2
+  absl::string_view response_nonce = encrypted_data.substr(0, secret_len);
+  absl::string_view encrypted_response = encrypted_data.substr(secret_len);
+
+  // Steps (1, 3 to 5) + AEAD context SetUp before 6th step is performed in
+  // CommonOperations.
+  auto common_ops_st = CommonOperationsToEncapDecap(
+      response_nonce, oblivious_http_request_context,
+      aead_params_st.value().aead_key_len,
+      aead_params_st.value().aead_nonce_len, aead_params_st.value().secret_len);
+  if (!common_ops_st.ok()) {
+    return common_ops_st.status();
+  }
+
+  std::string decrypted(encrypted_response.size(), '\0');
+  size_t decrypted_len;
+
+  // Decrypt with initialized AEAD context.
+  // response, error = Open(aead_key, aead_nonce, "", ct)
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-6
+  if (!EVP_AEAD_CTX_open(
+          common_ops_st.value().aead_ctx.get(),
+          reinterpret_cast<uint8_t*>(decrypted.data()), &decrypted_len,
+          decrypted.size(),
+          reinterpret_cast<const uint8_t*>(
+              common_ops_st.value().aead_nonce.data()),
+          aead_params_st.value().aead_nonce_len,
+          reinterpret_cast<const uint8_t*>(encrypted_response.data()),
+          encrypted_response.size(), nullptr, 0)) {
+    return SslErrorAsStatus(
+        "Failed to decrypt the response with derived AEAD key and nonce.");
+  }
+  decrypted.resize(decrypted_len);
+  ObliviousHttpResponse oblivious_response(
+      std::string(response_nonce), std::string(encrypted_response), decrypted);
+  return std::move(oblivious_response);
+}
+
+// Response Encapsulation.
+// Follows the Ohttp spec section-4.2 (Encapsulation of Responses) Ref
+// https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2
+// Use HPKE context from BoringSSL to export a secret and use it to Seal (AKA
+// encrypt) the response back to the Sender(client)
+absl::StatusOr<ObliviousHttpResponse>
+ObliviousHttpResponse::CreateServerObliviousResponse(
+    absl::string_view plaintext_payload,
+    ObliviousHttpRequest::Context& oblivious_http_request_context,
+    QuicheRandom* quiche_random) {
+  if (oblivious_http_request_context.hpke_context_ == nullptr) {
+    return absl::FailedPreconditionError(
+        "HPKE context wasn't initialized before proceeding with this Response "
+        "Encapsulation on Server-side.");
+  }
+  size_t expected_key_len = EVP_HPKE_KEM_enc_len(
+      EVP_HPKE_CTX_kem(oblivious_http_request_context.hpke_context_.get()));
+  if (oblivious_http_request_context.encapsulated_key_.size() !=
+      expected_key_len) {
+    return absl::InvalidArgumentError(absl::StrCat(
+        "Invalid len for encapsulated_key arg. Expected:", expected_key_len,
+        " Actual:", oblivious_http_request_context.encapsulated_key_.size()));
+  }
+  if (plaintext_payload.empty()) {
+    return absl::InvalidArgumentError("Empty plaintext_payload input param.");
+  }
+  absl::StatusOr<CommonAeadParamsResult> aead_params_st =
+      GetCommonAeadParams(oblivious_http_request_context);
+  if (!aead_params_st.ok()) {
+    return aead_params_st.status();
+  }
+  // response_nonce = random(max(Nn, Nk))
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-2.2
+  std::string response_nonce(random(quiche_random, aead_params_st->secret_len));
+
+  // Steps (1, 3 to 5) + AEAD context SetUp before 6th step is performed in
+  // CommonOperations.
+  auto common_ops_st = CommonOperationsToEncapDecap(
+      response_nonce, oblivious_http_request_context,
+      aead_params_st.value().aead_key_len,
+      aead_params_st.value().aead_nonce_len, aead_params_st.value().secret_len);
+  if (!common_ops_st.ok()) {
+    return common_ops_st.status();
+  }
+
+  // ct = Seal(aead_key, aead_nonce, "", response)
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-2.6
+  std::string ciphertext(
+      plaintext_payload.size() +
+          EVP_AEAD_max_overhead(EVP_HPKE_AEAD_aead(EVP_HPKE_CTX_aead(
+              oblivious_http_request_context.hpke_context_.get()))),
+      '\0');
+  size_t ciphertext_len;
+  if (!EVP_AEAD_CTX_seal(
+          common_ops_st.value().aead_ctx.get(),
+          reinterpret_cast<uint8_t*>(ciphertext.data()), &ciphertext_len,
+          ciphertext.size(),
+          reinterpret_cast<const uint8_t*>(
+              common_ops_st.value().aead_nonce.data()),
+          aead_params_st.value().aead_nonce_len,
+          reinterpret_cast<const uint8_t*>(plaintext_payload.data()),
+          plaintext_payload.size(), nullptr, 0)) {
+    return SslErrorAsStatus(
+        "Failed to encrypt the payload with derived AEAD key.");
+  }
+  ciphertext.resize(ciphertext_len);
+  if (response_nonce.empty() || ciphertext.empty()) {
+    return absl::InternalError(absl::StrCat(
+        "ObliviousHttpResponse Object wasn't initialized with required fields.",
+        (response_nonce.empty() ? "Generated nonce is empty." : ""),
+        (ciphertext.empty() ? "Generated Encrypted payload is empty." : "")));
+  }
+  ObliviousHttpResponse oblivious_response(response_nonce, ciphertext,
+                                           std::string(plaintext_payload));
+  return std::move(oblivious_response);
+}
+
+// Serialize.
+// enc_response = concat(response_nonce, ct)
+// https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-4
+std::string ObliviousHttpResponse::EncapsulateAndSerialize() const {
+  return absl::StrCat(response_nonce_, response_ciphertext_);
+}
+
+// Decrypted blob.
+absl::string_view ObliviousHttpResponse::GetPlaintextData() const {
+  return response_plaintext_;
+}
+
+// This section mainly deals with common operations performed by both
+// Sender(client) and Receiver(gateway) on ObliviousHttpResponse.
+
+absl::StatusOr<ObliviousHttpResponse::CommonAeadParamsResult>
+ObliviousHttpResponse::GetCommonAeadParams(
+    ObliviousHttpRequest::Context& oblivious_http_request_context) {
+  const EVP_AEAD* evp_hpke_aead = EVP_HPKE_AEAD_aead(
+      EVP_HPKE_CTX_aead(oblivious_http_request_context.hpke_context_.get()));
+  if (evp_hpke_aead == nullptr) {
+    return absl::FailedPreconditionError(
+        "Key Configuration not supported by HPKE AEADs. Check your key "
+        "config.");
+  }
+  // Nk = [AEAD key len], is determined by BoringSSL.
+  const size_t aead_key_len = EVP_AEAD_key_length(evp_hpke_aead);
+  // Nn = [AEAD nonce len], is determined by BoringSSL.
+  const size_t aead_nonce_len = EVP_AEAD_nonce_length(evp_hpke_aead);
+  const size_t secret_len = std::max(aead_key_len, aead_nonce_len);
+  CommonAeadParamsResult result{evp_hpke_aead, aead_key_len, aead_nonce_len,
+                                secret_len};
+  return std::move(result);
+}
+
+// Common Steps of AEAD key and AEAD nonce derivation common to both
+// client(decapsulation) & Gateway(encapsulation) in handling
+// Oblivious-Response. Ref Steps (1, 3-to-5, and setting up AEAD context in
+// preparation for 6th step's Seal/Open) in spec.
+// https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-4
+absl::StatusOr<ObliviousHttpResponse::CommonOperationsResult>
+ObliviousHttpResponse::CommonOperationsToEncapDecap(
+    absl::string_view response_nonce,
+    ObliviousHttpRequest::Context& oblivious_http_request_context,
+    const size_t aead_key_len, const size_t aead_nonce_len,
+    const size_t secret_len) {
+  if (response_nonce.empty()) {
+    return absl::InvalidArgumentError("Invalid input params.");
+  }
+  // secret = context.Export("message/bhttp response", Nk)
+  // Export secret of len [max(Nn, Nk)] where Nk and Nn are the length of AEAD
+  // key and nonce associated with context.
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-2.1
+  std::string secret(secret_len, '\0');
+  absl::string_view resp_label =
+      ObliviousHttpHeaderKeyConfig::kOhttpResponseLabel;
+  if (!EVP_HPKE_CTX_export(oblivious_http_request_context.hpke_context_.get(),
+                           reinterpret_cast<uint8_t*>(secret.data()),
+                           secret.size(),
+                           reinterpret_cast<const uint8_t*>(resp_label.data()),
+                           resp_label.size())) {
+    return SslErrorAsStatus("Failed to export secret.");
+  }
+
+  // salt = concat(enc, response_nonce)
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-2.3
+  std::string salt = absl::StrCat(
+      oblivious_http_request_context.encapsulated_key_, response_nonce);
+
+  // prk = Extract(salt, secret)
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-2.3
+  std::string pseudorandom_key(EVP_MAX_MD_SIZE, '\0');
+  size_t prk_len;
+  auto evp_md = EVP_HPKE_KDF_hkdf_md(
+      EVP_HPKE_CTX_kdf(oblivious_http_request_context.hpke_context_.get()));
+  if (evp_md == nullptr) {
+    QUICHE_BUG(Invalid Key Configuration
+               : Unsupported BoringSSL HPKE KDFs)
+        << "Update KeyConfig to support only BoringSSL HKDFs.";
+    return absl::FailedPreconditionError(
+        "Key Configuration not supported by BoringSSL HPKE KDFs. Check your "
+        "Key "
+        "Config.");
+  }
+  if (!HKDF_extract(
+          reinterpret_cast<uint8_t*>(pseudorandom_key.data()), &prk_len, evp_md,
+          reinterpret_cast<const uint8_t*>(secret.data()), secret_len,
+          reinterpret_cast<const uint8_t*>(salt.data()), salt.size())) {
+    return SslErrorAsStatus(
+        "Failed to derive pesudorandom key from salt and secret.");
+  }
+  pseudorandom_key.resize(prk_len);
+
+  // aead_key = Expand(prk, "key", Nk)
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-2.4
+  std::string aead_key(aead_key_len, '\0');
+  absl::string_view hkdf_info = ObliviousHttpHeaderKeyConfig::kKeyHkdfInfo;
+  // All currently supported KDFs are HKDF-based. See CheckKdfId in
+  // `ObliviousHttpHeaderKeyConfig`.
+  if (!HKDF_expand(reinterpret_cast<uint8_t*>(aead_key.data()), aead_key_len,
+                   evp_md,
+                   reinterpret_cast<const uint8_t*>(pseudorandom_key.data()),
+                   prk_len, reinterpret_cast<const uint8_t*>(hkdf_info.data()),
+                   hkdf_info.size())) {
+    return SslErrorAsStatus(
+        "Failed to expand AEAD key using pseudorandom key(prk).");
+  }
+
+  // aead_nonce = Expand(prk, "nonce", Nn)
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.2-2.5
+  std::string aead_nonce(aead_nonce_len, '\0');
+  hkdf_info = ObliviousHttpHeaderKeyConfig::kNonceHkdfInfo;
+  // All currently supported KDFs are HKDF-based. See CheckKdfId in
+  // `ObliviousHttpHeaderKeyConfig`.
+  if (!HKDF_expand(reinterpret_cast<uint8_t*>(aead_nonce.data()),
+                   aead_nonce_len, evp_md,
+                   reinterpret_cast<const uint8_t*>(pseudorandom_key.data()),
+                   prk_len, reinterpret_cast<const uint8_t*>(hkdf_info.data()),
+                   hkdf_info.size())) {
+    return SslErrorAsStatus(
+        "Failed to expand AEAD nonce using pseudorandom key(prk).");
+  }
+
+  const EVP_AEAD* evp_hpke_aead = EVP_HPKE_AEAD_aead(
+      EVP_HPKE_CTX_aead(oblivious_http_request_context.hpke_context_.get()));
+  if (evp_hpke_aead == nullptr) {
+    return absl::FailedPreconditionError(
+        "Key Configuration not supported by HPKE AEADs. Check your key "
+        "config.");
+  }
+
+  // Setup AEAD context for subsequent Seal/Open operation in response handling.
+  bssl::UniquePtr<EVP_AEAD_CTX> aead_ctx(EVP_AEAD_CTX_new(
+      evp_hpke_aead, reinterpret_cast<const uint8_t*>(aead_key.data()),
+      aead_key.size(), 0));
+  if (aead_ctx == nullptr) {
+    return SslErrorAsStatus("Failed to initialize AEAD context.");
+  }
+  if (!EVP_AEAD_CTX_init(aead_ctx.get(), evp_hpke_aead,
+                         reinterpret_cast<const uint8_t*>(aead_key.data()),
+                         aead_key.size(), 0, nullptr)) {
+    return SslErrorAsStatus(
+        "Failed to initialize AEAD context with derived key.");
+  }
+  CommonOperationsResult result{std::move(aead_ctx), std::move(aead_nonce)};
+  return std::move(result);
+}
+
+}  // namespace quiche
diff --git a/quiche/oblivious_http/buffers/oblivious_http_response.h b/quiche/oblivious_http/buffers/oblivious_http_response.h
new file mode 100644
index 0000000..3ebd25d
--- /dev/null
+++ b/quiche/oblivious_http/buffers/oblivious_http_response.h
@@ -0,0 +1,97 @@
+#ifndef QUICHE_OBLIVIOUS_HTTP_BUFFERS_OBLIVIOUS_HTTP_RESPONSE_H_
+#define QUICHE_OBLIVIOUS_HTTP_BUFFERS_OBLIVIOUS_HTTP_RESPONSE_H_
+
+#include <stddef.h>
+
+#include <string>
+
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "quiche/common/quiche_random.h"
+#include "quiche/oblivious_http/buffers/oblivious_http_request.h"
+
+namespace quiche {
+
+class QUICHE_EXPORT_PRIVATE ObliviousHttpResponse {
+ public:
+  // Parse and decrypt the OHttp response using ObliviousHttpContext context obj
+  // that was returned from `CreateClientObliviousRequest` method. On success,
+  // returns obj that callers will use to `GetDecryptedMessage`.
+  // @params: Note that `oblivious_http_request_context` is required to stay
+  // alive only for the lifetime of this factory method call.
+  static absl::StatusOr<ObliviousHttpResponse> CreateClientObliviousResponse(
+      absl::string_view encrypted_data,
+      ObliviousHttpRequest::Context& oblivious_http_request_context);
+
+  // Encrypt the input param `plaintext_payload` and create OHttp response using
+  // ObliviousHttpContext context obj that was returned from
+  // `CreateServerObliviousRequest` method. On success, returns obj that callers
+  // will use to `Serialize` OHttp response. Generic Usecase : server-side calls
+  // this method in the context of Response.
+  // @params: Note that `oblivious_http_request_context` is required to stay
+  // alive only for the lifetime of this factory method call.
+  // @params: If callers do not provide `quiche_random`, it will be initialized
+  // to default supplied `QuicheRandom::GetInstance()`. It's recommended that
+  // callers initialize `QuicheRandom* quiche_random` as a Singleton instance
+  // within their code and pass in the same, in order to have optimized random
+  // string generation. `quiche_random` is required to stay alive only for the
+  // lifetime of this factory method call.
+  static absl::StatusOr<ObliviousHttpResponse> CreateServerObliviousResponse(
+      absl::string_view plaintext_payload,
+      ObliviousHttpRequest::Context& oblivious_http_request_context,
+      QuicheRandom* quiche_random = nullptr);
+
+  // Copyable.
+  ObliviousHttpResponse(const ObliviousHttpResponse& other) = default;
+  ObliviousHttpResponse& operator=(const ObliviousHttpResponse& other) =
+      default;
+
+  // Movable.
+  ObliviousHttpResponse(ObliviousHttpResponse&& other) = default;
+  ObliviousHttpResponse& operator=(ObliviousHttpResponse&& other) = default;
+
+  ~ObliviousHttpResponse() = default;
+
+  // Generic Usecase : server-side calls this method in the context of Response
+  // to serialize OHTTP response that will be returned to client-side.
+  // Returns serialized OHTTP response bytestring.
+  std::string EncapsulateAndSerialize() const;
+
+  absl::string_view GetPlaintextData() const;
+
+ private:
+  struct CommonAeadParamsResult {
+    const EVP_AEAD* evp_hpke_aead;
+    const size_t aead_key_len;
+    const size_t aead_nonce_len;
+    const size_t secret_len;
+  };
+
+  struct CommonOperationsResult {
+    bssl::UniquePtr<EVP_AEAD_CTX> aead_ctx;
+    const std::string aead_nonce;
+  };
+
+  explicit ObliviousHttpResponse(std::string resp_nonce,
+                                 std::string resp_ciphertext,
+                                 std::string resp_plaintext);
+
+  // Determines AEAD key len(Nk), AEAD nonce len(Nn) based on HPKE context and
+  // further estimates secret_len = std::max(Nk, Nn)
+  static absl::StatusOr<CommonAeadParamsResult> GetCommonAeadParams(
+      ObliviousHttpRequest::Context& oblivious_http_request_context);
+  // Performs operations related to response handling that are common between
+  // client and server.
+  static absl::StatusOr<CommonOperationsResult> CommonOperationsToEncapDecap(
+      absl::string_view response_nonce,
+      ObliviousHttpRequest::Context& oblivious_http_request_context,
+      const size_t aead_key_len, const size_t aead_nonce_len,
+      const size_t secret_len);
+  std::string response_nonce_;
+  std::string response_ciphertext_;
+  std::string response_plaintext_;
+};
+
+}  // namespace quiche
+
+#endif  // QUICHE_OBLIVIOUS_HTTP_BUFFERS_OBLIVIOUS_HTTP_RESPONSE_H_
diff --git a/quiche/oblivious_http/buffers/oblivious_http_response_test.cc b/quiche/oblivious_http/buffers/oblivious_http_response_test.cc
new file mode 100644
index 0000000..775ba56
--- /dev/null
+++ b/quiche/oblivious_http/buffers/oblivious_http_response_test.cc
@@ -0,0 +1,220 @@
+#include "quiche/oblivious_http/buffers/oblivious_http_response.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/status/statusor.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "openssl/hpke.h"
+#include "quiche/common/platform/api/quiche_test.h"
+#include "quiche/oblivious_http/buffers/oblivious_http_request.h"
+
+namespace quiche {
+
+absl::StatusOr<ObliviousHttpRequest>
+CreateClientObliviousRequestWithSeedForTesting(
+    absl::string_view plaintext_payload, absl::string_view hpke_public_key,
+    const ObliviousHttpHeaderKeyConfig &ohttp_key_config,
+    absl::string_view seed) {
+  return ObliviousHttpRequest::EncapsulateWithSeed(
+      plaintext_payload, hpke_public_key, ohttp_key_config, seed);
+}
+
+namespace {
+std::string GetHpkePrivateKey() {
+  absl::string_view hpke_key_hex =
+      "b77431ecfa8f4cfc30d6e467aafa06944dffe28cb9dd1409e33a3045f5adc8a1";
+  return absl::HexStringToBytes(hpke_key_hex);
+}
+
+std::string GetHpkePublicKey() {
+  absl::string_view public_key =
+      "6d21cfe09fbea5122f9ebc2eb2a69fcc4f06408cd54aac934f012e76fcdcef62";
+  return absl::HexStringToBytes(public_key);
+}
+
+std::string GetSeed() {
+  absl::string_view seed =
+      "52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736";
+  return absl::HexStringToBytes(seed);
+}
+
+std::string GetSeededEncapsulatedKey() {
+  absl::string_view encapsulated_key =
+      "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431";
+  return absl::HexStringToBytes(encapsulated_key);
+}
+
+const ObliviousHttpHeaderKeyConfig GetOhttpKeyConfig(uint8_t key_id,
+                                                     uint16_t kem_id,
+                                                     uint16_t kdf_id,
+                                                     uint16_t aead_id) {
+  auto ohttp_key_config =
+      ObliviousHttpHeaderKeyConfig::Create(key_id, kem_id, kdf_id, aead_id);
+  EXPECT_TRUE(ohttp_key_config.ok());
+  return std::move(ohttp_key_config.value());
+}
+
+bssl::UniquePtr<EVP_HPKE_CTX> GetSeededClientContext(uint8_t key_id,
+                                                     uint16_t kem_id,
+                                                     uint16_t kdf_id,
+                                                     uint16_t aead_id) {
+  bssl::UniquePtr<EVP_HPKE_CTX> client_ctx(EVP_HPKE_CTX_new());
+  std::string encapsulated_key(EVP_HPKE_MAX_ENC_LENGTH, '\0');
+  size_t enc_len;
+  std::string info = GetOhttpKeyConfig(key_id, kem_id, kdf_id, aead_id)
+                         .SerializeRecipientContextInfo();
+
+  EXPECT_TRUE(EVP_HPKE_CTX_setup_sender_with_seed_for_testing(
+      client_ctx.get(), reinterpret_cast<uint8_t *>(encapsulated_key.data()),
+      &enc_len, encapsulated_key.size(), EVP_hpke_x25519_hkdf_sha256(),
+      EVP_hpke_hkdf_sha256(), EVP_hpke_aes_256_gcm(),
+      reinterpret_cast<const uint8_t *>(GetHpkePublicKey().data()),
+      GetHpkePublicKey().size(), reinterpret_cast<const uint8_t *>(info.data()),
+      info.size(), reinterpret_cast<const uint8_t *>(GetSeed().data()),
+      GetSeed().size()));
+  encapsulated_key.resize(enc_len);
+  EXPECT_EQ(encapsulated_key, GetSeededEncapsulatedKey());
+  return client_ctx;
+}
+
+bssl::UniquePtr<EVP_HPKE_KEY> ConstructHpkeKey(
+    absl::string_view hpke_key,
+    const ObliviousHttpHeaderKeyConfig &ohttp_key_config) {
+  bssl::UniquePtr<EVP_HPKE_KEY> bssl_hpke_key(EVP_HPKE_KEY_new());
+  EXPECT_NE(bssl_hpke_key, nullptr);
+  EXPECT_TRUE(EVP_HPKE_KEY_init(
+      bssl_hpke_key.get(), ohttp_key_config.GetHpkeKem(),
+      reinterpret_cast<const uint8_t *>(hpke_key.data()), hpke_key.size()));
+  return bssl_hpke_key;
+}
+
+const ObliviousHttpRequest SetUpObliviousHttpContext(
+    uint8_t key_id, uint16_t kem_id, uint16_t kdf_id, uint16_t aead_id,
+    absl::string_view plaintext) {
+  auto ohttp_key_config = GetOhttpKeyConfig(key_id, kem_id, kdf_id, aead_id);
+  auto client_request_encapsulate =
+      CreateClientObliviousRequestWithSeedForTesting(
+          plaintext, GetHpkePublicKey(), ohttp_key_config, GetSeed());
+  EXPECT_TRUE(client_request_encapsulate.ok());
+  auto oblivious_request =
+      client_request_encapsulate->EncapsulateAndSerialize();
+  auto server_request_decapsulate =
+      ObliviousHttpRequest::CreateServerObliviousRequest(
+          oblivious_request,
+          *(ConstructHpkeKey(GetHpkePrivateKey(), ohttp_key_config)),
+          ohttp_key_config);
+  EXPECT_TRUE(server_request_decapsulate.ok());
+  return std::move(server_request_decapsulate.value());
+}
+
+// QuicheRandom implementation.
+// Just fills the buffer with repeated chars that's initialized in seed.
+class TestQuicheRandom : public QuicheRandom {
+ public:
+  TestQuicheRandom(char seed) : seed_(seed) {}
+  ~TestQuicheRandom() override {}
+
+  void RandBytes(void *data, size_t len) override { memset(data, seed_, len); }
+
+  uint64_t RandUint64() override {
+    uint64_t random_int;
+    memset(&random_int, seed_, sizeof(random_int));
+    return random_int;
+  }
+
+  void InsecureRandBytes(void *data, size_t len) override {
+    return RandBytes(data, len);
+  }
+  uint64_t InsecureRandUint64() override { return RandUint64(); }
+
+ private:
+  char seed_;
+};
+
+size_t GetResponseNonceLength(const EVP_HPKE_CTX &hpke_context) {
+  EXPECT_NE(&hpke_context, nullptr);
+  const EVP_AEAD *evp_hpke_aead =
+      EVP_HPKE_AEAD_aead(EVP_HPKE_CTX_aead(&hpke_context));
+  EXPECT_NE(evp_hpke_aead, nullptr);
+  // Nk = [AEAD key len], is determined by BSSL.
+  const size_t aead_key_len = EVP_AEAD_key_length(evp_hpke_aead);
+  // Nn = [AEAD nonce len], is determined by BSSL.
+  const size_t aead_nonce_len = EVP_AEAD_nonce_length(evp_hpke_aead);
+  const size_t secret_len = std::max(aead_key_len, aead_nonce_len);
+  return secret_len;
+}
+
+TEST(ObliviousHttpResponse, TestDecapsulateReceivedResponse) {
+  // Construct encrypted payload with plaintext: "test response"
+  absl::string_view encrypted_response =
+      "39d5b03c02c97e216df444e4681007105974d4df1585aae05e7b53f3ccdb55d51f711d48"
+      "eeefbc1a555d6d928e35df33fd23c23846fa7b083e30692f7b";
+  auto decapsulated = ObliviousHttpResponse::CreateClientObliviousResponse(
+      absl::HexStringToBytes(encrypted_response),
+      *(SetUpObliviousHttpContext(4, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                                  EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM,
+                                  "test")
+            .oblivious_http_request_context()));
+  EXPECT_TRUE(decapsulated.ok());
+  auto decrypted = decapsulated->GetPlaintextData();
+  EXPECT_EQ(decrypted, "test response");
+}
+}  // namespace
+
+TEST(ObliviousHttpResponse, EndToEndTestForResponse) {
+  auto oblivious_ctx = ObliviousHttpRequest::Context(
+      GetSeededClientContext(5, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                             EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM),
+      GetSeededEncapsulatedKey());
+  auto server_response_encapsulate =
+      ObliviousHttpResponse::CreateServerObliviousResponse("test response",
+                                                           oblivious_ctx);
+  EXPECT_TRUE(server_response_encapsulate.ok());
+  auto oblivious_response =
+      server_response_encapsulate->EncapsulateAndSerialize();
+  auto client_response_encapsulate =
+      ObliviousHttpResponse::CreateClientObliviousResponse(oblivious_response,
+                                                           oblivious_ctx);
+  auto decrypted = client_response_encapsulate->GetPlaintextData();
+  EXPECT_EQ(decrypted, "test response");
+}
+
+TEST(ObliviousHttpResponse, TestEncapsulateWithQuicheRandom) {
+  auto random = TestQuicheRandom('z');
+  auto server_seeded_request = SetUpObliviousHttpContext(
+      6, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
+      EVP_HPKE_AES_256_GCM, "test");
+  auto server_response_encapsulate =
+      ObliviousHttpResponse::CreateServerObliviousResponse(
+          "test response",
+          *(server_seeded_request.oblivious_http_request_context()), &random);
+  EXPECT_TRUE(server_response_encapsulate.ok());
+  std::string response_nonce =
+      server_response_encapsulate->EncapsulateAndSerialize().substr(
+          0, GetResponseNonceLength(
+                 *(server_seeded_request.oblivious_http_request_context()
+                       ->hpke_context_)));
+  EXPECT_EQ(
+      response_nonce,
+      std::string(GetResponseNonceLength(
+                      *(server_seeded_request.oblivious_http_request_context()
+                            ->hpke_context_)),
+                  'z'));
+  absl::string_view expected_encrypted_response =
+      "2a3271ac4e6a501f51d0264d3dd7d0bc8a06973b58e89c26d6dac06144";
+  EXPECT_EQ(server_response_encapsulate->EncapsulateAndSerialize().substr(
+                GetResponseNonceLength(
+                    *(server_seeded_request.oblivious_http_request_context()
+                          ->hpke_context_))),
+            absl::HexStringToBytes(expected_encrypted_response));
+}
+
+}  // namespace quiche
diff --git a/quiche/oblivious_http/common/oblivious_http_header_key_config.cc b/quiche/oblivious_http/common/oblivious_http_header_key_config.cc
new file mode 100644
index 0000000..d378b4c
--- /dev/null
+++ b/quiche/oblivious_http/common/oblivious_http_header_key_config.cc
@@ -0,0 +1,282 @@
+#include "quiche/oblivious_http/common/oblivious_http_header_key_config.h"
+
+#include <cstdint>
+
+#include "absl/memory/memory.h"
+#include "absl/status/status.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_logging.h"
+#include "quiche/common/quiche_data_writer.h"
+#include "quiche/common/quiche_endian.h"
+
+namespace quiche {
+namespace {
+
+absl::StatusOr<const EVP_HPKE_KEM*> CheckKemId(uint16_t kem_id) {
+  switch (kem_id) {
+    case EVP_HPKE_DHKEM_X25519_HKDF_SHA256:
+      return EVP_hpke_x25519_hkdf_sha256();
+    default:
+      return absl::UnimplementedError("No support for this KEM ID.");
+  }
+}
+
+absl::StatusOr<const EVP_HPKE_KDF*> CheckKdfId(uint16_t kdf_id) {
+  switch (kdf_id) {
+    case EVP_HPKE_HKDF_SHA256:
+      return EVP_hpke_hkdf_sha256();
+    default:
+      return absl::UnimplementedError("No support for this KDF ID.");
+  }
+}
+
+absl::StatusOr<const EVP_HPKE_AEAD*> CheckAeadId(uint16_t aead_id) {
+  switch (aead_id) {
+    case EVP_HPKE_AES_128_GCM:
+      return EVP_hpke_aes_128_gcm();
+    case EVP_HPKE_AES_256_GCM:
+      return EVP_hpke_aes_256_gcm();
+    case EVP_HPKE_CHACHA20_POLY1305:
+      return EVP_hpke_chacha20_poly1305();
+    default:
+      return absl::UnimplementedError("No support for this AEAD ID.");
+  }
+}
+
+}  // namespace
+
+ObliviousHttpHeaderKeyConfig::ObliviousHttpHeaderKeyConfig(uint8_t key_id,
+                                                           uint16_t kem_id,
+                                                           uint16_t kdf_id,
+                                                           uint16_t aead_id)
+    : key_id_(key_id), kem_id_(kem_id), kdf_id_(kdf_id), aead_id_(aead_id) {}
+
+absl::StatusOr<ObliviousHttpHeaderKeyConfig>
+ObliviousHttpHeaderKeyConfig::Create(uint8_t key_id, uint16_t kem_id,
+                                     uint16_t kdf_id, uint16_t aead_id) {
+  ObliviousHttpHeaderKeyConfig instance(key_id, kem_id, kdf_id, aead_id);
+  auto is_config_ok = instance.ValidateKeyConfig();
+  if (!is_config_ok.ok()) {
+    return is_config_ok;
+  }
+  return instance;
+}
+
+absl::Status ObliviousHttpHeaderKeyConfig::ValidateKeyConfig() const {
+  auto supported_kem = CheckKemId(kem_id_);
+  if (!supported_kem.ok()) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Unsupported KEM ID:", kem_id_));
+  }
+  auto supported_kdf = CheckKdfId(kdf_id_);
+  if (!supported_kdf.ok()) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Unsupported KDF ID:", kdf_id_));
+  }
+  auto supported_aead = CheckAeadId(aead_id_);
+  if (!supported_aead.ok()) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Unsupported AEAD ID:", aead_id_));
+  }
+  return absl::OkStatus();
+}
+
+const EVP_HPKE_KEM* ObliviousHttpHeaderKeyConfig::GetHpkeKem() const {
+  auto kem = CheckKemId(kem_id_);
+  QUICHE_CHECK_OK(kem.status());
+  return kem.value();
+}
+const EVP_HPKE_KDF* ObliviousHttpHeaderKeyConfig::GetHpkeKdf() const {
+  auto kdf = CheckKdfId(kdf_id_);
+  QUICHE_CHECK_OK(kdf.status());
+  return kdf.value();
+}
+const EVP_HPKE_AEAD* ObliviousHttpHeaderKeyConfig::GetHpkeAead() const {
+  auto aead = CheckAeadId(aead_id_);
+  QUICHE_CHECK_OK(aead.status());
+  return aead.value();
+}
+
+std::string ObliviousHttpHeaderKeyConfig::SerializeRecipientContextInfo()
+    const {
+  uint8_t zero_byte = 0x00;
+  int buf_len = kOhttpRequestLabel.size() + kHeaderLength + sizeof(zero_byte);
+  std::string info(buf_len, '\0');
+  QuicheDataWriter writer(info.size(), info.data());
+  QUICHE_CHECK(writer.WriteStringPiece(kOhttpRequestLabel));
+  QUICHE_CHECK(writer.WriteUInt8(zero_byte));  // Zero byte.
+  QUICHE_CHECK(writer.WriteUInt8(key_id_));
+  QUICHE_CHECK(writer.WriteUInt16(kem_id_));
+  QUICHE_CHECK(writer.WriteUInt16(kdf_id_));
+  QUICHE_CHECK(writer.WriteUInt16(aead_id_));
+  return info;
+}
+
+/**
+ * Follows IETF Ohttp spec, section 4.1 (Encapsulation of Requests).
+ * https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.1-10
+ */
+absl::Status ObliviousHttpHeaderKeyConfig::ParseOhttpPayloadHeader(
+    absl::string_view payload_bytes) const {
+  if (payload_bytes.empty()) {
+    return absl::InvalidArgumentError("Empty request payload.");
+  }
+  QuicheDataReader reader(payload_bytes);
+  uint8_t key_id;
+  if (!reader.ReadUInt8(&key_id)) {
+    return absl::InvalidArgumentError("Failed to read key_id from header.");
+  }
+  if (key_id != key_id_) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("KeyID in request:", static_cast<uint16_t>(key_id),
+                     " doesn't match with server's public key "
+                     "configuration KeyID:",
+                     static_cast<uint16_t>(key_id_)));
+  }
+  uint16_t kem_id;
+  if (!reader.ReadUInt16(&kem_id)) {
+    return absl::InvalidArgumentError("Failed to read kem_id from header.");
+  }
+  if (kem_id != kem_id_) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Received Invalid kemID:", kem_id, " Expected:", kem_id_));
+  }
+  uint16_t kdf_id;
+  if (!reader.ReadUInt16(&kdf_id)) {
+    return absl::InvalidArgumentError("Failed to read kdf_id from header.");
+  }
+  if (kdf_id != kdf_id_) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Received Invalid kdfID:", kdf_id, " Expected:", kdf_id_));
+  }
+  uint16_t aead_id;
+  if (!reader.ReadUInt16(&aead_id)) {
+    return absl::InvalidArgumentError("Failed to read aead_id from header.");
+  }
+  if (aead_id != aead_id_) {
+    return absl::InvalidArgumentError(absl::StrCat(
+        "Received Invalid aeadID:", aead_id, " Expected:", aead_id_));
+  }
+  return absl::OkStatus();
+}
+
+absl::StatusOr<uint8_t>
+ObliviousHttpHeaderKeyConfig::ParseKeyIdFromObliviousHttpRequestPayload(
+    absl::string_view payload_bytes) {
+  if (payload_bytes.empty()) {
+    return absl::InvalidArgumentError("Empty request payload.");
+  }
+  QuicheDataReader reader(payload_bytes);
+  uint8_t key_id;
+  if (!reader.ReadUInt8(&key_id)) {
+    return absl::InvalidArgumentError("Failed to read key_id from payload.");
+  }
+  return key_id;
+}
+
+std::string ObliviousHttpHeaderKeyConfig::SerializeOhttpPayloadHeader() const {
+  int buf_len =
+      sizeof(key_id_) + sizeof(kem_id_) + sizeof(kdf_id_) + sizeof(aead_id_);
+  std::string hdr(buf_len, '\0');
+  QuicheDataWriter writer(hdr.size(), hdr.data());
+  QUICHE_CHECK(writer.WriteUInt8(key_id_));
+  QUICHE_CHECK(writer.WriteUInt16(kem_id_));   // kemID
+  QUICHE_CHECK(writer.WriteUInt16(kdf_id_));   // kdfID
+  QUICHE_CHECK(writer.WriteUInt16(aead_id_));  // aeadID
+  return hdr;
+}
+
+absl::StatusOr<ObliviousHttpKeyConfigs>
+ObliviousHttpKeyConfigs::ParseConcatenatedKeys(absl::string_view key_config) {
+  ConfigMap configs;
+  PublicKeyMap keys;
+  auto reader = QuicheDataReader(key_config);
+  while (!reader.IsDoneReading()) {
+    absl::Status status = ReadSingleKeyConfig(reader, configs, keys);
+    if (!status.ok()) return status;
+  }
+  return ObliviousHttpKeyConfigs(std::move(configs), std::move(keys));
+}
+
+ObliviousHttpHeaderKeyConfig ObliviousHttpKeyConfigs::PreferredConfig() const {
+  // configs_ is forced to have at least one object during construction.
+  return configs_.begin()->second.front();
+}
+
+absl::StatusOr<absl::string_view> ObliviousHttpKeyConfigs::GetPublicKeyForId(
+    uint8_t key_id) const {
+  auto key = public_keys_.find(key_id);
+  if (key == public_keys_.end()) {
+    return absl::NotFoundError("No public key found for key_id");
+  }
+  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;
+  uint16_t kem_id;
+  // First byte: key_id; next two bytes: kem_id.
+  if (!reader.ReadUInt8(&key_id) || !reader.ReadUInt16(&kem_id)) {
+    return absl::InvalidArgumentError("Invalid key_config!");
+  }
+
+  // Public key length depends on the kem_id.
+  auto maybe_key_length = KeyLength(kem_id);
+  if (!maybe_key_length.ok()) {
+    return maybe_key_length.status();
+  }
+  const int key_length = maybe_key_length.value();
+  std::string key_str(key_length, '\0');
+  if (!reader.ReadBytes(key_str.data(), key_length)) {
+    return absl::InvalidArgumentError("Invalid key_config!");
+  }
+  if (!keys.insert({key_id, std::move(key_str)}).second) {
+    return absl::InvalidArgumentError("Duplicate key_id's in key_config!");
+  }
+
+  // Extract the algorithms for this public key.
+  absl::string_view alg_bytes;
+  // Read the 16-bit length, then read that many bytes into alg_bytes.
+  if (!reader.ReadStringPiece16(&alg_bytes)) {
+    return absl::InvalidArgumentError("Invalid key_config!");
+  }
+  QuicheDataReader sub_reader(alg_bytes);
+  while (!sub_reader.IsDoneReading()) {
+    uint16_t kdf_id;
+    uint16_t aead_id;
+    if (!sub_reader.ReadUInt16(&kdf_id) || !sub_reader.ReadUInt16(&aead_id)) {
+      return absl::InvalidArgumentError("Invalid key_config!");
+    }
+
+    absl::StatusOr<ObliviousHttpHeaderKeyConfig> maybe_cfg =
+        ObliviousHttpHeaderKeyConfig::Create(key_id, kem_id, kdf_id, aead_id);
+    if (!maybe_cfg.ok()) {
+      // TODO(kmg): Add support to ignore key types in the server response that
+      // aren't supported by the client.
+      return maybe_cfg.status();
+    }
+    configs[key_id].emplace_back(std::move(maybe_cfg.value()));
+  }
+  return absl::OkStatus();
+}
+
+}  // namespace quiche
diff --git a/quiche/oblivious_http/common/oblivious_http_header_key_config.h b/quiche/oblivious_http/common/oblivious_http_header_key_config.h
new file mode 100644
index 0000000..1c89bc3
--- /dev/null
+++ b/quiche/oblivious_http/common/oblivious_http_header_key_config.h
@@ -0,0 +1,150 @@
+#ifndef QUICHE_OBLIVIOUS_HTTP_COMMON_OBLIVIOUS_HTTP_HEADER_KEY_CONFIG_H_
+#define QUICHE_OBLIVIOUS_HTTP_COMMON_OBLIVIOUS_HTTP_HEADER_KEY_CONFIG_H_
+
+#include <stdint.h>
+
+#include "absl/container/btree_map.h"
+#include "absl/container/flat_hash_map.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "openssl/base.h"
+#include "quiche/common/platform/api/quiche_export.h"
+#include "quiche/common/quiche_data_reader.h"
+
+namespace quiche {
+
+class QUICHE_EXPORT_PRIVATE ObliviousHttpHeaderKeyConfig {
+ public:
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.1-4.2
+  static constexpr absl::string_view kOhttpRequestLabel =
+      "message/bhttp request";
+  static constexpr absl::string_view kOhttpResponseLabel =
+      "message/bhttp response";
+  // Length of the Oblivious HTTP header.
+  static constexpr uint32_t kHeaderLength =
+      sizeof(uint8_t) + (3 * sizeof(uint16_t));
+  static constexpr absl::string_view kKeyHkdfInfo = "key";
+  static constexpr absl::string_view kNonceHkdfInfo = "nonce";
+
+  static absl::StatusOr<ObliviousHttpHeaderKeyConfig> Create(uint8_t key_id,
+                                                             uint16_t kem_id,
+                                                             uint16_t kdf_id,
+                                                             uint16_t aead_id);
+
+  // Copyable to support stack allocated pass-by-value for trivial data members.
+  ObliviousHttpHeaderKeyConfig(const ObliviousHttpHeaderKeyConfig& other) =
+      default;
+  ObliviousHttpHeaderKeyConfig& operator=(
+      const ObliviousHttpHeaderKeyConfig& other) = default;
+
+  // Movable.
+  ObliviousHttpHeaderKeyConfig(ObliviousHttpHeaderKeyConfig&& other) = default;
+  ObliviousHttpHeaderKeyConfig& operator=(
+      ObliviousHttpHeaderKeyConfig&& other) = default;
+
+  ~ObliviousHttpHeaderKeyConfig() = default;
+
+  const EVP_HPKE_KEM* GetHpkeKem() const;
+  const EVP_HPKE_KDF* GetHpkeKdf() const;
+  const EVP_HPKE_AEAD* GetHpkeAead() const;
+
+  uint8_t GetKeyId() const { return key_id_; }
+  uint16_t GetHpkeKemId() const { return kem_id_; }
+  uint16_t GetHpkeKdfId() const { return kdf_id_; }
+  uint16_t GetHpkeAeadId() const { return aead_id_; }
+
+  // Build HPKE context info ["message/bhttp request", 0x00, keyID(1 byte),
+  // kemID(2 bytes), kdfID(2 bytes), aeadID(2 bytes)] in network byte order and
+  // return a sequence of bytes(bytestring).
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.1-10
+  std::string SerializeRecipientContextInfo() const;
+
+  // TODO(anov): Replace with ObliviousRequest/Response abstraction
+  // Parses the below Header
+  // [keyID(1 byte), kemID(2 bytes), kdfID(2 bytes), aeadID(2 bytes)]
+  // from the payload received in Ohttp Request, and verifies that these values
+  // match with the info stored in `this` namely [key_id_, kem_id_, kdf_id_,
+  // aead_id_]
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#section-4.1-7
+  absl::Status ParseOhttpPayloadHeader(absl::string_view payload_bytes) const;
+
+  // Extracts Key ID from the OHTTP Request payload.
+  static absl::StatusOr<uint8_t> ParseKeyIdFromObliviousHttpRequestPayload(
+      absl::string_view payload_bytes);
+
+  // Build Request header according to network byte order and return string.
+  std::string SerializeOhttpPayloadHeader() const;
+
+ private:
+  // Constructor
+  explicit ObliviousHttpHeaderKeyConfig(uint8_t key_id, uint16_t kem_id,
+                                        uint16_t kdf_id, uint16_t aead_id);
+
+  // Helps validate Key configuration for supported schemes.
+  absl::Status ValidateKeyConfig() const;
+
+  // Public Key configuration hosted by Gateway to facilitate Oblivious HTTP
+  // HPKE encryption.
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#name-key-configuration-encoding
+  uint8_t key_id_;
+  uint16_t kem_id_;
+  uint16_t kdf_id_;
+  uint16_t aead_id_;
+};
+
+// Contains multiple ObliviousHttpHeaderKeyConfig objects and associated private
+// keys.  An ObliviousHttpHeaderKeyConfigs object can be constructed from the
+// "Key Configuration" defined in the Oblivious HTTP spec.  Multiple key
+// configurations maybe be supported by the server.
+//
+// See https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-04.html#section-3
+// for details of the "Key Configuration" spec.
+//
+// ObliviousHttpKeyConfigs objects are immutable after construction.
+class QUICHE_EXPORT_PRIVATE ObliviousHttpKeyConfigs {
+ public:
+  // 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);
+
+  int NumKeys() const { return public_keys_.size(); }
+
+  // Returns a preferred config to use.  The preferred key is the key with
+  // the highest key_id.  If more than one configuration exists for the
+  // preferred key any configuration may be returned.
+  //
+  // These methods are useful in the (common) case where only one key
+  // configuration is supported by the server.
+  ObliviousHttpHeaderKeyConfig PreferredConfig() const;
+
+  absl::StatusOr<absl::string_view> GetPublicKeyForId(uint8_t key_id) const;
+
+  // TODO(kmg): Add methods to somehow access other non-preferred key
+  // configurations.
+
+ private:
+  using PublicKeyMap = absl::flat_hash_map<uint8_t, std::string>;
+  using ConfigMap =
+      absl::btree_map<uint8_t, std::vector<ObliviousHttpHeaderKeyConfig>,
+                      std::greater<uint8_t>>;
+
+  ObliviousHttpKeyConfigs(ConfigMap cm, PublicKeyMap km)
+      : configs_(std::move(cm)), public_keys_(std::move(km)) {}
+
+  static absl::Status ReadSingleKeyConfig(QuicheDataReader& reader,
+                                          ConfigMap& configs,
+                                          PublicKeyMap& keys);
+
+  // A mapping from key_id to ObliviousHttpHeaderKeyConfig objects for that key.
+  const ConfigMap configs_;
+
+  // A mapping from key_id to the public key for that key_id.
+  const PublicKeyMap public_keys_;
+};
+
+}  // namespace quiche
+
+#endif  // QUICHE_OBLIVIOUS_HTTP_COMMON_OBLIVIOUS_HTTP_HEADER_KEY_CONFIG_H_
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
new file mode 100644
index 0000000..0227ee4
--- /dev/null
+++ b/quiche/oblivious_http/common/oblivious_http_header_key_config_test.cc
@@ -0,0 +1,202 @@
+#include "quiche/oblivious_http/common/oblivious_http_header_key_config.h"
+
+#include <cstdint>
+
+#include "absl/strings/escaping.h"
+#include "openssl/hpke.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/platform/api/quiche_test.h"
+#include "quiche/common/quiche_data_writer.h"
+
+namespace quiche {
+namespace {
+using ::testing::AllOf;
+using ::testing::Property;
+using ::testing::StrEq;
+
+/**
+ * Build Request header.
+ */
+std::string BuildHeader(uint8_t key_id, uint16_t kem_id, uint16_t kdf_id,
+                        uint16_t aead_id) {
+  int buf_len =
+      sizeof(key_id) + sizeof(kem_id) + sizeof(kdf_id) + sizeof(aead_id);
+  std::string hdr(buf_len, '\0');
+  QuicheDataWriter writer(hdr.size(), hdr.data());
+  EXPECT_TRUE(writer.WriteUInt8(key_id));
+  EXPECT_TRUE(writer.WriteUInt16(kem_id));   // kemID
+  EXPECT_TRUE(writer.WriteUInt16(kdf_id));   // kdfID
+  EXPECT_TRUE(writer.WriteUInt16(aead_id));  // aeadID
+  return hdr;
+}
+
+TEST(ObliviousHttpHeaderKeyConfig, TestSerializeRecipientContextInfo) {
+  uint8_t key_id = 3;
+  uint16_t kem_id = EVP_HPKE_DHKEM_X25519_HKDF_SHA256;
+  uint16_t kdf_id = EVP_HPKE_HKDF_SHA256;
+  uint16_t aead_id = EVP_HPKE_AES_256_GCM;
+  absl::string_view ohttp_req_label = "message/bhttp request";
+  std::string expected(ohttp_req_label);
+  uint8_t zero_byte = 0x00;
+  int buf_len = ohttp_req_label.size() + sizeof(zero_byte) + sizeof(key_id) +
+                sizeof(kem_id) + sizeof(kdf_id) + sizeof(aead_id);
+  expected.reserve(buf_len);
+  expected.push_back(zero_byte);
+  std::string ohttp_cfg(BuildHeader(key_id, kem_id, kdf_id, aead_id));
+  expected.insert(expected.end(), ohttp_cfg.begin(), ohttp_cfg.end());
+  auto instance =
+      ObliviousHttpHeaderKeyConfig::Create(key_id, kem_id, kdf_id, aead_id);
+  ASSERT_TRUE(instance.ok());
+  EXPECT_EQ(instance.value().SerializeRecipientContextInfo(), expected);
+}
+
+TEST(ObliviousHttpHeaderKeyConfig, TestValidKeyConfig) {
+  auto valid_key_config = ObliviousHttpHeaderKeyConfig::Create(
+      2, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
+      EVP_HPKE_AES_256_GCM);
+  ASSERT_TRUE(valid_key_config.ok());
+}
+
+TEST(ObliviousHttpHeaderKeyConfig, TestInvalidKeyConfig) {
+  auto invalid_kem = ObliviousHttpHeaderKeyConfig::Create(
+      3, 0, EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  EXPECT_EQ(invalid_kem.status().code(), absl::StatusCode::kInvalidArgument);
+  auto invalid_kdf = ObliviousHttpHeaderKeyConfig::Create(
+      3, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, 0, EVP_HPKE_AES_256_GCM);
+  EXPECT_EQ(invalid_kdf.status().code(), absl::StatusCode::kInvalidArgument);
+  auto invalid_aead = ObliviousHttpHeaderKeyConfig::Create(
+      3, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256, 0);
+  EXPECT_EQ(invalid_kdf.status().code(), absl::StatusCode::kInvalidArgument);
+}
+
+TEST(ObliviousHttpHeaderKeyConfig, TestParsingValidHeader) {
+  auto instance = ObliviousHttpHeaderKeyConfig::Create(
+      5, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
+      EVP_HPKE_AES_256_GCM);
+  ASSERT_TRUE(instance.ok());
+  std::string good_hdr(BuildHeader(5, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                                   EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM));
+  ASSERT_TRUE(instance.value().ParseOhttpPayloadHeader(good_hdr).ok());
+}
+
+TEST(ObliviousHttpHeaderKeyConfig, TestParsingInvalidHeader) {
+  auto instance = ObliviousHttpHeaderKeyConfig::Create(
+      8, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
+      EVP_HPKE_AES_256_GCM);
+  ASSERT_TRUE(instance.ok());
+  std::string keyid_mismatch_hdr(
+      BuildHeader(0, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
+                  EVP_HPKE_AES_256_GCM));
+  EXPECT_EQ(instance.value().ParseOhttpPayloadHeader(keyid_mismatch_hdr).code(),
+            absl::StatusCode::kInvalidArgument);
+  std::string invalid_hpke_hdr(BuildHeader(8, 0, 0, 0));
+  EXPECT_EQ(instance.value().ParseOhttpPayloadHeader(invalid_hpke_hdr).code(),
+            absl::StatusCode::kInvalidArgument);
+}
+
+TEST(ObliviousHttpHeaderKeyConfig, TestParsingKeyIdFromObliviousHttpRequest) {
+  std::string key_id(sizeof(uint8_t), '\0');
+  QuicheDataWriter writer(key_id.size(), key_id.data());
+  EXPECT_TRUE(writer.WriteUInt8(99));
+  auto parsed_key_id =
+      ObliviousHttpHeaderKeyConfig::ParseKeyIdFromObliviousHttpRequestPayload(
+          key_id);
+  ASSERT_TRUE(parsed_key_id.ok());
+  EXPECT_EQ(parsed_key_id.value(), 99);
+}
+
+TEST(ObliviousHttpHeaderKeyConfig, TestCopyable) {
+  auto obj1 = ObliviousHttpHeaderKeyConfig::Create(
+      4, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
+      EVP_HPKE_AES_256_GCM);
+  ASSERT_TRUE(obj1.ok());
+  auto copy_obj1_to_obj2 = obj1.value();
+  EXPECT_EQ(copy_obj1_to_obj2.kHeaderLength, obj1->kHeaderLength);
+  EXPECT_EQ(copy_obj1_to_obj2.SerializeRecipientContextInfo(),
+            obj1->SerializeRecipientContextInfo());
+}
+
+TEST(ObliviousHttpHeaderKeyConfig, TestSerializeOhttpPayloadHeader) {
+  auto instance = ObliviousHttpHeaderKeyConfig::Create(
+      7, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
+      EVP_HPKE_AES_128_GCM);
+  ASSERT_TRUE(instance.ok());
+  EXPECT_EQ(instance->SerializeOhttpPayloadHeader(),
+            BuildHeader(7, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM));
+}
+
+MATCHER_P(HasKeyId, id, "") {
+  *result_listener << "has key_id=" << arg.GetKeyId();
+  return arg.GetKeyId() == id;
+}
+MATCHER_P(HasKemId, id, "") {
+  *result_listener << "has kem_id=" << arg.GetHpkeKemId();
+  return arg.GetHpkeKemId() == id;
+}
+MATCHER_P(HasKdfId, id, "") {
+  *result_listener << "has kdf_id=" << arg.GetHpkeKdfId();
+  return arg.GetHpkeKdfId() == id;
+}
+MATCHER_P(HasAeadId, id, "") {
+  *result_listener << "has aead_id=" << arg.GetHpkeAeadId();
+  return arg.GetHpkeAeadId() == id;
+}
+
+TEST(ObliviousHttpKeyConfigs, SingleKeyConfig) {
+  std::string key = absl::HexStringToBytes(
+      "4b0020f83e0a17cbdb18d2684dd2a9b087a43e5f3fa3fa27a049bc746a6e97a1e0244b00"
+      "0400010002");
+  auto configs = ObliviousHttpKeyConfigs::ParseConcatenatedKeys(key).value();
+  EXPECT_THAT(configs, Property(&ObliviousHttpKeyConfigs::NumKeys, 1));
+  EXPECT_THAT(
+      configs.PreferredConfig(),
+      AllOf(HasKeyId(0x4b), HasKemId(EVP_HPKE_DHKEM_X25519_HKDF_SHA256),
+            HasKdfId(EVP_HPKE_HKDF_SHA256), HasAeadId(EVP_HPKE_AES_256_GCM)));
+  EXPECT_THAT(
+      configs.GetPublicKeyForId(configs.PreferredConfig().GetKeyId()).value(),
+      StrEq(absl::HexStringToBytes(
+          "f83e0a17cbdb18d2684dd2a9b087a43e5f3fa3fa27a049bc746a6e97a1e0244b")));
+}
+
+TEST(ObliviousHttpKeyConfigs, TwoSimilarKeyConfigs) {
+  std::string key = absl::HexStringToBytes(
+      "4b0020f83e0a17cbdb18d2684dd2a9b087a43e5f3fa3fa27a049bc746a6e97a1e0244b00"
+      "0400010002"  // Intentional concatenation
+      "4f0020f83e0a17cbdb18d2684dd2a9b087a43e5f3fa3fa27a049bc746a6e97a1e0244b00"
+      "0400010001");
+  EXPECT_THAT(ObliviousHttpKeyConfigs::ParseConcatenatedKeys(key).value(),
+              Property(&ObliviousHttpKeyConfigs::NumKeys, 2));
+  EXPECT_THAT(
+      ObliviousHttpKeyConfigs::ParseConcatenatedKeys(key)->PreferredConfig(),
+      AllOf(HasKeyId(0x4f), HasKemId(EVP_HPKE_DHKEM_X25519_HKDF_SHA256),
+            HasKdfId(EVP_HPKE_HKDF_SHA256), HasAeadId(EVP_HPKE_AES_128_GCM)));
+}
+
+TEST(ObliviousHttpKeyConfigs, RFCExample) {
+  std::string key = absl::HexStringToBytes(
+      "01002031e1f05a740102115220e9af918f738674aec95f54db6e04eb705aae8e79815500"
+      "080001000100010003");
+  auto configs = ObliviousHttpKeyConfigs::ParseConcatenatedKeys(key).value();
+  EXPECT_THAT(configs, Property(&ObliviousHttpKeyConfigs::NumKeys, 1));
+  EXPECT_THAT(
+      configs.PreferredConfig(),
+      AllOf(HasKeyId(0x01), HasKemId(EVP_HPKE_DHKEM_X25519_HKDF_SHA256),
+            HasKdfId(EVP_HPKE_HKDF_SHA256), HasAeadId(EVP_HPKE_AES_128_GCM)));
+  EXPECT_THAT(
+      configs.GetPublicKeyForId(configs.PreferredConfig().GetKeyId()).value(),
+      StrEq(absl::HexStringToBytes(
+          "31e1f05a740102115220e9af918f738674aec95f54db6e04eb705aae8e798155")));
+}
+
+TEST(ObliviousHttpKeyConfigs, DuplicateKeyId) {
+  std::string key = absl::HexStringToBytes(
+      "4b0020f83e0a17cbdb18d2684dd2a9b087a43e5f3fa3fa27a049bc746a6e97a1e0244b00"
+      "0400010002"  // Intentional concatenation
+      "4b0020f83e0a17cbdb18d2684dd2a9b087a43e5f3fa3fb27a049bc746a6e97a1e0244b00"
+      "0400010001");
+  EXPECT_FALSE(ObliviousHttpKeyConfigs::ParseConcatenatedKeys(key).ok());
+}
+
+}  // namespace
+}  // namespace quiche
diff --git a/quiche/oblivious_http/oblivious_http_client.cc b/quiche/oblivious_http/oblivious_http_client.cc
new file mode 100644
index 0000000..5bbf74c
--- /dev/null
+++ b/quiche/oblivious_http/oblivious_http_client.cc
@@ -0,0 +1,91 @@
+#include "quiche/oblivious_http/oblivious_http_client.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "quiche/common/quiche_crypto_logging.h"
+
+namespace quiche {
+
+namespace {
+
+// Use BoringSSL's setup_sender API to validate whether the HPKE public key
+// input provided by the user is valid.
+absl::Status ValidateClientParameters(
+    absl::string_view hpke_public_key,
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config) {
+  // Initialize HPKE client context and check if context can be setup with the
+  // given public key to verify if the public key is indeed valid.
+  bssl::UniquePtr<EVP_HPKE_CTX> client_ctx(EVP_HPKE_CTX_new());
+  if (client_ctx == nullptr) {
+    return SslErrorAsStatus(
+        "Failed to initialize HPKE ObliviousHttpClientSender Context.");
+  }
+  // Setup the sender (client)
+  std::string encapsulated_key(EVP_HPKE_MAX_ENC_LENGTH, '\0');
+  size_t enc_len;
+  absl::string_view info = "verify if given HPKE public key is valid";
+  if (!EVP_HPKE_CTX_setup_sender(
+          client_ctx.get(), reinterpret_cast<uint8_t*>(encapsulated_key.data()),
+          &enc_len, encapsulated_key.size(), ohttp_key_config.GetHpkeKem(),
+          ohttp_key_config.GetHpkeKdf(), ohttp_key_config.GetHpkeAead(),
+          reinterpret_cast<const uint8_t*>(hpke_public_key.data()),
+          hpke_public_key.size(), reinterpret_cast<const uint8_t*>(info.data()),
+          info.size())) {
+    return SslErrorAsStatus(
+        "Failed to setup HPKE context with given public key param "
+        "hpke_public_key.");
+  }
+  return absl::OkStatus();
+}
+
+}  // namespace
+
+// Constructor.
+ObliviousHttpClient::ObliviousHttpClient(
+    std::string client_public_key,
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config)
+    : hpke_public_key_(std::move(client_public_key)),
+      ohttp_key_config_(ohttp_key_config) {}
+
+// Initialize Bssl.
+absl::StatusOr<ObliviousHttpClient> ObliviousHttpClient::Create(
+    absl::string_view hpke_public_key,
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config) {
+  if (hpke_public_key.empty()) {
+    return absl::InvalidArgumentError("Invalid/Empty HPKE public key.");
+  }
+  auto is_valid_input =
+      ValidateClientParameters(hpke_public_key, ohttp_key_config);
+  if (!is_valid_input.ok()) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid input received in method parameters. ",
+                     is_valid_input.message()));
+  }
+  return ObliviousHttpClient(std::string(hpke_public_key), ohttp_key_config);
+}
+
+absl::StatusOr<ObliviousHttpRequest>
+ObliviousHttpClient::CreateObliviousHttpRequest(
+    absl::string_view plaintext_data) const {
+  return ObliviousHttpRequest::CreateClientObliviousRequest(
+      plaintext_data, hpke_public_key_, ohttp_key_config_);
+}
+
+absl::StatusOr<ObliviousHttpResponse>
+ObliviousHttpClient::DecryptObliviousHttpResponse(
+    absl::string_view encrypted_data,
+    ObliviousHttpRequest::Context& oblivious_http_request_context) const {
+  return ObliviousHttpResponse::CreateClientObliviousResponse(
+      encrypted_data, oblivious_http_request_context);
+}
+
+}  // namespace quiche
diff --git a/quiche/oblivious_http/oblivious_http_client.h b/quiche/oblivious_http/oblivious_http_client.h
new file mode 100644
index 0000000..10a2f60
--- /dev/null
+++ b/quiche/oblivious_http/oblivious_http_client.h
@@ -0,0 +1,80 @@
+#ifndef QUICHE_OBLIVIOUS_HTTP_OBLIVIOUS_HTTP_CLIENT_H_
+#define QUICHE_OBLIVIOUS_HTTP_OBLIVIOUS_HTTP_CLIENT_H_
+
+#include <string>
+
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "openssl/base.h"
+#include "openssl/hpke.h"
+#include "quiche/common/platform/api/quiche_export.h"
+#include "quiche/oblivious_http/buffers/oblivious_http_request.h"
+#include "quiche/oblivious_http/buffers/oblivious_http_response.h"
+#include "quiche/oblivious_http/common/oblivious_http_header_key_config.h"
+
+namespace quiche {
+// 1. Facilitates client side to intiate OHttp request flow by initializing the
+// HPKE public key obtained from server, and subsequently uses it to encrypt the
+// Binary HTTP request payload.
+// 2. After initializing this class with server's HPKE public key, users can
+// call `CreateObliviousHttpRequest` which constructs OHTTP request of the input
+// payload(Binary HTTP request).
+// 3. Handles decryption of response (that's in the form of encrypted Binary
+// HTTP response) that will be sent back from Server-to-Relay and
+// Relay-to-client in HTTP POST body.
+// 4. Handles BoringSSL HPKE context setup and bookkeeping.
+
+// This class is immutable (except moves) and thus trivially thread-safe.
+class QUICHE_EXPORT_PRIVATE ObliviousHttpClient {
+ public:
+  static absl::StatusOr<ObliviousHttpClient> Create(
+      absl::string_view hpke_public_key,
+      const ObliviousHttpHeaderKeyConfig& ohttp_key_config);
+
+  // Copyable.
+  ObliviousHttpClient(const ObliviousHttpClient& other) = default;
+  ObliviousHttpClient& operator=(const ObliviousHttpClient& other) = default;
+
+  // Movable.
+  ObliviousHttpClient(ObliviousHttpClient&& other) = default;
+  ObliviousHttpClient& operator=(ObliviousHttpClient&& other) = default;
+
+  ~ObliviousHttpClient() = default;
+
+  // After successful `Create`, callers will use the returned object to
+  // repeatedly call into this method in order to create Oblivious HTTP request
+  // with the initialized HPKE public key. Call sequence: Create ->
+  // CreateObliviousHttpRequest -> DecryptObliviousHttpResponse.
+  // Eg.,
+  //   auto ohttp_client_object = ObliviousHttpClient::Create( <HPKE
+  //    public key>, <OHTTP key configuration described in
+  //    `oblivious_http_header_key_config.h`>);
+  //   auto encrypted_request1 =
+  //    ohttp_client_object.CreateObliviousHttpRequest("binary http string 1");
+  //   auto encrypted_request2 =
+  //    ohttp_client_object.CreateObliviousHttpRequest("binary http string 2");
+  absl::StatusOr<ObliviousHttpRequest> CreateObliviousHttpRequest(
+      absl::string_view plaintext_data) const;
+
+  // After `CreateObliviousHttpRequest` operation, callers on client-side will
+  // extract `oblivious_http_request_context` from the returned object
+  // `ObliviousHttpRequest` and pass in to this method in order to decrypt the
+  // response that's received from Gateway for the given request at hand.
+  absl::StatusOr<ObliviousHttpResponse> DecryptObliviousHttpResponse(
+      absl::string_view encrypted_data,
+      ObliviousHttpRequest::Context& oblivious_http_request_context) const;
+
+ private:
+  explicit ObliviousHttpClient(
+      std::string client_public_key,
+      const ObliviousHttpHeaderKeyConfig& ohttp_key_config);
+  std::string hpke_public_key_;
+  // Holds server's keyID and HPKE related IDs that's published under HPKE
+  // public Key configuration.
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#name-key-configuration
+  ObliviousHttpHeaderKeyConfig ohttp_key_config_;
+};
+
+}  // namespace quiche
+
+#endif  // QUICHE_OBLIVIOUS_HTTP_OBLIVIOUS_HTTP_CLIENT_H_
diff --git a/quiche/oblivious_http/oblivious_http_client_test.cc b/quiche/oblivious_http/oblivious_http_client_test.cc
new file mode 100644
index 0000000..0382b05
--- /dev/null
+++ b/quiche/oblivious_http/oblivious_http_client_test.cc
@@ -0,0 +1,246 @@
+#include "quiche/oblivious_http/oblivious_http_client.h"
+
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/string_view.h"
+#include "quiche/common/platform/api/quiche_test.h"
+#include "quiche/common/platform/api/quiche_thread.h"
+
+namespace quiche {
+
+std::string GetHpkePrivateKey() {
+  // Dev/Test private key generated using Keystore.
+  absl::string_view hpke_key_hex =
+      "b77431ecfa8f4cfc30d6e467aafa06944dffe28cb9dd1409e33a3045f5adc8a1";
+  return absl::HexStringToBytes(hpke_key_hex);
+}
+
+std::string GetHpkePublicKey() {
+  // Dev/Test public key generated using Keystore.
+  absl::string_view public_key =
+      "6d21cfe09fbea5122f9ebc2eb2a69fcc4f06408cd54aac934f012e76fcdcef62";
+  return absl::HexStringToBytes(public_key);
+}
+
+const ObliviousHttpHeaderKeyConfig GetOhttpKeyConfig(uint8_t key_id,
+                                                     uint16_t kem_id,
+                                                     uint16_t kdf_id,
+                                                     uint16_t aead_id) {
+  auto ohttp_key_config =
+      ObliviousHttpHeaderKeyConfig::Create(key_id, kem_id, kdf_id, aead_id);
+  EXPECT_TRUE(ohttp_key_config.ok());
+  return std::move(ohttp_key_config.value());
+}
+
+bssl::UniquePtr<EVP_HPKE_KEY> ConstructHpkeKey(
+    absl::string_view hpke_key,
+    const ObliviousHttpHeaderKeyConfig &ohttp_key_config) {
+  bssl::UniquePtr<EVP_HPKE_KEY> bssl_hpke_key(EVP_HPKE_KEY_new());
+  EXPECT_NE(bssl_hpke_key, nullptr);
+  EXPECT_TRUE(EVP_HPKE_KEY_init(
+      bssl_hpke_key.get(), ohttp_key_config.GetHpkeKem(),
+      reinterpret_cast<const uint8_t *>(hpke_key.data()), hpke_key.size()));
+  return bssl_hpke_key;
+}
+
+TEST(ObliviousHttpClient, TestEncapsulate) {
+  auto client = ObliviousHttpClient::Create(
+      GetHpkePublicKey(),
+      GetOhttpKeyConfig(8, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM));
+  ASSERT_TRUE(client.ok());
+  auto encrypted_req = client->CreateObliviousHttpRequest("test string 1");
+  ASSERT_TRUE(encrypted_req.ok());
+  auto serialized_encrypted_req = encrypted_req->EncapsulateAndSerialize();
+  ASSERT_FALSE(serialized_encrypted_req.empty());
+}
+
+TEST(ObliviousHttpClient, TestEncryptingMultipleRequestsWithSingleInstance) {
+  auto client = ObliviousHttpClient::Create(
+      GetHpkePublicKey(),
+      GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM));
+  ASSERT_TRUE(client.ok());
+  auto ohttp_req_1 = client->CreateObliviousHttpRequest("test string 1");
+  ASSERT_TRUE(ohttp_req_1.ok());
+  auto serialized_ohttp_req_1 = ohttp_req_1->EncapsulateAndSerialize();
+  ASSERT_FALSE(serialized_ohttp_req_1.empty());
+  auto ohttp_req_2 = client->CreateObliviousHttpRequest("test string 2");
+  ASSERT_TRUE(ohttp_req_2.ok());
+  auto serialized_ohttp_req_2 = ohttp_req_2->EncapsulateAndSerialize();
+  ASSERT_FALSE(serialized_ohttp_req_2.empty());
+  EXPECT_NE(serialized_ohttp_req_1, serialized_ohttp_req_2);
+}
+
+TEST(ObliviousHttpClient, TestInvalidHPKEKey) {
+  // Invalid public key.
+  EXPECT_EQ(ObliviousHttpClient::Create(
+                "Invalid HPKE key",
+                GetOhttpKeyConfig(50, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                                  EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM))
+                .status()
+                .code(),
+            absl::StatusCode::kInvalidArgument);
+  // Empty public key.
+  EXPECT_EQ(ObliviousHttpClient::Create(
+                /*hpke_public_key*/ "",
+                GetOhttpKeyConfig(50, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                                  EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM))
+                .status()
+                .code(),
+            absl::StatusCode::kInvalidArgument);
+}
+
+TEST(ObliviousHttpClient,
+     TestTwoSamePlaintextsWillGenerateDifferentEncryptedPayloads) {
+  // Due to the nature of the encapsulated_key generated in HPKE being unique
+  // for every request, expect different encrypted payloads when encrypting same
+  // plaintexts.
+  auto client = ObliviousHttpClient::Create(
+      GetHpkePublicKey(),
+      GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM));
+  ASSERT_TRUE(client.ok());
+  auto encrypted_request_1 =
+      client->CreateObliviousHttpRequest("same plaintext");
+  ASSERT_TRUE(encrypted_request_1.ok());
+  auto serialized_encrypted_request_1 =
+      encrypted_request_1->EncapsulateAndSerialize();
+  ASSERT_FALSE(serialized_encrypted_request_1.empty());
+  auto encrypted_request_2 =
+      client->CreateObliviousHttpRequest("same plaintext");
+  ASSERT_TRUE(encrypted_request_2.ok());
+  auto serialized_encrypted_request_2 =
+      encrypted_request_2->EncapsulateAndSerialize();
+  ASSERT_FALSE(serialized_encrypted_request_2.empty());
+  EXPECT_NE(serialized_encrypted_request_1, serialized_encrypted_request_2);
+}
+
+TEST(ObliviousHttpClient, TestObliviousResponseHandling) {
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  auto encapsulate_req_on_client =
+      ObliviousHttpRequest::CreateClientObliviousRequest(
+          "test", GetHpkePublicKey(), ohttp_key_config);
+  ASSERT_TRUE(encapsulate_req_on_client.ok());
+  auto decapsulate_req_on_gateway =
+      ObliviousHttpRequest::CreateServerObliviousRequest(
+          encapsulate_req_on_client->EncapsulateAndSerialize(),
+          *(ConstructHpkeKey(GetHpkePrivateKey(), ohttp_key_config)),
+          ohttp_key_config);
+  ASSERT_TRUE(decapsulate_req_on_gateway.ok());
+  auto encapsulate_resp_on_gateway =
+      ObliviousHttpResponse::CreateServerObliviousResponse(
+          "test response",
+          *(decapsulate_req_on_gateway->oblivious_http_request_context()));
+  ASSERT_TRUE(encapsulate_resp_on_gateway.ok());
+
+  auto client =
+      ObliviousHttpClient::Create(GetHpkePublicKey(), ohttp_key_config);
+  ASSERT_TRUE(client.ok());
+  auto decapsulate_resp_on_client = client->DecryptObliviousHttpResponse(
+      absl::string_view(encapsulate_resp_on_gateway->EncapsulateAndSerialize()),
+      *(encapsulate_req_on_client->oblivious_http_request_context()));
+  ASSERT_TRUE(decapsulate_resp_on_client.ok());
+  EXPECT_EQ(decapsulate_resp_on_client->GetPlaintextData(), "test response");
+}
+
+TEST(ObliviousHttpClient,
+     DecryptResponseReceivedByTheClientUsingServersObliviousContext) {
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  auto encapsulate_req_on_client =
+      ObliviousHttpRequest::CreateClientObliviousRequest(
+          "test", GetHpkePublicKey(), ohttp_key_config);
+  ASSERT_TRUE(encapsulate_req_on_client.ok());
+  auto decapsulate_req_on_gateway =
+      ObliviousHttpRequest::CreateServerObliviousRequest(
+          encapsulate_req_on_client->EncapsulateAndSerialize(),
+          *(ConstructHpkeKey(GetHpkePrivateKey(), ohttp_key_config)),
+          ohttp_key_config);
+  ASSERT_TRUE(decapsulate_req_on_gateway.ok());
+  auto encapsulate_resp_on_gateway =
+      ObliviousHttpResponse::CreateServerObliviousResponse(
+          "test response",
+          *(decapsulate_req_on_gateway->oblivious_http_request_context()));
+  ASSERT_TRUE(encapsulate_resp_on_gateway.ok());
+
+  auto client =
+      ObliviousHttpClient::Create(GetHpkePublicKey(), ohttp_key_config);
+  ASSERT_TRUE(client.ok());
+  auto decapsulate_resp_on_client = client->DecryptObliviousHttpResponse(
+      absl::string_view(encapsulate_resp_on_gateway->EncapsulateAndSerialize()),
+      *(decapsulate_req_on_gateway->oblivious_http_request_context()));
+  ASSERT_TRUE(decapsulate_resp_on_client.ok());
+  EXPECT_EQ(decapsulate_resp_on_client->GetPlaintextData(), "test response");
+}
+
+TEST(ObliviousHttpGatewayReceiver, TestWithMultipleThreads) {
+  class TestQuicheThread : public QuicheThread {
+   public:
+    TestQuicheThread(const ObliviousHttpClient& client,
+                     std::string request_payload,
+                     ObliviousHttpHeaderKeyConfig ohttp_key_config)
+        : QuicheThread("client_thread"),
+          client_(client),
+          request_payload_(request_payload),
+          ohttp_key_config_(ohttp_key_config) {}
+
+   protected:
+    void Run() override {
+      auto encrypted_request =
+          client_.CreateObliviousHttpRequest(request_payload_);
+      ASSERT_TRUE(encrypted_request.ok());
+      ASSERT_FALSE(encrypted_request->EncapsulateAndSerialize().empty());
+      // Setup recipient and get encrypted response payload.
+      auto decapsulate_req_on_gateway =
+          ObliviousHttpRequest::CreateServerObliviousRequest(
+              encrypted_request->EncapsulateAndSerialize(),
+              *(ConstructHpkeKey(GetHpkePrivateKey(), ohttp_key_config_)),
+              ohttp_key_config_);
+      ASSERT_TRUE(decapsulate_req_on_gateway.ok());
+      auto encapsulate_resp_on_gateway =
+          ObliviousHttpResponse::CreateServerObliviousResponse(
+              "test response",
+              *(decapsulate_req_on_gateway->oblivious_http_request_context()));
+      ASSERT_TRUE(encapsulate_resp_on_gateway.ok());
+      ASSERT_FALSE(
+          encapsulate_resp_on_gateway->EncapsulateAndSerialize().empty());
+
+      auto decrypted_response = client_.DecryptObliviousHttpResponse(
+          encapsulate_resp_on_gateway->EncapsulateAndSerialize(),
+          *(encrypted_request->oblivious_http_request_context()));
+      ASSERT_TRUE(decrypted_response.ok());
+      ASSERT_FALSE(decrypted_response->GetPlaintextData().empty());
+    }
+
+   private:
+    const ObliviousHttpClient& client_;
+    std::string request_payload_;
+    ObliviousHttpHeaderKeyConfig ohttp_key_config_;
+  };
+
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  auto client =
+      ObliviousHttpClient::Create(GetHpkePublicKey(), ohttp_key_config);
+
+  TestQuicheThread t1(*client, "test request 1", ohttp_key_config);
+  TestQuicheThread t2(*client, "test request 2", ohttp_key_config);
+  t1.Start();
+  t2.Start();
+  t1.Join();
+  t2.Join();
+}
+
+}  // namespace quiche
diff --git a/quiche/oblivious_http/oblivious_http_gateway.cc b/quiche/oblivious_http/oblivious_http_gateway.cc
new file mode 100644
index 0000000..d8a4eb0
--- /dev/null
+++ b/quiche/oblivious_http/oblivious_http_gateway.cc
@@ -0,0 +1,66 @@
+#include "quiche/oblivious_http/oblivious_http_gateway.h"
+
+#include <stdint.h>
+
+#include <memory>
+#include <utility>
+
+#include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "quiche/common/quiche_crypto_logging.h"
+#include "quiche/common/quiche_random.h"
+
+namespace quiche {
+
+// Constructor.
+ObliviousHttpGateway::ObliviousHttpGateway(
+    bssl::UniquePtr<EVP_HPKE_KEY> recipient_key,
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
+    QuicheRandom* quiche_random)
+    : server_hpke_key_(std::move(recipient_key)),
+      ohttp_key_config_(ohttp_key_config),
+      quiche_random_(quiche_random) {}
+
+// Initialize ObliviousHttpGatewayReceiver(Recipient/Server) context.
+absl::StatusOr<ObliviousHttpGateway> ObliviousHttpGateway::Create(
+    absl::string_view hpke_private_key,
+    const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
+    QuicheRandom* quiche_random) {
+  if (hpke_private_key.empty()) {
+    return absl::InvalidArgumentError("Invalid/Empty HPKE private key.");
+  }
+  // Initialize HPKE key and context.
+  bssl::UniquePtr<EVP_HPKE_KEY> recipient_key(EVP_HPKE_KEY_new());
+  if (recipient_key == nullptr) {
+    return SslErrorAsStatus(
+        "Failed to initialize ObliviousHttpGatewayReceiver/Server's Key.");
+  }
+  if (!EVP_HPKE_KEY_init(
+          recipient_key.get(), ohttp_key_config.GetHpkeKem(),
+          reinterpret_cast<const uint8_t*>(hpke_private_key.data()),
+          hpke_private_key.size())) {
+    return SslErrorAsStatus("Failed to import HPKE private key.");
+  }
+  if (quiche_random == nullptr) quiche_random = QuicheRandom::GetInstance();
+  return ObliviousHttpGateway(std::move(recipient_key), ohttp_key_config,
+                              quiche_random);
+}
+
+absl::StatusOr<ObliviousHttpRequest>
+ObliviousHttpGateway::DecryptObliviousHttpRequest(
+    absl::string_view encrypted_data) const {
+  return ObliviousHttpRequest::CreateServerObliviousRequest(
+      encrypted_data, *(server_hpke_key_), ohttp_key_config_);
+}
+
+absl::StatusOr<ObliviousHttpResponse>
+ObliviousHttpGateway::CreateObliviousHttpResponse(
+    absl::string_view plaintext_data,
+    ObliviousHttpRequest::Context& oblivious_http_request_context) const {
+  return ObliviousHttpResponse::CreateServerObliviousResponse(
+      plaintext_data, oblivious_http_request_context, quiche_random_);
+}
+
+}  // namespace quiche
diff --git a/quiche/oblivious_http/oblivious_http_gateway.h b/quiche/oblivious_http/oblivious_http_gateway.h
new file mode 100644
index 0000000..8f70356
--- /dev/null
+++ b/quiche/oblivious_http/oblivious_http_gateway.h
@@ -0,0 +1,82 @@
+#ifndef QUICHE_OBLIVIOUS_HTTP_OBLIVIOUS_HTTP_GATEWAY_H_
+#define QUICHE_OBLIVIOUS_HTTP_OBLIVIOUS_HTTP_GATEWAY_H_
+
+#include <memory>
+
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "openssl/base.h"
+#include "openssl/hpke.h"
+#include "quiche/common/platform/api/quiche_export.h"
+#include "quiche/common/quiche_random.h"
+#include "quiche/oblivious_http/buffers/oblivious_http_request.h"
+#include "quiche/oblivious_http/buffers/oblivious_http_response.h"
+#include "quiche/oblivious_http/common/oblivious_http_header_key_config.h"
+
+namespace quiche {
+// 1. Handles server side decryption of the payload received in HTTP POST body
+// from Relay.
+// 2. Handles server side encryption of response (that's in the form of Binary
+// HTTP) that will be sent back to Relay in HTTP POST body.
+// 3. Handles BSSL initialization and HPKE context bookkeeping.
+
+// This class is immutable (except moves) and thus trivially thread-safe,
+// assuming the `QuicheRandom* quiche_random` passed in with `Create` is
+// thread-safe. Note that default `QuicheRandom::GetInstance()` is thread-safe.
+class QUICHE_EXPORT_PRIVATE ObliviousHttpGateway {
+ public:
+  // @params: If callers would like to pass in their own `QuicheRandom`
+  // instance, they can make use of the param `quiche_random`. Otherwise, the
+  // default `QuicheRandom::GetInstance()` will be used.
+  static absl::StatusOr<ObliviousHttpGateway> Create(
+      absl::string_view hpke_private_key,
+      const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
+      QuicheRandom* quiche_random = nullptr);
+
+  // only Movable (due to `UniquePtr server_hpke_key_`).
+  ObliviousHttpGateway(ObliviousHttpGateway&& other) = default;
+  ObliviousHttpGateway& operator=(ObliviousHttpGateway&& other) = default;
+
+  ~ObliviousHttpGateway() = default;
+
+  // After successful `Create`, callers will use the returned object to
+  // repeatedly call into this method in order to create Oblivious HTTP request
+  // with the initialized HPKE private key. Call sequence: Create ->
+  // DecryptObliviousHttpRequest -> CreateObliviousHttpResponse.
+  // Eg.,
+  //   auto ohttp_server_object = ObliviousHttpGateway::Create( <HPKE
+  //    private key>, <OHTTP key configuration described in
+  //    `oblivious_http_header_key_config.h`>);
+  //   auto decrypted_request1 =
+  //    ohttp_server_object.DecryptObliviousHttpRequest(<encrypted binary http
+  //    1>);
+  //   auto decrypted_request2 =
+  //    ohttp_server_object.DecryptObliviousHttpRequest(<encrypted binary http
+  //    2>);
+  absl::StatusOr<ObliviousHttpRequest> DecryptObliviousHttpRequest(
+      absl::string_view encrypted_data) const;
+
+  // After `DecryptObliviousHttpRequest` operation, callers on server-side will
+  // extract `oblivious_http_request_context` from the returned object
+  // `ObliviousHttpRequest` and pass in to this method in order to handle the
+  // response flow back to the client.
+  absl::StatusOr<ObliviousHttpResponse> CreateObliviousHttpResponse(
+      absl::string_view plaintext_data,
+      ObliviousHttpRequest::Context& oblivious_http_request_context) const;
+
+ private:
+  explicit ObliviousHttpGateway(
+      bssl::UniquePtr<EVP_HPKE_KEY> recipient_key,
+      const ObliviousHttpHeaderKeyConfig& ohttp_key_config,
+      QuicheRandom* quiche_random);
+  bssl::UniquePtr<EVP_HPKE_KEY> server_hpke_key_;
+  // Holds server's keyID and HPKE related IDs that's published under HPKE
+  // public Key configuration.
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#name-key-configuration
+  ObliviousHttpHeaderKeyConfig ohttp_key_config_;
+  QuicheRandom* quiche_random_;
+};
+
+}  // namespace quiche
+
+#endif  // QUICHE_OBLIVIOUS_HTTP_OBLIVIOUS_HTTP_GATEWAY_H_
diff --git a/quiche/oblivious_http/oblivious_http_gateway_test.cc b/quiche/oblivious_http/oblivious_http_gateway_test.cc
new file mode 100644
index 0000000..5482c1c
--- /dev/null
+++ b/quiche/oblivious_http/oblivious_http_gateway_test.cc
@@ -0,0 +1,228 @@
+#include "quiche/oblivious_http/oblivious_http_gateway.h"
+
+#include <stdint.h>
+
+#include <string>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/string_view.h"
+#include "quiche/common/platform/api/quiche_test.h"
+#include "quiche/common/platform/api/quiche_thread.h"
+#include "quiche/common/quiche_random.h"
+#include "quiche/oblivious_http/buffers/oblivious_http_request.h"
+
+namespace quiche {
+namespace {
+
+std::string GetHpkePrivateKey() {
+  // Dev/Test private key generated using Keystore.
+  absl::string_view hpke_key_hex =
+      "b77431ecfa8f4cfc30d6e467aafa06944dffe28cb9dd1409e33a3045f5adc8a1";
+  return absl::HexStringToBytes(hpke_key_hex);
+}
+
+std::string GetHpkePublicKey() {
+  // Dev/Test public key generated using Keystore.
+  absl::string_view public_key =
+      "6d21cfe09fbea5122f9ebc2eb2a69fcc4f06408cd54aac934f012e76fcdcef62";
+  return absl::HexStringToBytes(public_key);
+}
+
+const ObliviousHttpHeaderKeyConfig GetOhttpKeyConfig(uint8_t key_id,
+                                                     uint16_t kem_id,
+                                                     uint16_t kdf_id,
+                                                     uint16_t aead_id) {
+  auto ohttp_key_config =
+      ObliviousHttpHeaderKeyConfig::Create(key_id, kem_id, kdf_id, aead_id);
+  EXPECT_TRUE(ohttp_key_config.ok());
+  return std::move(ohttp_key_config.value());
+}
+
+TEST(ObliviousHttpGateway, TestProvisioningKeyAndDecapsulate) {
+  // X25519 Secret key (priv key).
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#appendix-A-2
+  constexpr absl::string_view kX25519SecretKey =
+      "3c168975674b2fa8e465970b79c8dcf09f1c741626480bd4c6162fc5b6a98e1a";
+
+  auto instance = ObliviousHttpGateway::Create(
+      /*hpke_private_key*/ absl::HexStringToBytes(kX25519SecretKey),
+      /*ohttp_key_config*/ GetOhttpKeyConfig(
+          /*key_id=*/1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
+          EVP_HPKE_AES_128_GCM));
+
+  // Encapsulated request.
+  // https://www.ietf.org/archive/id/draft-ietf-ohai-ohttp-03.html#appendix-A-14
+  constexpr absl::string_view kEncapsulatedRequest =
+      "010020000100014b28f881333e7c164ffc499ad9796f877f4e1051ee6d31bad19dec96c2"
+      "08b4726374e469135906992e1268c594d2a10c695d858c40a026e7965e7d86b83dd440b2"
+      "c0185204b4d63525";
+
+  auto decrypted_req = instance->DecryptObliviousHttpRequest(
+      absl::HexStringToBytes(kEncapsulatedRequest));
+  ASSERT_TRUE(decrypted_req.ok());
+  ASSERT_FALSE(decrypted_req->GetPlaintextData().empty());
+}
+
+TEST(ObliviousHttpGateway, TestDecryptingMultipleRequestsWithSingleInstance) {
+  auto instance = ObliviousHttpGateway::Create(
+      GetHpkePrivateKey(),
+      GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM));
+  // plaintext: "test request 1"
+  absl::string_view encrypted_req_1 =
+      "010020000100025f20b60306b61ad9ecad389acd752ca75c4e2969469809fe3d84aae137"
+      "f73e4ccfe9ba71f12831fdce6c8202fbd38a84c5d8a73ac4c8ea6c10592594845f";
+  auto decapsulated_req_1 = instance->DecryptObliviousHttpRequest(
+      absl::HexStringToBytes(encrypted_req_1));
+  ASSERT_TRUE(decapsulated_req_1.ok());
+  ASSERT_FALSE(decapsulated_req_1->GetPlaintextData().empty());
+
+  // plaintext: "test request 2"
+  absl::string_view encrypted_req_2 =
+      "01002000010002285ebc2fcad72cc91b378050cac29a62feea9cd97829335ee9fc87e672"
+      "4fa13ff2efdff620423d54225d3099088e7b32a5165f805a5d922918865a0a447a";
+  auto decapsulated_req_2 = instance->DecryptObliviousHttpRequest(
+      absl::HexStringToBytes(encrypted_req_2));
+  ASSERT_TRUE(decapsulated_req_2.ok());
+  ASSERT_FALSE(decapsulated_req_2->GetPlaintextData().empty());
+}
+
+TEST(ObliviousHttpGateway, TestInvalidHPKEKey) {
+  // Invalid private key.
+  EXPECT_EQ(ObliviousHttpGateway::Create(
+                "Invalid HPKE key",
+                GetOhttpKeyConfig(70, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                                  EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM))
+                .status()
+                .code(),
+            absl::StatusCode::kInternal);
+  // Empty private key.
+  EXPECT_EQ(ObliviousHttpGateway::Create(
+                /*hpke_private_key*/ "",
+                GetOhttpKeyConfig(70, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                                  EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM))
+                .status()
+                .code(),
+            absl::StatusCode::kInvalidArgument);
+}
+
+TEST(ObliviousHttpGateway, TestObliviousResponseHandling) {
+  auto ohttp_key_config =
+      GetOhttpKeyConfig(3, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM);
+  auto instance =
+      ObliviousHttpGateway::Create(GetHpkePrivateKey(), ohttp_key_config);
+  ASSERT_TRUE(instance.ok());
+  auto encapsualte_request_on_client =
+      ObliviousHttpRequest::CreateClientObliviousRequest(
+          "test", GetHpkePublicKey(), ohttp_key_config);
+  ASSERT_TRUE(encapsualte_request_on_client.ok());
+  // Setup Recipient to allow setting up the HPKE context, and subsequently use
+  // it to encrypt the response.
+  auto decapsulated_req_on_server = instance->DecryptObliviousHttpRequest(
+      encapsualte_request_on_client->EncapsulateAndSerialize());
+  ASSERT_TRUE(decapsulated_req_on_server.ok());
+  auto encapsulate_resp_on_gateway = instance->CreateObliviousHttpResponse(
+      "some response",
+      *(decapsulated_req_on_server->oblivious_http_request_context()));
+  ASSERT_TRUE(encapsulate_resp_on_gateway.ok());
+  ASSERT_FALSE(encapsulate_resp_on_gateway->EncapsulateAndSerialize().empty());
+}
+
+TEST(ObliviousHttpGateway,
+     TestHandlingMultipleResponsesForMultipleRequestsWithSingleInstance) {
+  auto instance = ObliviousHttpGateway::Create(
+      GetHpkePrivateKey(),
+      GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM),
+      QuicheRandom::GetInstance());
+  // Setup contexts first.
+  auto decrypted_request_1 = instance->DecryptObliviousHttpRequest(
+      absl::HexStringToBytes("010020000100025f20b60306b61ad9ecad389acd752ca75c4"
+                             "e2969469809fe3d84aae137"
+                             "f73e4ccfe9ba71f12831fdce6c8202fbd38a84c5d8a73ac4c"
+                             "8ea6c10592594845f"));
+  ASSERT_TRUE(decrypted_request_1.ok());
+  auto decrypted_request_2 = instance->DecryptObliviousHttpRequest(
+      absl::HexStringToBytes("01002000010002285ebc2fcad72cc91b378050cac29a62fee"
+                             "a9cd97829335ee9fc87e672"
+                             "4fa13ff2efdff620423d54225d3099088e7b32a5165f805a5"
+                             "d922918865a0a447a"));
+  ASSERT_TRUE(decrypted_request_2.ok());
+
+  // Extract contexts and handle the response for each corresponding request.
+  auto oblivious_request_context_1 =
+      decrypted_request_1->oblivious_http_request_context();
+  EXPECT_NE(oblivious_request_context_1, nullptr);
+  auto encrypted_response_1 = instance->CreateObliviousHttpResponse(
+      "test response 1", *(oblivious_request_context_1));
+  ASSERT_TRUE(encrypted_response_1.ok());
+  ASSERT_FALSE(encrypted_response_1->EncapsulateAndSerialize().empty());
+
+  auto oblivious_request_context_2 =
+      decrypted_request_1->oblivious_http_request_context();
+  EXPECT_NE(oblivious_request_context_2, nullptr);
+  auto encrypted_response_2 = instance->CreateObliviousHttpResponse(
+      "test response 2", *(oblivious_request_context_2));
+  ASSERT_TRUE(encrypted_response_2.ok());
+  ASSERT_FALSE(encrypted_response_2->EncapsulateAndSerialize().empty());
+}
+
+TEST(ObliviousHttpGateway, TestWithMultipleThreads) {
+  class TestQuicheThread : public QuicheThread {
+   public:
+    TestQuicheThread(const ObliviousHttpGateway& gateway_receiver,
+                     std::string request_payload, std::string response_payload)
+        : QuicheThread("gateway_thread"),
+          gateway_receiver_(gateway_receiver),
+          request_payload_(request_payload),
+          response_payload_(response_payload) {}
+
+   protected:
+    void Run() override {
+      auto decrypted_request =
+          gateway_receiver_.DecryptObliviousHttpRequest(request_payload_);
+      ASSERT_TRUE(decrypted_request.ok());
+      ASSERT_FALSE(decrypted_request->GetPlaintextData().empty());
+      auto encrypted_response = gateway_receiver_.CreateObliviousHttpResponse(
+          response_payload_,
+          *(decrypted_request->oblivious_http_request_context()));
+      ASSERT_TRUE(encrypted_response.ok());
+      ASSERT_FALSE(encrypted_response->EncapsulateAndSerialize().empty());
+    }
+
+   private:
+    const ObliviousHttpGateway& gateway_receiver_;
+    std::string request_payload_, response_payload_;
+  };
+
+  auto gateway_receiver = ObliviousHttpGateway::Create(
+      GetHpkePrivateKey(),
+      GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
+                        EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM),
+      QuicheRandom::GetInstance());
+
+  TestQuicheThread t1(
+      *gateway_receiver,
+      absl::HexStringToBytes("010020000100025f20b60306b61ad9ecad389acd752ca75c4"
+                             "e2969469809fe3d84aae137"
+                             "f73e4ccfe9ba71f12831fdce6c8202fbd38a84c5d8a73ac4c"
+                             "8ea6c10592594845f"),
+      "test response 1");
+  TestQuicheThread t2(
+      *gateway_receiver,
+      absl::HexStringToBytes("01002000010002285ebc2fcad72cc91b378050cac29a62fee"
+                             "a9cd97829335ee9fc87e672"
+                             "4fa13ff2efdff620423d54225d3099088e7b32a5165f805a5"
+                             "d922918865a0a447a"),
+      "test response 2");
+  t1.Start();
+  t2.Start();
+  t1.Join();
+  t2.Join();
+}
+}  // namespace
+}  // namespace quiche