blob: 683f0548a67a3f7a1b4cbab4cd7b4e06cd804125 [file] [log] [blame]
// Copyright 2019 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 <sstream>
#include <vector>
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/install_verifier.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/cert_verifier_browser_test.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include "chrome/browser/webauthn/chrome_authenticator_request_delegate.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/scoped_authenticator_environment_for_testing.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "device/fido/cable/cable_discovery_data.h"
#include "device/fido/features.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/virtual_ctap2_device.h"
#include "device/fido/virtual_fido_device.h"
#include "device/fido/virtual_fido_device_factory.h"
#include "extensions/common/extension_builder.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_WIN)
#include "device/fido/win/fake_webauthn_api.h"
#endif // BUILDFLAG(IS_WIN)
namespace {
static constexpr uint8_t kCredentialID[] = {1, 2, 3, 4};
// This file tests WebAuthn features that depend on specific //chrome behaviour.
// Tests that don't depend on that should go into
// content/browser/webauth/webauth_browsertest.cc.
class WebAuthnBrowserTest : public CertVerifierBrowserTest {
public:
WebAuthnBrowserTest() = default;
WebAuthnBrowserTest(const WebAuthnBrowserTest&) = delete;
WebAuthnBrowserTest& operator=(const WebAuthnBrowserTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
CertVerifierBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
void SetUp() override {
ASSERT_TRUE(https_server_.InitializeAndListen());
CertVerifierBrowserTest::SetUp();
}
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.
mock_cert_verifier()->set_default_result(net::OK);
}
protected:
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
};
static constexpr char kGetAssertionCredID1234[] = R"((() => {
let cred_id = new Uint8Array([1,2,3,4]);
return navigator.credentials.get({ publicKey: {
challenge: cred_id,
timeout: 10000,
userVerification: 'discouraged',
allowCredentials: [{type: 'public-key', id: cred_id}],
}}).then(c => 'webauthn: OK',
e => 'error ' + e);
})())";
static constexpr char kMakeCredential[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { name: "" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
}}).then(c => 'webauthn: OK',
e => 'error ' + e);
})())";
static constexpr char kMakeDiscoverableCredential[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { name: "" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
authenticatorSelection: {
requireResidentKey: true,
},
}}).then(c => 'webauthn: OK',
e => 'error ' + e);
})())";
IN_PROC_BROWSER_TEST_F(WebAuthnBrowserTest, ChromeExtensions) {
// Test that WebAuthn works inside of Chrome extensions. WebAuthn is based on
// Relying Party IDs, which are domain names. But Chrome extensions don't have
// domain names therefore the origin is used in their case.
//
// This test creates and installs an extension and then loads an HTML page
// from inside that extension. A WebAuthn call is injected into that context
// and it should get an assertion from a credential that's injected into the
// virtual authenticator, scoped to the origin string.
base::ScopedAllowBlockingForTesting allow_blocking;
extensions::ScopedInstallVerifierBypassForTest install_verifier_bypass;
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
static constexpr char kPageFile[] = "page.html";
base::Value::List resources;
resources.Append(std::string(kPageFile));
static constexpr char kContents[] = R"(
<html>
<head>
<title>WebAuthn in extensions test</title>
</head>
<body>
</body>
</html>
)";
WriteFile(temp_dir.GetPath().AppendASCII(kPageFile), kContents,
sizeof(kContents) - 1);
extensions::ExtensionBuilder builder("test");
builder.SetPath(temp_dir.GetPath())
.SetVersion("1.0")
.SetLocation(extensions::mojom::ManifestLocation::kExternalPolicyDownload)
.SetManifestKey("web_accessible_resources", std::move(resources));
extensions::ExtensionService* service =
extensions::ExtensionSystem::Get(browser()->profile())
->extension_service();
scoped_refptr<const extensions::Extension> extension = builder.Build();
service->OnExtensionInstalled(extension.get(), syncer::StringOrdinal(), 0);
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
const GURL url = extension->GetResourceURL(kPageFile);
auto extension_id = url.host();
virtual_device_factory->mutable_state()->InjectRegistration(
kCredentialID, "chrome-extension://" + extension_id);
content::ScopedAuthenticatorEnvironmentForTesting auth_env(
std::move(virtual_device_factory));
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
kGetAssertionCredID1234));
}
#if BUILDFLAG(IS_WIN)
// Integration test for Large Blob on Windows.
IN_PROC_BROWSER_TEST_F(WebAuthnBrowserTest, WinLargeBlob) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
device::FakeWinWebAuthnApi fake_api;
fake_api.set_version(WEBAUTHN_API_VERSION_3);
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
virtual_device_factory->set_win_webauthn_api(&fake_api);
content::ScopedAuthenticatorEnvironmentForTesting auth_env(
std::move(virtual_device_factory));
constexpr char kMakeCredentialLargeBlob[] = R"(
let cred_id;
const blob = "blobby volley";
navigator.credentials.create({ publicKey: {
challenge: new TextEncoder().encode('climb a mountain'),
rp: { name: 'Acme' },
user: {
id: new TextEncoder().encode('1098237235409872'),
name: 'avery.a.jones@example.com',
displayName: 'Avery A. Jones'},
pubKeyCredParams: [{ type: 'public-key', alg: '-257'}],
authenticatorSelection: {
requireResidentKey: true,
},
extensions: { largeBlob: { support: 'required' } },
}}).then(cred => {
cred_id = cred.rawId;
if (!cred.getClientExtensionResults().largeBlob ||
!cred.getClientExtensionResults().largeBlob.supported) {
throw new Error('large blob not supported');
}
return navigator.credentials.get({ publicKey: {
challenge: new TextEncoder().encode('run a marathon'),
allowCredentials: [{type: 'public-key', id: cred_id}],
extensions: {
largeBlob: {
write: new TextEncoder().encode(blob),
},
},
}});
}).then(assertion => {
if (!assertion.getClientExtensionResults().largeBlob.written) {
throw new Error('large blob not written to');
}
return navigator.credentials.get({ publicKey: {
challenge: new TextEncoder().encode('solve p=np'),
allowCredentials: [{type: 'public-key', id: cred_id}],
extensions: {
largeBlob: {
read: true,
},
},
}});
}).then(assertion => {
if (new TextDecoder().decode(
assertion.getClientExtensionResults().largeBlob.blob) != blob) {
throw new Error('blob does not match');
}
return 'webauthn: OK';
}).catch(error => 'webauthn: ' + error.toString());)";
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
kMakeCredentialLargeBlob));
}
#endif // BUILDFLAG(IS_WIN)
class WebAuthnConditionalUITest : public WebAuthnBrowserTest {
class Observer : public ChromeAuthenticatorRequestDelegate::TestObserver {
public:
enum State {
kHasNotShowedUI,
kWaitingForUI,
kShowedUI,
};
virtual ~Observer() = default;
void WaitForUI() {
if (state_ != kHasNotShowedUI) {
return;
}
state_ = kWaitingForUI;
run_loop_.Run();
}
// ChromeAuthenticatorRequestDelegate::TestObserver:
void Created(ChromeAuthenticatorRequestDelegate* delegate) override {
delegate_ = delegate;
}
std::vector<std::unique_ptr<device::cablev2::Pairing>>
GetCablePairingsFromSyncedDevices() override {
return {};
}
void OnTransportAvailabilityEnumerated(
ChromeAuthenticatorRequestDelegate* delegate,
device::FidoRequestHandlerBase::TransportAvailabilityInfo* tai)
override {}
void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {
if (state_ == kWaitingForUI) {
// When the content layer controls authenticator dispatch, dispatching
// happens on tasks posted right before the UI is shown. We need to
// QuitWhenIdle to make sure that, if an authenticator is dispatched to,
// that task has a chance to finish before the test continues. That way
// we can catch any potentially unexpected authenticator dispatches.
run_loop_.QuitWhenIdle();
}
state_ = kShowedUI;
}
void CableV2ExtensionSeen(
base::span<const uint8_t> server_link_data) override {}
void AccountSelectorShown(
const std::vector<device::AuthenticatorGetAssertionResponse>& responses)
override {
for (const auto& response : responses) {
accounts_.emplace_back(base::HexEncode(response.credential->id));
}
}
raw_ptr<ChromeAuthenticatorRequestDelegate, DanglingUntriaged> delegate_ =
nullptr;
std::vector<std::string> accounts_;
private:
State state_ = kHasNotShowedUI;
base::RunLoop run_loop_;
};
void SetUpOnMainThread() override {
WebAuthnBrowserTest::SetUpOnMainThread();
observer_ = std::make_unique<Observer>();
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
virtual_device_factory_ = virtual_device_factory.get();
virtual_device_factory->mutable_state()->InjectResidentKey(
kCredentialID, "www.example.com", 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));
auth_env_ =
std::make_unique<content::ScopedAuthenticatorEnvironmentForTesting>(
std::move(virtual_device_factory));
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(
observer_.get());
}
void PostRunTestOnMainThread() override {
// To avoid dangling raw_ptr's these values need to be destroyed before
// this test class.
virtual_device_factory_ = nullptr;
auth_env_.reset();
WebAuthnBrowserTest::PostRunTestOnMainThread();
}
protected:
std::unique_ptr<Observer> observer_;
raw_ptr<device::test::VirtualFidoDeviceFactory> virtual_device_factory_;
std::unique_ptr<content::ScopedAuthenticatorEnvironmentForTesting> auth_env_;
};
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));
})())";
// Tests that the "Sign in with another device…" button dispatches requests to
// plugged in authenticators.
IN_PROC_BROWSER_TEST_F(WebAuthnConditionalUITest,
ConditionalUIOtherDeviceButton) {
// Make a Conditional UI request. The authenticator should not be dispatched
// to before the user clicks the "Sign in with another device…" button.
virtual_device_factory_->mutable_state()->simulate_press_callback =
base::BindLambdaForTesting([](device::VirtualFidoDevice* device) {
CHECK(false) << "Virtual device should not have been dispatched to";
return false;
});
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kConditionalUIRequest);
observer_->WaitForUI();
// Allow the virtual device to respond to requests, then simulate clicking the
// "Sign in with another device…" button and wait for a result.
base::RunLoop run_loop;
virtual_device_factory_->mutable_state()->simulate_press_callback =
base::BindLambdaForTesting(
[&](device::VirtualFidoDevice* device) { return true; });
ChromeWebAuthnCredentialsDelegate delegate(web_contents);
delegate.LaunchWebAuthnFlow();
std::string result;
ASSERT_TRUE(message_queue.WaitForMessage(&result));
EXPECT_EQ(result, "\"webauthn: OK\"");
EXPECT_EQ(observer_->accounts_.size(), 1u);
EXPECT_EQ(observer_->accounts_.at(0), "01020304");
}
// WebAuthnCableExtension exercises code paths where a server sends a caBLEv2
// extension in a get() request.
class WebAuthnCableExtension : public WebAuthnBrowserTest {
public:
WebAuthnCableExtension() {
scoped_feature_list_.InitWithFeatures(
{device::kWebAuthCableExtensionAnywhere}, {});
}
protected:
static constexpr char kRequest[] = R"((() => {
return navigator.credentials.get({
publicKey: {
timeout: 1000,
challenge: new Uint8Array([
0x79, 0x50, 0x68, 0x71, 0xDA, 0xEE, 0xEE, 0xB9,
0x94, 0xC3, 0xC2, 0x15, 0x67, 0x65, 0x26, 0x22,
0xE3, 0xF3, 0xAB, 0x3B, 0x78, 0x2E, 0xD5, 0x6F,
0x81, 0x26, 0xE2, 0xA6, 0x01, 0x7D, 0x74, 0x50
]).buffer,
allowCredentials: [{
type: 'public-key',
id: new Uint8Array([1, 2, 3, 4]).buffer,
}],
userVerification: 'discouraged',
extensions: {
"cableAuthentication": [{
version: 2,
sessionPreKey: new Uint8Array([$1]).buffer,
clientEid: new Uint8Array(),
authenticatorEid: new Uint8Array(),
}],
},
},
}).then(c => 'webauthn: OK',
e => 'error ' + e);
})())";
void DoRequest(std::string server_link_data) {
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
auto virtual_device_factory =
std::make_unique<device::test::VirtualFidoDeviceFactory>();
virtual_device_factory->mutable_state()->InjectRegistration(
kCredentialID, "www.example.com");
std::unique_ptr<content::ScopedAuthenticatorEnvironmentForTesting>
auth_env =
std::make_unique<content::ScopedAuthenticatorEnvironmentForTesting>(
std::move(virtual_device_factory));
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(&observer_);
const std::string request =
base::ReplaceStringPlaceholders(kRequest, {server_link_data}, nullptr);
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
request));
}
class ExtensionObserver
: public ChromeAuthenticatorRequestDelegate::TestObserver {
public:
void Created(ChromeAuthenticatorRequestDelegate* delegate) override {}
std::vector<std::unique_ptr<device::cablev2::Pairing>>
GetCablePairingsFromSyncedDevices() override {
return {};
}
void OnTransportAvailabilityEnumerated(
ChromeAuthenticatorRequestDelegate* delegate,
device::FidoRequestHandlerBase::TransportAvailabilityInfo* tai)
override {}
void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {}
void CableV2ExtensionSeen(
base::span<const uint8_t> server_link_data) override {
extensions_.emplace_back(base::HexEncode(server_link_data));
}
std::vector<std::string> extensions_;
};
ExtensionObserver observer_;
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(WebAuthnCableExtension, ServerLink) {
DoRequest("1,2,3,4");
ASSERT_EQ(observer_.extensions_.size(), 1u);
EXPECT_EQ(observer_.extensions_[0], "01020304");
}
// WebAuthnCableSecondFactor primarily exercises
// ChromeAuthenticatorRequestDelegate and AuthenticatorRequestDialogModel. It
// mocks out the discovery process and thus allows the caBLE UI to be tested.
// It uses a trace-based approach: events are recorded (as strings) in an event
// trace which is then compared against the expected trace at the end.
class WebAuthnCableSecondFactor : public WebAuthnBrowserTest {
public:
WebAuthnCableSecondFactor() {
// This makes it a little easier to compare against.
trace_ << std::endl;
}
std::ostringstream& trace() { return trace_; }
AuthenticatorRequestDialogModel*& model() { return model_; }
protected:
// DiscoveryFactory vends a single discovery that doesn't discover anything
// until requested to. The authenticator that is then discovered is a virtual
// authenticator that serves simply to end the overall WebAuthn request.
// Otherwise, DiscoveryFactory is responsible for tracing the caBLEv2 Pairing
// objects and driving the simulation when the UI requests that a phone be
// triggered.
class DiscoveryFactory : public device::FidoDiscoveryFactory {
public:
explicit DiscoveryFactory(WebAuthnCableSecondFactor* test)
: parent_(test) {}
std::vector<std::unique_ptr<device::FidoDiscoveryBase>> Create(
device::FidoTransportProtocol transport) override {
if (transport != device::FidoTransportProtocol::kHybrid) {
return {};
}
auto discovery = std::make_unique<PendingDiscovery>(
device::FidoTransportProtocol::kHybrid);
add_authenticator_callback_ = discovery->GetAddAuthenticatorCallback();
return SingleDiscovery(std::move(discovery));
}
void set_cable_data(
device::FidoRequestType request_type,
std::vector<device::CableDiscoveryData> cable_data,
const absl::optional<std::array<uint8_t, device::cablev2::kQRKeySize>>&
qr_generator_key,
std::vector<std::unique_ptr<device::cablev2::Pairing>> v2_pairings)
override {
for (const auto& pairing : v2_pairings) {
parent_->trace() << "PAIRING: " << pairing->name << " "
<< base::HexEncode(base::span<const uint8_t>(
pairing->peer_public_key_x962)
.subspan(0, 4))
<< " " << base::HexEncode(pairing->id) << std::endl;
}
}
void set_cable_invalidated_pairing_callback(
base::RepeatingCallback<void(size_t)> callback) override {
invalid_pairing_callback_ = std::move(callback);
}
base::RepeatingCallback<void(size_t)> get_cable_contact_callback()
override {
return base::BindLambdaForTesting([this](size_t n) {
parent_->trace() << "CONTACT: phone_instance=" << n
<< " step=" << contact_step_number_ << std::endl;
switch (contact_step_number_) {
case 0:
// Simiulate the first tunnel failing with a Gone status. This
// should trigger a fallback to the second-priority phone with the
// same name.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindLambdaForTesting([this, n]() {
invalid_pairing_callback_.Run(n);
}));
break;
case 1:
// Simulate the user clicking back and trying the phone again. This
// should fallback to the lower-priority phone with the same name.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindLambdaForTesting([this]() {
parent_->model()->ContactPhoneForTesting("name2");
}));
break;
case 2:
// Try some other phones.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindLambdaForTesting([this]() {
parent_->model()->ContactPhoneForTesting("zzz");
}));
break;
case 3:
// Try some other phones.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindLambdaForTesting([this]() {
parent_->model()->ContactPhoneForTesting("aaa");
}));
break;
case 4:
// All done. Discover a virtual authenticator in order to resolve
// the request.
add_authenticator_callback_.Run();
break;
default:
CHECK(false);
}
contact_step_number_++;
});
}
private:
// PendingDiscovery yields a single virtual authenticator when requested to
// do so by calling the result of |GetAddAuthenticatorCallback|.
class PendingDiscovery : public device::FidoDeviceDiscovery,
public base::SupportsWeakPtr<PendingDiscovery> {
public:
explicit PendingDiscovery(device::FidoTransportProtocol transport)
: FidoDeviceDiscovery(transport) {}
base::RepeatingClosure GetAddAuthenticatorCallback() {
return base::BindRepeating(&PendingDiscovery::AddAuthenticator,
AsWeakPtr());
}
protected:
void StartInternal() override {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&PendingDiscovery::NotifyDiscoveryStarted,
AsWeakPtr(), /*success=*/true));
}
private:
void AddAuthenticator() {
scoped_refptr<device::VirtualFidoDevice::State> state(
new device::VirtualFidoDevice::State);
state->InjectRegistration(kCredentialID, "www.example.com");
state->fingerprints_enrolled = true;
device::VirtualCtap2Device::Config config;
config.resident_key_support = true;
config.internal_uv_support = true;
AddDevice(std::make_unique<device::VirtualCtap2Device>(state, config));
}
};
const raw_ptr<WebAuthnCableSecondFactor> parent_;
base::RepeatingCallback<void(size_t)> invalid_pairing_callback_;
base::RepeatingClosure add_authenticator_callback_;
int contact_step_number_ = 0;
};
class DelegateObserver
: public ChromeAuthenticatorRequestDelegate::TestObserver {
public:
explicit DelegateObserver(WebAuthnCableSecondFactor* test)
: parent_(test) {}
void Created(ChromeAuthenticatorRequestDelegate* delegate) override {
// Only a single delegate should be observed.
CHECK(!parent_->model());
}
std::vector<std::unique_ptr<device::cablev2::Pairing>>
GetCablePairingsFromSyncedDevices() override {
std::vector<std::unique_ptr<device::cablev2::Pairing>> ret;
ret.emplace_back(TestPhone("name1", /*public_key=*/0,
/*last_updated=*/base::Time::FromTimeT(1),
/*channel_priority=*/1));
// The same public key as phone1, but a newer timestamp. It
// should shadow the first.
ret.emplace_back(TestPhone("name2", /*public_key=*/0,
/*last_updated=*/base::Time::FromTimeT(2),
/*channel_priority=*/1));
// Same name as the second, but a higher channel priority. It should take
// priority over it.
ret.emplace_back(TestPhone("name2", /*public_key=*/1,
/*last_updated=*/base::Time::FromTimeT(2),
/*channel_priority=*/2));
// Same name as second and third, but a newer timestamp than the third. It
// should be tried first.
ret.emplace_back(TestPhone("name2", /*public_key=*/2,
/*last_updated=*/base::Time::FromTimeT(3),
/*channel_priority=*/2));
// A different device with a name that should sort first.
ret.emplace_back(TestPhone("aaa", /*public_key=*/3,
/*last_updated=*/base::Time::FromTimeT(3),
/*channel_priority=*/2));
// A different device with a name that should sort last.
ret.emplace_back(TestPhone("zzz", /*public_key=*/4,
/*last_updated=*/base::Time::FromTimeT(3),
/*channel_priority=*/2));
return ret;
}
void OnTransportAvailabilityEnumerated(
ChromeAuthenticatorRequestDelegate* delegate,
device::FidoRequestHandlerBase::TransportAvailabilityInfo* tai)
override {
tai->available_transports.insert(device::FidoTransportProtocol::kHybrid);
tai->is_ble_powered = true;
}
void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {
parent_->model() = delegate->dialog_model();
for (const auto& name : parent_->model()->paired_phone_names()) {
parent_->trace() << "UINAME: " << name << std::endl;
}
// Simulate a click on the transport selection sheet.
parent_->model()->ContactPhoneForTesting("name2");
}
void CableV2ExtensionSeen(
base::span<const uint8_t> server_link_data) override {}
void ConfiguringCable(device::FidoRequestType request_type) override {
switch (request_type) {
case device::FidoRequestType::kMakeCredential:
parent_->trace() << "TYPE: mc" << std::endl;
break;
case device::FidoRequestType::kGetAssertion:
parent_->trace() << "TYPE: ga" << std::endl;
break;
}
}
private:
std::unique_ptr<device::cablev2::Pairing> TestPhone(const char* name,
uint8_t public_key,
base::Time last_updated,
int channel_priority) {
auto phone = std::make_unique<device::cablev2::Pairing>();
phone->name = name;
phone->contact_id = {10, 11, 12};
phone->id = {4, 5, 6};
std::fill(phone->peer_public_key_x962.begin(),
phone->peer_public_key_x962.end(), public_key);
phone->last_updated = last_updated;
phone->channel_priority = channel_priority;
return phone;
}
const raw_ptr<WebAuthnCableSecondFactor> parent_;
};
protected:
std::ostringstream trace_;
// This field is not a raw_ptr<> to avoid returning a reference to a temporary
// T* (result of implicitly casting raw_ptr<T> to T*).
RAW_PTR_EXCLUSION AuthenticatorRequestDialogModel* model_ = nullptr;
};
// TODO(https://crbug.com/1219708): this test is flaky on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_Test DISABLED_Test
#else
#define MAYBE_Test Test
#endif
IN_PROC_BROWSER_TEST_F(WebAuthnCableSecondFactor, MAYBE_Test) {
DelegateObserver observer(this);
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(&observer);
content::ScopedAuthenticatorEnvironmentForTesting auth_env(
std::make_unique<DiscoveryFactory>(this));
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
kGetAssertionCredID1234));
constexpr char kExpectedTrace[] = R"(
TYPE: ga
PAIRING: aaa 03030303 040506
PAIRING: name2 02020202 040506
PAIRING: name2 01010101 040506
PAIRING: name2 00000000 040506
PAIRING: zzz 04040404 040506
UINAME: aaa
UINAME: name2
UINAME: zzz
CONTACT: phone_instance=1 step=0
CONTACT: phone_instance=2 step=1
CONTACT: phone_instance=3 step=2
CONTACT: phone_instance=4 step=3
CONTACT: phone_instance=0 step=4
)";
EXPECT_EQ(kExpectedTrace, trace_.str());
}
// These two tests are separate, rather than a for loop, because the testing
// infrastructure needs to be reset for each test and having a separate test
// is the easiest way to do that.
IN_PROC_BROWSER_TEST_F(WebAuthnCableSecondFactor, RequestTypesMakeCredential) {
// Check that the correct request types are plumbed through.
DelegateObserver observer(this);
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(&observer);
content::ScopedAuthenticatorEnvironmentForTesting auth_env(
std::make_unique<DiscoveryFactory>(this));
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
kMakeCredential));
EXPECT_TRUE(trace_.str().find("TYPE: mc\n") != std::string::npos)
<< trace_.str();
}
IN_PROC_BROWSER_TEST_F(WebAuthnCableSecondFactor,
RequestTypesMakeDiscoverableCredential) {
// Check that the correct request types are plumbed through.
DelegateObserver observer(this);
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(&observer);
content::ScopedAuthenticatorEnvironmentForTesting auth_env(
std::make_unique<DiscoveryFactory>(this));
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
EXPECT_EQ(
"webauthn: OK",
content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
kMakeDiscoverableCredential));
EXPECT_TRUE(trace_.str().find("TYPE: mc\n") != std::string::npos)
<< trace_.str();
}
} // namespace