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