blob: 73882ac0470d4013aa63d09213aa2103bbe20305 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include <vector>
#include "base/feature_list.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_logging_settings.h"
#include "build/build_config.h"
#include "chrome/browser/password_manager/profile_password_store_factory.h"
#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
#include "chrome/browser/ssl/cert_verifier_browser_test.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/ui/autofill/autofill_popup_controller_impl.h"
#include "chrome/browser/ui/autofill/autofill_popup_controller_impl_test_api.h"
#include "chrome/browser/ui/autofill/autofill_suggestion_controller.h"
#include "chrome/browser/ui/autofill/chrome_autofill_client.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/webauthn/chrome_authenticator_request_delegate.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/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/core/browser/suggestions/suggestion_hiding_reason.h"
#include "components/autofill/core/browser/suggestions/suggestion_type.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "components/password_manager/core/browser/password_store/password_store_interface.h"
#include "components/strings/grit/components_strings.h"
#include "components/sync/base/features.h"
#include "components/sync/test/test_sync_service.h"
#include "components/sync_device_info/device_info.h"
#include "components/sync_device_info/fake_device_info_sync_service.h"
#include "components/sync_device_info/fake_device_info_tracker.h"
#include "components/trusted_vault/proto/vault.pb.h"
#include "components/trusted_vault/test/mock_trusted_vault_throttling_connection.h"
#include "components/trusted_vault/trusted_vault_connection.h"
#include "components/webauthn/core/browser/test_passkey_model.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/scoped_authenticator_environment_for_testing.h"
#include "content/public/test/browser_test.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/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/features.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/virtual_ctap2_device.h"
#include "device/fido/virtual_fido_device_factory.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_WIN)
#include "device/fido/win/fake_webauthn_api.h"
#include "device/fido/win/util.h"
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_MAC)
#include "device/fido/mac/util.h"
#endif // BUILDFLAG(IS_MAC)
namespace {
static constexpr char kRpId[] = "example.com";
static constexpr uint8_t kCredentialID1[] = {1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16};
static constexpr uint8_t kCredentialID2[] = {2, 3, 4, 5};
static constexpr char kConditionalUIRequest[] = R"((() => {
window.requestAbortController = new AbortController();
navigator.credentials.get({
signal: window.requestAbortController.signal,
mediation: 'conditional',
publicKey: {
challenge: new Uint8Array([1,2,3,4]),
timeout: 10000,
allowCredentials: [],
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kConditionalUIRequestFiltered[] = R"((() => {
window.requestAbortController = new AbortController();
let cred_id = new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);
navigator.credentials.get({
signal: window.requestAbortController.signal,
mediation: 'conditional',
publicKey: {
challenge: cred_id,
timeout: 10000,
userVerification: 'discouraged',
allowCredentials: [{type: 'public-key', id: cred_id}],
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
// Autofill integration tests. This file contains end-to-end tests for
// integration between WebAuthn and Autofill. These tests are sensitive to focus
// changes, so they are interactive UI tests.
// Base class for autofill integration tests, contains the actual test code but
// no setup.
class WebAuthnAutofillIntegrationTest : public CertVerifierBrowserTest {
public:
class DelegateObserver
: public ChromeAuthenticatorRequestDelegate::TestObserver {
public:
explicit DelegateObserver(WebAuthnAutofillIntegrationTest* test_instance)
: test_instance_(test_instance) {
run_loop_ = std::make_unique<base::RunLoop>();
}
virtual ~DelegateObserver() = default;
void WaitForUI() {
run_loop_->Run();
run_loop_ = std::make_unique<base::RunLoop>();
}
// ChromeAuthenticatorRequestDelegate::TestObserver:
void Created(ChromeAuthenticatorRequestDelegate* delegate) override {
auto connection = std::make_unique<testing::NiceMock<
trusted_vault::MockTrustedVaultThrottlingConnection>>();
ON_CALL(*connection, DownloadAuthenticationFactorsRegistrationState(
testing::_, testing::_, testing::_))
.WillByDefault(
[](const CoreAccountInfo&,
base::OnceCallback<void(
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult)>
callback,
base::RepeatingClosure _) mutable {
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult result;
result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::
State::kEmpty;
std::move(callback).Run(std::move(result));
return std::make_unique<
trusted_vault::TrustedVaultConnection::Request>();
});
GpmTrustedVaultConnectionProvider::SetOverrideForFrame(
delegate->GetRenderFrameHost(), std::move(connection));
}
void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {
run_loop_->QuitWhenIdle();
}
private:
const raw_ptr<WebAuthnAutofillIntegrationTest> test_instance_;
std::unique_ptr<base::RunLoop> run_loop_;
};
WebAuthnAutofillIntegrationTest() = default;
WebAuthnAutofillIntegrationTest(const WebAuthnAutofillIntegrationTest&) =
delete;
WebAuthnAutofillIntegrationTest& operator=(
const WebAuthnAutofillIntegrationTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
CertVerifierBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
void SetUp() override {
ASSERT_TRUE(https_server_.InitializeAndListen());
create_services_subscription_ =
BrowserContextDependencyManager::GetInstance()
->RegisterCreateServicesCallbackForTesting(base::BindRepeating(
&WebAuthnAutofillIntegrationTest::RegisterTestServiceFactories,
base::Unretained(this)));
CertVerifierBrowserTest::SetUp();
// Log call `FIDO_LOG` messages.
scoped_vmodule_.InitWithSwitches("device_event_log_impl=2");
}
void SetUpOnMainThread() override {
CertVerifierBrowserTest::SetUpOnMainThread();
https_server_.ServeFilesFromSourceDirectory(GetChromeTestDataDir());
https_server_.StartAcceptingConnections();
host_resolver()->AddRule("*", "127.0.0.1");
// Allowlist all certs for the HTTPS server.
auto cert = https_server_.GetCertificate();
net::CertVerifyResult verify_result;
verify_result.cert_status = 0;
verify_result.verified_cert = cert;
mock_cert_verifier()->AddResultForCert(cert.get(), verify_result, net::OK);
// Save a credential to the password store. This will let us wait on the
// popup to appear after aborting the request.
password_manager::PasswordStoreInterface* password_store =
ProfilePasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::EXPLICIT_ACCESS)
.get();
password_manager::PasswordForm signin_form;
GURL url = https_server_.GetURL(kRpId, "/");
signin_form.signon_realm = url.spec();
signin_form.url = url;
signin_form.action = url;
signin_form.username_value = u"remilia";
signin_form.password_value = u"shouldbeusingapasskeyinstead";
base::RunLoop run_loop;
password_store->AddLogin(signin_form, run_loop.QuitClosure());
// Mock bluetooth support to allow discovery of fake hybrid devices.
mock_bluetooth_adapter_ =
base::MakeRefCounted<testing::NiceMock<device::MockBluetoothAdapter>>();
ON_CALL(*mock_bluetooth_adapter_, IsPresent)
.WillByDefault(testing::Return(true));
ON_CALL(*mock_bluetooth_adapter_, IsPowered)
.WillByDefault(testing::Return(true));
device::BluetoothAdapterFactory::SetAdapterForTesting(
mock_bluetooth_adapter_);
// Other parts of Chrome may keep a reference to the bluetooth adapter.
// Since we do not verify any expectations, it is okay to leak this mock.
testing::Mock::AllowLeak(mock_bluetooth_adapter_.get());
identity_test_env_adaptor_ =
std::make_unique<IdentityTestEnvironmentProfileAdaptor>(
browser()->profile());
identity_test_env_adaptor_->identity_test_env()->SetPrimaryAccount(
"test@gmail.com", signin::ConsentLevel::kSync);
delegate_observer_ = std::make_unique<DelegateObserver>(this);
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(
delegate_observer_.get());
fake_hw_provider_ =
std::make_unique<crypto::ScopedFakeUnexportableKeyProvider>();
fake_uv_provider_ =
std::make_unique<crypto::ScopedFakeUserVerifyingKeyProvider>();
#if BUILDFLAG(IS_MAC)
biometrics_override_.reset();
biometrics_override_ =
std::make_unique<device::fido::mac::ScopedBiometricsOverride>(true);
#elif BUILDFLAG(IS_WIN)
biometrics_override_.reset();
biometrics_override_ =
std::make_unique<device::fido::win::ScopedBiometricsOverride>(true);
#endif
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(),
https_server_.GetURL(kRpId, "/webauthn_conditional_mediation.html")));
}
void RegisterTestServiceFactories(content::BrowserContext* context) {
IdentityTestEnvironmentProfileAdaptor::
SetIdentityTestEnvironmentFactoriesOnBrowserContext(context);
PasskeyModelFactory::GetInstance()->SetTestingFactory(
context,
base::BindRepeating(
[](content::BrowserContext*) -> std::unique_ptr<KeyedService> {
return std::make_unique<webauthn::TestPasskeyModel>();
}));
// Disable the sync service by injecting a test fake. The sync service fails
// to start when overriding the DeviceInfoSyncService with a test fake.
SyncServiceFactory::GetInstance()->SetTestingFactory(
context,
base::BindRepeating(
[](content::BrowserContext*) -> std::unique_ptr<KeyedService> {
return std::make_unique<syncer::TestSyncService>();
}));
}
void RunSelectAccountTest(const char* request) {
// Make sure input events cannot close the autofill popup.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
autofill::ChromeAutofillClient* autofill_client =
autofill::ChromeAutofillClient::FromWebContentsForTesting(web_contents);
autofill_client->SetKeepPopupOpenForTesting(true);
// Execute the Conditional UI request.
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, request);
delegate_observer_->WaitForUI();
// Interact with the username field until the popup shows up. This has the
// effect of waiting for the browser to send the renderer the password
// information, and waiting for the UI to render.
base::WeakPtr<autofill::AutofillSuggestionController> suggestion_controller;
while (!suggestion_controller) {
content::SimulateMouseClickOrTapElementWithId(web_contents, "username");
suggestion_controller =
autofill_client->suggestion_controller_for_testing();
}
// Find the webauthn credential on the suggestions list.
std::vector<autofill::Suggestion> suggestions =
suggestion_controller->GetSuggestions();
auto it = std::ranges::find(suggestions,
autofill::SuggestionType::kWebauthnCredential,
&autofill::Suggestion::type);
ASSERT_EQ(std::ranges::count(suggestions,
autofill::SuggestionType::kWebauthnCredential,
&autofill::Suggestion::type),
1u);
ASSERT_NE(it, suggestions.end()) << "WebAuthn entry not found";
EXPECT_EQ(it->main_text.value, u"flandre");
EXPECT_EQ(it->labels.at(0).at(0).value, GetDeviceString());
EXPECT_EQ(it->icon, autofill::Suggestion::Icon::kGlobe);
// Click the credential.
test_api(static_cast<autofill::AutofillPopupControllerImpl&>(
*suggestion_controller))
.DisableThreshold(true);
suggestion_controller->AcceptSuggestion(
it - suggestions.begin(),
autofill::AutofillMetrics::SuggestionAcceptedMethod::kMouse);
std::string result;
ASSERT_TRUE(message_queue.WaitForMessage(&result));
EXPECT_EQ(result, "\"webauthn: OK\"");
}
void RunAbortTest() {
// Make sure input events cannot close the autofill popup.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
autofill::ChromeAutofillClient* autofill_client =
autofill::ChromeAutofillClient::FromWebContentsForTesting(web_contents);
autofill_client->SetKeepPopupOpenForTesting(true);
// Execute the Conditional UI request.
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kConditionalUIRequest);
delegate_observer_->WaitForUI();
// Interact with the username field until the popup shows up. This has the
// effect of waiting for the browser to send the renderer the password
// information, and waiting for the UI to render.
base::WeakPtr<autofill::AutofillSuggestionController> suggestion_controller;
while (!suggestion_controller) {
content::SimulateMouseClickOrTapElementWithId(web_contents, "username");
suggestion_controller =
autofill_client->suggestion_controller_for_testing();
}
// Find the webauthn credential on the suggestions list.
std::vector<autofill::Suggestion> suggestions =
suggestion_controller->GetSuggestions();
auto it = std::ranges::find(suggestions,
autofill::SuggestionType::kWebauthnCredential,
&autofill::Suggestion::type);
ASSERT_NE(it, suggestions.end()) << "WebAuthn entry not found";
EXPECT_EQ(it->main_text.value, u"flandre");
EXPECT_EQ(it->labels.at(0).at(0).value, GetDeviceString());
EXPECT_EQ(it->icon, autofill::Suggestion::Icon::kGlobe);
// Abort the request.
content::ExecuteScriptAsync(web_contents,
"window.requestAbortController.abort()");
std::string result;
ASSERT_TRUE(message_queue.WaitForMessage(&result));
EXPECT_EQ(result, "\"error AbortError: signal is aborted without reason\"");
// The popup may have gone away while waiting. If not, make sure it's gone.
if (suggestion_controller) {
suggestion_controller->Hide(
autofill::SuggestionHidingReason::kUserAborted);
}
// Interact with the username field. Since there is still a saved password,
// the popup should eventually show up.
while (!suggestion_controller) {
content::SimulateMouseClickOrTapElementWithId(web_contents, "username");
suggestion_controller =
autofill_client->suggestion_controller_for_testing();
}
for (const auto& suggestion : suggestion_controller->GetSuggestions()) {
EXPECT_NE(suggestion.type, autofill::SuggestionType::kWebauthnCredential);
EXPECT_NE(suggestion.type,
autofill::SuggestionType::kWebauthnSignInWithAnotherDevice);
}
}
virtual std::u16string GetDeviceString() = 0;
std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
identity_test_env_adaptor_;
scoped_refptr<device::MockBluetoothAdapter> mock_bluetooth_adapter_ = nullptr;
base::CallbackListSubscription create_services_subscription_;
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
device::FidoRequestHandlerBase::ScopedAlwaysAllowBLECalls always_allow_ble_;
std::unique_ptr<DelegateObserver> delegate_observer_;
base::test::ScopedFeatureList scoped_feature_list_;
logging::ScopedVmoduleSwitches scoped_vmodule_;
std::unique_ptr<crypto::ScopedFakeUnexportableKeyProvider> fake_hw_provider_;
std::unique_ptr<crypto::ScopedFakeUserVerifyingKeyProvider> fake_uv_provider_;
#if BUILDFLAG(IS_WIN)
std::unique_ptr<device::fido::win::ScopedBiometricsOverride>
biometrics_override_;
#elif BUILDFLAG(IS_MAC)
std::unique_ptr<device::fido::mac::ScopedBiometricsOverride>
biometrics_override_;
#endif
};
// Autofill integration test using the devtools virtual environment.
class WebAuthnDevtoolsAutofillIntegrationTest
: public WebAuthnAutofillIntegrationTest {
public:
void SetUpOnMainThread() override {
WebAuthnAutofillIntegrationTest::SetUpOnMainThread();
// Set up a fake virtual device.
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
virtual_device_factory->SetTransport(
device::FidoTransportProtocol::kInternal);
virtual_device_factory_ = virtual_device_factory.get();
virtual_device_factory->mutable_state()->InjectResidentKey(
kCredentialID1, kRpId, std::vector<uint8_t>{5, 6, 7, 8}, "flandre",
"Flandre Scarlet");
virtual_device_factory->mutable_state()->fingerprints_enrolled = true;
device::VirtualCtap2Device::Config config;
config.resident_key_support = true;
config.internal_uv_support = true;
virtual_device_factory->SetCtap2Config(std::move(config));
scoped_auth_env_ =
std::make_unique<content::ScopedAuthenticatorEnvironmentForTesting>(
std::move(virtual_device_factory));
}
void PostRunTestOnMainThread() override {
// To avoid dangling raw_ptr's, these objects need to be destroyed before
// the test class.
virtual_device_factory_ = nullptr;
scoped_auth_env_.reset();
WebAuthnAutofillIntegrationTest::PostRunTestOnMainThread();
}
std::u16string GetDeviceString() override {
return l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_USE_GENERIC_DEVICE);
}
protected:
std::unique_ptr<content::ScopedAuthenticatorEnvironmentForTesting>
scoped_auth_env_;
raw_ptr<device::test::VirtualFidoDeviceFactory> virtual_device_factory_;
};
IN_PROC_BROWSER_TEST_F(WebAuthnDevtoolsAutofillIntegrationTest, SelectAccount) {
RunSelectAccountTest(kConditionalUIRequest);
}
IN_PROC_BROWSER_TEST_F(WebAuthnDevtoolsAutofillIntegrationTest, Abort) {
RunAbortTest();
}
IN_PROC_BROWSER_TEST_F(WebAuthnDevtoolsAutofillIntegrationTest,
SelectAccountWithAllowCredentials) {
RunSelectAccountTest(kConditionalUIRequestFiltered);
}
IN_PROC_BROWSER_TEST_F(WebAuthnDevtoolsAutofillIntegrationTest,
SelectAccountWithAllowCredentialsFiltered) {
virtual_device_factory_->mutable_state()->InjectResidentKey(
kCredentialID2, kRpId, std::vector<uint8_t>{6, 7, 8, 9}, "sakuya",
"Sakuya Izayoi");
RunSelectAccountTest(kConditionalUIRequestFiltered);
}
#if BUILDFLAG(IS_WIN)
// Autofill integration test using the Windows fake API.
class WebAuthnWindowsAutofillIntegrationTest
: public WebAuthnAutofillIntegrationTest {
public:
void SetUpOnMainThread() override {
WebAuthnAutofillIntegrationTest::SetUpOnMainThread();
// Set up the fake Windows platform authenticator.
fake_webauthn_api_ = std::make_unique<device::FakeWinWebAuthnApi>();
fake_webauthn_api_->set_version(WEBAUTHN_API_VERSION_4);
fake_webauthn_api_->set_is_uvpaa(true);
fake_webauthn_api_->set_supports_silent_discovery(true);
device::PublicKeyCredentialUserEntity user({1, 2, 3, 4}, "flandre",
"Flandre Scarlet");
device::PublicKeyCredentialRpEntity rp(kRpId);
fake_webauthn_api_->InjectDiscoverableCredential(
kCredentialID1, std::move(rp), std::move(user),
/*provider_name=*/std::nullopt);
win_webauthn_api_override_ =
std::make_unique<device::WinWebAuthnApi::ScopedOverride>(
fake_webauthn_api_.get());
}
void PostRunTestOnMainThread() override {
// To avoid dangling raw_ptr's, these objects need to be destroyed before
// the test class.
win_webauthn_api_override_.reset();
WebAuthnAutofillIntegrationTest::PostRunTestOnMainThread();
}
std::u16string GetDeviceString() override {
return l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_PASSKEY_FROM_WINDOWS_HELLO);
}
protected:
std::unique_ptr<device::FakeWinWebAuthnApi> fake_webauthn_api_;
std::unique_ptr<device::WinWebAuthnApi::ScopedOverride>
win_webauthn_api_override_;
};
IN_PROC_BROWSER_TEST_F(WebAuthnWindowsAutofillIntegrationTest, SelectAccount) {
RunSelectAccountTest(kConditionalUIRequest);
}
IN_PROC_BROWSER_TEST_F(WebAuthnWindowsAutofillIntegrationTest,
SelectAccountWithAllowCredentials) {
RunSelectAccountTest(kConditionalUIRequestFiltered);
}
IN_PROC_BROWSER_TEST_F(WebAuthnWindowsAutofillIntegrationTest,
SelectAccountWithAllowCredentialsFiltered) {
device::PublicKeyCredentialUserEntity user({6, 7, 8, 9}, "sakuya",
"Sakuya Izayoi");
device::PublicKeyCredentialRpEntity rp(kRpId);
fake_webauthn_api_->InjectDiscoverableCredential(
kCredentialID2, std::move(rp), std::move(user),
/*provider_name=*/std::nullopt);
RunSelectAccountTest(kConditionalUIRequestFiltered);
}
IN_PROC_BROWSER_TEST_F(WebAuthnWindowsAutofillIntegrationTest, Abort) {
RunAbortTest();
}
#endif // BUILDFLAG(IS_WIN)
} // namespace