| // Copyright 2025 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_authenticator_browsertest_base.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/check_deref.h" |
| #include "base/command_line.h" |
| #include "base/containers/to_vector.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/test/bind.h" |
| #include "base/test/test_mock_time_task_runner.h" |
| #include "build/buildflag.h" |
| #include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" |
| #include "chrome/browser/sync/sync_service_factory.h" |
| #include "chrome/browser/sync/test/integration/sync_service_impl_harness.h" |
| #include "chrome/browser/sync/test/integration/sync_test.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/webauthn/enclave_keys_waiter.h" |
| #include "chrome/browser/webauthn/enclave_manager.h" |
| #include "chrome/browser/webauthn/enclave_manager_factory.h" |
| #include "chrome/browser/webauthn/fake_recovery_key_store.h" |
| #include "chrome/browser/webauthn/fake_security_domain_service.h" |
| #include "chrome/browser/webauthn/gpm_enclave_controller.h" |
| #include "chrome/browser/webauthn/passkey_model_factory.h" |
| #include "chrome/browser/webauthn/test_util.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/keyed_service/content/browser_context_dependency_manager.h" |
| #include "components/network_session_configurator/common/network_switches.h" |
| #include "components/signin/public/identity_manager/identity_test_environment.h" |
| #include "components/sync/service/sync_service.h" |
| #include "components/sync/service/sync_service_impl.h" |
| #include "components/sync/test/fake_server.h" |
| #include "components/trusted_vault/test/mock_trusted_vault_throttling_connection.h" |
| #include "components/trusted_vault/trusted_vault_connection.h" |
| #include "components/trusted_vault/trusted_vault_server_constants.h" |
| #include "components/webauthn/core/browser/passkey_model.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "crypto/scoped_fake_unexportable_key_provider.h" |
| #include "crypto/scoped_fake_user_verifying_key_provider.h" |
| #include "device/bluetooth/bluetooth_adapter_factory.h" |
| #include "device/fido/enclave/enclave_protocol_utils.h" |
| #include "enclave_authenticator_browsertest_base.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/http/http_status_code.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "chrome/browser/webauthn/chrome_authenticator_request_delegate_mac.h" |
| #include "device/fido/mac/fake_icloud_keychain.h" |
| #include "device/fido/mac/util.h" |
| #endif // BUILDFLAG(IS_MAC) |
| |
| namespace { |
| |
| static constexpr char kIsUVPAA[] = R"((() => { |
| window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(). |
| then(result => window.domAutomationController.send('IsUVPAA: ' + result), |
| error => window.domAutomationController.send('error ' + error)); |
| })())"; |
| |
| } // namespace |
| |
| // Helper struct for managing a temporary directory. |
| struct TempDir { |
| public: |
| TempDir() { CHECK(dir_.CreateUniqueTempDir()); } |
| base::FilePath GetPath() const { return dir_.GetPath(); } |
| |
| private: |
| base::ScopedTempDir dir_; |
| }; |
| |
| EnclaveAuthenticatorTestBase::EnclaveAuthenticatorTestBase() |
| : SyncTest(SINGLE_CLIENT), |
| timer_task_runner_(base::MakeRefCounted<base::TestMockTimeTaskRunner>()), |
| temp_dir_(std::make_unique<TempDir>()), |
| process_and_port_(StartWebAuthnEnclave(temp_dir_->GetPath())), |
| enclave_override_(TestWebAuthnEnclaveIdentity(process_and_port_.second)), |
| security_domain_service_(FakeSecurityDomainService::New(kSecretVersion)), |
| #if BUILDFLAG(IS_WIN) |
| fake_webauthn_dll_(std::make_unique<device::FakeWinWebAuthnApi>()), |
| webauthn_dll_override_( |
| std::make_unique<device::WinWebAuthnApi::ScopedOverride>( |
| fake_webauthn_dll_.get())), |
| #endif |
| recovery_key_store_(FakeRecoveryKeyStore::New()), |
| fake_hw_provider_( |
| std::make_unique<WebAuthnScopedFakeUnexportableKeyProvider>()) { |
| #if BUILDFLAG(IS_WIN) |
| fake_webauthn_dll_->set_available(false); |
| biometrics_override_ = |
| std::make_unique<device::fido::win::ScopedBiometricsOverride>(false); |
| #elif BUILDFLAG(IS_MAC) |
| biometrics_override_ = |
| std::make_unique<device::fido::mac::ScopedBiometricsOverride>(false); |
| if (__builtin_available(macOS 13.5, *)) { |
| fake_icloud_keychain_ = device::fido::icloud_keychain::NewFake(); |
| } |
| scoped_icloud_keychain_override_ = |
| OverrideICloudKeychainConfiguredForPasskeys(false); |
| #endif |
| scoped_vmodule_.InitWithSwitches("device_event_log_impl=2"); |
| |
| auto security_domain_service_callback = |
| security_domain_service_->GetCallback(); |
| auto recovery_key_store_callback = recovery_key_store_->GetCallback(); |
| url_loader_factory_.SetInterceptor(base::BindLambdaForTesting( |
| [sds_callback = std::move(security_domain_service_callback), |
| rks_callback = std::move(recovery_key_store_callback), |
| this](const network::ResourceRequest& request) { |
| std::optional<std::pair<net::HttpStatusCode, std::string>> response = |
| sds_callback.Run(request); |
| if (!response) { |
| response = rks_callback.Run(request); |
| } |
| if (response) { |
| url_loader_factory_.AddResponse( |
| request.url.spec(), std::move(response->second), response->first); |
| } |
| })); |
| |
| fake_uv_provider_.emplace<crypto::ScopedNullUserVerifyingKeyProvider>(); |
| |
| bluetooth_values_for_testing_ = |
| device::BluetoothAdapterFactory::Get()->InitGlobalOverrideValues(); |
| bluetooth_values_for_testing_->SetLESupported(false); |
| } |
| |
| EnclaveAuthenticatorTestBase::~EnclaveAuthenticatorTestBase() { |
| EnclaveManagerFactory::SetUrlLoaderFactoryForTesting(nullptr); |
| CHECK(process_and_port_.first.Terminate(/*exit_code=*/1, /*wait=*/true)); |
| } |
| |
| base::FilePath EnclaveAuthenticatorTestBase::GetTempDirPath() { |
| return temp_dir_->GetPath(); |
| } |
| |
| void EnclaveAuthenticatorTestBase::SetUpCommandLine( |
| base::CommandLine* command_line) { |
| SyncTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitch(switches::kIgnoreCertificateErrors); |
| command_line->AppendSwitch(switches::kDisableFakeServerFailureOutput); |
| } |
| |
| void EnclaveAuthenticatorTestBase::SetUp() { |
| ASSERT_TRUE(https_server_.InitializeAndListen()); |
| EnclaveManagerFactory::SetUrlLoaderFactoryForTesting( |
| url_loader_factory_.GetSafeWeakWrapper().get()); |
| SyncTest::SetUp(); |
| } |
| |
| void EnclaveAuthenticatorTestBase::SetUpInProcessBrowserTestFixture() { |
| SyncTest::SetUpInProcessBrowserTestFixture(); |
| subscription_ = |
| BrowserContextDependencyManager::GetInstance() |
| ->RegisterCreateServicesCallbackForTesting( |
| base::BindRepeating([](content::BrowserContext* context) { |
| IdentityTestEnvironmentProfileAdaptor:: |
| SetIdentityTestEnvironmentFactoriesOnBrowserContext( |
| context); |
| })); |
| } |
| |
| void EnclaveAuthenticatorTestBase::SetUpOnMainThread() { |
| SyncTest::SetUpOnMainThread(); |
| |
| identity_test_env_adaptor_ = |
| std::make_unique<IdentityTestEnvironmentProfileAdaptor>( |
| browser()->GetProfile()); |
| identity_test_env().SetAutomaticIssueOfAccessTokens(true); |
| |
| sync_harness_ = SyncServiceImplHarness::Create( |
| browser()->GetProfile(), SyncServiceImplHarness::SigninType::FAKE_SIGNIN); |
| if (sync_feature_enabled_) { |
| ASSERT_TRUE(sync_harness_->SetupSync()); |
| } else { |
| ASSERT_TRUE(sync_harness_->SignInNoWaitForCompletion()); |
| } |
| syncer::SyncServiceImpl* sync_service = |
| SyncServiceFactory::GetAsSyncServiceImplForProfileForTesting( |
| browser()->GetProfile()); |
| ASSERT_EQ(kSyncEmail, sync_service->GetAccountInfo().email); |
| sync_service->GetUserSettings()->SetSelectedTypes( |
| /*sync_everything=*/false, |
| /*types=*/{syncer::UserSelectableType::kPasswords}); |
| |
| https_server_.ServeFilesFromSourceDirectory(GetChromeTestDataDir()); |
| https_server_.StartAcceptingConnections(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| } |
| |
| void EnclaveAuthenticatorTestBase::TearDownOnMainThread() { |
| identity_test_env_adaptor_.reset(); |
| SyncTest::TearDownOnMainThread(); |
| } |
| |
| signin::IdentityTestEnvironment& |
| EnclaveAuthenticatorTestBase::identity_test_env() { |
| return CHECK_DEREF(identity_test_env_adaptor_->identity_test_env()); |
| } |
| |
| webauthn::PasskeyModel& EnclaveAuthenticatorTestBase::passkey_model() { |
| return CHECK_DEREF(PasskeyModelFactory::GetInstance()->GetForProfile( |
| browser()->GetProfile())); |
| } |
| |
| EnclaveManager& EnclaveAuthenticatorTestBase::enclave_manager() { |
| return CHECK_DEREF(EnclaveManagerFactory::GetAsEnclaveManagerForProfile( |
| browser()->GetProfile())); |
| } |
| |
| void EnclaveAuthenticatorTestBase::EnableUVKeySupport( |
| bool fake_hardware_backing) { |
| fake_uv_provider_.emplace<crypto::ScopedFakeUserVerifyingKeyProvider>( |
| fake_hardware_backing); |
| } |
| |
| void EnclaveAuthenticatorTestBase::OverrideUVKeyAvailability(bool available) { |
| uvkey_override_ = |
| std::make_unique<crypto::ScopedUserVerifyingKeysSupportedOverride>( |
| base::BindLambdaForTesting( |
| [available]( |
| crypto::UserVerifyingKeysSupportedCallback uv_callback) { |
| std::move(uv_callback).Run(available); |
| })); |
| } |
| |
| bool EnclaveAuthenticatorTestBase::IsUVPAA() { |
| content::WebContents* web_contents = |
| browser()->GetTabStripModel()->GetActiveWebContents(); |
| content::DOMMessageQueue message_queue(web_contents); |
| content::ExecuteScriptAsync(web_contents, kIsUVPAA); |
| |
| std::string script_result; |
| CHECK(message_queue.WaitForMessage(&script_result)); |
| if (script_result == "\"IsUVPAA: true\"") { |
| return true; |
| } else if (script_result == "\"IsUVPAA: false\"") { |
| return false; |
| } |
| NOTREACHED() << "unexpected IsUVPAA result: " << script_result; |
| } |
| |
| void EnclaveAuthenticatorTestBase::SetBiometricsEnabled(bool enabled) { |
| #if BUILDFLAG(IS_MAC) |
| biometrics_override_.reset(); |
| biometrics_override_ = |
| std::make_unique<device::fido::mac::ScopedBiometricsOverride>(enabled); |
| #elif BUILDFLAG(IS_WIN) |
| biometrics_override_.reset(); |
| biometrics_override_ = |
| std::make_unique<device::fido::win::ScopedBiometricsOverride>(enabled); |
| #endif |
| } |
| |
| void EnclaveAuthenticatorTestBase::AddTestPasskeyToModel() { |
| sync_pb::WebauthnCredentialSpecifics passkey; |
| CHECK(passkey.ParseFromArray(kTestProtobuf, sizeof(kTestProtobuf))); |
| passkey_model().AddNewPasskeyForTesting(passkey); |
| } |
| |
| void EnclaveAuthenticatorTestBase::SimulateTrustedVaultKeyRetrieval( |
| base::span<const uint8_t> trusted_vault_key, |
| int trusted_vault_key_version, |
| bool with_store_keys_lock) { |
| if (with_store_keys_lock) { |
| auto store_keys_lock = enclave_manager().GetStoreKeysLock(); |
| enclave_manager().StoreKeys( |
| kSyncGaiaId, |
| {trusted_vault::TrustedVaultKeyAndVersion( |
| base::ToVector(trusted_vault_key), trusted_vault_key_version)}, |
| std::nullopt); |
| } else { |
| enclave_manager().StoreKeys( |
| kSyncGaiaId, |
| {trusted_vault::TrustedVaultKeyAndVersion( |
| base::ToVector(trusted_vault_key), trusted_vault_key_version)}, |
| std::nullopt); |
| } |
| } |
| |
| void EnclaveAuthenticatorTestBase::SimulateTrustedVaultKeyRetrieval( |
| bool with_store_keys_lock) { |
| SimulateTrustedVaultKeyRetrieval(kSecurityDomainSecret, kSecretVersion, |
| with_store_keys_lock); |
| } |
| |
| void EnclaveAuthenticatorTestBase:: |
| SimulateOpportunisticTrustedVaultKeyRetrieval() { |
| EnclaveKeysWaiter enclave_keys_waiter(&enclave_manager()); |
| // Performing key retrieval without acquiring a lock via |
| // `EnclaveManager::GetStoreKeysLock()`. The absence of acquired lock |
| // indicates an opportunistic key retrieval logic. In this case (if either a |
| // system UV or a usable GPM PIN is present) Enclave Manager stores keys and |
| // adds device to account. |
| SimulateTrustedVaultKeyRetrieval(/*with_store_keys_lock=*/false); |
| EXPECT_EQ(enclave_keys_waiter.Wait(), |
| EnclaveManager::OutOfContextRecoveryOutcome:: |
| kStoreKeysFromOpportunisticFlowSucceeded); |
| } |
| |
| void EnclaveAuthenticatorTestBase::SetMockVaultConnectionOnRequestDelegate( |
| AuthenticationFactorsResult result, |
| content::RenderFrameHost* rfh) { |
| auto connection = std::make_unique< |
| testing::NiceMock<trusted_vault::MockTrustedVaultThrottlingConnection>>(); |
| EXPECT_CALL(*connection, |
| DownloadAuthenticationFactorsRegistrationState( |
| testing::_, trusted_vault::SecurityDomainId::kPasskeys, |
| testing::_, testing::_)) |
| .WillOnce( |
| [result = std::move(result)]( |
| const CoreAccountInfo&, trusted_vault::SecurityDomainId, |
| base::OnceCallback<void(AuthenticationFactorsResult)> callback, |
| base::RepeatingClosure _) mutable { |
| std::move(callback).Run(std::move(result)); |
| return std::make_unique< |
| trusted_vault::TrustedVaultConnection::Request>(); |
| }); |
| if (rfh == nullptr) { |
| rfh = browser() |
| ->GetTabStripModel() |
| ->GetActiveWebContents() |
| ->GetPrimaryMainFrame(); |
| } |
| GpmTrustedVaultConnectionProvider::SetOverrideForFrame(rfh, |
| std::move(connection)); |
| } |
| |
| void EnclaveAuthenticatorTestBase::SetTrustedVaultEmpty() { |
| AuthenticationFactorsResult registration_state_result; |
| registration_state_result.state = AuthenticationFactorsResult::State::kEmpty; |
| SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result)); |
| } |
| |
| void EnclaveAuthenticatorTestBase::SetTrustedVaultRecoverable( |
| int32_t key_version, |
| content::RenderFrameHost* rfh) { |
| AuthenticationFactorsResult registration_state_result; |
| registration_state_result.state = |
| AuthenticationFactorsResult::State::kRecoverable; |
| registration_state_result.key_version = key_version; |
| SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result), |
| rfh); |
| security_domain_service_->pretend_there_are_members(); |
| } |
| |
| void EnclaveAuthenticatorTestBase::SetTrustedVaultSlowAndCacheCallback() { |
| auto connection_callback = [this](const CoreAccountInfo&, |
| trusted_vault::SecurityDomainId, |
| base::OnceCallback<void( |
| AuthenticationFactorsResult)> callback, |
| base::RepeatingClosure) { |
| cached_connection_cb_ = std::move(callback); |
| return std::make_unique<trusted_vault::TrustedVaultConnection::Request>(); |
| }; |
| auto connection = std::make_unique< |
| testing::NiceMock<trusted_vault::MockTrustedVaultThrottlingConnection>>(); |
| EXPECT_CALL(*connection, |
| DownloadAuthenticationFactorsRegistrationState( |
| testing::_, trusted_vault::SecurityDomainId::kPasskeys, |
| testing::_, testing::_)) |
| .WillOnce(connection_callback); |
| GpmTrustedVaultConnectionProvider::SetOverrideForFrame( |
| browser() |
| ->GetTabStripModel() |
| ->GetActiveWebContents() |
| ->GetPrimaryMainFrame(), |
| std::move(connection)); |
| } |
| |
| void EnclaveAuthenticatorTestBase::SimulateSuccessfulGpmPinCreation( |
| const std::string& pin_value) { |
| WaitForEnclaveLoaded(); |
| |
| SimulateTrustedVaultKeyRetrieval(kSecurityDomainSecret, |
| /*trusted_vault_key_version=*/0, |
| /*with_store_keys_lock=*/true); |
| |
| base::test::TestFuture<bool> add_device_future; |
| enclave_manager().AddDeviceAndPINToAccount( |
| "123456", |
| /*previous_pin_public_key=*/std::nullopt, |
| add_device_future.GetCallback()); |
| ASSERT_TRUE(add_device_future.Wait()) << "AddDeviceAndPINToAccount timed out"; |
| ASSERT_TRUE(add_device_future.Get()) << "AddDeviceAndPINToAccount failed"; |
| |
| ASSERT_TRUE(enclave_manager().IsReady()); |
| ASSERT_TRUE(enclave_manager().has_wrapped_pin()); |
| } |
| |
| void EnclaveAuthenticatorTestBase::WaitForEnclaveLoaded() { |
| if (!enclave_manager().IsLoaded()) { |
| base::test::TestFuture<void> load_future; |
| enclave_manager().Load(load_future.GetCallback()); |
| ASSERT_TRUE(load_future.Wait()); |
| } |
| } |