blob: a94e6de4ea4d4374b8acb500c845b0ed7cea711a [file]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/device_bound_sessions/registration_fetcher.h"
#include <memory>
#include <string>
#include <utility>
#include "base/base64url.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "components/unexportable_keys/background_task_origin.h"
#include "components/unexportable_keys/mock_unexportable_key_service.h"
#include "components/unexportable_keys/unexportable_key_id.h"
#include "components/unexportable_keys/unexportable_key_service.h"
#include "components/unexportable_keys/unexportable_key_service_impl.h"
#include "components/unexportable_keys/unexportable_key_task_manager.h"
#include "crypto/scoped_fake_unexportable_key_provider.h"
#include "net/base/features.h"
#include "net/base/network_anonymization_key.h"
#include "net/base/schemeful_site.h"
#include "net/cookies/cookie_access_result.h"
#include "net/cookies/cookie_store.h"
#include "net/cookies/cookie_store_test_callbacks.h"
#include "net/cookies/parsed_cookie.h"
#include "net/device_bound_sessions/mock_session_service.h"
#include "net/device_bound_sessions/proto/storage.pb.h"
#include "net/device_bound_sessions/registration_request_param.h"
#include "net/device_bound_sessions/session_params.h"
#include "net/device_bound_sessions/session_service.h"
#include "net/device_bound_sessions/test_support.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/log/test_net_log.h"
#include "net/socket/socket_test_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/test_with_task_environment.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_builder.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace net::device_bound_sessions {
namespace {
using ::testing::_;
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Invoke;
using ::testing::Property;
using ::testing::Return;
using ::testing::WithArg;
using ::unexportable_keys::UnexportableSigningKeyId;
constexpr char kBasicValidJson[] =
R"({
"session_identifier": "session_id",
"refresh_url": "/refresh",
"scope": {
"origin": "https://a.test",
"include_site": true,
"scope_specification" : [
{
"type": "include",
"domain": "trusted.a.test",
"path": "/only_trusted_path"
}
]
},
"credentials": [{
"type": "cookie",
"name": "auth_cookie",
"attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
}]
})";
constexpr char kSessionIdentifier[] = "session_id";
constexpr char kRedirectPath[] = "/redirect";
constexpr char kChallenge[] = "test_challenge";
constexpr unexportable_keys::BackgroundTaskPriority kTaskPriority =
unexportable_keys::BackgroundTaskPriority::kBestEffort;
constexpr char kSessionChallengeHeaderName[] = "Secure-Session-Challenge";
constexpr char kSessionResponseHeaderName[] = "Secure-Session-Response";
constexpr char kSessionIdHeaderName[] = "Sec-Secure-Session-Id";
constexpr unexportable_keys::BackgroundTaskOrigin kTaskOrigin =
unexportable_keys::BackgroundTaskOrigin::kDeviceBoundSessionCredentials;
std::vector<crypto::SignatureVerifier::SignatureAlgorithm> CreateAlgArray() {
return {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256,
crypto::SignatureVerifier::SignatureAlgorithm::RSA_PKCS1_SHA256};
}
struct InvokeCallbackArgumentAction {};
class TestRegistrationCallback {
public:
TestRegistrationCallback() = default;
RegistrationFetcher::RegistrationCompleteCallback callback() {
return base::BindOnce(&TestRegistrationCallback::OnRegistrationComplete,
base::Unretained(this));
}
void WaitForCall() {
if (outcome_.has_value()) {
return;
}
base::RunLoop run_loop;
waiting_ = true;
closure_ = run_loop.QuitClosure();
run_loop.Run();
}
const RegistrationResult& outcome() {
EXPECT_TRUE(outcome_.has_value());
return *outcome_;
}
private:
void OnRegistrationComplete(RegistrationFetcher* fetcher,
RegistrationResult params) {
EXPECT_FALSE(outcome_.has_value());
outcome_ = std::move(params);
if (waiting_) {
waiting_ = false;
std::move(closure_).Run();
}
}
std::optional<RegistrationResult> outcome_ = std::nullopt;
bool waiting_ = false;
base::OnceClosure closure_;
};
class RegistrationTest : public TestWithTaskEnvironment {
protected:
RegistrationTest()
: server_(test_server::EmbeddedTestServer::TYPE_HTTPS),
host_resolver_(
base::MakeRefCounted<net::RuleBasedHostResolverProc>(nullptr)) {
host_resolver_->AddRule("*", "127.0.0.1");
server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
auto context_builder = CreateTestURLRequestContextBuilder();
auto network_delegate = std::make_unique<TestNetworkDelegate>();
network_delegate_ = network_delegate.get();
context_builder->set_network_delegate(std::move(network_delegate));
context_ = context_builder->Build();
}
void TearDown() override {
// Reset the `network_delegate_` to avoid a dangling pointer.
network_delegate_ = nullptr;
}
unexportable_keys::UnexportableKeyService& unexportable_key_service() {
return unexportable_key_service_;
}
SessionServiceMock& session_service() { return session_service_; }
TestNetworkDelegate* network_delegate() { return network_delegate_; }
// In order to get HTTPS with a registered domain, use one of the sites
// under [test_names] in net/data/ssl/scripts/ee.cnf. We arbitrarily
// choose *.a.test.
GURL GetBaseURL() { return server_.GetURL("a.test", "/"); }
RegistrationRequestParam GetBasicParam(
std::optional<GURL> url = std::nullopt) {
if (!url) {
url = GetBaseURL();
}
return RegistrationRequestParam::CreateForTesting(
*url, /*session_identifier=*/std::nullopt, std::string(kChallenge),
/*authorization=*/std::nullopt);
}
UnexportableSigningKeyId CreateSigningKey() {
base::test::TestFuture<
unexportable_keys::ServiceErrorOr<UnexportableSigningKeyId>>
future;
unexportable_key_service_.GenerateSigningKeySlowlyAsync(
CreateAlgArray(), kTaskPriority, future.GetCallback());
return *future.Take();
}
RegistrationResult FetchWithFederatedKey(RegistrationRequestParam param,
UnexportableSigningKeyId key,
const GURL& provider_url) {
base::test::TestFuture<RegistrationFetcher*, RegistrationResult> future;
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(),
context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithFederatedKey(param, key, provider_url,
future.GetCallback());
return std::get<1>(future.Take());
}
std::unique_ptr<Session> CreateTestSession(std::string session_identifier) {
SessionParams::Scope scope;
scope.origin = url::Origin::Create(GetBaseURL()).Serialize();
auto session_or_error = Session::CreateIfValid(SessionParams(
std::move(session_identifier), GetBaseURL(), GetBaseURL().spec(),
std::move(scope),
/*creds=*/{}, unexportable_keys::UnexportableSigningKeyId(),
/*allowed_refresh_initiators=*/{}));
return std::move(*session_or_error);
}
test_server::EmbeddedTestServer server_;
raw_ptr<TestNetworkDelegate> network_delegate_;
std::unique_ptr<URLRequestContext> context_;
const url::Origin kOrigin = url::Origin::Create(GURL("https://origin/"));
unexportable_keys::UnexportableKeyTaskManager task_manager_;
unexportable_keys::UnexportableKeyServiceImpl unexportable_key_service_{
task_manager_, kTaskOrigin, crypto::UnexportableKeyProvider::Config()};
SessionServiceMock session_service_;
scoped_refptr<net::RuleBasedHostResolverProc> host_resolver_;
};
std::unique_ptr<test_server::HttpResponse> ReturnResponse(
HttpStatusCode code,
std::string_view response_text,
const test_server::HttpRequest& request) {
auto response = std::make_unique<test_server::BasicHttpResponse>();
response->set_code(code);
response->set_content_type("application/json");
response->set_content(response_text);
return response;
}
std::unique_ptr<test_server::HttpResponse> ReturnUnauthorized(
const test_server::HttpRequest& request) {
auto response = std::make_unique<test_server::BasicHttpResponse>();
response->set_code(HTTP_UNAUTHORIZED);
response->AddCustomHeader(kSessionChallengeHeaderName, R"("challenge")");
return response;
}
std::unique_ptr<test_server::HttpResponse> ReturnForbidden(
const test_server::HttpRequest& request) {
auto response = std::make_unique<test_server::BasicHttpResponse>();
response->set_code(HTTP_FORBIDDEN);
response->AddCustomHeader(kSessionChallengeHeaderName, R"("challenge")");
return response;
}
std::unique_ptr<test_server::HttpResponse> ReturnTextResponse(
const test_server::HttpRequest& request) {
auto response = std::make_unique<test_server::BasicHttpResponse>();
response->set_code(HTTP_OK);
response->set_content_type("text/plain");
response->set_content("some content");
return response;
}
std::unique_ptr<test_server::HttpResponse> ReturnInvalidResponse(
const test_server::HttpRequest& request) {
return std::make_unique<test_server::RawHttpResponse>(
"", "Not a valid HTTP response.");
}
std::unique_ptr<test_server::HttpResponse> ReturnForHostAndPath(
std::string_view host,
std::string_view path,
test_server::EmbeddedTestServer::HandleRequestCallback callback,
const test_server::HttpRequest& request) {
// `base_url` resolved to 127.0.0.1, so get the host and port
// from the Host header.
auto it = request.headers.find("host");
if (it == request.headers.end()) {
return nullptr;
}
if (it->second.find(host) == std::string::npos) {
return nullptr;
}
if (request.relative_url != path) {
return nullptr;
}
return callback.Run(request);
}
// The .well-known will usually need to contain a port assigned
// dynamically by `EmbeddedTestServer`. We work around that by getting
// the port from `request.base_url` and replacing "$1" with the required
// port in the .well-known contents.
std::unique_ptr<test_server::HttpResponse> ReturnWellKnown(
const std::string& contents,
const test_server::HttpRequest& request) {
auto response = std::make_unique<test_server::BasicHttpResponse>();
response->set_content_type("application/json");
response->set_code(HTTP_OK);
response->set_content(base::ReplaceStringPlaceholders(
contents, {request.base_url.GetPort()}, /*offsets=*/nullptr));
return response;
}
class UnauthorizedThenSuccessResponseContainer {
public:
explicit UnauthorizedThenSuccessResponseContainer(
int unauthorize_response_times)
: error_response_times(unauthorize_response_times) {}
std::unique_ptr<test_server::HttpResponse> Return(
const test_server::HttpRequest& request) {
if (run_times++ < error_response_times) {
return ReturnUnauthorized(request);
}
return ReturnResponse(HTTP_OK, kBasicValidJson, request);
}
private:
int run_times = 0;
int error_response_times = 0;
};
class ForbiddenThenSuccessResponseContainer {
public:
explicit ForbiddenThenSuccessResponseContainer(int forbidden_response_times)
: error_response_times(forbidden_response_times) {}
std::unique_ptr<test_server::HttpResponse> Return(
const test_server::HttpRequest& request) {
if (run_times++ < error_response_times) {
return ReturnForbidden(request);
}
return ReturnResponse(HTTP_OK, kBasicValidJson, request);
}
private:
int run_times = 0;
int error_response_times = 0;
};
MATCHER_P3(EqualsInclusionRule, rule_type, rule_host, rule_path, "") {
return testing::ExplainMatchResult(
AllOf(
Property("rule_type", &proto::UrlRule::rule_type, Eq(rule_type)),
Property("host_pattern", &proto::UrlRule::host_pattern,
Eq(rule_host)),
Property("path_prefix", &proto::UrlRule::path_prefix, Eq(rule_path))),
arg, result_listener);
}
MATCHER_P2(EqualsCredential, name, attributes, "") {
ParsedCookie cookie(std::string(name) + "=value;" + attributes);
EXPECT_TRUE(cookie.IsValid());
proto::CookieSameSite expected_same_site;
switch (cookie.SameSite().first) {
case CookieSameSite::UNSPECIFIED:
expected_same_site = proto::CookieSameSite::COOKIE_SAME_SITE_UNSPECIFIED;
break;
case CookieSameSite::NO_RESTRICTION:
expected_same_site = proto::CookieSameSite::NO_RESTRICTION;
break;
case CookieSameSite::LAX_MODE:
expected_same_site = proto::CookieSameSite::LAX_MODE;
break;
case CookieSameSite::STRICT_MODE:
expected_same_site = proto::CookieSameSite::STRICT_MODE;
break;
}
return testing::ExplainMatchResult(
AllOf(Property("name", &proto::CookieCraving::name, Eq(name)),
Property("domain", &proto::CookieCraving::domain,
Eq(cookie.Domain())),
Property("path", &proto::CookieCraving::path, Eq(cookie.Path())),
Property("secure", &proto::CookieCraving::secure,
Eq(cookie.IsSecure())),
Property("httponly", &proto::CookieCraving::httponly,
Eq(cookie.IsHttpOnly())),
Property("same_site", &proto::CookieCraving::same_site,
Eq(expected_same_site))),
arg, result_listener);
}
std::optional<std::string> GetRequestChallenge(
const test_server::HttpRequest& request) {
auto resp_iter = request.headers.find(kSessionResponseHeaderName);
if (resp_iter == request.headers.end()) {
return std::nullopt;
}
const std::string& jwt = resp_iter->second;
std::vector<std::string> jwt_sections =
base::SplitString(jwt, ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (jwt_sections.size() != 3) {
return std::nullopt;
}
std::string payload;
if (!base::Base64UrlDecode(jwt_sections[1],
base::Base64UrlDecodePolicy::DISALLOW_PADDING,
&payload)) {
return std::nullopt;
}
const std::optional<base::DictValue> payload_json =
base::JSONReader::ReadDict(payload, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
if (!payload_json.has_value()) {
return std::nullopt;
}
const std::string* challenge = payload_json->FindString("jti");
if (!challenge) {
return std::nullopt;
}
return *challenge;
}
TEST_F(RegistrationTest, BasicSuccess) {
base::HistogramTester histogram_tester;
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating([](const test_server::HttpRequest& request) {
auto resp_iter = request.headers.find(kSessionResponseHeaderName);
EXPECT_TRUE(resp_iter != request.headers.end());
if (resp_iter != request.headers.end()) {
EXPECT_TRUE(VerifyEs256Jwt(resp_iter->second));
}
return ReturnResponse(HTTP_OK, kBasicValidJson, request);
}));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const Session& session = callback.outcome().SessionForTesting();
proto::Session session_proto = session.ToProto();
EXPECT_TRUE(session_proto.session_inclusion_rules().do_include_site());
EXPECT_THAT(
session_proto.session_inclusion_rules().url_rules(),
ElementsAre(
EqualsInclusionRule(proto::RuleType::INCLUDE, "trusted.a.test",
"/only_trusted_path"),
EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
EXPECT_THAT(
session_proto.cookie_cravings(),
ElementsAre(EqualsCredential(
"auth_cookie", "Domain=.a.test; Path=/; Secure; SameSite=None")));
histogram_tester.ExpectUniqueSample(
"Net.DeviceBoundSessions.Registration.Network.Result", HTTP_OK, 1);
}
TEST_F(RegistrationTest, NoScopeJson) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"credentials": [{
"type": "cookie",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kMissingScope);
}
TEST_F(RegistrationTest, NoSessionIdJson) {
constexpr char kTestingJson[] =
R"({
"scope": {
"include_site": true
},
"credentials": [{
"type": "cookie",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidSessionId);
}
TEST_F(RegistrationTest, EmptySessionIdJson) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "",
"scope": {
"include_site": true
},
"credentials": [{
"type": "cookie",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidSessionId);
}
TEST_F(RegistrationTest, SpecificationNotDictJson) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"scope": {
"include_site": true,
"scope_specification" : [
"type", "domain", "path"
]
},
"credentials": [{
"type": "cookie",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidScopeSpecification);
}
TEST_F(RegistrationTest, MissingPathDefaults) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"refresh_url": "/refresh",
"scope": {
"include_site": true,
"scope_specification" : [
{
"type": "include",
"domain": "trusted.a.test"
},
{
"type": "exclude",
"domain": "new.a.test",
"path": "/only_trusted_path"
}
]
},
"credentials": [{
"type": "cookie",
"name": "other_cookie",
"attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const auto& session = callback.outcome().SessionForTesting();
proto::Session session_proto = session.ToProto();
EXPECT_THAT(
session_proto.session_inclusion_rules().url_rules(),
ElementsAre(
EqualsInclusionRule(proto::RuleType::INCLUDE, "trusted.a.test", "/"),
EqualsInclusionRule(proto::RuleType::EXCLUDE, "new.a.test",
"/only_trusted_path"),
EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
}
TEST_F(RegistrationTest, MissingDomainDefaults) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"refresh_url": "/refresh",
"scope": {
"include_site": true,
"scope_specification" : [
{
"type": "include",
"path": "/included"
},
{
"type": "exclude",
"domain": "new.a.test",
"path": "/only_trusted_path"
}
]
},
"credentials": [{
"type": "cookie",
"name": "other_cookie",
"attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const auto& session = callback.outcome().SessionForTesting();
proto::Session session_proto = session.ToProto();
EXPECT_THAT(
session_proto.session_inclusion_rules().url_rules(),
ElementsAre(
EqualsInclusionRule(proto::RuleType::INCLUDE, "*", "/included"),
EqualsInclusionRule(proto::RuleType::EXCLUDE, "new.a.test",
"/only_trusted_path"),
EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
}
TEST_F(RegistrationTest, MissingRefreshUrlDefault) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"scope": {
"include_site": true,
"scope_specification" : [
{
"type": "include",
"domain": "trusted.a.test"
},
{
"type": "exclude",
"domain": "new.a.test",
"path": "/only_trusted_path"
}
]
},
"credentials": [{
"type": "cookie",
"name": "other_cookie",
"attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const auto& session = callback.outcome().SessionForTesting();
EXPECT_EQ(session.refresh_url(), GetBaseURL());
}
TEST_F(RegistrationTest, OneSpecTypeInvalid) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"scope": {
"include_site": true,
"scope_specification" : [
{
"type": "invalid",
"domain": "trusted.example.com",
"path": "/only_trusted_path"
},
{
"type": "exclude",
"domain": "new.example.com",
"path": "/only_trusted_path"
}
]
},
"credentials": [{
"type": "cookie",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidScopeSpecificationType);
}
TEST_F(RegistrationTest, InvalidTypeSpecList) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"refresh_url": "/refresh",
"scope": {
"include_site": true,
"scope_specification" : "missing"
},
"credentials": [{
"type": "cookie",
"name": "auth_cookie",
"attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const auto& session = callback.outcome().SessionForTesting();
proto::Session session_proto = session.ToProto();
EXPECT_TRUE(session_proto.session_inclusion_rules().do_include_site());
EXPECT_THAT(session_proto.session_inclusion_rules().url_rules(),
ElementsAre(EqualsInclusionRule(proto::RuleType::EXCLUDE,
"a.test", "/refresh")));
}
TEST_F(RegistrationTest, TypeIsNotCookie) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"scope": {
"include_site": true
},
"credentials": [{
"type": "sync auth",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidCredentialsType);
}
TEST_F(RegistrationTest, TwoTypesCookie_NotCookie) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"scope": {
"include_site": true
},
"credentials": [
{
"type": "cookie",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
},
{
"type": "sync auth",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
}
]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidCredentialsType);
}
TEST_F(RegistrationTest, TwoTypesNotCookie_Cookie) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"scope": {
"include_site": true
},
"credentials": [
{
"type": "sync auth",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
},
{
"type": "cookie",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
}
]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidCredentialsType);
}
TEST_F(RegistrationTest, CredEntryWithoutDict) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"scope": {
"include_site": true
},
"credentials": [{
"type": "cookie",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
},
"test"]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidCredentialsConfig);
}
TEST_F(RegistrationTest, CredEntryWithoutAttributes) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"scope": {
"include_site": true
},
"credentials": [{
"type": "cookie",
"name": "auth_cookie"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
// Since the cookie has no attributes, it's SameSite Lax. We set
// a same-origin initiator to avoid registration being rejected,
auto origin = url::Origin::Create(GetBaseURL());
auto isolation_info = net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kOther, origin, origin,
net::SiteForCookies::FromOrigin(origin));
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(param, session_service(),
unexportable_key_service(),
context_.get(), isolation_info,
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/origin);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
callback.outcome().SessionForTesting();
}
TEST_F(RegistrationTest, CredEntryWithEmptyName) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"scope": {
"include_site": true
},
"credentials": [{
"type": "cookie",
"name": "",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidCredentialsEmptyName);
}
TEST_F(RegistrationTest, ReturnTextFile) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(&ReturnTextResponse));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
RegistrationRequestParam params = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
params, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(params, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidConfigJson);
}
TEST_F(RegistrationTest, ReturnInvalidJson) {
std::string invalid_json = "*{}";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, invalid_json));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
RegistrationRequestParam param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidConfigJson);
}
TEST_F(RegistrationTest, ReturnEmptyJson) {
std::string empty_json = "{}";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, empty_json));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
RegistrationRequestParam param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidSessionId);
}
TEST_F(RegistrationTest, NetworkErrorServerShutdown) {
base::HistogramTester histogram_tester;
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
ASSERT_TRUE(server_.Start());
GURL url = server_.GetURL("/");
ASSERT_TRUE(server_.ShutdownAndWaitUntilComplete());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
RegistrationRequestParam param = GetBasicParam(url);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kNetError);
histogram_tester.ExpectUniqueSample(
"Net.DeviceBoundSessions.Registration.Network.Result",
net::ERR_CONNECTION_REFUSED, 1);
}
TEST_F(RegistrationTest, NetworkErrorInvalidResponse) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(&ReturnInvalidResponse));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
RegistrationRequestParam param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kNetError);
}
TEST_F(RegistrationTest, ResponseErrorCaptured) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_NOT_FOUND, "Not Found Body"));
ASSERT_TRUE(server_.Start());
TestRegistrationCallback callback;
RegistrationRequestParam param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
SessionError::ErrorType error =
callback.outcome().SessionErrorForTesting()->type;
EXPECT_EQ(error, SessionError::kPersistentHttpError);
std::optional<FailedRequest> failed_request =
callback.outcome().SessionErrorForTesting()->failed_request;
EXPECT_EQ(failed_request->request_url, GetBaseURL());
EXPECT_EQ(failed_request->net_error, std::nullopt);
EXPECT_EQ(failed_request->response_error, 404);
EXPECT_EQ(failed_request->response_error_body, "Not Found Body");
}
TEST_F(RegistrationTest, NetErrorCaptured) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
ASSERT_TRUE(server_.Start());
GURL url = server_.GetURL("/");
ASSERT_TRUE(server_.ShutdownAndWaitUntilComplete());
TestRegistrationCallback callback;
RegistrationRequestParam param = GetBasicParam(url);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
SessionError::ErrorType error =
callback.outcome().SessionErrorForTesting()->type;
EXPECT_EQ(error, SessionError::kNetError);
std::optional<FailedRequest> failed_request =
callback.outcome().SessionErrorForTesting()->failed_request;
EXPECT_EQ(failed_request->request_url, url);
EXPECT_EQ(failed_request->net_error, net::ERR_CONNECTION_REFUSED);
EXPECT_EQ(failed_request->response_error, std::nullopt);
EXPECT_EQ(failed_request->response_error_body, std::nullopt);
}
TEST_F(RegistrationTest, ServerError407) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnResponse, HTTP_PROXY_AUTHENTICATION_REQUIRED, kBasicValidJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
RegistrationRequestParam param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kNetError);
}
TEST_F(RegistrationTest, ServerError400) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_BAD_REQUEST, kBasicValidJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
RegistrationRequestParam param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kPersistentHttpError);
}
TEST_F(RegistrationTest, ServerError500) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnResponse, HTTP_INTERNAL_SERVER_ERROR, kBasicValidJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
RegistrationRequestParam param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kTransientHttpError);
}
TEST_F(RegistrationTest, ServerErrorReturnOne403ThenSuccess) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
auto* container = new ForbiddenThenSuccessResponseContainer(1);
server_.RegisterRequestHandler(base::BindRepeating(
&ForbiddenThenSuccessResponseContainer::Return, base::Owned(container)));
ASSERT_TRUE(server_.Start());
std::unique_ptr<Session> session = CreateTestSession(kSessionIdentifier);
session->set_cached_challenge("challenge");
EXPECT_CALL(
session_service(),
GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
.WillRepeatedly(Return(session.get()));
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), kSessionIdentifier, std::string(kChallenge),
/*authorization=*/std::nullopt);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const auto& result_session = callback.outcome().SessionForTesting();
proto::Session session_proto = result_session.ToProto();
EXPECT_TRUE(session_proto.session_inclusion_rules().do_include_site());
EXPECT_THAT(
session_proto.session_inclusion_rules().url_rules(),
ElementsAre(
EqualsInclusionRule(proto::RuleType::INCLUDE, "trusted.a.test",
"/only_trusted_path"),
EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
EXPECT_THAT(
session_proto.cookie_cravings(),
ElementsAre(EqualsCredential(
"auth_cookie", "Domain=.a.test; Path=/; Secure; SameSite=None")));
}
std::unique_ptr<test_server::HttpResponse> ReturnRedirect(
const std::string& location,
const test_server::HttpRequest& request) {
if (request.relative_url != "/") {
return nullptr;
}
auto response = std::make_unique<test_server::BasicHttpResponse>();
response->set_code(HTTP_FOUND);
response->AddCustomHeader("Location", location);
response->set_content("Redirected");
response->set_content_type("text/plain");
return std::move(response);
}
std::unique_ptr<test_server::HttpResponse> CheckRedirect(
bool* redirect_followed_out,
const test_server::HttpRequest& request) {
if (request.relative_url != kRedirectPath) {
return nullptr;
}
*redirect_followed_out = true;
return ReturnResponse(HTTP_OK, kBasicValidJson, request);
}
// Should be allowed: https://a.test -> https://a.test/redirect.
TEST_F(RegistrationTest, FollowHttpsToHttpsRedirect) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
bool followed = false;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnRedirect, kRedirectPath));
server_.RegisterRequestHandler(
base::BindRepeating(&CheckRedirect, &followed));
// Required to add a certificate for a.test, which is used below.
server_.SetSSLConfig(EmbeddedTestServer::CERT_TEST_NAMES);
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
RegistrationRequestParam param = GetBasicParam(server_.GetURL("a.test", "/"));
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_TRUE(followed);
callback.outcome().SessionForTesting();
}
TEST_F(RegistrationTest, FailOnSslErrorExpired) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
server_.SetSSLConfig(net::EmbeddedTestServer::CERT_EXPIRED);
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
RegistrationRequestParam param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kNetError);
}
std::unique_ptr<test_server::HttpResponse> ReturnResponseForRefreshRequest(
const test_server::HttpRequest& request) {
auto response = std::make_unique<test_server::BasicHttpResponse>();
auto resp_iter = request.headers.find(kSessionResponseHeaderName);
std::string session_response =
resp_iter != request.headers.end() ? resp_iter->second : "";
if (session_response.empty()) {
const auto session_iter = request.headers.find(kSessionIdHeaderName);
EXPECT_TRUE(session_iter != request.headers.end() &&
!session_iter->second.empty());
response->set_code(HTTP_FORBIDDEN);
response->AddCustomHeader(kSessionChallengeHeaderName,
R"("test_challenge";id="session_id")");
return response;
}
response->set_code(HTTP_OK);
response->set_content_type("application/json");
response->set_content(kBasicValidJson);
return response;
}
TEST_F(RegistrationTest, BasicSuccessForExistingKey) {
base::HistogramTester histogram_tester;
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(),
std::ref(unexportable_key_service()), context_.get(),
std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
const auto& session = callback.outcome().SessionForTesting();
proto::Session session_proto = session.ToProto();
EXPECT_TRUE(session_proto.session_inclusion_rules().do_include_site());
EXPECT_THAT(
session_proto.session_inclusion_rules().url_rules(),
ElementsAre(
EqualsInclusionRule(proto::RuleType::INCLUDE, "trusted.a.test",
"/only_trusted_path"),
EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
EXPECT_THAT(
session_proto.cookie_cravings(),
ElementsAre(EqualsCredential(
"auth_cookie", "Domain=.a.test; Path=/; Secure; SameSite=None")));
histogram_tester.ExpectBucketCount(
"Net.DeviceBoundSessions.Refresh.Network.Result", HTTP_OK, 1);
}
TEST_F(RegistrationTest, FetchRegistrationWithCachedChallenge) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponseForRefreshRequest));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(),
std::ref(unexportable_key_service()), context_.get(),
std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
const auto& session = callback.outcome().SessionForTesting();
proto::Session session_proto = session.ToProto();
EXPECT_TRUE(session_proto.session_inclusion_rules().do_include_site());
EXPECT_THAT(
session_proto.session_inclusion_rules().url_rules(),
ElementsAre(
EqualsInclusionRule(proto::RuleType::INCLUDE, "trusted.a.test",
"/only_trusted_path"),
EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
EXPECT_THAT(
session_proto.cookie_cravings(),
ElementsAre(EqualsCredential(
"auth_cookie", "Domain=.a.test; Path=/; Secure; SameSite=None")));
}
TEST_F(RegistrationTest, FetchRegistrationAndChallengeRequired) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(&ReturnForbidden));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), /*session_identifier=*/std::nullopt, kChallenge,
/*authorization=*/std::nullopt);
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(),
std::ref(unexportable_key_service()), context_.get(),
std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kRegistrationAttemptedChallenge);
}
TEST_F(RegistrationTest, FetchRefreshAndChallengeRequired_NoChallenge) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(&ReturnForbidden));
ASSERT_TRUE(server_.Start());
std::unique_ptr<Session> session = CreateTestSession("session_identifier");
EXPECT_CALL(
session_service(),
GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
.WillRepeatedly(Return(session.get()));
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), "session_identifier", kChallenge,
/*authorization=*/std::nullopt);
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(),
std::ref(unexportable_key_service()), context_.get(),
std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kInvalidChallenge);
}
TEST_F(RegistrationTest,
FetchRefreshAndChallengeRequired_NoChallengeToNewChallenge) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
ASSERT_TRUE(server_.InitializeAndListen());
std::unique_ptr<Session> session = CreateTestSession(kSessionIdentifier);
EXPECT_CALL(
session_service(),
GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
.WillOnce(Return(session.get()));
server_.RegisterRequestHandler(base::BindRepeating(
[](Session* session, const test_server::HttpRequest& request)
-> std::unique_ptr<test_server::HttpResponse> {
auto response = std::make_unique<test_server::BasicHttpResponse>();
const std::optional<std::string> challenge =
GetRequestChallenge(request);
if (!challenge.has_value()) {
response->set_code(HTTP_FORBIDDEN);
session->set_cached_challenge("updated_challenge");
return response;
}
if (*challenge == "updated_challenge") {
response->set_code(HTTP_OK);
response->set_content_type("application/json");
response->set_content(kBasicValidJson);
return response;
}
response->set_code(HTTP_FORBIDDEN);
return response;
},
session.get()));
server_.StartAcceptingConnections();
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto request_param = RegistrationRequestParam::CreateForRefresh(*session);
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(),
std::ref(unexportable_key_service()), context_.get(),
std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
callback.outcome().SessionForTesting();
}
TEST_F(RegistrationTest,
FetchRefreshAndChallengeRequired_ExistingChallengeToNewChallenge) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
ASSERT_TRUE(server_.InitializeAndListen());
std::unique_ptr<Session> session = CreateTestSession(kSessionIdentifier);
EXPECT_CALL(
session_service(),
GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
.WillOnce(Return(session.get()));
server_.RegisterRequestHandler(base::BindRepeating(
[](Session* session, const test_server::HttpRequest& request)
-> std::unique_ptr<test_server::HttpResponse> {
auto response = std::make_unique<test_server::BasicHttpResponse>();
const std::optional<std::string> challenge =
GetRequestChallenge(request);
if (*challenge == kChallenge) {
response->set_code(HTTP_FORBIDDEN);
session->set_cached_challenge("updated_challenge");
return response;
}
if (*challenge == "updated_challenge") {
response->set_code(HTTP_OK);
response->set_content_type("application/json");
response->set_content(kBasicValidJson);
return response;
}
response->set_code(HTTP_FORBIDDEN);
return response;
},
session.get()));
server_.StartAcceptingConnections();
session->set_cached_challenge(kChallenge);
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto request_param = RegistrationRequestParam::CreateForRefresh(*session);
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(),
std::ref(unexportable_key_service()), context_.get(),
std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
callback.outcome().SessionForTesting();
}
TEST_F(RegistrationTest, ContinueFalse) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"continue": false
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kServerRequestedTermination);
}
TEST_F(RegistrationTest, TerminateSessionOnRepeatedFailure_Refresh) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
unexportable_keys::MockUnexportableKeyService mock_service;
EXPECT_CALL(mock_service, GetAlgorithm(_))
.WillRepeatedly(
Invoke(&unexportable_key_service(),
&unexportable_keys::UnexportableKeyService::GetAlgorithm));
EXPECT_CALL(mock_service, GetSubjectPublicKeyInfo(_))
.WillRepeatedly(Invoke(
&unexportable_key_service(),
&unexportable_keys::UnexportableKeyService::GetSubjectPublicKeyInfo));
EXPECT_CALL(mock_service, SignSlowlyAsync)
.WillRepeatedly(base::test::RunOnceCallbackRepeatedly<3>(
base::unexpected(unexportable_keys::ServiceError::kCryptoApiFailed)));
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(), std::ref(mock_service),
context_.get(), std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kSigningError);
}
TEST_F(RegistrationTest, TerminateSessionOnRepeatedFailure_Registration) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
unexportable_keys::MockUnexportableKeyService mock_service;
EXPECT_CALL(mock_service, GetAlgorithm(_))
.WillRepeatedly(
Invoke(&unexportable_key_service(),
&unexportable_keys::UnexportableKeyService::GetAlgorithm));
EXPECT_CALL(mock_service, GetSubjectPublicKeyInfo(_))
.WillRepeatedly(Invoke(
&unexportable_key_service(),
&unexportable_keys::UnexportableKeyService::GetSubjectPublicKeyInfo));
EXPECT_CALL(mock_service, SignSlowlyAsync)
.WillRepeatedly(base::test::RunOnceCallbackRepeatedly<3>(
base::unexpected(unexportable_keys::ServiceError::kCryptoApiFailed)));
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), /*session_identifier=*/std::nullopt, kChallenge,
/*authorization=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(), std::ref(mock_service),
context_.get(), std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kSigningError);
}
TEST_F(RegistrationTest, NetLogRegistrationResultLogged) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(net_log_observer
.GetEntriesWithType(NetLogEventType::DBSC_REGISTRATION_RESULT)
.size(),
1u);
}
TEST_F(RegistrationTest, NetLogRefreshResultLogged) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(),
std::ref(unexportable_key_service()), context_.get(),
std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(
net_log_observer.GetEntriesWithType(NetLogEventType::DBSC_REFRESH_RESULT)
.size(),
1u);
}
TEST_F(RegistrationTest, TerminateSessionOnRepeatedChallenge) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
auto* container = new ForbiddenThenSuccessResponseContainer(100);
server_.RegisterRequestHandler(base::BindRepeating(
&ForbiddenThenSuccessResponseContainer::Return, base::Owned(container)));
ASSERT_TRUE(server_.Start());
std::unique_ptr<Session> session = CreateTestSession(kSessionIdentifier);
session->set_cached_challenge("challenge");
EXPECT_CALL(
session_service(),
GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
.WillRepeatedly(Return(session.get()));
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(),
std::ref(unexportable_key_service()), context_.get(),
std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
SessionError::kTooManyChallenges);
}
TEST_F(RegistrationTest, RefreshCachesSignedChallenge) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
features::kDeviceBoundSessionSigningQuotaAndCaching);
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
unexportable_keys::MockUnexportableKeyService mock_key_service;
auto [spki, jwk] = GetRS256SpkiAndJwkForTesting();
std::vector<unsigned char> spki_vector(spki.begin(), spki.end());
EXPECT_CALL(mock_key_service, GetAlgorithm(_))
.WillOnce(Return(crypto::SignatureVerifier::RSA_PKCS1_SHA256));
EXPECT_CALL(mock_key_service, GetSubjectPublicKeyInfo(_))
.WillOnce(Return(spki_vector));
EXPECT_CALL(mock_key_service, SignSlowlyAsync(_, _, _, _))
.WillOnce(WithArg<3>([](auto callback) {
std::move(callback).Run(std::vector<uint8_t>{'s', 'i', 'g'});
}));
// No cached challenge initially.
EXPECT_CALL(session_service(), GetLatestSignedRefreshChallenge(_))
.WillOnce(Return(nullptr));
// Expect a signing occurrence and the new signed challenge to be cached.
EXPECT_CALL(session_service(), AddSigningOccurrence(_)).Times(1);
EXPECT_CALL(session_service(), SetLatestSignedRefreshChallenge(_, _))
.Times(1);
// Quota check should return false to allow signing.
EXPECT_CALL(session_service(), SigningQuotaExceeded(_))
.WillOnce(Return(false));
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(), std::ref(mock_key_service),
context_.get(), std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, UnexportableSigningKeyId(),
callback.callback());
callback.WaitForCall();
callback.outcome().SessionForTesting();
}
TEST_F(RegistrationTest, RefreshCachedSignedChallengeUsed) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
features::kDeviceBoundSessionSigningQuotaAndCaching);
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
// No calls to actual signing.
unexportable_keys::MockUnexportableKeyService mock_key_service;
EXPECT_CALL(mock_key_service, GetAlgorithm(_)).Times(0);
EXPECT_CALL(mock_key_service, GetSubjectPublicKeyInfo(_)).Times(0);
EXPECT_CALL(mock_key_service, SignSlowlyAsync(_, _, _, _)).Times(0);
// Create a matching cached challenge.
SessionService::SignedRefreshChallenge cached_challenge;
cached_challenge.challenge = kChallenge;
cached_challenge.signed_challenge = "mock_signed_challenge";
EXPECT_CALL(session_service(), GetLatestSignedRefreshChallenge(_))
.WillOnce(Return(&cached_challenge));
// There should be no signing or quota checking since that's skipped.
EXPECT_CALL(session_service(), AddSigningOccurrence(_)).Times(0);
EXPECT_CALL(session_service(), SigningQuotaExceeded(_)).Times(0);
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(), std::ref(mock_key_service),
context_.get(), std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, cached_challenge.key_id,
callback.callback());
callback.WaitForCall();
callback.outcome().SessionForTesting();
}
TEST_F(RegistrationTest, RefreshCachedSignedChallengeDoesNotMatch) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
features::kDeviceBoundSessionSigningQuotaAndCaching);
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
unexportable_keys::MockUnexportableKeyService mock_key_service;
auto [spki, jwk] = GetRS256SpkiAndJwkForTesting();
std::vector<unsigned char> spki_vector(spki.begin(), spki.end());
EXPECT_CALL(mock_key_service, GetAlgorithm(_))
.WillOnce(Return(crypto::SignatureVerifier::RSA_PKCS1_SHA256));
EXPECT_CALL(mock_key_service, GetSubjectPublicKeyInfo(_))
.WillOnce(Return(spki_vector));
EXPECT_CALL(mock_key_service, SignSlowlyAsync(_, _, _, _))
.WillOnce(WithArg<3>([](auto callback) {
std::move(callback).Run(std::vector<uint8_t>{'s', 'i', 'g'});
}));
// Add cached signed challenge that doesn't match (the challenge used is
// different).
SessionService::SignedRefreshChallenge cached_challenge;
cached_challenge.challenge = "different_challenge";
cached_challenge.signed_challenge = "mock_signed_challenge";
EXPECT_CALL(session_service(), GetLatestSignedRefreshChallenge(_))
.WillOnce(Return(&cached_challenge));
EXPECT_CALL(session_service(), AddSigningOccurrence(_)).Times(1);
EXPECT_CALL(session_service(), SetLatestSignedRefreshChallenge(_, _))
.Times(1);
EXPECT_CALL(session_service(), SigningQuotaExceeded(_))
.WillOnce(Return(false));
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(), std::ref(mock_key_service),
context_.get(), std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, UnexportableSigningKeyId(),
callback.callback());
callback.WaitForCall();
callback.outcome().SessionForTesting();
}
TEST_F(RegistrationTest, RegistrationTriggersSigningOccurrence) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
features::kDeviceBoundSessionSigningQuotaAndCaching);
// Expect the signing checks are done only when the feature is enabled.
EXPECT_CALL(session_service(), AddSigningOccurrence(_)).Times(1);
EXPECT_CALL(session_service(), SigningQuotaExceeded(_))
.WillOnce(Return(false));
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating([](const test_server::HttpRequest& request) {
auto resp_iter = request.headers.find(kSessionResponseHeaderName);
EXPECT_TRUE(resp_iter != request.headers.end());
if (resp_iter != request.headers.end()) {
EXPECT_TRUE(VerifyEs256Jwt(resp_iter->second));
}
return ReturnResponse(HTTP_OK, kBasicValidJson, request);
}));
ASSERT_TRUE(server_.Start());
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
}
TEST_F(RegistrationTest, RefreshWithNewSessionIdFails) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), "old_session_id", kChallenge,
/*authorization=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(),
std::ref(unexportable_key_service()), context_.get(),
std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
SessionError::ErrorType session_error =
out_session.SessionErrorForTesting()->type;
EXPECT_EQ(session_error, SessionError::kMismatchedSessionId);
}
TEST_F(RegistrationTest, RegistrationWithNonStringRefreshInitiatorsFails) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
constexpr char kNonStringInitiator[] =
R"({
"session_identifier": "session_id",
"scope": {
"include_site": true,
"scope_specification" : [
{
"type": "include",
"domain": "trusted.example.com",
"path": "/only_trusted_path"
}
]
},
"credentials": [{
"type": "cookie",
"name": "auth_cookie",
"attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
}],
"allowed_refresh_initiators": [ 12345 ]
})";
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kNonStringInitiator));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(),
std::ref(unexportable_key_service()), context_.get(),
std::ref(isolation_info),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
SessionError::ErrorType session_error =
out_session.SessionErrorForTesting()->type;
EXPECT_EQ(session_error, SessionError::kRefreshInitiatorNotString);
}
TEST_F(RegistrationTest, MissingIncludeSiteFails) {
constexpr char kTestingJson[] =
R"({
"session_identifier": "session_id",
"refresh_url": "/refresh",
"scope": {
"scope_specification" : [
{
"type": "include",
"domain": "trusted.a.test"
},
{
"type": "exclude",
"domain": "new.a.test",
"path": "/only_trusted_path"
}
]
},
"credentials": [{
"type": "cookie",
"name": "other_cookie",
"attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
}]
})";
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
EXPECT_EQ(out_session.SessionErrorForTesting()->type,
SessionError::kMissingScopeIncludeSite);
}
TEST_F(RegistrationTest, ShutdownDuringRequest) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
base::RunLoop run_loop;
server_.RegisterRequestHandler(base::BindRepeating(
[](base::RunLoop* run_loop, const test_server::HttpRequest& request)
-> std::unique_ptr<test_server::HttpResponse> {
run_loop->Quit();
return std::make_unique<test_server::HungResponse>();
},
&run_loop));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
run_loop.Run();
EXPECT_EQ(context_->url_requests()->size(), 1u);
fetcher.reset();
EXPECT_EQ(context_->url_requests()->size(), 0u);
}
TEST_F(RegistrationTest, EmptyResponseOnRegistration) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, ""));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
EXPECT_EQ(out_session.SessionErrorForTesting()->type,
SessionError::kEmptySessionConfig);
}
TEST_F(RegistrationTest, EmptyResponseOnRefresh) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, ""));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto request_param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
UnexportableSigningKeyId key = CreateSigningKey();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_param, session_service(), unexportable_key_service(),
context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartFetchWithExistingKey(request_param, std::move(key),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
out_session.NoSessionConfigChangeForTesting();
}
TEST_F(RegistrationTest, SetChallengeOnRegistration) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating([](const test_server::HttpRequest& request)
-> std::unique_ptr<test_server::HttpResponse> {
auto response = std::make_unique<test_server::BasicHttpResponse>();
response->set_code(HTTP_OK);
response->AddCustomHeader(kSessionChallengeHeaderName,
R"("test_challenge";id="session_id")");
response->set_content_type("application/json");
response->set_content(kBasicValidJson);
return response;
}));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam();
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
EXPECT_EQ(out_session.SessionForTesting().cached_challenge(),
"test_challenge");
}
TEST_F(RegistrationTest, RegistrationBySubdomain_Success) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"registering_origins": [ "https://subdomain.a.test:$1" ]
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
GURL registration_url = server_.GetURL("subdomain.a.test", "/");
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam(registration_url);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
callback.outcome().SessionForTesting();
}
TEST_F(RegistrationTest, RegistrationBySubdomain_WellKnownUnavailable) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnResponse, HTTP_BAD_REQUEST, "")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
GURL registration_url = server_.GetURL("subdomain.a.test", "/");
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam(registration_url);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
EXPECT_EQ(out_session.SessionErrorForTesting()->type,
SessionError::kSubdomainRegistrationWellKnownUnavailable);
}
TEST_F(RegistrationTest, RegistrationBySubdomain_WellKnownMalformed) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnResponse, HTTP_OK, "invalid JSON")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
GURL registration_url = server_.GetURL("subdomain.a.test", "/");
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam(registration_url);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
EXPECT_EQ(out_session.SessionErrorForTesting()->type,
SessionError::kSubdomainRegistrationWellKnownMalformed);
}
TEST_F(RegistrationTest, RegistrationBySubdomain_WellKnownMalformedEntry) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnResponse, HTTP_OK,
"{\"registering_origins\": [ 12345 ]}")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
GURL registration_url = server_.GetURL("subdomain.a.test", "/");
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam(registration_url);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
EXPECT_EQ(out_session.SessionErrorForTesting()->type,
SessionError::kSubdomainRegistrationWellKnownMalformed);
}
TEST_F(RegistrationTest, RegistrationBySubdomain_Unauthorized) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"registering_origins": [ "https://subdomain.a.test:$1" ]
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
GURL registration_url = server_.GetURL("not-allowed-subdomain.a.test", "/");
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam(registration_url);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
EXPECT_EQ(out_session.SessionErrorForTesting()->type,
SessionError::kSubdomainRegistrationUnauthorized);
}
TEST_F(RegistrationTest, RegistrationBySubdomain_MultipleAllowed) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"registering_origins": [
"https://subdomain.a.test:$1",
"https://other-subdomain.a.test:$1"
]
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
GURL registration_url = server_.GetURL("subdomain.a.test", "/");
auto param = GetBasicParam(registration_url);
{
TestRegistrationCallback callback;
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(),
context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
out_session.SessionForTesting();
}
{
TestRegistrationCallback callback;
registration_url = server_.GetURL("other-subdomain.a.test", "/");
param = GetBasicParam(registration_url);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(),
context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
callback.outcome().SessionForTesting();
}
}
TEST_F(RegistrationTest, FederatedSuccess) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
"/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"relying_origins": [ "https://rp.a.test:$1" ]
})json")));
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"provider_origin": "https://provider.a.test:$1"
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
UnexportableSigningKeyId key = CreateSigningKey();
auto param = RegistrationRequestParam::CreateForTesting(
server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
auto session_or_error =
FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
EXPECT_EQ(session_or_error.SessionForTesting().unexportable_key_id(), key);
}
TEST_F(RegistrationTest, FederatedProviderHasProvider) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
"/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"provider_origin": "https://provider-provider.a.test:$1",
"relying_origins": [ "https://rp.a.test:$1" ]
})json")));
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"provider_origin": "https://provider.a.test:$1",
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
UnexportableSigningKeyId key = CreateSigningKey();
auto param = RegistrationRequestParam::CreateForTesting(
server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
auto session_or_error =
FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
SessionError::kSessionProviderWellKnownHasProviderOrigin);
}
TEST_F(RegistrationTest, FederatedProviderUnvailable) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "provider.a.test",
"/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnResponse, HTTP_BAD_REQUEST, "")));
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"provider_origin": "https://provider.a.test:$1",
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
UnexportableSigningKeyId key = CreateSigningKey();
auto param = RegistrationRequestParam::CreateForTesting(
server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
auto session_or_error =
FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
SessionError::kSessionProviderWellKnownUnavailable);
}
TEST_F(RegistrationTest, FederatedProviderUnauthorized) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
"/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"relying_origins": [ "https://other-rp.a.test:$1" ]
})json")));
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"provider_origin": "https://provider.a.test:$1"
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
UnexportableSigningKeyId key = CreateSigningKey();
auto param = RegistrationRequestParam::CreateForTesting(
server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
auto session_or_error =
FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
SessionError::kFederatedNotAuthorizedByProvider);
}
TEST_F(RegistrationTest, FederatedRelyingUnavailable) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
"/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"relying_origins": [ "https://rp.a.test:$1" ]
})json")));
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnResponse, HTTP_BAD_REQUEST, "")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
UnexportableSigningKeyId key = CreateSigningKey();
auto param = RegistrationRequestParam::CreateForTesting(
server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
auto session_or_error =
FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
SessionError::kRelyingPartyWellKnownUnavailable);
}
TEST_F(RegistrationTest, FederatedRelyingHasRelying) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
"/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"relying_origins": [ "https://rp.a.test:$1" ]
})json")));
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"provider_origin": "https://provider.a.test:$1",
"relying_origins": [ "https://rp-rp.a.test:$1" ]
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
UnexportableSigningKeyId key = CreateSigningKey();
auto param = RegistrationRequestParam::CreateForTesting(
server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
auto session_or_error =
FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
SessionError::kRelyingPartyWellKnownHasRelyingOrigins);
}
TEST_F(RegistrationTest, FederatedRelyingNotAuthorized) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
"/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"relying_origins": [ "https://rp.a.test:$1" ]
})json")));
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"provider_origin": "https://other-provider.a.test:$1"
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
UnexportableSigningKeyId key = CreateSigningKey();
auto param = RegistrationRequestParam::CreateForTesting(
server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
auto session_or_error =
FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
SessionError::kFederatedNotAuthorizedByRelyingParty);
}
TEST_F(RegistrationTest, FederatedTooManyRelying) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
"/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"relying_origins": [
"https://rp.b1.test:$1",
"https://rp.b2.test:$1",
"https://rp.b3.test:$1",
"https://rp.b4.test:$1",
"https://rp.b5.test:$1",
"https://rp.a.test:$1"
]
})json")));
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"provider_origin": "https://provider.a.test:$1"
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
UnexportableSigningKeyId key = CreateSigningKey();
auto param = RegistrationRequestParam::CreateForTesting(
server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
auto session_or_error =
FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
SessionError::kTooManyRelyingOriginLabels);
}
TEST_F(RegistrationTest, FederatedTooManyRelyingFirstLabelAllowed) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
"/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"relying_origins": [
"https://a-is-allowed-because-its-first.a.test:$1",
"https://rp.b1.test:$1",
"https://rp.b2.test:$1",
"https://rp.b3.test:$1",
"https://rp.b4.test:$1",
"https://rp.b5.test:$1",
"https://rp.a.test:$1"
]
})json")));
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"provider_origin": "https://provider.a.test:$1"
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
UnexportableSigningKeyId key = CreateSigningKey();
auto param = RegistrationRequestParam::CreateForTesting(
server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
auto session_or_error =
FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
EXPECT_EQ(session_or_error.SessionForTesting().unexportable_key_id(), key);
}
TEST_F(RegistrationTest, FederatedNotRegistrableDoesNotCount) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
"/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"relying_origins": [
"https://tld",
"http://?not-a=url",
"http:///path",
"http:///path2",
"http:///path3",
"https://rp.a.test:$1"
]
})json")));
server_.RegisterRequestHandler(base::BindRepeating(
&ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
base::BindRepeating(&ReturnWellKnown,
R"json({
"provider_origin": "https://provider.a.test:$1"
})json")));
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
UnexportableSigningKeyId key = CreateSigningKey();
auto param = RegistrationRequestParam::CreateForTesting(
server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
/*authorization=*/std::nullopt);
auto session_or_error =
FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
EXPECT_EQ(session_or_error.SessionForTesting().unexportable_key_id(), key);
}
TEST_F(RegistrationTest, RegistrationFailsIfCantSetCookies) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
GURL registration_url = server_.GetURL("a.test", "/");
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = GetBasicParam(registration_url);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
network_delegate()->set_cookie_options(TestNetworkDelegate::NO_SET_COOKIE);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
const RegistrationResult& out_session = callback.outcome();
EXPECT_EQ(out_session.SessionErrorForTesting()->type,
SessionError::kBoundCookieSetForbidden);
}
TEST_F(RegistrationTest, RegisterAuthorizationNoChallenge) {
base::HistogramTester histogram_tester;
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
server_.RegisterRequestHandler(
base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
ASSERT_TRUE(server_.Start());
RecordingNetLogObserver net_log_observer;
TestRegistrationCallback callback;
auto param = RegistrationRequestParam::CreateForTesting(
GetBaseURL(), /*session_identifier=*/std::nullopt,
/*challenge=*/std::nullopt, "authorization");
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
param, session_service(), unexportable_key_service(), context_.get(),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
/*net_log_source=*/std::nullopt,
/*original_request_initiator=*/std::nullopt);
fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
callback.callback());
callback.WaitForCall();
// Validate the result is a session instead of an error.
callback.outcome().SessionForTesting();
}
class RegistrationTokenHelperTest : public testing::Test {
public:
unexportable_keys::UnexportableKeyService& unexportable_key_service() {
return unexportable_key_service_;
}
void RunBackgroundTasks() { task_environment_.RunUntilIdle(); }
private:
base::test::TaskEnvironment task_environment_{
// QUEUED - tasks don't run until `RunUntilIdle()` is called.
base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
unexportable_keys::UnexportableKeyTaskManager task_manager_;
unexportable_keys::UnexportableKeyServiceImpl unexportable_key_service_{
task_manager_, kTaskOrigin, crypto::UnexportableKeyProvider::Config()};
};
TEST_F(RegistrationTokenHelperTest, CreateSuccess) {
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
base::test::TestFuture<std::optional<RegistrationFetcher::RegistrationToken>>
future;
RegistrationFetcher::CreateRegistrationTokenAsyncForTesting(
unexportable_key_service(), "test_challenge",
/*authorization=*/std::nullopt, future.GetCallback());
RunBackgroundTasks();
ASSERT_TRUE(future.Get().has_value());
}
TEST_F(RegistrationTokenHelperTest, CreateFail) {
crypto::ScopedNullUnexportableKeyProvider scoped_null_key_provider;
base::test::TestFuture<std::optional<RegistrationFetcher::RegistrationToken>>
future;
RegistrationFetcher::CreateRegistrationTokenAsyncForTesting(
unexportable_key_service(), "test_challenge",
/*authorization=*/std::nullopt, future.GetCallback());
RunBackgroundTasks();
EXPECT_FALSE(future.Get().has_value());
}
} // namespace
} // namespace net::device_bound_sessions