blob: 8423a91bf505d378441f817470af75ff6b9d4495 [file] [log] [blame]
// 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 "chrome/browser/webauthn/enclave_manager.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/webauthn/proto/enclave_local_state.pb.h"
#include "components/os_crypt/sync/os_crypt_mocker.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
#include "components/trusted_vault/command_line_switches.h"
#include "components/trusted_vault/proto/vault.pb.h"
#include "components/trusted_vault/trusted_vault_server_constants.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/enclave/constants.h"
#include "device/fido/enclave/enclave_authenticator.h"
#include "device/fido/enclave/types.h"
#include "net/base/port_util.h"
#include "net/http/http_status_code.h"
#include "services/network/network_service.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/test/fake_test_cert_verifier_params_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
// The communication with the enclave process would need to be ported to Windows
// for these tests to run there.
//
// These tests are also disabled under MSAN. The enclave subprocess is written
// in Rust and FFI from Rust to C++ doesn't work in Chromium at this time
// (crbug.com/1369167).
#if BUILDFLAG(IS_POSIX) && !defined(MEMORY_SANITIZER)
namespace enclave = device::enclave;
namespace {
constexpr std::array<uint8_t, 32> kTestKey = {
0xc4, 0xdf, 0xa4, 0xed, 0xfc, 0xf9, 0x7c, 0xc0, 0x3a, 0xb1, 0xcb,
0x3c, 0x03, 0x02, 0x9b, 0x5a, 0x05, 0xec, 0x88, 0x48, 0x54, 0x42,
0xf1, 0x20, 0xb4, 0x75, 0x01, 0xde, 0x61, 0xf1, 0x39, 0x5d,
};
constexpr uint8_t kTestProtobuf[] = {
0x0a, 0x10, 0x71, 0xfd, 0xf9, 0x65, 0xa8, 0x7c, 0x61, 0xe2, 0xff, 0x27,
0x0c, 0x76, 0x25, 0x23, 0xe0, 0xa4, 0x12, 0x10, 0x77, 0xf2, 0x3c, 0x31,
0x3c, 0xe8, 0x94, 0x9a, 0x9f, 0xbc, 0xdf, 0x44, 0xfc, 0xf5, 0x41, 0x97,
0x1a, 0x0b, 0x77, 0x65, 0x62, 0x61, 0x75, 0x74, 0x68, 0x6e, 0x2e, 0x69,
0x6f, 0x22, 0x06, 0x56, 0x47, 0x56, 0x7a, 0x64, 0x41, 0x2a, 0x10, 0x60,
0x07, 0x19, 0x5b, 0x4e, 0x19, 0xf9, 0x6e, 0xc1, 0xfc, 0xfd, 0x0a, 0xf6,
0x0c, 0x00, 0x7e, 0x30, 0xf9, 0xa0, 0xea, 0xf3, 0xc8, 0x31, 0x3a, 0x04,
0x54, 0x65, 0x73, 0x74, 0x42, 0x04, 0x54, 0x65, 0x73, 0x74, 0x4a, 0xa6,
0x01, 0xdc, 0xc5, 0x16, 0x15, 0x91, 0x24, 0xd2, 0x31, 0xfc, 0x85, 0x8b,
0xe2, 0xec, 0x22, 0x09, 0x8f, 0x8d, 0x0f, 0xbe, 0x9b, 0x59, 0x71, 0x04,
0xcd, 0xaa, 0x3d, 0x32, 0x23, 0xbd, 0x25, 0x46, 0x14, 0x86, 0x9c, 0xfe,
0x74, 0xc8, 0xd3, 0x37, 0x70, 0xed, 0xb0, 0x25, 0xd4, 0x1b, 0xdd, 0xa4,
0x3c, 0x02, 0x13, 0x8c, 0x69, 0x03, 0xff, 0xd1, 0xb0, 0x72, 0x00, 0x29,
0xcf, 0x5f, 0x06, 0xb3, 0x94, 0xe2, 0xea, 0xca, 0x68, 0xdd, 0x0b, 0x07,
0x98, 0x7a, 0x2c, 0x8f, 0x08, 0xee, 0x7d, 0xad, 0x16, 0x35, 0xc7, 0x10,
0xf3, 0xa4, 0x90, 0x84, 0xd1, 0x8e, 0x2e, 0xdb, 0xb9, 0xfa, 0x72, 0x9a,
0xcf, 0x12, 0x1b, 0x3c, 0xca, 0xfa, 0x79, 0x4a, 0x1e, 0x1b, 0xe1, 0x15,
0xdf, 0xab, 0xee, 0x75, 0xbb, 0x5c, 0x5a, 0x94, 0x14, 0xeb, 0x72, 0xae,
0x37, 0x97, 0x03, 0xa8, 0xe7, 0x62, 0x9d, 0x2e, 0xfd, 0x28, 0xce, 0x03,
0x34, 0x20, 0xa7, 0xa2, 0x7b, 0x00, 0xc8, 0x12, 0x62, 0x12, 0x7f, 0x54,
0x73, 0x8c, 0x21, 0xc8, 0x85, 0x15, 0xce, 0x36, 0x14, 0xd9, 0x41, 0x22,
0xe8, 0xbf, 0x88, 0xf9, 0x45, 0xe4, 0x1c, 0x89, 0x7d, 0xa4, 0x23, 0x58,
0x00, 0x68, 0x98, 0xf5, 0x81, 0xef, 0xad, 0xf4, 0xda, 0x17, 0x70, 0xab,
0x03,
};
std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> GetTestEntity() {
auto ret = std::make_unique<sync_pb::WebauthnCredentialSpecifics>();
CHECK(ret->ParseFromArray(kTestProtobuf, sizeof(kTestProtobuf)));
return ret;
}
struct TempDir {
public:
TempDir() { CHECK(dir_.CreateUniqueTempDir()); }
base::FilePath GetPath() const { return dir_.GetPath(); }
private:
base::ScopedTempDir dir_;
};
std::pair<base::Process, uint16_t> StartEnclave(base::FilePath cwd) {
base::FilePath data_root;
CHECK(base::PathService::Get(base::DIR_OUT_TEST_DATA_ROOT, &data_root));
const base::FilePath enclave_bin_path =
data_root.AppendASCII("cloud_authenticator_test_service");
base::LaunchOptions subprocess_opts;
subprocess_opts.current_directory = cwd;
std::optional<base::Process> enclave_process;
uint16_t port;
for (int i = 0; i < 10; i++) {
int fds[2];
CHECK(!pipe(fds));
subprocess_opts.fds_to_remap.emplace_back(fds[1], 1);
enclave_process = base::LaunchProcess(base::CommandLine(enclave_bin_path),
subprocess_opts);
CHECK(enclave_process->IsValid());
close(fds[1]);
char port_str[6];
const ssize_t read_bytes =
HANDLE_EINTR(read(fds[0], port_str, sizeof(port_str)));
CHECK(read_bytes > 0);
port_str[read_bytes - 1] = 0;
unsigned u_port;
CHECK(base::StringToUint(port_str, &u_port)) << port_str;
port = base::checked_cast<uint16_t>(u_port);
close(fds[0]);
if (net::IsPortAllowedForScheme(port, "wss")) {
break;
}
LOG(INFO) << "Port " << port << " not allowed. Trying again.";
// The kernel randomly picked a port that Chromium will refuse to connect
// to. Try again.
enclave_process->Terminate(/*exit_code=*/1, /*wait=*/false);
}
return std::make_pair(std::move(*enclave_process), port);
}
enclave::ScopedEnclaveOverride TestEnclaveIdentity(uint16_t port) {
constexpr std::array<uint8_t, device::kP256X962Length> kTestPublicKey = {
0x04, 0x6b, 0x17, 0xd1, 0xf2, 0xe1, 0x2c, 0x42, 0x47, 0xf8, 0xbc,
0xe6, 0xe5, 0x63, 0xa4, 0x40, 0xf2, 0x77, 0x03, 0x7d, 0x81, 0x2d,
0xeb, 0x33, 0xa0, 0xf4, 0xa1, 0x39, 0x45, 0xd8, 0x98, 0xc2, 0x96,
0x4f, 0xe3, 0x42, 0xe2, 0xfe, 0x1a, 0x7f, 0x9b, 0x8e, 0xe7, 0xeb,
0x4a, 0x7c, 0x0f, 0x9e, 0x16, 0x2b, 0xce, 0x33, 0x57, 0x6b, 0x31,
0x5e, 0xce, 0xcb, 0xb6, 0x40, 0x68, 0x37, 0xbf, 0x51, 0xf5,
};
const std::string url = "ws://127.0.0.1:" + base::NumberToString(port);
enclave::EnclaveIdentity identity;
identity.url = GURL(url);
identity.public_key = kTestPublicKey;
return enclave::ScopedEnclaveOverride(std::move(identity));
}
trusted_vault_pb::JoinSecurityDomainsResponse MakeJoinSecurityDomainsResponse(
int current_epoch) {
trusted_vault_pb::JoinSecurityDomainsResponse response;
trusted_vault_pb::SecurityDomain* security_domain =
response.mutable_security_domain();
security_domain->set_name(
GetSecurityDomainPath(trusted_vault::SecurityDomainId::kPasskeys));
security_domain->set_current_epoch(current_epoch);
return response;
}
std::unique_ptr<network::NetworkService> CreateNetwork(
mojo::Remote<network::mojom::NetworkContext>* network_context) {
network::mojom::NetworkContextParamsPtr params =
network::mojom::NetworkContextParams::New();
params->cert_verifier_params =
network::FakeTestCertVerifierParamsFactory::GetCertVerifierParams();
auto service = network::NetworkService::CreateForTesting();
service->CreateNetworkContext(network_context->BindNewPipeAndPassReceiver(),
std::move(params));
return service;
}
scoped_refptr<device::JSONRequest> JSONFromString(base::StringPiece json_str) {
base::Value json_request = base::JSONReader::Read(json_str).value();
return base::MakeRefCounted<device::JSONRequest>(std::move(json_request));
}
class EnclaveManagerTest : public testing::Test, EnclaveManager::Observer {
public:
EnclaveManagerTest()
// `IdentityTestEnvironment` wants to run on an IO thread.
: task_env_(base::test::TaskEnvironment::MainThreadType::IO),
temp_dir_(),
process_and_port_(StartEnclave(temp_dir_.GetPath())),
enclave_override_(TestEnclaveIdentity(process_and_port_.second)),
network_service_(CreateNetwork(&network_context_)),
manager_(temp_dir_.GetPath(),
identity_test_env_.identity_manager(),
network_context_.get(),
url_loader_factory_.GetSafeWeakWrapper()) {
OSCryptMocker::SetUp();
identity_test_env_.MakePrimaryAccountAvailable(
"test@gmail.com", signin::ConsentLevel::kSignin);
gaia_id_ = identity_test_env_.identity_manager()
->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
.gaia;
identity_test_env_.SetAutomaticIssueOfAccessTokens(true);
manager_.AddObserver(this);
}
~EnclaveManagerTest() override {
if (manager_.RunWhenStoppedForTesting(task_env_.QuitClosure())) {
task_env_.RunUntilQuit();
}
CHECK(process_and_port_.first.Terminate(/*exit_code=*/1, /*wait=*/true));
OSCryptMocker::TearDown();
}
protected:
void RunUntilIdle() {
quit_closure_ = task_env_.QuitClosure();
task_env_.RunUntilQuit();
}
base::flat_set<std::string> GaiaAccountsInState() const {
const webauthn_pb::EnclaveLocalState& state =
manager_.local_state_for_testing();
base::flat_set<std::string> ret;
for (const auto& it : state.users()) {
ret.insert(it.first);
}
return ret;
}
void OnEnclaveManagerIdle() override {
if (manager_.is_idle() && quit_closure_.has_value()) {
auto quit_closure = std::move(quit_closure_.value());
quit_closure_.reset();
quit_closure.Run();
}
}
base::test::TaskEnvironment task_env_;
std::optional<base::RepeatingClosure> quit_closure_;
const TempDir temp_dir_;
const std::pair<base::Process, uint16_t> process_and_port_;
const enclave::ScopedEnclaveOverride enclave_override_;
network::TestURLLoaderFactory url_loader_factory_;
mojo::Remote<network::mojom::NetworkContext> network_context_;
std::unique_ptr<network::NetworkService> network_service_;
signin::IdentityTestEnvironment identity_test_env_;
std::string gaia_id_;
EnclaveManager manager_;
};
TEST_F(EnclaveManagerTest, TestInfrastructure) {
// Tests that the enclave starts up.
}
TEST_F(EnclaveManagerTest, Basic) {
ASSERT_FALSE(manager_.is_loaded());
ASSERT_FALSE(manager_.is_registered());
ASSERT_FALSE(manager_.is_ready());
manager_.Start();
ASSERT_FALSE(manager_.is_idle());
RunUntilIdle();
ASSERT_TRUE(manager_.is_idle());
ASSERT_TRUE(manager_.is_loaded());
ASSERT_FALSE(manager_.is_registered());
ASSERT_FALSE(manager_.is_ready());
manager_.RegisterIfNeeded();
ASSERT_FALSE(manager_.is_idle());
RunUntilIdle();
ASSERT_TRUE(manager_.is_idle());
ASSERT_TRUE(manager_.is_loaded());
ASSERT_TRUE(manager_.is_registered());
ASSERT_FALSE(manager_.is_ready());
const int32_t kSecretVersion = 417;
url_loader_factory_.AddResponse(
GetFullJoinSecurityDomainsURLForTesting(
trusted_vault::ExtractTrustedVaultServiceURLFromCommandLine(),
trusted_vault::SecurityDomainId::kPasskeys)
.spec(),
MakeJoinSecurityDomainsResponse(/*current_epoch=*/1).SerializeAsString());
std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end());
manager_.StoreKeys(gaia_id_, {std::move(key)},
/*last_key_version=*/kSecretVersion);
ASSERT_FALSE(manager_.is_idle());
RunUntilIdle();
ASSERT_TRUE(manager_.is_idle());
ASSERT_TRUE(manager_.is_loaded());
ASSERT_TRUE(manager_.is_registered());
ASSERT_TRUE(manager_.is_ready());
{
auto ui_request = std::make_unique<enclave::CredentialRequest>();
ui_request->signing_callback = manager_.HardwareKeySigningCallback();
ui_request->wrapped_secrets = {
*manager_.GetWrappedSecret(/*version=*/kSecretVersion)};
ui_request->entity = GetTestEntity();
enclave::EnclaveAuthenticator authenticator(
std::move(ui_request), /*save_passkey_callback=*/
base::BindRepeating(
[](sync_pb::WebauthnCredentialSpecifics) { NOTREACHED(); }),
network_context_.get());
device::CtapGetAssertionRequest ctap_request("test.com",
R"({"foo": "bar"})");
ctap_request.allow_list.emplace_back(device::PublicKeyCredentialDescriptor(
device::CredentialType::kPublicKey, /*id=*/{1, 2, 3, 4}));
device::CtapGetAssertionOptions ctap_options;
ctap_options.json = JSONFromString(R"({
"allowCredentials": [ ],
"challenge": "CYO8B30gOPIOVFAaU61J7PvoETG_sCZQ38Gzpu",
"rpId": "webauthn.io",
"userVerification": "preferred"
})");
auto quit_closure = task_env_.QuitClosure();
std::optional<device::CtapDeviceResponseCode> status;
std::vector<device::AuthenticatorGetAssertionResponse> responses;
authenticator.GetAssertion(
std::move(ctap_request), std::move(ctap_options),
base::BindLambdaForTesting(
[&quit_closure, &status, &responses](
device::CtapDeviceResponseCode in_status,
std::vector<device::AuthenticatorGetAssertionResponse>
in_responses) {
status = in_status;
responses = std::move(in_responses);
quit_closure.Run();
}));
task_env_.RunUntilQuit();
ASSERT_TRUE(status.has_value());
ASSERT_EQ(status, device::CtapDeviceResponseCode::kSuccess);
ASSERT_EQ(responses.size(), 1u);
}
{
auto ui_request = std::make_unique<enclave::CredentialRequest>();
ui_request->signing_callback = manager_.HardwareKeySigningCallback();
int32_t secret_version;
std::vector<uint8_t> wrapped_secret;
std::tie(secret_version, wrapped_secret) =
manager_.GetCurrentWrappedSecret();
EXPECT_EQ(secret_version, kSecretVersion);
ui_request->wrapped_secrets = {std::move(wrapped_secret)};
ui_request->wrapped_secret_version = kSecretVersion;
std::optional<sync_pb::WebauthnCredentialSpecifics> specifics;
enclave::EnclaveAuthenticator authenticator(
std::move(ui_request), /*save_passkey_callback=*/
base::BindLambdaForTesting(
[&specifics](sync_pb::WebauthnCredentialSpecifics in_specifics) {
specifics.emplace(std::move(in_specifics));
}),
network_context_.get());
std::vector<device::PublicKeyCredentialParams::CredentialInfo>
pub_key_params;
pub_key_params.emplace_back(
device::PublicKeyCredentialParams::CredentialInfo());
device::MakeCredentialOptions ctap_options;
ctap_options.json = JSONFromString(R"({
"attestation": "none",
"authenticatorSelection": {
"residentKey": "preferred",
"userVerification": "preferred"
},
"challenge": "xHyLYEorFsaL6vb",
"extensions": { "credProps": true },
"pubKeyCredParams": [
{ "alg": -7, "type": "public-key" },
{ "alg": -257, "type": "public-key" }
],
"rp": {
"id": "webauthn.io",
"name": "webauthn.io"
},
"user": {
"displayName": "test",
"id": "ZEdWemRB",
"name": "test"
}
})");
auto quit_closure = task_env_.QuitClosure();
std::optional<device::CtapDeviceResponseCode> status;
std::optional<device::AuthenticatorMakeCredentialResponse> response;
authenticator.MakeCredential(
/*request=*/{R"({"foo": "bar"})",
/*rp=*/{"rpid", "rpname"},
/*user=*/{{'u', 'i', 'd'}, "user", "display name"},
device::PublicKeyCredentialParams(
std::move(pub_key_params))},
std::move(ctap_options),
base::BindLambdaForTesting(
[&quit_closure, &status, &response](
device::CtapDeviceResponseCode in_status,
std::optional<device::AuthenticatorMakeCredentialResponse>
in_responses) {
status = in_status;
response = std::move(in_responses);
quit_closure.Run();
}));
task_env_.RunUntilQuit();
ASSERT_TRUE(status.has_value());
ASSERT_EQ(status, device::CtapDeviceResponseCode::kSuccess);
ASSERT_TRUE(response.has_value());
ASSERT_TRUE(specifics.has_value());
EXPECT_EQ(specifics->rp_id(), "rpid");
EXPECT_EQ(specifics->user_id(), "uid");
EXPECT_EQ(specifics->user_name(), "user");
EXPECT_EQ(specifics->user_display_name(), "display name");
EXPECT_EQ(specifics->key_version(), kSecretVersion);
}
}
TEST_F(EnclaveManagerTest, SecretsArriveBeforeRegistrationRequested) {
manager_.Start();
ASSERT_FALSE(manager_.is_registered());
// If secrets are provided before `RegisterIfNeeded` is called, the state
// machine should still trigger registration.
url_loader_factory_.AddResponse(
GetFullJoinSecurityDomainsURLForTesting(
trusted_vault::ExtractTrustedVaultServiceURLFromCommandLine(),
trusted_vault::SecurityDomainId::kPasskeys)
.spec(),
MakeJoinSecurityDomainsResponse(/*current_epoch=*/1).SerializeAsString());
std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end());
manager_.StoreKeys(gaia_id_, {std::move(key)},
/*last_key_version=*/417);
RunUntilIdle();
ASSERT_TRUE(manager_.is_idle());
ASSERT_TRUE(manager_.is_loaded());
ASSERT_TRUE(manager_.is_registered());
ASSERT_TRUE(manager_.is_ready());
}
TEST_F(EnclaveManagerTest, SecretsArriveBeforeRegistrationCompleted) {
manager_.Start();
manager_.RegisterIfNeeded();
ASSERT_FALSE(manager_.is_registered());
// Provide the domain secrets before the registration has completed. The
// system should still end up in the correct state.
url_loader_factory_.AddResponse(
GetFullJoinSecurityDomainsURLForTesting(
trusted_vault::ExtractTrustedVaultServiceURLFromCommandLine(),
trusted_vault::SecurityDomainId::kPasskeys)
.spec(),
MakeJoinSecurityDomainsResponse(/*current_epoch=*/1).SerializeAsString());
std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end());
manager_.StoreKeys(gaia_id_, {std::move(key)},
/*last_key_version=*/417);
RunUntilIdle();
ASSERT_TRUE(manager_.is_idle());
ASSERT_TRUE(manager_.is_loaded());
ASSERT_TRUE(manager_.is_registered());
ASSERT_TRUE(manager_.is_ready());
}
TEST_F(EnclaveManagerTest, RegistrationFailureAndRetry) {
const std::string gaia =
identity_test_env_.identity_manager()
->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
.gaia;
// Override the enclave with port=100, which will cause connection failures.
{
device::enclave::ScopedEnclaveOverride override(
TestEnclaveIdentity(/*port=*/100));
manager_.Start();
manager_.RegisterIfNeeded();
RunUntilIdle();
}
ASSERT_FALSE(manager_.is_registered());
const std::string public_key = manager_.local_state_for_testing()
.users()
.find(gaia)
->second.hardware_public_key();
ASSERT_FALSE(public_key.empty());
manager_.RegisterIfNeeded();
RunUntilIdle();
ASSERT_TRUE(manager_.is_registered());
// The public key should not have changed because re-registration attempts
// must try the same public key again in case they actually worked the first
// time.
ASSERT_TRUE(public_key == manager_.local_state_for_testing()
.users()
.find(gaia)
->second.hardware_public_key());
}
TEST_F(EnclaveManagerTest, PrimaryUserChange) {
const std::string gaia1 =
identity_test_env_.identity_manager()
->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
.gaia;
manager_.Start();
manager_.RegisterIfNeeded();
RunUntilIdle();
ASSERT_TRUE(manager_.is_registered());
EXPECT_THAT(GaiaAccountsInState(), testing::UnorderedElementsAre(gaia1));
identity_test_env_.MakePrimaryAccountAvailable("test2@gmail.com",
signin::ConsentLevel::kSignin);
const std::string gaia2 =
identity_test_env_.identity_manager()
->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
.gaia;
ASSERT_FALSE(manager_.is_registered());
manager_.RegisterIfNeeded();
RunUntilIdle();
ASSERT_TRUE(manager_.is_registered());
EXPECT_THAT(GaiaAccountsInState(),
testing::UnorderedElementsAre(gaia1, gaia2));
// Remove all accounts from the cookie jar. The primary account should be
// retained.
identity_test_env_.SetCookieAccounts({});
EXPECT_THAT(GaiaAccountsInState(), testing::UnorderedElementsAre(gaia2));
// When the primary account changes, the second account should be dropped
// because it was removed from the cookie jar.
identity_test_env_.MakePrimaryAccountAvailable("test3@gmail.com",
signin::ConsentLevel::kSignin);
const std::string gaia3 =
identity_test_env_.identity_manager()
->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
.gaia;
EXPECT_THAT(GaiaAccountsInState(), testing::UnorderedElementsAre(gaia3));
}
} // namespace
#endif // IS_POSIX && !USING_MSAN