blob: 6d2c1f21e4e1253f1a4361332843610430a1d604 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <cstddef>
#include <cstdint>
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "mojo/public/cpp/test_support/fake_message_dispatch_context.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/http/http_status_code.h"
#include "net/log/test_net_log.h"
#include "net/log/test_net_log_util.h"
#include "net/third_party/quiche/src/quiche/binary_http/binary_http_message.h"
#include "net/third_party/quiche/src/quiche/common/quiche_data_writer.h"
#include "net/third_party/quiche/src/quiche/oblivious_http/oblivious_http_gateway.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/network_context.h"
#include "services/network/network_service.h"
#include "services/network/oblivious_http_request_handler.h"
#include "services/network/public/mojom/fetch_api.mojom-shared.h"
#include "services/network/public/mojom/oblivious_http_request.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "services/network/test/fake_test_cert_verifier_params_factory.h"
#include "services/network/test/test_network_context.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/boringssl/src/include/openssl/hpke.h"
namespace {
const char kRelayURL[] = "https://relay.test:13/";
const char kResourceURL[] = "https://resource.test:37/path";
// These keys were randomly generated as follows:
// EVP_HPKE_KEY keys;
// EVP_HPKE_KEY_generate(&keys, EVP_hpke_x25519_hkdf_sha256());
// and then EVP_HPKE_KEY_public_key and EVP_HPKE_KEY_private_key were used to
// extract the keys.
const uint8_t kTestPrivateKey[] = {
0xff, 0x1f, 0x47, 0xb1, 0x68, 0xb6, 0xb9, 0xea, 0x65, 0xf7, 0x97,
0x4f, 0xf2, 0x2e, 0xf2, 0x36, 0x94, 0xe2, 0xf6, 0xb6, 0x8d, 0x66,
0xf3, 0xa7, 0x64, 0x14, 0x28, 0xd4, 0x45, 0x35, 0x01, 0x8f,
};
const uint8_t kTestPublicKey[] = {
0xa1, 0x5f, 0x40, 0x65, 0x86, 0xfa, 0xc4, 0x7b, 0x99, 0x59, 0x70,
0xf1, 0x85, 0xd9, 0xd8, 0x91, 0xc7, 0x4d, 0xcf, 0x1e, 0xb9, 0x1a,
0x7d, 0x50, 0xa5, 0x8b, 0x01, 0x68, 0x3e, 0x60, 0x05, 0x2d,
};
testing::Matcher<net::HttpRequestHeaders::HeaderVector> UnorderedHeadersAre(
const net::HttpRequestHeaders::HeaderVector& expected) {
std::vector<testing::Matcher<net::HttpRequestHeaders::HeaderKeyValuePair>> ms;
for (const auto& expected_pair : expected) {
ms.emplace_back(testing::AllOf(
testing::Field("key", &net::HttpRequestHeaders::HeaderKeyValuePair::key,
expected_pair.key),
testing::Field("value",
&net::HttpRequestHeaders::HeaderKeyValuePair::value,
expected_pair.value)));
}
return testing::UnorderedElementsAreArray(ms);
}
class TestOhttpClient : public network::mojom::ObliviousHttpClient {
public:
enum class ResponseType { kSuccess, kNetError, kOuterResponseErrorCode };
TestOhttpClient() : receiver_(this) {}
void SetExpectedNetError(int expected_net_error) {
expected_response_type_ = ResponseType::kNetError;
expected_net_error_ = expected_net_error;
}
void SetExpectedOuterResponseErrorCode(
int expected_outer_response_error_code) {
expected_response_type_ = ResponseType::kOuterResponseErrorCode;
expected_outer_response_error_code_ = expected_outer_response_error_code;
}
void SetExpectedInnerResponse(
int expected_inner_response_code,
std::string expected_body,
std::multimap<std::string, std::string> expected_headers) {
expected_response_type_ = ResponseType::kSuccess;
expected_inner_response_code_ = expected_inner_response_code;
expected_body_ = expected_body;
expected_headers_ = std::move(expected_headers);
}
mojo::PendingRemote<network::mojom::ObliviousHttpClient>
CreatePendingRemote() {
return receiver_.BindNewPipeAndPassRemote();
}
void OnCompleted(
network::mojom::ObliviousHttpCompletionResultPtr status) override {
is_on_completed_called_ = true;
switch (expected_response_type_) {
case ResponseType::kSuccess: {
ASSERT_TRUE(status->is_inner_response());
EXPECT_EQ(expected_inner_response_code_,
status->get_inner_response()->response_code);
EXPECT_EQ(expected_body_, status->get_inner_response()->response_body);
// Verify headers.
size_t iter = 0;
std::string name;
std::string value;
std::multimap<std::string, std::string> actual_headers;
while (status->get_inner_response()->headers->EnumerateHeaderLines(
&iter, &name, &value)) {
actual_headers.insert(
std::pair<std::string, std::string>(name, value));
}
EXPECT_EQ(expected_headers_, actual_headers);
break;
}
case ResponseType::kNetError: {
ASSERT_TRUE(status->is_net_error());
EXPECT_EQ(expected_net_error_, status->get_net_error());
break;
}
case ResponseType::kOuterResponseErrorCode: {
ASSERT_TRUE(status->is_outer_response_error_code());
EXPECT_EQ(expected_outer_response_error_code_,
status->get_outer_response_error_code());
break;
}
}
run_loop_.Quit();
}
void WaitForCall() { run_loop_.Run(); }
bool IsOnCompletedCalled() { return is_on_completed_called_; }
private:
ResponseType expected_response_type_;
int expected_inner_response_code_ = 0;
std::string expected_body_;
std::multimap<std::string, std::string> expected_headers_;
int expected_outer_response_error_code_ = 0;
int expected_net_error_ = 0;
bool is_on_completed_called_ = false;
mojo::Receiver<network::mojom::ObliviousHttpClient> receiver_;
base::RunLoop run_loop_;
};
} // namespace
class TestObliviousHttpRequestHandler : public testing::Test {
public:
TestObliviousHttpRequestHandler()
: task_environment_(
base::test::TaskEnvironment::MainThreadType::IO,
base::test::SingleThreadTaskEnvironment::TimeSource::MOCK_TIME),
network_service_(network::NetworkService::CreateForTesting()),
loader_factory_receiver_(&loader_factory_) {
network::mojom::NetworkContextParamsPtr context_params =
network::CreateNetworkContextParamsForTesting();
context_params->cert_verifier_params =
network::FakeTestCertVerifierParamsFactory::GetCertVerifierParams();
network_context_ = std::make_unique<network::NetworkContext>(
network_service_.get(),
network_context_remote_.BindNewPipeAndPassReceiver(),
std::move(context_params),
network::NetworkContext::OnConnectionCloseCallback());
auto key_configs = quiche::ObliviousHttpKeyConfigs::ParseConcatenatedKeys(
CreateTestKeyConfig());
CHECK(key_configs.ok()) << key_configs.status();
quiche::ObliviousHttpHeaderKeyConfig key_config =
key_configs->PreferredConfig();
auto ohttp_gateway = quiche::ObliviousHttpGateway::Create(
std::string(reinterpret_cast<const char*>(&kTestPrivateKey[0]),
sizeof(kTestPrivateKey)),
key_config);
CHECK(ohttp_gateway.ok()) << ohttp_gateway.status();
ohttp_gateway_ = std::move(ohttp_gateway).value();
}
network::NetworkContext* network_context() { return network_context_.get(); }
network::TestURLLoaderFactory* loader_factory() { return &loader_factory_; }
std::unique_ptr<network::ObliviousHttpRequestHandler> CreateHandler() {
auto handler = std::make_unique<network::ObliviousHttpRequestHandler>(
network_context());
handler->SetURLLoaderFactoryForTesting(
loader_factory_receiver_.BindNewPipeAndPassRemote());
return handler;
}
std::string DecryptRequest(std::string cipher_text) {
auto request = ohttp_gateway_->DecryptObliviousHttpRequest(cipher_text);
EXPECT_TRUE(request.ok()) << request.status();
return std::string(request->GetPlaintextData());
}
void RespondToPendingRequest(std::string body,
std::multimap<std::string, std::string> headers,
GURL relay_url = GURL(kRelayURL),
net::HttpStatusCode status = net::HTTP_OK) {
const network::ResourceRequest* pending_request;
ASSERT_TRUE(
loader_factory()->IsPending(relay_url.spec(), &pending_request));
ASSERT_TRUE(pending_request->request_body);
ASSERT_EQ(1u, pending_request->request_body->elements()->size());
std::string request_body =
std::string(pending_request->request_body->elements()
->at(0)
.As<network::DataElementBytes>()
.AsStringPiece());
auto request = ohttp_gateway_->DecryptObliviousHttpRequest(request_body);
ASSERT_TRUE(request.ok()) << request.status();
auto ohttp_context = std::move(*request).ReleaseContext();
quiche::BinaryHttpResponse bhttp_response(status);
bhttp_response.set_body(std::move(body));
for (auto kv : headers) {
bhttp_response.AddHeaderField({kv.first, kv.second});
}
auto payload = bhttp_response.Serialize();
ASSERT_TRUE(payload.ok()) << payload.status();
auto response =
ohttp_gateway_->CreateObliviousHttpResponse(*payload, ohttp_context);
ASSERT_TRUE(response.ok()) << response.status();
EXPECT_TRUE(loader_factory()->SimulateResponseForPendingRequest(
/*url=*/relay_url,
/*completion_status=*/network::URLLoaderCompletionStatus(),
/*response_head=*/network::CreateURLResponseHead(net::HTTP_OK),
/*content=*/response->EncapsulateAndSerialize()));
}
std::string CreateTestKeyConfig() {
const size_t kOHTTPKeySizeBytes =
1 + 2 + sizeof(kTestPublicKey) + 2 + 2 + 2;
std::string ohttp_key_config(kOHTTPKeySizeBytes, '\0');
quiche::QuicheDataWriter writer(ohttp_key_config.size(),
ohttp_key_config.data());
EXPECT_TRUE(writer.WriteUInt8('K')); // Key ID can be arbitrary.
EXPECT_TRUE(writer.WriteUInt16(EVP_HPKE_DHKEM_X25519_HKDF_SHA256));
EXPECT_TRUE(writer.WriteBytes(&kTestPublicKey[0], sizeof(kTestPublicKey)));
EXPECT_TRUE(writer.WriteUInt16(4u));
EXPECT_TRUE(writer.WriteUInt16(EVP_HPKE_HKDF_SHA256));
EXPECT_TRUE(writer.WriteUInt16(EVP_HPKE_AES_256_GCM));
EXPECT_EQ(ohttp_key_config.length(), writer.length());
return ohttp_key_config;
}
network::mojom::ObliviousHttpRequestPtr CreateRequest() {
network::mojom::ObliviousHttpRequestPtr request =
CreateRequestWithoutBody();
request->request_body = network::mojom::ObliviousHttpRequestBody::New(
/*content=*/"test data", /*content_type=*/"application/testdata");
return request;
}
network::mojom::ObliviousHttpRequestPtr CreateRequestWithoutBody() {
network::mojom::ObliviousHttpRequestPtr request =
network::mojom::ObliviousHttpRequest::New();
request->relay_url = GURL(kRelayURL);
request->key_config = CreateTestKeyConfig();
request->resource_url = GURL(kResourceURL);
request->method = net::HttpRequestHeaders::kGetMethod;
request->traffic_annotation =
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
return request;
}
void FastForward(base::TimeDelta delta) {
task_environment_.FastForwardBy(delta);
task_environment_.RunUntilIdle();
}
void VerifyNetLog(const net::RecordingNetLogObserver& net_log_observer,
bool expected_has_response_data_and_headers,
int expected_net_error,
std::optional<int> expected_outer_response_error_code,
std::optional<int> expected_inner_response_code) {
auto entries = net_log_observer.GetEntries();
size_t pos = net::ExpectLogContainsSomewhereAfter(
entries, /*start_offset=*/0,
net::NetLogEventType::OBLIVIOUS_HTTP_REQUEST,
net::NetLogEventPhase::BEGIN);
pos = net::ExpectLogContainsSomewhere(
entries, /*start_offset=*/pos + 1,
net::NetLogEventType::OBLIVIOUS_HTTP_REQUEST_DATA,
net::NetLogEventPhase::NONE);
EXPECT_TRUE(
net::GetOptionalIntegerValueFromParams(entries[pos], "byte_count"));
if (expected_has_response_data_and_headers) {
pos = net::ExpectLogContainsSomewhereAfter(
entries, /*start_offset=*/pos + 1,
net::NetLogEventType::OBLIVIOUS_HTTP_RESPONSE_DATA,
net::NetLogEventPhase::NONE);
EXPECT_TRUE(
net::GetOptionalIntegerValueFromParams(entries[pos], "byte_count"));
pos = net::ExpectLogContainsSomewhereAfter(
entries, /*start_offset=*/pos + 1,
net::NetLogEventType::OBLIVIOUS_HTTP_RESPONSE_HEADERS,
net::NetLogEventPhase::NONE);
EXPECT_TRUE(entries[pos].params.FindList("headers"));
}
pos = net::ExpectLogContainsSomewhereAfter(
entries, /*start_offset=*/pos + 1,
net::NetLogEventType::OBLIVIOUS_HTTP_REQUEST,
net::NetLogEventPhase::END);
EXPECT_EQ(expected_net_error,
net::GetOptionalNetErrorCodeFromParams(entries[pos]));
EXPECT_EQ(expected_outer_response_error_code,
net::GetOptionalIntegerValueFromParams(
entries[pos], "outer_response_error_code"));
EXPECT_EQ(expected_inner_response_code,
net::GetOptionalIntegerValueFromParams(entries[pos],
"inner_response_code"));
}
private:
std::optional<quiche::ObliviousHttpGateway> ohttp_gateway_;
base::test::TaskEnvironment task_environment_;
std::unique_ptr<network::NetworkService> network_service_;
mojo::Remote<network::mojom::NetworkContext> network_context_remote_;
std::unique_ptr<network::NetworkContext> network_context_;
network::TestURLLoaderFactory loader_factory_;
mojo::Receiver<network::mojom::URLLoaderFactory> loader_factory_receiver_;
};
TEST_F(TestObliviousHttpRequestHandler, TestDisconnect) {
std::unique_ptr<network::ObliviousHttpRequestHandler> handler =
CreateHandler();
{
TestOhttpClient client;
client.SetExpectedInnerResponse(
/*expected_inner_response_code=*/net::HTTP_OK,
/*expected_body=*/"",
/*expected_headers=*/{});
handler->StartRequest(CreateRequest(), client.CreatePendingRemote());
}
{
TestOhttpClient client;
client.SetExpectedInnerResponse(
/*expected_inner_response_code=*/net::HTTP_OK,
/*expected_body=*/"",
/*expected_headers=*/{});
handler->StartRequest(CreateRequest(), client.CreatePendingRemote());
RespondToPendingRequest("", {});
client.WaitForCall();
}
// Empty body
{
TestOhttpClient client;
client.SetExpectedInnerResponse(
/*expected_inner_response_code=*/net::HTTP_OK,
/*expected_body=*/"",
/*expected_headers=*/{});
handler->StartRequest(CreateRequestWithoutBody(),
client.CreatePendingRemote());
RespondToPendingRequest("", {});
client.WaitForCall();
}
}
TEST_F(TestObliviousHttpRequestHandler, TestInvalidArguments) {
std::unique_ptr<network::ObliviousHttpRequestHandler> handler =
CreateHandler();
{
mojo::FakeMessageDispatchContext context;
mojo::test::BadMessageObserver obs;
TestOhttpClient client;
client.SetExpectedNetError(net::ERR_INVALID_URL);
network::mojom::ObliviousHttpRequestPtr request =
network::mojom::ObliviousHttpRequest::New();
handler->StartRequest(std::move(request), client.CreatePendingRemote());
EXPECT_EQ("Invalid OHTTP Relay URL", obs.WaitForBadMessage());
}
{
mojo::FakeMessageDispatchContext context;
mojo::test::BadMessageObserver obs;
TestOhttpClient client;
client.SetExpectedNetError(net::ERR_INVALID_URL);
network::mojom::ObliviousHttpRequestPtr request = CreateRequest();
request->relay_url = GURL();
handler->StartRequest(std::move(request), client.CreatePendingRemote());
EXPECT_EQ("Invalid OHTTP Relay URL", obs.WaitForBadMessage());
}
{
mojo::FakeMessageDispatchContext context;
mojo::test::BadMessageObserver obs;
TestOhttpClient client;
client.SetExpectedNetError(net::ERR_INVALID_URL);
network::mojom::ObliviousHttpRequestPtr request = CreateRequest();
request->resource_url = GURL();
handler->StartRequest(std::move(request), client.CreatePendingRemote());
EXPECT_EQ("Invalid OHTTP Resource URL", obs.WaitForBadMessage());
}
{
mojo::FakeMessageDispatchContext context;
mojo::test::BadMessageObserver obs;
TestOhttpClient client;
client.SetExpectedNetError(net::ERR_INVALID_ARGUMENT);
network::mojom::ObliviousHttpRequestPtr request = CreateRequest();
request->traffic_annotation = net::MutableNetworkTrafficAnnotationTag();
handler->StartRequest(std::move(request), client.CreatePendingRemote());
EXPECT_EQ("Invalid OHTTP Traffic Annotation", obs.WaitForBadMessage());
}
{
mojo::FakeMessageDispatchContext context;
mojo::test::BadMessageObserver obs;
TestOhttpClient client;
client.SetExpectedNetError(net::ERR_INVALID_ARGUMENT);
network::mojom::ObliviousHttpRequestPtr request = CreateRequest();
request->method = std::string(17, 'A');
handler->StartRequest(std::move(request), client.CreatePendingRemote());
EXPECT_EQ("Invalid OHTTP Method", obs.WaitForBadMessage());
}
{
mojo::FakeMessageDispatchContext context;
mojo::test::BadMessageObserver obs;
TestOhttpClient client;
client.SetExpectedNetError(net::ERR_INVALID_ARGUMENT);
network::mojom::ObliviousHttpRequestPtr request = CreateRequest();
request->request_body->content = std::string(5 * 1024 * 1024 + 1, ' ');
handler->StartRequest(std::move(request), client.CreatePendingRemote());
EXPECT_EQ("Request body too large", obs.WaitForBadMessage());
}
{
mojo::FakeMessageDispatchContext context;
mojo::test::BadMessageObserver obs;
TestOhttpClient client;
client.SetExpectedNetError(net::ERR_INVALID_ARGUMENT);
network::mojom::ObliviousHttpRequestPtr request = CreateRequest();
request->request_body->content_type = std::string(257, ' ');
handler->StartRequest(std::move(request), client.CreatePendingRemote());
EXPECT_EQ("Content-Type too large", obs.WaitForBadMessage());
}
}
TEST_F(TestObliviousHttpRequestHandler, TestRequestFormat) {
std::unique_ptr<network::ObliviousHttpRequestHandler> handler =
CreateHandler();
{
net::RecordingNetLogObserver net_log_observer;
TestOhttpClient client;
client.SetExpectedInnerResponse(
/*expected_inner_response_code=*/net::HTTP_OK,
/*expected_body=*/"response body",
/*expected_headers=*/
{{"cache-control", "s-maxage=3600"}, {"content-type", "text/html"}});
handler->StartRequest(CreateRequest(), client.CreatePendingRemote());
const network::ResourceRequest* pending_request;
ASSERT_TRUE(loader_factory()->IsPending(kRelayURL, &pending_request));
EXPECT_EQ(net::HttpRequestHeaders::kPostMethod, pending_request->method);
EXPECT_EQ(network::mojom::RedirectMode::kError,
pending_request->redirect_mode);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
pending_request->credentials_mode);
EXPECT_TRUE(pending_request->site_for_cookies.IsNull());
EXPECT_FALSE(pending_request->trust_token_params);
EXPECT_THAT(
pending_request->headers.GetHeaderVector(),
UnorderedHeadersAre({
{net::HttpRequestHeaders::kContentType, "message/ohttp-req"},
}));
ASSERT_TRUE(pending_request->request_body);
ASSERT_EQ(1u, pending_request->request_body->elements()->size());
std::string body = std::string(pending_request->request_body->elements()
->at(0)
.As<network::DataElementBytes>()
.AsStringPiece());
std::string plain_text_body = DecryptRequest(body);
auto maybe_request = quiche::BinaryHttpRequest::Create(plain_text_body);
ASSERT_TRUE(maybe_request.ok()) << maybe_request.status();
quiche::BinaryHttpRequest request = std::move(maybe_request).value();
EXPECT_EQ(request.control_data().method, "GET");
EXPECT_EQ(request.control_data().scheme, "https");
EXPECT_EQ(request.control_data().authority, ""); // Stored in headers.
EXPECT_EQ(request.control_data().path, "/path");
EXPECT_THAT(request.GetHeaderFields(),
testing::IsSupersetOf<quiche::BinaryHttpRequest::Field>({
{"host", "resource.test:37"},
{"content-length", "9"},
{"content-type", "application/testdata"},
}));
EXPECT_EQ(request.body(), "test data");
RespondToPendingRequest(
"response body",
{{"cache-control", "s-maxage=3600"}, {"content-type", "text/html"}});
client.WaitForCall();
VerifyNetLog(net_log_observer,
/*expected_has_response_data_and_headers=*/true,
/*expected_net_error=*/net::OK,
/*expected_outer_response_error_code=*/std::nullopt,
/*expected_inner_response_code=*/net::HTTP_OK);
}
}
TEST_F(TestObliviousHttpRequestHandler, TestTimeout) {
std::unique_ptr<network::ObliviousHttpRequestHandler> handler =
CreateHandler();
// Default timeout.
{
net::RecordingNetLogObserver net_log_observer;
TestOhttpClient client;
client.SetExpectedNetError(net::ERR_TIMED_OUT);
handler->StartRequest(CreateRequest(), client.CreatePendingRemote());
FastForward(base::Seconds(59));
EXPECT_FALSE(client.IsOnCompletedCalled());
FastForward(base::Seconds(1));
EXPECT_TRUE(client.IsOnCompletedCalled());
VerifyNetLog(net_log_observer,
/*expected_has_response_data_and_headers=*/false,
/*expected_net_error=*/net::ERR_TIMED_OUT,
/*expected_outer_response_error_code=*/std::nullopt,
/*expected_inner_response_code=*/std::nullopt);
}
// Configured timeout.
{
TestOhttpClient client;
client.SetExpectedNetError(net::ERR_TIMED_OUT);
network::mojom::ObliviousHttpRequestPtr request = CreateRequest();
request->timeout_duration = base::Seconds(3);
handler->StartRequest(std::move(request), client.CreatePendingRemote());
FastForward(base::Seconds(2));
EXPECT_FALSE(client.IsOnCompletedCalled());
FastForward(base::Seconds(1));
EXPECT_TRUE(client.IsOnCompletedCalled());
}
}
TEST_F(TestObliviousHttpRequestHandler, HandlesOuterHttpError) {
std::unique_ptr<network::ObliviousHttpRequestHandler> handler =
CreateHandler();
{
net::RecordingNetLogObserver net_log_observer;
loader_factory()->AddResponse(kRelayURL, "", net::HTTP_NOT_FOUND);
TestOhttpClient client;
client.SetExpectedOuterResponseErrorCode(net::HTTP_NOT_FOUND);
handler->StartRequest(CreateRequest(), client.CreatePendingRemote());
client.WaitForCall();
VerifyNetLog(net_log_observer,
/*expected_has_response_data_and_headers=*/false,
/*expected_net_error=*/net::ERR_HTTP_RESPONSE_CODE_FAILURE,
/*expected_outer_response_error_code=*/net::HTTP_NOT_FOUND,
/*expected_inner_response_code=*/std::nullopt);
}
{
net::RecordingNetLogObserver net_log_observer;
loader_factory()->AddResponse(
GURL(kRelayURL), network::CreateURLResponseHead(net::HTTP_OK), "",
network::URLLoaderCompletionStatus(net::ERR_CONNECTION_RESET),
network::TestURLLoaderFactory::Redirects(),
network::TestURLLoaderFactory::ResponseProduceFlags::
kSendHeadersOnNetworkError);
TestOhttpClient client;
// The outer HTTP error code should be set only when the net error is
// ERR_HTTP_RESPONSE_CODE_FAILURE. Otherwise, log the net error instead.
client.SetExpectedNetError(net::ERR_CONNECTION_RESET);
handler->StartRequest(CreateRequest(), client.CreatePendingRemote());
client.WaitForCall();
VerifyNetLog(net_log_observer,
/*expected_has_response_data_and_headers=*/false,
/*expected_net_error=*/net::ERR_CONNECTION_RESET,
/*expected_outer_response_error_code=*/std::nullopt,
/*expected_inner_response_code=*/std::nullopt);
}
}
TEST_F(TestObliviousHttpRequestHandler, HandlesInnerHttpError) {
std::unique_ptr<network::ObliviousHttpRequestHandler> handler =
CreateHandler();
{
net::RecordingNetLogObserver net_log_observer;
TestOhttpClient client;
client.SetExpectedInnerResponse(
/*expected_inner_response_code=*/net::HTTP_NOT_FOUND,
/*expected_body=*/"",
/*expected_headers=*/
{{"cache-control", "s-maxage=60"}});
handler->StartRequest(CreateRequest(), client.CreatePendingRemote());
RespondToPendingRequest("", {{"cache-control", "s-maxage=60"}},
GURL(kRelayURL), net::HTTP_NOT_FOUND);
client.WaitForCall();
VerifyNetLog(net_log_observer,
/*expected_has_response_data_and_headers=*/true,
/*expected_net_error=*/net::OK,
/*expected_outer_response_error_code=*/std::nullopt,
/*expected_inner_response_code=*/net::HTTP_NOT_FOUND);
}
{
net::RecordingNetLogObserver net_log_observer;
TestOhttpClient client;
client.SetExpectedNetError(net::ERR_INVALID_RESPONSE);
handler->StartRequest(CreateRequest(), client.CreatePendingRemote());
ASSERT_TRUE(loader_factory()->IsPending(kRelayURL));
EXPECT_TRUE(loader_factory()->SimulateResponseForPendingRequest(
/*url=*/GURL(kRelayURL),
/*completion_status=*/network::URLLoaderCompletionStatus(),
/*response_head=*/network::CreateURLResponseHead(net::HTTP_OK),
/*content=*/"malformed inner response"));
client.WaitForCall();
VerifyNetLog(net_log_observer,
/*expected_has_response_data_and_headers=*/false,
/*expected_net_error=*/net::ERR_INVALID_RESPONSE,
/*expected_outer_response_error_code=*/std::nullopt,
/*expected_inner_response_code=*/std::nullopt);
}
}
TEST_F(TestObliviousHttpRequestHandler, HandlesMultipleRequests) {
std::unique_ptr<network::ObliviousHttpRequestHandler> handler =
CreateHandler();
{
TestOhttpClient client_a;
client_a.SetExpectedInnerResponse(
/*expected_inner_response_code=*/net::HTTP_OK,
/*expected_body=*/"Response a",
/*expected_headers=*/{{"cache-control", "s-maxage=60"}});
network::mojom::ObliviousHttpRequestPtr request_a = CreateRequest();
TestOhttpClient client_b;
client_b.SetExpectedInnerResponse(
/*expected_inner_response_code=*/net::HTTP_OK,
/*expected_body=*/"Response b",
/*expected_headers=*/{{"cache-control", "s-maxage=600"}});
network::mojom::ObliviousHttpRequestPtr request_b = CreateRequest();
request_b->relay_url = GURL("https://another.relay.test");
handler->StartRequest(std::move(request_a), client_a.CreatePendingRemote());
handler->StartRequest(std::move(request_b), client_b.CreatePendingRemote());
RespondToPendingRequest("Response b", {{"cache-control", "s-maxage=600"}},
GURL("https://another.relay.test"));
client_b.WaitForCall();
RespondToPendingRequest("Response a", {{"cache-control", "s-maxage=60"}});
client_a.WaitForCall();
}
}
TEST_F(TestObliviousHttpRequestHandler, PadsUpToNextPowerOfTwo) {
std::unique_ptr<network::ObliviousHttpRequestHandler> handler =
CreateHandler();
{
TestOhttpClient client;
client.SetExpectedInnerResponse(
/*expected_inner_response_code=*/net::HTTP_OK,
/*expected_body=*/"response body",
/*expected_headers=*/{});
network::mojom::ObliviousHttpRequestPtr request = CreateRequest();
request->padding_params =
network::mojom::ObliviousHttpPaddingParameters::New(
/*add_exponential_pad=*/false, /*exponential_mean=*/0,
/*pad_to_next_power_of_two=*/true);
handler->StartRequest(std::move(request), client.CreatePendingRemote());
const network::ResourceRequest* pending_request;
ASSERT_TRUE(loader_factory()->IsPending(kRelayURL, &pending_request));
ASSERT_TRUE(pending_request->request_body);
ASSERT_EQ(1u, pending_request->request_body->elements()->size());
std::string body = std::string(pending_request->request_body->elements()
->at(0)
.As<network::DataElementBytes>()
.AsStringPiece());
std::string plain_text_body = DecryptRequest(body);
EXPECT_EQ(256u, plain_text_body.size());
EXPECT_EQ(std::string(56, '\0'), plain_text_body.substr(200));
}
}
TEST_F(TestObliviousHttpRequestHandler, DoesntPadsIfAlreadyPowerOfTwo) {
std::unique_ptr<network::ObliviousHttpRequestHandler> handler =
CreateHandler();
{
TestOhttpClient client;
client.SetExpectedInnerResponse(
/*expected_inner_response_code=*/net::HTTP_OK,
/*expected_body=*/"response body",
/*expected_headers=*/{});
network::mojom::ObliviousHttpRequestPtr request = CreateRequest();
request->padding_params =
network::mojom::ObliviousHttpPaddingParameters::New(
/*add_exponential_pad=*/false, /*exponential_mean=*/0,
/*pad_to_next_power_of_two=*/false);
request->request_body = network::mojom::ObliviousHttpRequestBody::New(
/*content=*/std::string(380, ' '),
/*content_type=*/"application/testdata");
handler->StartRequest(std::move(request), client.CreatePendingRemote());
const network::ResourceRequest* pending_request;
ASSERT_TRUE(loader_factory()->IsPending(kRelayURL, &pending_request));
ASSERT_TRUE(pending_request->request_body);
ASSERT_EQ(1u, pending_request->request_body->elements()->size());
std::string body = std::string(pending_request->request_body->elements()
->at(0)
.As<network::DataElementBytes>()
.AsStringPiece());
std::string plain_text_body = DecryptRequest(body);
EXPECT_EQ(512u, plain_text_body.size());
EXPECT_EQ(std::string(380, ' '), plain_text_body.substr(512 - 380));
}
}
TEST_F(TestObliviousHttpRequestHandler, PadsExponentiallyRandomly) {
std::unique_ptr<network::ObliviousHttpRequestHandler> handler =
CreateHandler();
const size_t kNumRuns = 100;
double accum_size = 0;
double accum_size_squared = 0;
for (size_t i = 0; i < kNumRuns; i++) {
{
TestOhttpClient client;
client.SetExpectedInnerResponse(
/*expected_inner_response_code=*/net::HTTP_OK,
/*expected_body=*/"response body",
/*expected_headers=*/{});
network::mojom::ObliviousHttpRequestPtr request = CreateRequest();
request->padding_params =
network::mojom::ObliviousHttpPaddingParameters::New(
/*add_exponential_pad=*/true, /*exponential_mean=*/10,
/*pad_to_next_power_of_two=*/false);
handler->StartRequest(std::move(request), client.CreatePendingRemote());
const network::ResourceRequest* pending_request;
ASSERT_TRUE(loader_factory()->IsPending(kRelayURL, &pending_request));
ASSERT_TRUE(pending_request->request_body);
ASSERT_EQ(1u, pending_request->request_body->elements()->size());
std::string body = std::string(pending_request->request_body->elements()
->at(0)
.As<network::DataElementBytes>()
.AsStringPiece());
accum_size += body.size();
accum_size_squared += body.size() * body.size();
}
}
double mean = accum_size / kNumRuns;
double variance = accum_size_squared / kNumRuns - mean * mean;
// True variance should be 100, but we're not running enough iterations for
// the estimate to converge. This at least excludes the case where the padding
// is constant.
EXPECT_LT(200l, mean);
EXPECT_GT(210l, mean);
EXPECT_LT(16l, variance);
EXPECT_GT(256l, variance);
}
TEST_F(TestObliviousHttpRequestHandler,
PadsBothExponentiallyRandomlyAndPowerOfTwo) {
std::unique_ptr<network::ObliviousHttpRequestHandler> handler =
CreateHandler();
base::flat_set<size_t> sizes_seen;
while (sizes_seen.size() < 2) {
TestOhttpClient client;
client.SetExpectedInnerResponse(
/*expected_inner_response_code=*/net::HTTP_OK,
/*expected_body=*/"response body",
/*expected_headers=*/{});
network::mojom::ObliviousHttpRequestPtr request = CreateRequest();
request->padding_params =
network::mojom::ObliviousHttpPaddingParameters::New(
/*add_exponential_pad=*/true, /*exponential_mean=*/10,
/*pad_to_next_power_of_two=*/true);
// Set message size to 246 bytes plus an average of 10 bytes padding.
request->request_body = network::mojom::ObliviousHttpRequestBody::New(
/*content=*/std::string(114, ' '),
/*content_type=*/"application/testdata");
handler->StartRequest(std::move(request), client.CreatePendingRemote());
const network::ResourceRequest* pending_request;
ASSERT_TRUE(loader_factory()->IsPending(kRelayURL, &pending_request));
ASSERT_TRUE(pending_request->request_body);
ASSERT_EQ(1u, pending_request->request_body->elements()->size());
std::string body = std::string(pending_request->request_body->elements()
->at(0)
.As<network::DataElementBytes>()
.AsStringPiece());
std::string plain_text_body = DecryptRequest(body);
size_t body_size = plain_text_body.size();
sizes_seen.insert(body_size);
// body_size must be a power of 2.
// That means all of the bits except the high bit are 0.
size_t cur = body_size;
bool has_bad_bits = false;
while (cur > 1) {
has_bad_bits |= (cur & 0x1) != 0u;
cur >>= 1;
}
EXPECT_FALSE(has_bad_bits) << "Got non-power of 2 body size " << body_size;
}
}