[Chunked OHTTP] Implementing chunked OHTTP Gateway PiperOrigin-RevId: 798239391
diff --git a/build/source_list.bzl b/build/source_list.bzl index 4db4368..d019d69 100644 --- a/build/source_list.bzl +++ b/build/source_list.bzl
@@ -1634,6 +1634,8 @@ oblivious_http_hdrs = [ "oblivious_http/buffers/oblivious_http_request.h", "oblivious_http/buffers/oblivious_http_response.h", + "oblivious_http/common/oblivious_http_chunk_handler.h", + "oblivious_http/common/oblivious_http_definitions.h", "oblivious_http/common/oblivious_http_header_key_config.h", "oblivious_http/oblivious_http_client.h", "oblivious_http/oblivious_http_gateway.h",
diff --git a/build/source_list.gni b/build/source_list.gni index 2e1cf09..8563629 100644 --- a/build/source_list.gni +++ b/build/source_list.gni
@@ -1638,6 +1638,8 @@ 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_chunk_handler.h", + "src/quiche/oblivious_http/common/oblivious_http_definitions.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",
diff --git a/build/source_list.json b/build/source_list.json index 7e24315..3819711 100644 --- a/build/source_list.json +++ b/build/source_list.json
@@ -1637,6 +1637,8 @@ "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_chunk_handler.h", + "quiche/oblivious_http/common/oblivious_http_definitions.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"
diff --git a/quiche/binary_http/binary_http_message.h b/quiche/binary_http/binary_http_message.h index 41a6bc1..ed04f16 100644 --- a/quiche/binary_http/binary_http_message.h +++ b/quiche/binary_http/binary_http_message.h
@@ -187,7 +187,7 @@ class QUICHE_EXPORT IndeterminateLengthDecoder { public: // The handler to invoke when a section is decoded successfully. - class MessageSectionHandler { + class QUICHE_EXPORT MessageSectionHandler { public: virtual ~MessageSectionHandler() = default; virtual void OnControlData(const ControlData& control_data) = 0;
diff --git a/quiche/oblivious_http/buffers/oblivious_http_request.cc b/quiche/oblivious_http/buffers/oblivious_http_request.cc index 246141c..562833f 100644 --- a/quiche/oblivious_http/buffers/oblivious_http_request.cc +++ b/quiche/oblivious_http/buffers/oblivious_http_request.cc
@@ -11,6 +11,7 @@ #include "absl/memory/memory.h" #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/hpke.h" @@ -18,6 +19,7 @@ #include "quiche/common/platform/api/quiche_logging.h" #include "quiche/common/quiche_crypto_logging.h" #include "quiche/common/quiche_data_reader.h" +#include "quiche/oblivious_http/common/oblivious_http_definitions.h" #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" namespace quiche { @@ -229,4 +231,29 @@ return Context(std::move(gateway_ctx), std::string(enc_key_received)); } +absl::StatusOr<std::string> ObliviousHttpRequest::DecryptChunk( + Context& context, absl::string_view encrypted_chunk, bool is_final_chunk) { + uint8_t* ad = nullptr; + size_t ad_len = 0; + std::string final_ad_bytes; + if (is_final_chunk) { + ad = const_cast<uint8_t*>(kFinalAdBytes); + ad_len = sizeof(kFinalAdBytes); + } + + std::string decrypted(encrypted_chunk.size(), '\0'); + size_t decrypted_len; + if (!EVP_HPKE_CTX_open( + context.hpke_context_.get(), + reinterpret_cast<uint8_t*>(decrypted.data()), &decrypted_len, + decrypted.size(), + reinterpret_cast<const uint8_t*>(encrypted_chunk.data()), + encrypted_chunk.size(), ad, ad_len)) { + return SslErrorAsStatus("Failed to decrypt.", + absl::StatusCode::kInvalidArgument); + } + decrypted.resize(decrypted_len); + return decrypted; +} + } // namespace quiche
diff --git a/quiche/oblivious_http/buffers/oblivious_http_request.h b/quiche/oblivious_http/buffers/oblivious_http_request.h index 8fa6e3e..3293e4a 100644 --- a/quiche/oblivious_http/buffers/oblivious_http_request.h +++ b/quiche/oblivious_http/buffers/oblivious_http_request.h
@@ -1,13 +1,14 @@ #ifndef QUICHE_OBLIVIOUS_HTTP_BUFFERS_OBLIVIOUS_HTTP_REQUEST_H_ #define QUICHE_OBLIVIOUS_HTTP_BUFFERS_OBLIVIOUS_HTTP_REQUEST_H_ -#include <memory> #include <optional> #include <string> +#include <utility> #include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "openssl/hpke.h" +#include "quiche/common/platform/api/quiche_export.h" #include "quiche/common/quiche_data_reader.h" #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" @@ -111,6 +112,10 @@ const ObliviousHttpHeaderKeyConfig& ohttp_key_config, absl::string_view request_label); + // Decrypts an encrypted chunk. + static absl::StatusOr<std::string> DecryptChunk( + Context& context, absl::string_view encrypted_chunk, bool is_final_chunk); + private: explicit ObliviousHttpRequest( bssl::UniquePtr<EVP_HPKE_CTX> hpke_context, std::string encapsulated_key,
diff --git a/quiche/oblivious_http/buffers/oblivious_http_request_test.cc b/quiche/oblivious_http/buffers/oblivious_http_request_test.cc index af80873..ceef6af 100644 --- a/quiche/oblivious_http/buffers/oblivious_http_request_test.cc +++ b/quiche/oblivious_http/buffers/oblivious_http_request_test.cc
@@ -136,6 +136,119 @@ .status() .code(), absl::StatusCode::kInvalidArgument); + + // Valid key header but missing encapsulated secret. + payload = "010020000100014b"; + ASSERT_TRUE(absl::HexStringToBytes(payload, &payload_bytes)); + QuicheDataReader reader3(payload_bytes); + + // Missing encapsulated secret. + EXPECT_EQ(ObliviousHttpRequest::DecodeEncapsulatedRequestHeader( + reader3, *hpke_key, ohttp_key_config, "test") + .status() + .code(), + absl::StatusCode::kFailedPrecondition); +} + +TEST(ObliviousHttpRequest, DecryptChunks) { + // Example from + // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html#appendix-A + auto ohttp_key_config = + GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, + EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM); + + std::string kX25519SecretKey = + "1c190d72acdbe4dbc69e680503bb781a932c70a12c8f3754434c67d8640d8698"; + std::string x25519_secret_key_bytes; + EXPECT_TRUE( + absl::HexStringToBytes(kX25519SecretKey, &x25519_secret_key_bytes)); + auto hpke_key = ConstructHpkeKey(x25519_secret_key_bytes, ohttp_key_config); + + std::string encapsulated_request_headers = + "01002000010001" + "8811eb457e100811c40a0aa71340a1b81d804bb986f736f2f566a7199761a032"; + std::string encapsulated_request_headers_bytes; + EXPECT_TRUE(absl::HexStringToBytes(encapsulated_request_headers, + &encapsulated_request_headers_bytes)); + QuicheDataReader reader(encapsulated_request_headers_bytes); + + auto context = ObliviousHttpRequest::DecodeEncapsulatedRequestHeader( + reader, *hpke_key, ohttp_key_config, + ObliviousHttpHeaderKeyConfig::kChunkedOhttpRequestLabel); + QUICHE_EXPECT_OK(context); + + // Encrypted chunks from + // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html#appendix-A + std::string decrypted_payload = ""; + std::string encrypted_non_final_chunks[] = { + "2ad24942d4d692563012f2980c8fef437a336b9b2fc938ef77a5834f", + "2e33d8fd25577afe31bd1c79d094f76b6250ae6549b473ecd950501311"}; + + for (const auto& encrypted_chunk : encrypted_non_final_chunks) { + std::string encrypted_chunk_bytes; + EXPECT_TRUE( + absl::HexStringToBytes(encrypted_chunk, &encrypted_chunk_bytes)); + auto decrypted_chunk = ObliviousHttpRequest::DecryptChunk( + *context, encrypted_chunk_bytes, /*is_final_chunk=*/false); + QUICHE_EXPECT_OK(decrypted_chunk); + absl::StrAppend(&decrypted_payload, *decrypted_chunk); + auto debug = absl::BytesToHexString(*decrypted_chunk); + EXPECT_NE(debug, encrypted_chunk); + } + + std::string final_encrypted_chunk = "1c6c1395d0ef7c1022297966307b8a7f"; + std::string final_encrypted_chunk_bytes; + EXPECT_TRUE(absl::HexStringToBytes(final_encrypted_chunk, + &final_encrypted_chunk_bytes)); + auto decrypted_final_chunk = ObliviousHttpRequest::DecryptChunk( + *context, final_encrypted_chunk_bytes, /*is_final_chunk=*/true); + QUICHE_EXPECT_OK(decrypted_final_chunk); + absl::StrAppend(&decrypted_payload, *decrypted_final_chunk); + + std::string decrypted_payload_hex = absl::BytesToHexString(decrypted_payload); + + EXPECT_EQ(decrypted_payload_hex, + "00034745540568747470730b6578616d706c652e636f6d012f"); +} + +TEST(ObliviousHttpRequest, DecryptChunkHandleError) { + auto ohttp_key_config = + GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, + EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM); + + std::string kX25519SecretKey = + "1c190d72acdbe4dbc69e680503bb781a932c70a12c8f3754434c67d8640d8698"; + std::string x25519_secret_key_bytes; + EXPECT_TRUE( + absl::HexStringToBytes(kX25519SecretKey, &x25519_secret_key_bytes)); + auto hpke_key = ConstructHpkeKey(x25519_secret_key_bytes, ohttp_key_config); + + std::string encapsulated_request_headers = + "01002000010001" + "8811eb457e100811c40a0aa71340a1b81d804bb986f736f2f566a7199761a032"; + std::string encapsulated_request_headers_bytes; + EXPECT_TRUE(absl::HexStringToBytes(encapsulated_request_headers, + &encapsulated_request_headers_bytes)); + QuicheDataReader reader(encapsulated_request_headers_bytes); + + auto context = ObliviousHttpRequest::DecodeEncapsulatedRequestHeader( + reader, *hpke_key, ohttp_key_config, + ObliviousHttpHeaderKeyConfig::kChunkedOhttpRequestLabel); + QUICHE_EXPECT_OK(context); + + // Encrypted chunks from + // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html#appendix-A + std::string decrypted_payload = ""; + // Last byte altered to make the chunk invalid. + std::string encrypted_chunk = + "2ad24942d4d692563012f2980c8fef437a336b9b2fc938ef77a5834e"; + + std::string encrypted_chunk_bytes; + EXPECT_TRUE(absl::HexStringToBytes(encrypted_chunk, &encrypted_chunk_bytes)); + auto decrypted_chunk = ObliviousHttpRequest::DecryptChunk( + *context, encrypted_chunk_bytes, /*is_final_chunk=*/false); + EXPECT_EQ(decrypted_chunk.status().code(), + absl::StatusCode::kInvalidArgument); } // Direct test example from RFC.
diff --git a/quiche/oblivious_http/buffers/oblivious_http_response.cc b/quiche/oblivious_http/buffers/oblivious_http_response.cc index ea872bd..321415b 100644 --- a/quiche/oblivious_http/buffers/oblivious_http_response.cc +++ b/quiche/oblivious_http/buffers/oblivious_http_response.cc
@@ -19,6 +19,7 @@ #include "quiche/common/quiche_crypto_logging.h" #include "quiche/common/quiche_random.h" #include "quiche/oblivious_http/buffers/oblivious_http_request.h" +#include "quiche/oblivious_http/common/oblivious_http_definitions.h" #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" namespace quiche { @@ -336,4 +337,106 @@ return result; } +absl::StatusOr<ObliviousHttpResponse::AeadContextData> +ObliviousHttpResponse::GetAeadContextData( + ObliviousHttpRequest::Context& oblivious_http_request_context, + CommonAeadParamsResult& aead_params, absl::string_view response_label, + absl::string_view response_nonce) { + // Steps (1, 3 to 5) + AEAD context SetUp before 6th step is performed in + // CommonOperations. + // https://www.rfc-editor.org/rfc/rfc9458.html#section-4.4-3 + auto common_ops_st = CommonOperationsToEncapDecap( + response_nonce, oblivious_http_request_context, response_label, + aead_params.aead_key_len, aead_params.aead_nonce_len, + aead_params.secret_len); + if (!common_ops_st.ok()) { + return common_ops_st.status(); + } + return AeadContextData{std::move(common_ops_st.value().aead_ctx), + std::move(common_ops_st.value().aead_nonce)}; +} + +// Encrypts the chunk following +// https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html#section-6.1 +// The plaintext payload can only be empty if this is the final chunk. The chunk +// nonce cannot be empty. If the operation succeeds then the returned encrypted +// data is guaranteed to be non-empty. +absl::StatusOr<std::string> ObliviousHttpResponse::EncryptChunk( + ObliviousHttpRequest::Context& oblivious_http_request_context, + const AeadContextData& aead_context_data, + absl::string_view plaintext_payload, absl::string_view chunk_nonce, + bool is_final_chunk) { + // Empty plaintext_payload is only allowed for the final chunk. + if (!is_final_chunk && plaintext_payload.empty()) { + return absl::InvalidArgumentError( + "Payload cannot be empty for non-final chunks."); + } + if (chunk_nonce.empty()) { + return absl::InvalidArgumentError("Chunk nonce cannot be empty."); + } + + uint8_t* ad = nullptr; + size_t ad_len = 0; + if (is_final_chunk) { + ad = const_cast<uint8_t*>(kFinalAdBytes); + ad_len = sizeof(kFinalAdBytes); + } + + const size_t max_encrypted_data_size = + plaintext_payload.size() + + EVP_AEAD_max_overhead(EVP_HPKE_AEAD_aead(EVP_HPKE_CTX_aead( + oblivious_http_request_context.hpke_context_.get()))); + std::string encrypted_data(max_encrypted_data_size, '\0'); + size_t ciphertext_len; + + if (!EVP_AEAD_CTX_seal( + aead_context_data.aead_ctx.get(), + reinterpret_cast<uint8_t*>(encrypted_data.data()), &ciphertext_len, + encrypted_data.size(), + reinterpret_cast<const uint8_t*>(chunk_nonce.data()), + aead_context_data.aead_nonce.size(), + reinterpret_cast<const uint8_t*>(plaintext_payload.data()), + plaintext_payload.size(), ad, ad_len)) { + return SslErrorAsStatus( + "Failed to encrypt the payload with derived AEAD key."); + } + encrypted_data.resize(ciphertext_len); + if (ciphertext_len == 0) { + return absl::InternalError("Generated Encrypted payload cannot be empty."); + } + return encrypted_data; +} + +absl::StatusOr<ObliviousHttpResponse::ChunkCounter> +ObliviousHttpResponse::ChunkCounter::Create(std::string nonce) { + if (nonce.empty()) { + return absl::InvalidArgumentError("Nonce must not be empty."); + } + return ObliviousHttpResponse::ChunkCounter(std::move(nonce)); +} + +ObliviousHttpResponse::ChunkCounter::ChunkCounter(std::string nonce) + : nonce_(std::move(nonce)), encoded_counter_(nonce_.size(), '\0') {} + +void ObliviousHttpResponse::ChunkCounter::Increment() { + uint64_t pos = nonce_.size(); + // Start with the least significant byte and increment it by 1, if it wraps + // then proceed to the next byte and repeat. + do { + pos--; + encoded_counter_[pos] += static_cast<uint8_t>(1); + } while (pos != 0 && encoded_counter_[pos] == '\0'); + if (pos == 0 && encoded_counter_[pos] == '\0') { + // Counter has wrapped. + limit_exceeded_ = true; + } +} + +std::string ObliviousHttpResponse::ChunkCounter::GetChunkNonce() const { + std::string chunk_nonce(nonce_.size(), '\0'); + for (size_t i = 0; i < chunk_nonce.size(); i++) { + chunk_nonce[i] = nonce_[i] ^ encoded_counter_[i]; + } + return chunk_nonce; +} } // namespace quiche
diff --git a/quiche/oblivious_http/buffers/oblivious_http_response.h b/quiche/oblivious_http/buffers/oblivious_http_response.h index 2bc7caf..29b6763 100644 --- a/quiche/oblivious_http/buffers/oblivious_http_response.h +++ b/quiche/oblivious_http/buffers/oblivious_http_response.h
@@ -4,9 +4,11 @@ #include <stddef.h> #include <string> +#include <utility> #include "absl/status/statusor.h" #include "absl/strings/string_view.h" +#include "openssl/base.h" #include "quiche/common/quiche_random.h" #include "quiche/oblivious_http/buffers/oblivious_http_request.h" #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" @@ -15,6 +17,41 @@ class QUICHE_EXPORT ObliviousHttpResponse { public: + // A counter of the number of chunks sent/received in the response, used to + // get the appropriate chunk nonce for encryption/decryption. See + // (https://datatracker.ietf.org/doc/html/draft-ietf-ohai-chunked-ohttp-05#section-6.2). + class QUICHE_EXPORT ChunkCounter { + public: + static absl::StatusOr<ChunkCounter> Create(std::string nonce); + // Returns true if the counter has exceeded the maximum allowed value. + bool LimitExceeded() const { return limit_exceeded_; } + // Increments the chunk counter. + void Increment(); + // XORs the nonce with the encoded counter to get the chunk nonce. + std::string GetChunkNonce() const; + + private: + explicit ChunkCounter(std::string nonce); + // The nonce used to initialize the counter. + const std::string nonce_; + // Represents the counter value encoded to `Nn` bytes in network + // byte order. + std::string encoded_counter_; + bool limit_exceeded_ = false; + }; + + // Common AEAD context data used for sealing/opening response chunks. + struct QUICHE_EXPORT AeadContextData { + bssl::UniquePtr<EVP_AEAD_CTX> aead_ctx; + const std::string aead_nonce; + }; + struct QUICHE_EXPORT CommonAeadParamsResult { + const EVP_AEAD* evp_hpke_aead; + const size_t aead_key_len; + const size_t aead_nonce_len; + const size_t secret_len; + }; + // 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`. @@ -57,6 +94,18 @@ ~ObliviousHttpResponse() = default; + // Generates the AEAD context data from the response nonce. + static absl::StatusOr<AeadContextData> GetAeadContextData( + ObliviousHttpRequest::Context& oblivious_http_request_context, + CommonAeadParamsResult& aead_params, absl::string_view response_label, + absl::string_view response_nonce); + + static absl::StatusOr<std::string> EncryptChunk( + ObliviousHttpRequest::Context& oblivious_http_request_context, + const AeadContextData& aead_context_data, + absl::string_view plaintext_payload, absl::string_view chunk_nonce, + bool is_final_chunk); + // 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. @@ -67,14 +116,12 @@ return std::move(response_plaintext_); } - 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; - }; + // 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); + private: struct CommonOperationsResult { bssl::UniquePtr<EVP_AEAD_CTX> aead_ctx; const std::string aead_nonce; @@ -83,10 +130,6 @@ explicit ObliviousHttpResponse(std::string encrypted_data, 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(
diff --git a/quiche/oblivious_http/buffers/oblivious_http_response_test.cc b/quiche/oblivious_http/buffers/oblivious_http_response_test.cc index bcb91a8..c76007a 100644 --- a/quiche/oblivious_http/buffers/oblivious_http_response_test.cc +++ b/quiche/oblivious_http/buffers/oblivious_http_response_test.cc
@@ -9,12 +9,15 @@ #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/hpke.h" #include "quiche/common/platform/api/quiche_test.h" +#include "quiche/common/quiche_data_reader.h" +#include "quiche/common/test_tools/quiche_test_utils.h" #include "quiche/oblivious_http/buffers/oblivious_http_request.h" #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" @@ -225,4 +228,172 @@ expected_encrypted_response_bytes); } +struct EncryptChunkTestParams { + ObliviousHttpRequest::Context context; + ObliviousHttpResponse::CommonAeadParamsResult aead_params; + ObliviousHttpResponse::AeadContextData aead_context_data; +}; + +absl::StatusOr<EncryptChunkTestParams> SetUpEncryptChunkTest() { + // Example from + // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html#appendix-A + auto ohttp_key_config = + GetOhttpKeyConfig(1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, + EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM); + + std::string kX25519SecretKey = + "1c190d72acdbe4dbc69e680503bb781a932c70a12c8f3754434c67d8640d8698"; + std::string x25519_secret_key_bytes; + EXPECT_TRUE( + absl::HexStringToBytes(kX25519SecretKey, &x25519_secret_key_bytes)); + auto hpke_key = ConstructHpkeKey(x25519_secret_key_bytes, ohttp_key_config); + + std::string encapsulated_request_headers = + "01002000010001" + "8811eb457e100811c40a0aa71340a1b81d804bb986f736f2f566a7199761a032"; + std::string encapsulated_request_headers_bytes; + EXPECT_TRUE(absl::HexStringToBytes(encapsulated_request_headers, + &encapsulated_request_headers_bytes)); + QuicheDataReader reader(encapsulated_request_headers_bytes); + + auto context = ObliviousHttpRequest::DecodeEncapsulatedRequestHeader( + reader, *hpke_key, ohttp_key_config, + ObliviousHttpHeaderKeyConfig::kChunkedOhttpRequestLabel); + QUICHE_EXPECT_OK(context); + + absl::StatusOr<ObliviousHttpResponse::CommonAeadParamsResult> aead_params = + ObliviousHttpResponse::GetCommonAeadParams(*context); + EXPECT_TRUE(aead_params.ok()); + + auto response_nonce = "bcce7f4cb921309ba5d62edf1769ef09"; + std::string response_nonce_bytes; + EXPECT_TRUE(absl::HexStringToBytes(response_nonce, &response_nonce_bytes)); + auto aead_context_data = ObliviousHttpResponse::GetAeadContextData( + *context, *aead_params, + ObliviousHttpHeaderKeyConfig::kChunkedOhttpResponseLabel, + response_nonce_bytes); + QUICHE_EXPECT_OK(aead_context_data); + + return EncryptChunkTestParams{ + .context = std::move(*context), + .aead_params = std::move(*aead_params), + .aead_context_data = std::move(*aead_context_data)}; +} + +TEST(ObliviousHttpResponse, TestEncryptChunks) { + auto test_params = SetUpEncryptChunkTest(); + QUICHE_EXPECT_OK(test_params); + auto& [context, aead_params, aead_context_data] = *test_params; + + std::string plaintext_payload = "01"; + std::string plaintext_payload_bytes; + EXPECT_TRUE( + absl::HexStringToBytes(plaintext_payload, &plaintext_payload_bytes)); + std::string chunk_nonce = "fead854635d2d5527d64f546"; + std::string chunk_nonce_bytes; + EXPECT_TRUE(absl::HexStringToBytes(chunk_nonce, &chunk_nonce_bytes)); + + auto encrypted_chunk = ObliviousHttpResponse::EncryptChunk( + context, aead_context_data, plaintext_payload_bytes, chunk_nonce_bytes, + /*is_final_chunk=*/false); + QUICHE_EXPECT_OK(encrypted_chunk); + std::string encrypted_chunk_hex = absl::BytesToHexString(*encrypted_chunk); + EXPECT_EQ(encrypted_chunk_hex, "79bf1cc87fa0e2c02de4546945aa3d1e48"); + + plaintext_payload = "40c8"; + EXPECT_TRUE( + absl::HexStringToBytes(plaintext_payload, &plaintext_payload_bytes)); + chunk_nonce = "fead854635d2d5527d64f547"; + EXPECT_TRUE(absl::HexStringToBytes(chunk_nonce, &chunk_nonce_bytes)); + + encrypted_chunk = ObliviousHttpResponse::EncryptChunk( + context, aead_context_data, plaintext_payload_bytes, chunk_nonce_bytes, + /*is_final_chunk=*/false); + QUICHE_EXPECT_OK(encrypted_chunk); + encrypted_chunk_hex = absl::BytesToHexString(*encrypted_chunk); + EXPECT_EQ(encrypted_chunk_hex, "b348b5bd4c594c16b6170b07b475845d1f32"); + + chunk_nonce = "fead854635d2d5527d64f544"; + EXPECT_TRUE(absl::HexStringToBytes(chunk_nonce, &chunk_nonce_bytes)); + + encrypted_chunk = ObliviousHttpResponse::EncryptChunk( + context, aead_context_data, /*plaintext_payload=*/"", chunk_nonce_bytes, + /*is_final_chunk=*/true); + QUICHE_EXPECT_OK(encrypted_chunk); + encrypted_chunk_hex = absl::BytesToHexString(*encrypted_chunk); + EXPECT_EQ(encrypted_chunk_hex, "ed9d8a796617a5b27265f4d73247f639"); +} + +TEST(OblviousHttpResponse, EncryptNonFinalChunkWithEmptyPayloadError) { + auto test_params = SetUpEncryptChunkTest(); + QUICHE_EXPECT_OK(test_params); + auto& [context, aead_params, aead_context_data] = *test_params; + + EXPECT_EQ(ObliviousHttpResponse::EncryptChunk(context, aead_context_data, + /*plaintext_payload=*/"", "", + /*is_final_chunk=*/false) + .status() + .code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(OblviousHttpResponse, EncryptChunkWithEmptyNonceError) { + auto test_params = SetUpEncryptChunkTest(); + QUICHE_EXPECT_OK(test_params); + auto& [context, aead_params, aead_context_data] = *test_params; + + EXPECT_EQ(ObliviousHttpResponse::EncryptChunk(context, aead_context_data, + /*plaintext_payload=*/"111", "", + /*is_final_chunk=*/false) + .status() + .code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ChunkCounter, EmptyNonceIsInvalid) { + EXPECT_EQ(ObliviousHttpResponse::ChunkCounter::Create("").status().code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ChunkCounter, GetChunkNonce) { + // Chunk nonces from + // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html#appendix-A + std::string nonce_hex = "fead854635d2d5527d64f546"; + std::string nonce; + EXPECT_TRUE(absl::HexStringToBytes(nonce_hex, &nonce)); + auto chunk_counter = ObliviousHttpResponse::ChunkCounter::Create(nonce); + EXPECT_TRUE(chunk_counter.ok()); + + std::string expected_chunk_nonce_hex = "fead854635d2d5527d64f546"; + std::string chunk_nonce; + EXPECT_TRUE(absl::HexStringToBytes(expected_chunk_nonce_hex, &chunk_nonce)); + EXPECT_EQ(chunk_counter->GetChunkNonce(), chunk_nonce); + + chunk_counter->Increment(); + expected_chunk_nonce_hex = "fead854635d2d5527d64f547"; + EXPECT_TRUE(absl::HexStringToBytes(expected_chunk_nonce_hex, &chunk_nonce)); + EXPECT_EQ(chunk_counter->GetChunkNonce(), chunk_nonce); + + chunk_counter->Increment(); + expected_chunk_nonce_hex = "fead854635d2d5527d64f544"; + EXPECT_TRUE(absl::HexStringToBytes(expected_chunk_nonce_hex, &chunk_nonce)); + EXPECT_EQ(chunk_counter->GetChunkNonce(), chunk_nonce); +} + +TEST(ChunkCounter, LimitExceeded) { + std::string nonce_hex = "00"; + std::string nonce; + EXPECT_TRUE(absl::HexStringToBytes(nonce_hex, &nonce)); + auto chunk_counter = ObliviousHttpResponse::ChunkCounter::Create(nonce); + EXPECT_TRUE(chunk_counter.ok()); + + for (int i = 0; i < 256; ++i) { + EXPECT_FALSE(chunk_counter->LimitExceeded()); + chunk_counter->Increment(); + } + + // Counter limit reached at 2^(nonce_size * 8) + EXPECT_TRUE(chunk_counter->LimitExceeded()); +} + } // namespace quiche
diff --git a/quiche/oblivious_http/common/oblivious_http_chunk_handler.h b/quiche/oblivious_http/common/oblivious_http_chunk_handler.h new file mode 100644 index 0000000..7c7bd77 --- /dev/null +++ b/quiche/oblivious_http/common/oblivious_http_chunk_handler.h
@@ -0,0 +1,25 @@ +#ifndef QUICHE_OBLIVIOUS_HTTP_COMMON_CHUNK_HANDLER_H_ +#define QUICHE_OBLIVIOUS_HTTP_COMMON_CHUNK_HANDLER_H_ + +#include "absl/status/status.h" +#include "absl/strings/string_view.h" +#include "quiche/common/platform/api/quiche_export.h" + +namespace quiche { + +// Methods to be invoked upon decryption of request/response OHTTP chunks. +class QUICHE_EXPORT ObliviousHttpChunkHandler { + public: + virtual ~ObliviousHttpChunkHandler() = default; + // This method is invoked once a chunk of data has been decrypted. It returns + // a Status to allow the implementation to signal a potential error, such as a + // decoding issue with the decrypted data. + virtual absl::Status OnDecryptedChunk(absl::string_view decrypted_chunk) = 0; + // This method is invoked once all chunks have been decrypted. It returns + // a Status to allow the implementation to signal a potential error. + virtual absl::Status OnChunksDone() = 0; +}; + +} // namespace quiche + +#endif // QUICHE_OBLIVIOUS_HTTP_COMMON_CHUNK_HANDLER_H_
diff --git a/quiche/oblivious_http/common/oblivious_http_definitions.h b/quiche/oblivious_http/common/oblivious_http_definitions.h new file mode 100644 index 0000000..6403fee --- /dev/null +++ b/quiche/oblivious_http/common/oblivious_http_definitions.h
@@ -0,0 +1,14 @@ +#ifndef QUICHE_OBLIVIOUS_HTTP_COMMON_OBLIVIOUS_HTTP_DEFINITIONS_H_ +#define QUICHE_OBLIVIOUS_HTTP_COMMON_OBLIVIOUS_HTTP_DEFINITIONS_H_ + +#include <stdint.h> + +namespace quiche { +// This 5-byte array represents the string "final", which is used as AD when +// encrypting/decrypting the final OHTTP chunk. See sections 6.1 and 6.2 of +// https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html +constexpr uint8_t kFinalAdBytes[] = {0x66, 0x69, 0x6E, 0x61, 0x6C}; + +} // namespace quiche + +#endif // QUICHE_OBLIVIOUS_HTTP_COMMON_OBLIVIOUS_HTTP_DEFINITIONS_H_
diff --git a/quiche/oblivious_http/common/oblivious_http_header_key_config.h b/quiche/oblivious_http/common/oblivious_http_header_key_config.h index f57efbd..9e48336 100644 --- a/quiche/oblivious_http/common/oblivious_http_header_key_config.h +++ b/quiche/oblivious_http/common/oblivious_http_header_key_config.h
@@ -25,6 +25,10 @@ "message/bhttp request"; static constexpr absl::string_view kOhttpResponseLabel = "message/bhttp response"; + static constexpr absl::string_view kChunkedOhttpRequestLabel = + "message/bhttp chunked request"; + static constexpr absl::string_view kChunkedOhttpResponseLabel = + "message/bhttp chunked response"; // Length of the Oblivious HTTP header. static constexpr uint32_t kHeaderLength = sizeof(uint8_t) + (3 * sizeof(uint16_t));
diff --git a/quiche/oblivious_http/common/oblivious_http_header_key_config_test.cc b/quiche/oblivious_http/common/oblivious_http_header_key_config_test.cc index 67a78d3..fb3688d 100644 --- a/quiche/oblivious_http/common/oblivious_http_header_key_config_test.cc +++ b/quiche/oblivious_http/common/oblivious_http_header_key_config_test.cc
@@ -60,12 +60,11 @@ return ohttp_key; } -TEST(ObliviousHttpHeaderKeyConfig, TestSerializeRecipientContextInfo) { +void ExpectSerializedRecipientContextInfo(absl::string_view ohttp_req_label) { 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) + @@ -77,10 +76,21 @@ auto instance = ObliviousHttpHeaderKeyConfig::Create(key_id, kem_id, kdf_id, aead_id); ASSERT_TRUE(instance.ok()); - EXPECT_EQ(instance.value().SerializeRecipientContextInfo(), expected); + EXPECT_EQ(instance.value().SerializeRecipientContextInfo(ohttp_req_label), + expected); EXPECT_THAT(instance->DebugString(), HasSubstr("AES-256-GCM")); } +TEST(ObliviousHttpHeaderKeyConfig, + TestSerializeRecipientContextInfoStandardLabel) { + ExpectSerializedRecipientContextInfo("message/bhttp request"); +} + +TEST(ObliviousHttpHeaderKeyConfig, + TestSerializeRecipientContextInfoChunkedLabel) { + ExpectSerializedRecipientContextInfo("message/bhttp chunked request"); +} + TEST(ObliviousHttpHeaderKeyConfig, TestValidKeyConfig) { auto valid_key_config = ObliviousHttpHeaderKeyConfig::Create( 2, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
diff --git a/quiche/oblivious_http/oblivious_http_gateway.cc b/quiche/oblivious_http/oblivious_http_gateway.cc index 4a15cb6..5cb7b20 100644 --- a/quiche/oblivious_http/oblivious_http_gateway.cc +++ b/quiche/oblivious_http/oblivious_http_gateway.cc
@@ -6,14 +6,28 @@ #include <string> #include <utility> +#include "absl/base/attributes.h" #include "absl/memory/memory.h" #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/base.h" +#include "openssl/hpke.h" #include "quiche/common/quiche_crypto_logging.h" +#include "quiche/common/quiche_data_reader.h" +#include "quiche/common/quiche_data_writer.h" +#include "quiche/common/quiche_endian.h" #include "quiche/common/quiche_random.h" +#include "quiche/oblivious_http/buffers/oblivious_http_request.h" +#include "quiche/oblivious_http/common/oblivious_http_chunk_handler.h" +#include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" namespace quiche { +namespace { +constexpr uint64_t kFinalChunkIndicator = 0; +} // Constructor. ObliviousHttpGateway::ObliviousHttpGateway( @@ -77,4 +91,276 @@ quiche_random_); } +// Constructor. +ChunkedObliviousHttpGateway::ChunkedObliviousHttpGateway( + bssl::UniquePtr<EVP_HPKE_KEY> recipient_key, + const ObliviousHttpHeaderKeyConfig& ohttp_key_config, + ObliviousHttpChunkHandler& chunk_handler, QuicheRandom* quiche_random) + : server_hpke_key_(std::move(recipient_key)), + ohttp_key_config_(ohttp_key_config), + chunk_handler_(chunk_handler), + quiche_random_(quiche_random) {} + +absl::StatusOr<ChunkedObliviousHttpGateway> ChunkedObliviousHttpGateway::Create( + absl::string_view hpke_private_key, + const ObliviousHttpHeaderKeyConfig& ohttp_key_config, + ObliviousHttpChunkHandler& chunk_handler, QuicheRandom* quiche_random) { + absl::StatusOr<bssl::UniquePtr<EVP_HPKE_KEY>> recipient_key = + CreateServerRecipientKey(hpke_private_key, ohttp_key_config); + if (!recipient_key.ok()) { + return recipient_key.status(); + } + if (quiche_random == nullptr) { + quiche_random = QuicheRandom::GetInstance(); + } + return ChunkedObliviousHttpGateway(std::move(*recipient_key), + ohttp_key_config, chunk_handler, + quiche_random); +} + +void ChunkedObliviousHttpGateway::InitializeRequestCheckpoint( + absl::string_view data) { + request_checkpoint_view_ = data; + // Prepend buffered data if present. This is the data from a previous call to + // DecryptRequest that could not finish because it needed this new data. + if (!request_buffer_.empty()) { + if (!data.empty()) { + absl::StrAppend(&request_buffer_, data); + } + request_checkpoint_view_ = request_buffer_; + } +} + +absl::Status ChunkedObliviousHttpGateway::DecryptRequestCheckpoint( + bool end_stream) { + QuicheDataReader reader(request_checkpoint_view_); + switch (request_current_section_) { + case RequestMessageSection::kEnd: + return absl::InternalError("Request is invalid."); + case RequestMessageSection::kHeader: { + // Check there is enough data for the chunked request header. + // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html#name-request-format + if (reader.PeekRemainingPayload().size() < + ObliviousHttpHeaderKeyConfig::kHeaderLength + + EVP_HPKE_KEM_enc_len(EVP_HPKE_KEY_kem(server_hpke_key_.get()))) { + return absl::OutOfRangeError("Not enough data to read header."); + } + absl::StatusOr<ObliviousHttpRequest::Context> context = + ObliviousHttpRequest::DecodeEncapsulatedRequestHeader( + reader, *server_hpke_key_, ohttp_key_config_, + ObliviousHttpHeaderKeyConfig::kChunkedOhttpRequestLabel); + if (!context.ok()) { + return context.status(); + } + + oblivious_http_request_context_ = std::move(*context); + SaveCheckpoint(reader); + request_current_section_ = RequestMessageSection::kChunk; + } + ABSL_FALLTHROUGH_INTENDED; + case RequestMessageSection::kChunk: { + uint64_t length_or_final_chunk_indicator; + do { + if (!reader.ReadVarInt62(&length_or_final_chunk_indicator)) { + return absl::OutOfRangeError("Not enough data to read chunk length."); + } + absl::string_view chunk; + if (length_or_final_chunk_indicator != kFinalChunkIndicator) { + if (!reader.ReadStringPiece(&chunk, + length_or_final_chunk_indicator)) { + return absl::OutOfRangeError("Not enough data to read chunk."); + } + if (!oblivious_http_request_context_.has_value()) { + return absl::InternalError( + "HPKE context has not been derived from an encrypted request."); + } + absl::StatusOr<std::string> decrypted_chunk = + ObliviousHttpRequest::DecryptChunk( + *oblivious_http_request_context_, chunk, + /*is_final_chunk=*/false); + if (!decrypted_chunk.ok()) { + return decrypted_chunk.status(); + } + absl::Status handle_chunk_status = + chunk_handler_.OnDecryptedChunk(*decrypted_chunk); + if (!handle_chunk_status.ok()) { + return handle_chunk_status; + } + } + + SaveCheckpoint(reader); + } while (length_or_final_chunk_indicator != kFinalChunkIndicator); + + request_current_section_ = RequestMessageSection::kFinalChunk; + } + ABSL_FALLTHROUGH_INTENDED; + case RequestMessageSection::kFinalChunk: { + if (!end_stream) { + return absl::OutOfRangeError("Not enough data to read final chunk."); + } + if (!oblivious_http_request_context_.has_value()) { + return absl::InternalError( + "HPKE context has not been derived from an encrypted request."); + } + absl::StatusOr<std::string> decrypted_chunk = + ObliviousHttpRequest::DecryptChunk(*oblivious_http_request_context_, + reader.PeekRemainingPayload(), + /*is_final_chunk=*/true); + if (!decrypted_chunk.ok()) { + return decrypted_chunk.status(); + } + absl::Status handle_chunk_status = + chunk_handler_.OnDecryptedChunk(*decrypted_chunk); + if (!handle_chunk_status.ok()) { + return handle_chunk_status; + } + handle_chunk_status = chunk_handler_.OnChunksDone(); + if (!handle_chunk_status.ok()) { + return handle_chunk_status; + } + } + } + return absl::OkStatus(); +} + +absl::Status ChunkedObliviousHttpGateway::DecryptRequest(absl::string_view data, + bool end_stream) { + if (request_current_section_ == RequestMessageSection::kEnd) { + return absl::InternalError("Decrypting is marked as invalid."); + } + InitializeRequestCheckpoint(data); + absl::Status status = DecryptRequestCheckpoint(end_stream); + if (end_stream) { + request_current_section_ = RequestMessageSection::kEnd; + if (absl::IsOutOfRange(status)) { + // OutOfRange only used internally for buffering, so return + // InvalidArgument if this is the end of the stream. + status = absl::InvalidArgumentError(status.message()); + } + return status; + } + if (absl::IsOutOfRange(status)) { + BufferRequestCheckpoint(); + return absl::OkStatus(); + } + if (!status.ok()) { + request_current_section_ = RequestMessageSection::kEnd; + } + + request_buffer_.clear(); + return status; +} + +absl::StatusOr<std::string> ChunkedObliviousHttpGateway::EncryptResponse( + absl::string_view plaintext_payload, bool is_final_chunk) { + if (response_current_section_ == ResponseMessageSection::kEnd) { + return absl::InvalidArgumentError("Encrypting is marked as invalid."); + } + absl::StatusOr<std::string> response_chunk = + EncryptResponseChunk(plaintext_payload, is_final_chunk); + if (!response_chunk.ok()) { + response_current_section_ = ResponseMessageSection::kEnd; + } + return response_chunk; +} + +absl::StatusOr<std::string> ChunkedObliviousHttpGateway::EncryptResponseChunk( + absl::string_view plaintext_payload, bool is_final_chunk) { + if (response_chunk_counter_.has_value() && + response_chunk_counter_->LimitExceeded()) { + return absl::InternalError( + "Response chunk counter has exceeded the maximum allowed value."); + } + if (!oblivious_http_request_context_.has_value()) { + return absl::InternalError( + "HPKE context has not been derived from an encrypted request."); + } + + if (!aead_context_data_.has_value()) { + absl::StatusOr<ObliviousHttpResponse::CommonAeadParamsResult> aead_params = + ObliviousHttpResponse::GetCommonAeadParams( + *oblivious_http_request_context_); + if (!aead_params.ok()) { + return aead_params.status(); + } + + // secret_len represents max(Nn, Nk)) + response_nonce_ = std::string(aead_params->secret_len, '\0'); + quiche_random_->RandBytes(response_nonce_.data(), response_nonce_.size()); + + auto aead_context_data = ObliviousHttpResponse::GetAeadContextData( + *oblivious_http_request_context_, *aead_params, + ObliviousHttpHeaderKeyConfig::kChunkedOhttpResponseLabel, + response_nonce_); + if (!aead_context_data.ok()) { + return aead_context_data.status(); + } + aead_context_data_.emplace(std::move(*aead_context_data)); + + auto response_chunk_counter = ObliviousHttpResponse::ChunkCounter::Create( + aead_context_data_->aead_nonce); + if (!response_chunk_counter.ok()) { + return response_chunk_counter.status(); + } + response_chunk_counter_.emplace(std::move(*response_chunk_counter)); + } + + if (!response_chunk_counter_.has_value()) { + return absl::InternalError( + "Response chunk counter has not been initialized."); + } + + absl::StatusOr<std::string> encrypted_data = + ObliviousHttpResponse::EncryptChunk( + *oblivious_http_request_context_, *aead_context_data_, + plaintext_payload, response_chunk_counter_->GetChunkNonce(), + is_final_chunk); + if (!encrypted_data.ok()) { + return encrypted_data.status(); + } + + absl::string_view maybe_nonce; + if (response_current_section_ == ResponseMessageSection::kNonce) { + maybe_nonce = response_nonce_; + response_current_section_ = ResponseMessageSection::kChunk; + } + + uint8_t chunk_var_int_length = + QuicheDataWriter::GetVarInt62Len(encrypted_data->size()); + uint64_t chunk_var_int = encrypted_data->size(); + if (is_final_chunk) { + response_current_section_ = ResponseMessageSection::kEnd; + chunk_var_int_length = + QuicheDataWriter::GetVarInt62Len(kFinalChunkIndicator); + // encrypted_data is guaranteed to be non-empty, so chunk_var_int_length + // should never be 0. + if (chunk_var_int_length == 0) { + return absl::InvalidArgumentError( + "Encrypted data is too large to be represented as a varint."); + } + chunk_var_int = kFinalChunkIndicator; + } + + std::string response_buffer( + maybe_nonce.size() + chunk_var_int_length + encrypted_data->size(), '\0'); + QuicheDataWriter writer(response_buffer.size(), response_buffer.data()); + + if (!writer.WriteStringPiece(maybe_nonce)) { + return absl::InternalError("Failed to write response nonce to buffer."); + } + if (!writer.WriteVarInt62(chunk_var_int)) { + return absl::InternalError("Failed to write chunk to buffer."); + } + if (!writer.WriteStringPiece(*encrypted_data)) { + return absl::InternalError("Failed to write encrypted data to buffer."); + } + + if (writer.remaining() != 0) { + return absl::InternalError("Failed to write all data."); + } + + response_chunk_counter_->Increment(); + return response_buffer; +} + } // namespace quiche
diff --git a/quiche/oblivious_http/oblivious_http_gateway.h b/quiche/oblivious_http/oblivious_http_gateway.h index fce1f04..6c84c5c 100644 --- a/quiche/oblivious_http/oblivious_http_gateway.h +++ b/quiche/oblivious_http/oblivious_http_gateway.h
@@ -1,17 +1,23 @@ #ifndef QUICHE_OBLIVIOUS_HTTP_OBLIVIOUS_HTTP_GATEWAY_H_ #define QUICHE_OBLIVIOUS_HTTP_OBLIVIOUS_HTTP_GATEWAY_H_ +#include <cmath> #include <memory> +#include <optional> #include <string> +#include <utility> +#include "absl/status/status.h" #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_data_reader.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_chunk_handler.h" #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" namespace quiche { @@ -82,6 +88,116 @@ QuicheRandom* quiche_random_; }; +// Manages a chunked Oblivious HTTP request and response. +// It's designed to process incoming request data in chunks, decrypting each one +// as it arrives and passing it to a handler function. It then continuously +// encrypts and sends back response chunks. This object maintains an internal +// state, so it can only be used for one complete request-response cycle. +class QUICHE_EXPORT ChunkedObliviousHttpGateway { + public: + // Creates a ChunkedObliviousHttpGateway. Like `ObliviousHttpGateway`, + // `hpke_private_key` must outlive the gateway. `quiche_random` can be + // initialized to nullptr, in which case the default + // `QuicheRandom::GetInstance()` will be used. + static absl::StatusOr<ChunkedObliviousHttpGateway> Create( + absl::string_view hpke_private_key, + const ObliviousHttpHeaderKeyConfig& ohttp_key_config, + ObliviousHttpChunkHandler& chunk_handler, + QuicheRandom* quiche_random = nullptr); + + // only Movable (due to `UniquePtr server_hpke_key_`). + ChunkedObliviousHttpGateway(ChunkedObliviousHttpGateway&& other) = default; + + ~ChunkedObliviousHttpGateway() = default; + + // Parses the data into the corresponding chunks and decrypts them. This can + // be invoked multiple times as data arrives, incomplete chunks will be + // buffered. The first time it is called it will also decode the HPKE header. + // On successful decryption, the chunk handler will be invoked. The + // `end_stream` parameter must be set to true if the data contains the final + // portion of the final chunk. + absl::Status DecryptRequest(absl::string_view data, bool end_stream); + + // Encrypts the data as a single chunk. If `is_final_chunk` is true, the + // response will be encoded with the final chunk indicator. + absl::StatusOr<std::string> EncryptResponse( + absl::string_view plaintext_payload, bool is_final_chunk); + + private: + enum class RequestMessageSection { + kHeader, + kChunk, + kFinalChunk, + // Set by end_stream or if there is an error. + kEnd, + }; + enum class ResponseMessageSection { + kNonce, + kChunk, + // Set after the final chunk is encrypted or if there is an error. + kEnd, + }; + + explicit ChunkedObliviousHttpGateway( + bssl::UniquePtr<EVP_HPKE_KEY> recipient_key, + const ObliviousHttpHeaderKeyConfig& ohttp_key_config, + ObliviousHttpChunkHandler& chunk_handler, QuicheRandom* quiche_random); + + // Initializes the checkpoint with the provided data and any buffered data. + void InitializeRequestCheckpoint(absl::string_view data); + // Carries out the decrypting logic from the checkpoint. Returns + // OutOfRangeError if there is not enough data to process the current + // section. When a section is fully processed, the checkpoint is updated. + absl::Status DecryptRequestCheckpoint(bool end_stream); + // Saves the checkpoint based on the current position of the reader. + void SaveCheckpoint(const QuicheDataReader& reader) { + request_checkpoint_view_ = reader.PeekRemainingPayload(); + } + // Buffers the request checkpoint. + void BufferRequestCheckpoint() { + if (request_buffer_ != request_checkpoint_view_) { + request_buffer_ = std::string(request_checkpoint_view_); + } + } + absl::StatusOr<std::string> EncryptResponseChunk( + absl::string_view plaintext_payload, bool is_final_chunk); + + 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.rfc-editor.org/rfc/rfc9458.html#section-3 + ObliviousHttpHeaderKeyConfig ohttp_key_config_; + // The handler to invoke when a chunk is decrypted successfully. + ObliviousHttpChunkHandler& chunk_handler_; + QuicheRandom* quiche_random_; + + std::string request_buffer_; + // Tracks the remaining data to be processed or buffered. + // When decoding fails due to missing data, we buffer based on this + // checkpoint and return. When decoding succeeds, we update the checkpoint + // to not buffer the already processed data. + absl::string_view request_checkpoint_view_; + RequestMessageSection request_current_section_ = + RequestMessageSection::kHeader; + ResponseMessageSection response_current_section_ = + ResponseMessageSection::kNonce; + + // HPKE data derived from successfully decoding the chunked + // request header when calling `DecryptRequest`. + std::optional<ObliviousHttpRequest::Context> oblivious_http_request_context_; + // The nonce for the response. + std::string response_nonce_; + // AEAD context data for the response. This is derived from the request HPKE + // context data and response nonce. + std::optional<ObliviousHttpResponse::AeadContextData> aead_context_data_; + + // Used to determine whether the response nonce has already been encoded. + bool has_encoded_response_nonce_ = false; + // Counter to keep track of the number of response chunks generated and to + // generate the corresponding chunk nonce. + std::optional<ObliviousHttpResponse::ChunkCounter> response_chunk_counter_; +}; + } // 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 index 209af36..04360f0 100644 --- a/quiche/oblivious_http/oblivious_http_gateway_test.cc +++ b/quiche/oblivious_http/oblivious_http_gateway_test.cc
@@ -2,21 +2,66 @@ #include <stdint.h> +#include <algorithm> +#include <cstddef> +#include <cstring> #include <string> #include <utility> +#include <vector> #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/hpke.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/common/test_tools/quiche_test_utils.h" #include "quiche/oblivious_http/buffers/oblivious_http_request.h" +#include "quiche/oblivious_http/common/oblivious_http_chunk_handler.h" +#include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" namespace quiche { namespace { +constexpr absl::string_view kEncapsulatedChunkedRequest = + "01002000010001" + "8811eb457e100811c40a0aa71340a1b81d804bb986f736f2f566a7199761a032" + "1c2ad24942d4d692563012f2980c8fef437a336b9b2fc938ef77a5834f" + "1d2e33d8fd25577afe31bd1c79d094f76b6250ae6549b473ecd950501311" + "001c6c1395d0ef7c1022297966307b8a7f"; + +class TestChunkHandler : public ObliviousHttpChunkHandler { + public: + TestChunkHandler() = default; + ~TestChunkHandler() override = default; + absl::Status OnDecryptedChunk(absl::string_view decrypted_chunk) override { + EXPECT_FALSE(on_chunks_done_called_); + chunk_count_++; + absl::StrAppend(&concatenated_decrypted_chunks_, decrypted_chunk); + return absl::OkStatus(); + } + absl::Status OnChunksDone() override { + EXPECT_FALSE(on_chunks_done_called_); + on_chunks_done_called_ = true; + std::string expected_request; + EXPECT_TRUE(absl::HexStringToBytes( + "00034745540568747470730b6578616d706c652e636f6d012f", + &expected_request)); + EXPECT_EQ(concatenated_decrypted_chunks_, expected_request); + return absl::OkStatus(); + } + uint64_t GetChunkCount() const { return chunk_count_; } + bool GetOnChunksDoneCalled() const { return on_chunks_done_called_; } + + private: + uint64_t chunk_count_ = 0; + bool on_chunks_done_called_ = false; + std::string concatenated_decrypted_chunks_; +}; + std::string GetHpkePrivateKey() { // Dev/Test private key generated using Keystore. absl::string_view hpke_key_hex = @@ -76,6 +121,167 @@ ASSERT_FALSE(decrypted_req->GetPlaintextData().empty()); } +absl::StatusOr<ChunkedObliviousHttpGateway> CreateChunkedObliviousHttpGateway( + ObliviousHttpChunkHandler& chunk_handler, + QuicheRandom* quiche_random = nullptr) { + constexpr absl::string_view kX25519SecretKey = + "1c190d72acdbe4dbc69e680503bb781a932c70a12c8f3754434c67d8640d8698"; + std::string x25519_secret_key_bytes; + EXPECT_TRUE( + absl::HexStringToBytes(kX25519SecretKey, &x25519_secret_key_bytes)); + + return ChunkedObliviousHttpGateway::Create( + x25519_secret_key_bytes, + GetOhttpKeyConfig( + /*key_id=*/1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256, + EVP_HPKE_AES_128_GCM), + chunk_handler, quiche_random); +} + +TEST(ChunkedObliviousHttpGateway, ProvisionKeyAndDecapsulateFullRequest) { + // Example from + // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html#appendix-A + TestChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + std::string encapsulated_request_bytes; + ASSERT_TRUE(absl::HexStringToBytes(kEncapsulatedChunkedRequest, + &encapsulated_request_bytes)); + + QUICHE_EXPECT_OK(instance->DecryptRequest(encapsulated_request_bytes, true)); + EXPECT_TRUE(chunk_handler.GetOnChunksDoneCalled()); + EXPECT_EQ(chunk_handler.GetChunkCount(), 3); +} + +TEST(ChunkedObliviousHttpGateway, ProvisionKeyAndDecapsulateBufferedRequest) { + // Example from + // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html#appendix-A + TestChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + std::string encapsulated_request_bytes; + ASSERT_TRUE(absl::HexStringToBytes(kEncapsulatedChunkedRequest, + &encapsulated_request_bytes)); + + for (size_t i = 0; i < encapsulated_request_bytes.size(); i++) { + absl::string_view current_byte(&encapsulated_request_bytes[i], 1); + QUICHE_EXPECT_OK(instance->DecryptRequest(current_byte, false)); + } + + QUICHE_EXPECT_OK(instance->DecryptRequest("", true)); + EXPECT_TRUE(chunk_handler.GetOnChunksDoneCalled()); + EXPECT_EQ(chunk_handler.GetChunkCount(), 3); +} + +TEST(ChunkedObliviousHttpGateway, DecryptingAfterDoneReturnsInvalidArgument) { + TestChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + std::string encapsulated_request_bytes; + ASSERT_TRUE(absl::HexStringToBytes(kEncapsulatedChunkedRequest, + &encapsulated_request_bytes)); + + QUICHE_EXPECT_OK(instance->DecryptRequest(encapsulated_request_bytes, true)); + + auto second_decrypt = + instance->DecryptRequest(encapsulated_request_bytes, true); + EXPECT_EQ(second_decrypt.code(), absl::StatusCode::kInternal); + EXPECT_EQ(second_decrypt.message(), "Decrypting is marked as invalid."); +} + +TEST(ChunkedObliviousHttpGateway, FinalChunkNotDoneReturnsInvalidArgument) { + TestChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + std::string encapsulated_request_bytes; + ASSERT_TRUE(absl::HexStringToBytes("010020", &encapsulated_request_bytes)); + + EXPECT_EQ(instance->DecryptRequest(encapsulated_request_bytes, true).code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ChunkedObliviousHttpGateway, GettingDecryptErrorSetsGatewayToInvalid) { + TestChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + std::string invalid_key_request = + "020020000100014b28f881333e7c164ffc499ad9796f877f4e1051ee6d31bad19dec96c2" + "08b4726374e469135906992e"; + std::string encapsulated_request_bytes; + ASSERT_TRUE( + absl::HexStringToBytes(invalid_key_request, &encapsulated_request_bytes)); + + EXPECT_EQ(instance->DecryptRequest(encapsulated_request_bytes, false).code(), + absl::StatusCode::kInvalidArgument); + + auto second_decrypt = + instance->DecryptRequest(encapsulated_request_bytes, true); + EXPECT_EQ(second_decrypt.code(), absl::StatusCode::kInternal); + EXPECT_EQ(second_decrypt.message(), "Decrypting is marked as invalid."); +} + +TEST(ChunkedObliviousHttpGateway, InvalidKeyConfigReturnsInvalidArgument) { + TestChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + std::string encapsulated_request_bytes; + ASSERT_TRUE( + absl::HexStringToBytes("990020000100018811eb457e100811c40a0aa71340a1b81d8" + "04bb986f736f2f566a7199761a032", + &encapsulated_request_bytes)); + + EXPECT_EQ(instance->DecryptRequest(encapsulated_request_bytes, false).code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ChunkedObliviousHttpGateway, ChunkHandlerOnChunkErrorPropagates) { + class FailingChunkHandler : public ObliviousHttpChunkHandler { + public: + FailingChunkHandler() = default; + ~FailingChunkHandler() override = default; + absl::Status OnDecryptedChunk( + absl::string_view /*decrypted_chunk*/) override { + return absl::InvalidArgumentError("Invalid data"); + } + absl::Status OnChunksDone() override { + return absl::InvalidArgumentError("Invalid data"); + } + }; + FailingChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + std::string encapsulated_request_bytes; + ASSERT_TRUE(absl::HexStringToBytes(kEncapsulatedChunkedRequest, + &encapsulated_request_bytes)); + + EXPECT_EQ(instance->DecryptRequest(encapsulated_request_bytes, true).code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ChunkedObliviousHttpGateway, ChunkHandlerOnChunksDoneErrorPropagates) { + class FailingChunkHandler : public ObliviousHttpChunkHandler { + public: + FailingChunkHandler() = default; + ~FailingChunkHandler() override = default; + absl::Status OnDecryptedChunk( + absl::string_view /*decrypted_chunk*/) override { + return absl::OkStatus(); + } + absl::Status OnChunksDone() override { + return absl::InvalidArgumentError("Invalid data"); + } + }; + FailingChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + std::string encapsulated_request_bytes; + ASSERT_TRUE(absl::HexStringToBytes(kEncapsulatedChunkedRequest, + &encapsulated_request_bytes)); + + EXPECT_EQ(instance->DecryptRequest(encapsulated_request_bytes, true).code(), + absl::StatusCode::kInvalidArgument); +} + TEST(ObliviousHttpGateway, TestDecryptingMultipleRequestsWithSingleInstance) { auto instance = ObliviousHttpGateway::Create( GetHpkePrivateKey(), @@ -123,6 +329,28 @@ absl::StatusCode::kInvalidArgument); } +TEST(ChunkedObliviousHttpGateway, TestInvalidHPKEKey) { + TestChunkHandler chunk_handler; + // Invalid private key. + EXPECT_EQ(ChunkedObliviousHttpGateway::Create( + "Invalid HPKE key", + GetOhttpKeyConfig(70, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, + EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM), + chunk_handler) + .status() + .code(), + absl::StatusCode::kInternal); + // Empty private key. + EXPECT_EQ(ChunkedObliviousHttpGateway::Create( + /*hpke_private_key*/ "", + GetOhttpKeyConfig(70, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, + EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM), + chunk_handler) + .status() + .code(), + absl::StatusCode::kInvalidArgument); +} + TEST(ObliviousHttpGateway, TestObliviousResponseHandling) { auto ohttp_key_config = GetOhttpKeyConfig(3, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, @@ -147,6 +375,142 @@ ASSERT_FALSE(encapsulate_resp_on_gateway->EncapsulateAndSerialize().empty()); } +class TestQuicheRandom : public QuicheRandom { + public: + TestQuicheRandom(std::string seed) : seed_(seed) {} + ~TestQuicheRandom() override {} + + void RandBytes(void* data, size_t len) override { + size_t copy_len = std::min(len, seed_.length()); + memcpy(data, seed_.c_str(), copy_len); + } + + uint64_t RandUint64() override { return 0; } + + void InsecureRandBytes(void* /*data*/, size_t /*len*/) override {} + uint64_t InsecureRandUint64() override { return 0; } + + private: + std::string seed_; +}; + +TEST(ChunkedObliviousHttpGateway, SingleChunkResponse) { + TestChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + // Request decryption implicitly sets up the context for response encryption + std::string encapsulated_request_bytes; + ASSERT_TRUE(absl::HexStringToBytes(kEncapsulatedChunkedRequest, + &encapsulated_request_bytes)); + QUICHE_EXPECT_OK(instance->DecryptRequest(encapsulated_request_bytes, true)); + + // 63 byte response to test final chunk indicator length. + std::string plaintext_response = + "111111111111111111111111111111111111111111111111111111111111111111111111" + "111111111111111111111111111111111111111111111111111111"; + absl::StatusOr<std::string> encrypted_response = + instance->EncryptResponse(plaintext_response, true); + QUICHE_EXPECT_OK(encrypted_response); + EXPECT_FALSE(encrypted_response->empty()); + EXPECT_NE(*encrypted_response, plaintext_response); +} + +TEST(ChunkedObliviousHttpGateway, MultipleChunkResponse) { + // Example from + // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-05.html#appendix-A + TestChunkHandler chunk_handler; + std::string response_nonce = "bcce7f4cb921309ba5d62edf1769ef09"; + std::string response_nonce_bytes; + EXPECT_TRUE(absl::HexStringToBytes(response_nonce, &response_nonce_bytes)); + TestQuicheRandom quiche_random(response_nonce_bytes); + auto instance = + CreateChunkedObliviousHttpGateway(chunk_handler, &quiche_random); + + // Request decrypting implicitly sets up the context for response encryption + std::string encapsulated_request_bytes; + ASSERT_TRUE(absl::HexStringToBytes(kEncapsulatedChunkedRequest, + &encapsulated_request_bytes)); + QUICHE_EXPECT_OK(instance->DecryptRequest(encapsulated_request_bytes, true)); + + std::string plaintext_response = "01"; + std::string plaintext_response_bytes; + EXPECT_TRUE( + absl::HexStringToBytes(plaintext_response, &plaintext_response_bytes)); + std::vector<std::string> encrypted_response_chunks; + absl::StatusOr<std::string> encrypted_response_chunk = + instance->EncryptResponse(plaintext_response_bytes, false); + QUICHE_EXPECT_OK(encrypted_response_chunk); + std::string encrypted_response_chunk_hex = + absl::BytesToHexString(*encrypted_response_chunk); + // The first chunk should contain the response nonce. + EXPECT_EQ( + encrypted_response_chunk_hex, + "bcce7f4cb921309ba5d62edf1769ef091179bf1cc87fa0e2c02de4546945aa3d1e48"); + + plaintext_response = "40c8"; + EXPECT_TRUE( + absl::HexStringToBytes(plaintext_response, &plaintext_response_bytes)); + encrypted_response_chunk = + instance->EncryptResponse(plaintext_response_bytes, false); + QUICHE_EXPECT_OK(encrypted_response_chunk); + encrypted_response_chunk_hex = + absl::BytesToHexString(*encrypted_response_chunk); + EXPECT_EQ(encrypted_response_chunk_hex, + "12b348b5bd4c594c16b6170b07b475845d1f32"); + + EXPECT_TRUE( + absl::HexStringToBytes(plaintext_response, &plaintext_response_bytes)); + encrypted_response_chunk = + instance->EncryptResponse(/*plaintext_payload=*/"", true); + QUICHE_EXPECT_OK(encrypted_response_chunk); + encrypted_response_chunk_hex = + absl::BytesToHexString(*encrypted_response_chunk); + EXPECT_EQ(encrypted_response_chunk_hex, "00ed9d8a796617a5b27265f4d73247f639"); +} + +TEST(ChunkedObliviousHttpGateway, EncryptingAfterFinalChunkFails) { + TestChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + // Request decryption implicitly sets up the context for response encryption + std::string encapsulated_request_bytes; + ASSERT_TRUE(absl::HexStringToBytes(kEncapsulatedChunkedRequest, + &encapsulated_request_bytes)); + QUICHE_EXPECT_OK(instance->DecryptRequest(encapsulated_request_bytes, true)); + + std::string plaintext_response = "0140c8"; + absl::StatusOr<std::string> encrypted_response = + instance->EncryptResponse(plaintext_response, true); + QUICHE_EXPECT_OK(encrypted_response); + EXPECT_EQ( + instance->EncryptResponse(plaintext_response, false).status().code(), + absl::StatusCode::kInvalidArgument); +} + +TEST(ChunkedObliviousHttpGateway, EncryptingBeforeDecryptingFails) { + TestChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + std::string plaintext_response = "0140c8"; + EXPECT_EQ( + instance->EncryptResponse(plaintext_response, false).status().code(), + absl::StatusCode::kInternal); +} + +TEST(ChunkedObliviousHttpGateway, EncryptionErrorMarksGatewayInvalid) { + TestChunkHandler chunk_handler; + auto instance = CreateChunkedObliviousHttpGateway(chunk_handler); + + std::string plaintext_response = "0140c8"; + EXPECT_EQ( + instance->EncryptResponse(plaintext_response, false).status().code(), + absl::StatusCode::kInternal); + + EXPECT_EQ( + instance->EncryptResponse(plaintext_response, false).status().message(), + "Encrypting is marked as invalid."); +} + TEST(ObliviousHttpGateway, TestHandlingMultipleResponsesForMultipleRequestsWithSingleInstance) { auto instance = ObliviousHttpGateway::Create(