| #include "quiche/oblivious_http/oblivious_http_client.h" |
| |
| #include <stdint.h> |
| |
| #include <memory> |
| #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/base.h" |
| #include "openssl/hpke.h" |
| #include "quiche/common/platform/api/quiche_test.h" |
| #include "quiche/common/platform/api/quiche_thread.h" |
| #include "quiche/common/test_tools/quiche_test_utils.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" |
| #include "quiche/oblivious_http/oblivious_http_gateway.h" |
| |
| namespace quiche { |
| |
| namespace { |
| |
| constexpr absl::string_view kPlaintextRequest = "plaintext_request"; |
| constexpr absl::string_view kPlaintextResponse = "plaintext_response"; |
| |
| using ::quiche::test::StatusIs; |
| using ::testing::ElementsAre; |
| |
| } // namespace |
| |
| // Chunk handler that tests the callback was called in the right order and |
| // saves the decrypted chunks for later validation. |
| class TestChunkHandler : public ObliviousHttpChunkHandler { |
| public: |
| TestChunkHandler() = default; |
| ~TestChunkHandler() override = default; |
| absl::Status OnDecryptedChunk(absl::string_view decrypted_chunk) override { |
| if (on_chunks_done_called_) { |
| return absl::FailedPreconditionError( |
| "OnDecryptedChunk called after OnChunksDone."); |
| } |
| if (fail_on_decrypted_chunk_) { |
| return absl::InternalError("Failed to decrypt chunk."); |
| } |
| decrypted_chunks_.push_back(std::string(decrypted_chunk)); |
| return absl::OkStatus(); |
| } |
| |
| absl::Status OnChunksDone() override { |
| if (on_chunks_done_called_) { |
| return absl::FailedPreconditionError( |
| "OnChunksDone called more than once."); |
| } |
| if (fail_on_chunks_done_) { |
| return absl::InternalError("Failed to handle chunks done."); |
| } |
| on_chunks_done_called_ = true; |
| return absl::OkStatus(); |
| } |
| |
| bool OnChunksDoneCalled() const { return on_chunks_done_called_; } |
| const std::vector<std::string>& GetDecryptedChunks() const { |
| return decrypted_chunks_; |
| } |
| |
| void SetFailOnDecryptedChunk(bool fail) { fail_on_decrypted_chunk_ = fail; } |
| |
| void SetFailOnChunksDone(bool fail) { fail_on_chunks_done_ = fail; } |
| |
| private: |
| bool on_chunks_done_called_ = false; |
| std::vector<std::string> decrypted_chunks_; |
| bool fail_on_decrypted_chunk_ = false; |
| bool fail_on_chunks_done_ = false; |
| }; |
| |
| std::string GetHpkePrivateKey() { |
| // Dev/Test private key generated using Keystore. |
| absl::string_view hpke_key_hex = |
| "b77431ecfa8f4cfc30d6e467aafa06944dffe28cb9dd1409e33a3045f5adc8a1"; |
| std::string hpke_key_bytes; |
| EXPECT_TRUE(absl::HexStringToBytes(hpke_key_hex, &hpke_key_bytes)); |
| return hpke_key_bytes; |
| } |
| |
| std::string GetHpkePublicKey() { |
| // Dev/Test public key generated using Keystore. |
| absl::string_view public_key = |
| "6d21cfe09fbea5122f9ebc2eb2a69fcc4f06408cd54aac934f012e76fcdcef62"; |
| std::string public_key_bytes; |
| EXPECT_TRUE(absl::HexStringToBytes(public_key, &public_key_bytes)); |
| return public_key_bytes; |
| } |
| |
| 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 ohttp_key_config.value(); |
| } |
| |
| absl::StatusOr<ChunkedObliviousHttpClient> CreateChunkedObliviousHttpClient( |
| ObliviousHttpChunkHandler& chunk_handler) { |
| // Public key from |
| // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-06.html#appendix-A-4 |
| constexpr absl::string_view hpke_public_key = |
| "668eb21aace159803974a4c67f08b4152d29bed10735fd08f98ccdd6fe095708"; |
| std::string hpke_key_bytes; |
| if (!absl::HexStringToBytes(hpke_public_key, &hpke_key_bytes)) { |
| return absl::InvalidArgumentError("Invalid HPKE public key."); |
| } |
| |
| return ChunkedObliviousHttpClient::Create( |
| hpke_key_bytes, |
| GetOhttpKeyConfig( |
| /*key_id=*/1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256, |
| EVP_HPKE_AES_128_GCM), |
| &chunk_handler); |
| } |
| |
| absl::StatusOr<ChunkedObliviousHttpGateway> CreateChunkedObliviousHttpGateway( |
| ObliviousHttpChunkHandler& chunk_handler) { |
| // Private key from |
| // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-06.html#appendix-A-2 |
| constexpr absl::string_view kX25519SecretKey = |
| "1c190d72acdbe4dbc69e680503bb781a932c70a12c8f3754434c67d8640d8698"; |
| std::string x25519_secret_key_bytes; |
| if (!absl::HexStringToBytes(kX25519SecretKey, &x25519_secret_key_bytes)) { |
| return absl::FailedPreconditionError("Invalid X25519 secret key."); |
| } |
| |
| 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); |
| } |
| |
| 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 gateway_request_context = |
| std::move(decapsulate_req_on_gateway.value()).ReleaseContext(); |
| auto encapsulate_resp_on_gateway = |
| ObliviousHttpResponse::CreateServerObliviousResponse( |
| "test response", gateway_request_context); |
| ASSERT_TRUE(encapsulate_resp_on_gateway.ok()); |
| |
| auto client = |
| ObliviousHttpClient::Create(GetHpkePublicKey(), ohttp_key_config); |
| ASSERT_TRUE(client.ok()); |
| auto client_request_context = |
| std::move(encapsulate_req_on_client.value()).ReleaseContext(); |
| auto decapsulate_resp_on_client = client->DecryptObliviousHttpResponse( |
| encapsulate_resp_on_gateway->EncapsulateAndSerialize(), |
| client_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 gateway_request_context = |
| std::move(decapsulate_req_on_gateway.value()).ReleaseContext(); |
| auto encapsulate_resp_on_gateway = |
| ObliviousHttpResponse::CreateServerObliviousResponse( |
| "test response", gateway_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( |
| encapsulate_resp_on_gateway->EncapsulateAndSerialize(), |
| gateway_request_context); |
| ASSERT_TRUE(decapsulate_resp_on_client.ok()); |
| EXPECT_EQ(decapsulate_resp_on_client->GetPlaintextData(), "test response"); |
| } |
| |
| TEST(ObliviousHttpClient, 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 gateway_request_context = |
| std::move(decapsulate_req_on_gateway.value()).ReleaseContext(); |
| auto encapsulate_resp_on_gateway = |
| ObliviousHttpResponse::CreateServerObliviousResponse( |
| "test response", gateway_request_context); |
| ASSERT_TRUE(encapsulate_resp_on_gateway.ok()); |
| ASSERT_FALSE( |
| encapsulate_resp_on_gateway->EncapsulateAndSerialize().empty()); |
| auto client_request_context = |
| std::move(encrypted_request.value()).ReleaseContext(); |
| auto decrypted_response = client_.DecryptObliviousHttpResponse( |
| encapsulate_resp_on_gateway->EncapsulateAndSerialize(), |
| client_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(); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, EncryptRequestNonFinalChunkCanNotBeEmpty) { |
| TestChunkHandler chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| // TODO(b/425346950): Remove all the redundant checks once ClangTidy gets |
| // configured for QUICHE_ASSERT_OK |
| if (!chunk_client.ok()) { |
| return; |
| } |
| EXPECT_THAT(chunk_client->EncryptRequestChunk("", /*is_final_chunk=*/false), |
| StatusIs(absl::StatusCode::kInvalidArgument)); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, EncryptRequestFinalChunkCanBeEmpty) { |
| TestChunkHandler chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| absl::StatusOr<std::string> final_chunk = |
| chunk_client->EncryptRequestChunk("", |
| /*is_final_chunk=*/true); |
| QUICHE_ASSERT_OK(final_chunk); |
| if (!final_chunk.ok()) { |
| return; |
| } |
| // Final chunk uses a non-empty AAD, so encrypting an empty payload results in |
| // a non-empty ciphertext. |
| // https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-06.html#section-6.1-5 |
| EXPECT_FALSE(final_chunk->empty()); |
| |
| TestChunkHandler gateway_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpGateway> chunk_gateway = |
| CreateChunkedObliviousHttpGateway(gateway_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_gateway); |
| if (!chunk_gateway.ok()) { |
| return; |
| } |
| QUICHE_ASSERT_OK(chunk_gateway->DecryptRequest(*final_chunk, |
| /*end_stream=*/true)); |
| EXPECT_THAT(gateway_chunk_handler.GetDecryptedChunks(), ElementsAre("")); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, EncryptRequestAfterFinalChunkReturnsError) { |
| TestChunkHandler chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| absl::StatusOr<std::string> final_chunk = |
| chunk_client->EncryptRequestChunk("", /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(final_chunk); |
| absl::StatusOr<std::string> next_chunk = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/false); |
| EXPECT_THAT(next_chunk.status(), |
| StatusIs(absl::StatusCode::kInvalidArgument)); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, EncryptRequestFirstChunkHasHeaderData) { |
| TestChunkHandler chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| absl::StatusOr<std::string> first_chunk = chunk_client->EncryptRequestChunk( |
| kPlaintextRequest, /*is_final_chunk=*/false); |
| QUICHE_ASSERT_OK(first_chunk); |
| if (!first_chunk.ok()) { |
| return; |
| } |
| absl::StatusOr<std::string> second_chunk = chunk_client->EncryptRequestChunk( |
| kPlaintextRequest, /*is_final_chunk=*/false); |
| QUICHE_ASSERT_OK(second_chunk); |
| if (!second_chunk.ok()) { |
| return; |
| } |
| // Encoded header data results in a bigger first chunk. |
| EXPECT_GT(first_chunk->size(), second_chunk->size()); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, |
| EncryptRequestMultipleTimesWithSamePlaintextReturnsDifferentCiphertexts) { |
| TestChunkHandler chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| absl::StatusOr<std::string> request_with_header = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/false); |
| QUICHE_ASSERT_OK(request_with_header); |
| if (!request_with_header.ok()) { |
| return; |
| } |
| absl::StatusOr<std::string> same_plaintext_chunk_1 = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/false); |
| QUICHE_ASSERT_OK(same_plaintext_chunk_1); |
| if (!same_plaintext_chunk_1.ok()) { |
| return; |
| } |
| absl::StatusOr<std::string> same_plaintext_chunk_2 = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/false); |
| QUICHE_ASSERT_OK(same_plaintext_chunk_2); |
| if (!same_plaintext_chunk_2.ok()) { |
| return; |
| } |
| |
| EXPECT_GT(request_with_header->size(), same_plaintext_chunk_1->size()); |
| EXPECT_GT(request_with_header->size(), same_plaintext_chunk_2->size()); |
| EXPECT_NE(*same_plaintext_chunk_1, *same_plaintext_chunk_2); |
| |
| TestChunkHandler gateway_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpGateway> chunk_gateway = |
| CreateChunkedObliviousHttpGateway(gateway_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_gateway); |
| if (!chunk_gateway.ok()) { |
| return; |
| } |
| QUICHE_EXPECT_OK(chunk_gateway->DecryptRequest(*request_with_header, |
| /*end_stream=*/false)); |
| QUICHE_EXPECT_OK(chunk_gateway->DecryptRequest(*same_plaintext_chunk_1, |
| /*end_stream=*/false)); |
| QUICHE_EXPECT_OK(chunk_gateway->DecryptRequest(*same_plaintext_chunk_2, |
| /*end_stream=*/false)); |
| |
| EXPECT_THAT( |
| gateway_chunk_handler.GetDecryptedChunks(), |
| ElementsAre(kPlaintextRequest, kPlaintextRequest, kPlaintextRequest)); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, |
| SingleChunkEncryptRequestAndDecryptResponseSuccess) { |
| TestChunkHandler client_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(client_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| TestChunkHandler gateway_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpGateway> chunk_gateway = |
| CreateChunkedObliviousHttpGateway(gateway_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_gateway); |
| if (!chunk_gateway.ok()) { |
| return; |
| } |
| |
| absl::StatusOr<std::string> request_chunk = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(request_chunk); |
| if (!request_chunk.ok()) { |
| return; |
| } |
| QUICHE_EXPECT_OK( |
| chunk_gateway->DecryptRequest(*request_chunk, /*end_stream=*/true)); |
| EXPECT_THAT(gateway_chunk_handler.GetDecryptedChunks(), |
| ElementsAre(kPlaintextRequest)); |
| |
| absl::StatusOr<std::string> response_chunk = |
| chunk_gateway->EncryptResponse(kPlaintextResponse, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(response_chunk); |
| if (!response_chunk.ok()) { |
| return; |
| } |
| QUICHE_EXPECT_OK( |
| chunk_client->DecryptResponse(*response_chunk, /*end_stream=*/true)); |
| EXPECT_THAT(client_chunk_handler.GetDecryptedChunks(), |
| ElementsAre(kPlaintextResponse)); |
| EXPECT_TRUE(client_chunk_handler.OnChunksDoneCalled()); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, |
| MultipleChunksDecryptResponseWhileBufferingSuccess) { |
| TestChunkHandler client_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(client_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| TestChunkHandler gateway_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpGateway> chunk_gateway = |
| CreateChunkedObliviousHttpGateway(gateway_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_gateway); |
| if (!chunk_gateway.ok()) { |
| return; |
| } |
| std::string plaintext_response_1 = "plaintext_response_1"; |
| std::string plaintext_response_2 = "plaintext_response_2"; |
| std::string plaintext_response_3 = "plaintext_response_3"; |
| |
| absl::StatusOr<std::string> request_chunk = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/false); |
| QUICHE_ASSERT_OK(request_chunk); |
| if (!request_chunk.ok()) { |
| return; |
| } |
| // Initial gateway decrypt is needed to implicitly set up context. |
| QUICHE_EXPECT_OK( |
| chunk_gateway->DecryptRequest(*request_chunk, /*end_stream=*/false)); |
| |
| absl::StatusOr<std::string> response_chunk_1 = |
| chunk_gateway->EncryptResponse(plaintext_response_1, |
| /*is_final_chunk=*/false); |
| QUICHE_EXPECT_OK(response_chunk_1); |
| if (!response_chunk_1.ok()) { |
| return; |
| } |
| absl::StatusOr<std::string> response_chunk_2 = |
| chunk_gateway->EncryptResponse(plaintext_response_2, |
| /*is_final_chunk=*/false); |
| QUICHE_EXPECT_OK(response_chunk_2); |
| if (!response_chunk_2.ok()) { |
| return; |
| } |
| absl::StatusOr<std::string> response_chunk_3 = |
| chunk_gateway->EncryptResponse(plaintext_response_3, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(response_chunk_3); |
| if (!response_chunk_3.ok()) { |
| return; |
| } |
| |
| std::string full_response = |
| absl::StrCat(*response_chunk_1, *response_chunk_2, *response_chunk_3); |
| // For each byte, call DecryptResponse with end_stream=false. |
| for (size_t i = 0; i < full_response.size() - 1; ++i) { |
| QUICHE_EXPECT_OK(chunk_client->DecryptResponse(full_response.substr(i, 1), |
| /*end_stream=*/false)); |
| } |
| // The last call to DecryptResponse will have end_stream=true. |
| QUICHE_EXPECT_OK(chunk_client->DecryptResponse( |
| full_response.substr(full_response.size() - 1, 1), |
| /*end_stream=*/true)); |
| |
| EXPECT_THAT(client_chunk_handler.GetDecryptedChunks(), |
| ElementsAre(plaintext_response_1, plaintext_response_2, |
| plaintext_response_3)); |
| EXPECT_TRUE(client_chunk_handler.OnChunksDoneCalled()); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, CreateClientWithEmptyPublicKeyFails) { |
| TestChunkHandler chunk_handler; |
| EXPECT_THAT(ChunkedObliviousHttpClient::Create( |
| /*hpke_public_key=*/"", |
| GetOhttpKeyConfig( |
| /*key_id=*/1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, |
| EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM), |
| &chunk_handler), |
| StatusIs(absl::StatusCode::kInvalidArgument)); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, CreateClientWithInvalidPublicKeyFails) { |
| TestChunkHandler chunk_handler; |
| EXPECT_THAT(ChunkedObliviousHttpClient::Create( |
| /*hpke_public_key=*/"invalid_key", |
| GetOhttpKeyConfig( |
| /*key_id=*/1, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, |
| EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_128_GCM), |
| &chunk_handler), |
| StatusIs(absl::StatusCode::kInvalidArgument)); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, DecryptResponseWithCorruptedNonceFails) { |
| TestChunkHandler client_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(client_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| TestChunkHandler gateway_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpGateway> chunk_gateway = |
| CreateChunkedObliviousHttpGateway(gateway_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_gateway); |
| if (!chunk_gateway.ok()) { |
| return; |
| } |
| |
| absl::StatusOr<std::string> request = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(request); |
| if (!request.ok()) { |
| return; |
| } |
| QUICHE_EXPECT_OK( |
| chunk_gateway->DecryptRequest(*request, /*end_stream=*/true)); |
| |
| absl::StatusOr<std::string> response = |
| chunk_gateway->EncryptResponse(kPlaintextResponse, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(response); |
| if (!response.ok()) { |
| return; |
| } |
| std::string corrupted_response = *response; |
| corrupted_response[0] ^= 0x01; // Corrupt first byte of nonce. |
| EXPECT_THAT( |
| chunk_client->DecryptResponse(corrupted_response, /*end_stream=*/true), |
| StatusIs(absl::StatusCode::kInternal)); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, DecryptResponseWithCorruptedChunkFails) { |
| TestChunkHandler client_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(client_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| TestChunkHandler gateway_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpGateway> chunk_gateway = |
| CreateChunkedObliviousHttpGateway(gateway_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_gateway); |
| if (!chunk_gateway.ok()) { |
| return; |
| } |
| |
| absl::StatusOr<std::string> request = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(request); |
| if (!request.ok()) { |
| return; |
| } |
| QUICHE_EXPECT_OK( |
| chunk_gateway->DecryptRequest(*request, /*end_stream=*/true)); |
| |
| absl::StatusOr<std::string> response1 = |
| chunk_gateway->EncryptResponse(kPlaintextResponse, |
| /*is_final_chunk=*/false); |
| QUICHE_EXPECT_OK(response1); |
| if (!response1.ok()) { |
| return; |
| } |
| |
| std::string corrupted_chunk_response = *response1; |
| corrupted_chunk_response[15] ^= 0x01; // Corrupt byte in chunk data. |
| // 12 bytes nonce + 1 byte len. |
| EXPECT_THAT(chunk_client->DecryptResponse(corrupted_chunk_response, |
| /*end_stream=*/false), |
| StatusIs(absl::StatusCode::kInternal)); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, DecryptResponseWithCorruptedFinalChunkFails) { |
| TestChunkHandler client_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(client_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| TestChunkHandler gateway_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpGateway> chunk_gateway = |
| CreateChunkedObliviousHttpGateway(gateway_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_gateway); |
| if (!chunk_gateway.ok()) { |
| return; |
| } |
| |
| absl::StatusOr<std::string> request = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(request); |
| if (!request.ok()) { |
| return; |
| } |
| QUICHE_EXPECT_OK( |
| chunk_gateway->DecryptRequest(*request, /*end_stream=*/true)); |
| |
| absl::StatusOr<std::string> response = |
| chunk_gateway->EncryptResponse(kPlaintextResponse, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(response); |
| if (!response.ok()) { |
| return; |
| } |
| std::string corrupted_response = *response; |
| corrupted_response[15] ^= |
| 0x01; // Corrupt byte in chunk data. |
| // 12 bytes nonce + 1 byte chunk indicator==0. |
| EXPECT_THAT( |
| chunk_client->DecryptResponse(corrupted_response, /*end_stream=*/true), |
| StatusIs(absl::StatusCode::kInternal)); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, DecryptResponseAfterEndStreamReturnsError) { |
| TestChunkHandler client_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(client_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| TestChunkHandler gateway_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpGateway> chunk_gateway = |
| CreateChunkedObliviousHttpGateway(gateway_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_gateway); |
| if (!chunk_gateway.ok()) { |
| return; |
| } |
| |
| absl::StatusOr<std::string> request = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(request); |
| if (!request.ok()) { |
| return; |
| } |
| QUICHE_EXPECT_OK( |
| chunk_gateway->DecryptRequest(*request, /*end_stream=*/true)); |
| |
| absl::StatusOr<std::string> response = |
| chunk_gateway->EncryptResponse(kPlaintextResponse, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(response); |
| if (!response.ok()) { |
| return; |
| } |
| QUICHE_EXPECT_OK( |
| chunk_client->DecryptResponse(*response, /*end_stream=*/true)); |
| EXPECT_THAT(client_chunk_handler.GetDecryptedChunks(), |
| ElementsAre(kPlaintextResponse)); |
| EXPECT_TRUE(client_chunk_handler.OnChunksDoneCalled()); |
| |
| EXPECT_THAT(chunk_client->DecryptResponse("data", /*end_stream=*/false), |
| StatusIs(absl::StatusCode::kInternal)); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, |
| DecryptResponseFailsIfHandlerFailsOnDecryptedChunk) { |
| TestChunkHandler client_chunk_handler; |
| client_chunk_handler.SetFailOnDecryptedChunk(true); |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(client_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| TestChunkHandler gateway_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpGateway> chunk_gateway = |
| CreateChunkedObliviousHttpGateway(gateway_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_gateway); |
| if (!chunk_gateway.ok()) { |
| return; |
| } |
| |
| absl::StatusOr<std::string> request = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(request); |
| if (!request.ok()) { |
| return; |
| } |
| QUICHE_EXPECT_OK( |
| chunk_gateway->DecryptRequest(*request, /*end_stream=*/true)); |
| |
| absl::StatusOr<std::string> response = |
| chunk_gateway->EncryptResponse(kPlaintextResponse, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(response); |
| if (!response.ok()) { |
| return; |
| } |
| EXPECT_THAT(chunk_client->DecryptResponse(*response, /*end_stream=*/true), |
| StatusIs(absl::StatusCode::kInternal)); |
| } |
| |
| TEST(ChunkedObliviousHttpClient, |
| DecryptResponseFailsIfHandlerFailsOnChunksDone) { |
| TestChunkHandler client_chunk_handler; |
| client_chunk_handler.SetFailOnChunksDone(true); |
| absl::StatusOr<ChunkedObliviousHttpClient> chunk_client = |
| CreateChunkedObliviousHttpClient(client_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_client); |
| if (!chunk_client.ok()) { |
| return; |
| } |
| TestChunkHandler gateway_chunk_handler; |
| absl::StatusOr<ChunkedObliviousHttpGateway> chunk_gateway = |
| CreateChunkedObliviousHttpGateway(gateway_chunk_handler); |
| QUICHE_ASSERT_OK(chunk_gateway); |
| if (!chunk_gateway.ok()) { |
| return; |
| } |
| |
| absl::StatusOr<std::string> request = |
| chunk_client->EncryptRequestChunk(kPlaintextRequest, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(request); |
| if (!request.ok()) { |
| return; |
| } |
| QUICHE_EXPECT_OK( |
| chunk_gateway->DecryptRequest(*request, /*end_stream=*/true)); |
| |
| absl::StatusOr<std::string> response = |
| chunk_gateway->EncryptResponse(kPlaintextResponse, |
| /*is_final_chunk=*/true); |
| QUICHE_EXPECT_OK(response); |
| if (!response.ok()) { |
| return; |
| } |
| EXPECT_THAT(chunk_client->DecryptResponse(*response, /*end_stream=*/true), |
| StatusIs(absl::StatusCode::kInternal)); |
| } |
| |
| } // namespace quiche |