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