blob: 925b7f0091d1cc74cf87573007cddcd5e627b23a [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 <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <variant>
#include <vector>
#include "base/base64.h"
#include "base/base64url.h"
#include "base/callback_list.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/notreached.h"
#include "base/process/process.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/condition_variable.h"
#include "base/synchronization/lock.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_logging_settings.h"
#include "base/test/simple_test_clock.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/test/test_future.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/thread_annotations.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/password_manager/account_password_store_factory.h"
#include "chrome/browser/password_manager/profile_password_store_factory.h"
#include "chrome/browser/signin/identity_manager_factory.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.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_controller.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include "chrome/browser/webauthn/chrome_authenticator_request_delegate.h"
#include "chrome/browser/webauthn/enclave_manager.h"
#include "chrome/browser/webauthn/enclave_manager_factory.h"
#include "chrome/browser/webauthn/fake_magic_arch.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/browser/webauthn/webauthn_pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/in_process_browser_test.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/os_crypt/sync/os_crypt_mocker.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_store/password_store_interface.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "components/sync/base/user_selectable_type.h"
#include "components/sync/protocol/webauthn_credential_specifics.pb.h"
#include "components/sync/service/sync_service.h"
#include "components/sync/service/sync_service_impl.h"
#include "components/sync/test/fake_server_network_resources.h"
#include "components/trusted_vault/securebox.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 "components/webauthn/core/browser/passkey_model_change.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_features.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 "crypto/unexportable_key.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/fido/enclave/constants.h"
#include "device/fido/features.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/fido_types.h"
#include "google_apis/gaia/gaia_id.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_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"
#include "ui/base/l10n/l10n_util.h"
#if BUILDFLAG(IS_WIN)
#include "device/fido/win/fake_webauthn_api.h"
#include "device/fido/win/util.h"
#include "device/fido/win/webauthn_api.h"
#endif
#if BUILDFLAG(IS_MAC)
#include "base/test/test_future.h"
#include "chrome/browser/webauthn/chrome_authenticator_request_delegate_mac.h"
#include "chrome/common/chrome_version.h"
#include "components/trusted_vault/icloud_recovery_key_mac.h"
#include "components/trusted_vault/proto/vault.pb.h"
#include "components/trusted_vault/proto_string_bytes_conversion.h"
#include "crypto/scoped_fake_apple_keychain_v2.h"
#include "device/fido/mac/fake_icloud_keychain.h"
#include "device/fido/mac/util.h"
#endif // BUILDFLAG(IS_MAC)
// These tests are 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 !defined(MEMORY_SANITIZER)
namespace {
using trusted_vault::MockTrustedVaultThrottlingConnection;
constexpr int32_t kSecretVersion = 417;
constexpr uint8_t kSecurityDomainSecret[32] = {};
constexpr char kEmail[] = "test@gmail.com";
constexpr char kEmailLocalPartOnly[] = "test";
// This value is derived by the Sync testing code from `kEmail` but is needed
// directly in these tests in order to simulate the `StoreKeys` calls to the
// `EnclaveManager`.
constexpr GaiaId::Literal kGaiaId("gaia_id_for_test_gmail.com");
// Protobuf generated by printing one generated by an enclave using
// `kSecurityDomainSecret`.
constexpr uint8_t kTestProtobuf[] = {
0x0A, 0x10, 0x8E, 0x48, 0x4B, 0x1C, 0x4F, 0xF9, 0x01, 0x14, 0xEF, 0xEA,
0xB3, 0x18, 0x40, 0x21, 0xEB, 0xF9, 0x12, 0x10, 0x48, 0x74, 0x02, 0x2C,
0xC5, 0x85, 0x38, 0xDA, 0x22, 0xD8, 0x8C, 0xAF, 0xD4, 0x05, 0x29, 0x84,
0x1A, 0x0F, 0x77, 0x77, 0x77, 0x2E, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C,
0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x22, 0x01, 0x00, 0x30, 0xE4, 0xFA, 0x86,
0x8D, 0xAC, 0x86, 0xDD, 0x17, 0x3A, 0x03, 0x66, 0x6F, 0x6F, 0x42, 0x00,
0x62, 0xCB, 0x01, 0x30, 0x89, 0x28, 0x56, 0xC4, 0x9C, 0xC4, 0xAD, 0x19,
0x4D, 0x4B, 0x91, 0x12, 0xD4, 0xA0, 0x05, 0xF0, 0xA4, 0xCA, 0x87, 0x66,
0x4C, 0x9E, 0x49, 0x58, 0xED, 0x08, 0x92, 0xB9, 0x5C, 0x5C, 0xCD, 0x7D,
0xA7, 0xD4, 0xEA, 0x54, 0xE9, 0x7E, 0xF2, 0x93, 0xDA, 0x17, 0x43, 0x7F,
0x41, 0x15, 0x25, 0x94, 0xB8, 0x04, 0x08, 0xAD, 0xE7, 0x67, 0xFA, 0xE2,
0x38, 0xD3, 0x37, 0xCE, 0x68, 0x1C, 0x2C, 0x82, 0xCA, 0xED, 0x8D, 0x10,
0x32, 0x31, 0xD9, 0xED, 0x7F, 0x51, 0x74, 0x66, 0x63, 0x14, 0x12, 0xD3,
0xA1, 0xC0, 0xFE, 0x52, 0xA3, 0x07, 0x01, 0x58, 0xDD, 0x3F, 0xD4, 0x97,
0xD8, 0xFA, 0x7F, 0x9A, 0xB2, 0xC1, 0x65, 0x36, 0xE2, 0xBE, 0xDF, 0x00,
0xFB, 0xAC, 0x59, 0xFE, 0x93, 0x25, 0x18, 0xA3, 0x92, 0xBF, 0x06, 0x8E,
0x0F, 0x2E, 0xD6, 0xE8, 0xFE, 0xCD, 0xE5, 0x76, 0xB8, 0x92, 0x3D, 0xB1,
0x42, 0xE9, 0xBB, 0x54, 0x36, 0x99, 0x5C, 0x21, 0xB7, 0x63, 0x33, 0x20,
0x8E, 0x93, 0xAA, 0x00, 0x83, 0xC6, 0xCC, 0x23, 0xAD, 0x63, 0x2B, 0x34,
0xAA, 0x4F, 0x8E, 0x9B, 0xFA, 0x40, 0x0E, 0xDB, 0x30, 0x37, 0x58, 0xE4,
0x60, 0xA2, 0xDF, 0x99, 0x85, 0x4B, 0x5C, 0xDD, 0x44, 0x23, 0x12, 0x64,
0x4C, 0x50, 0x34, 0x9D, 0x24, 0x1B, 0x37, 0x40, 0xC5, 0xB5, 0xA1, 0x5A,
0x70, 0x33, 0xF7, 0x80, 0x75, 0x1D, 0x22, 0x13, 0x37, 0xCD, 0x1F, 0x24,
0x40, 0xDA, 0x70, 0xA1, 0x03};
base::span<const uint8_t, 16> TestProtobufCredId() {
return base::span<const uint8_t>(kTestProtobuf).subspan<20, 16>();
}
base::span<const uint8_t, 1> TestProtobufUserId() {
return base::span<const uint8_t>(kTestProtobuf).subspan<55, 1>();
}
static constexpr char kIsUVPAA[] = R"((() => {
window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().
then(result => window.domAutomationController.send('IsUVPAA: ' + result),
error => window.domAutomationController.send('error ' + error));
})())";
static constexpr char kMakeCredentialUvDiscouraged[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { name: "www.example.com" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: true,
},
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kMakeCredentialSecurePaymentConfirmation[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { name: "www.example.com" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
authenticatorSelection: {
userVerification: 'required',
residentKey: 'preferred',
authenticatorAttachment: 'platform',
},
extensions: {payment: {isPayment: true}},
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kMakeCredentialReturnId[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { name: "www.example.com" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: true,
},
}}).then(c => window.domAutomationController.send(
'webauthn: ' + c.id),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kMakeCredentialWithExcludedCredential[] = R"((() => {
const base64ToArrayBuffer = (base64) => {
const bytes = window.atob(base64);
const len = bytes.length;
const ret = new Uint8Array(len);
for (var i = 0; i < len; i++) {
ret[i] = bytes.charCodeAt(i);
}
return ret.buffer;
}
return navigator.credentials.create({ publicKey: {
rp: { name: "www.example.com" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
authenticatorSelection: {
userVerification: 'required',
requireResidentKey: true,
authenticatorAttachment: "platform",
},
excludeCredentials: [{type: "public-key",
transports: [],
id: base64ToArrayBuffer("$1")}],
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kMakeCredentialCrossPlatform[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { name: "www.example.com" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: true,
authenticatorAttachment: "cross-platform",
},
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kMakeCredentialAttachmentPlatform[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { name: "www.example.com" },
user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
authenticatorSelection: {
authenticatorAttachment: 'platform',
userVerification: 'discouraged',
requireResidentKey: true,
},
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kMakeCredentialUvRequired[] = 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,
authenticatorSelection: {
requireResidentKey: true,
userVerification: 'required',
},
}}).then(c => window.domAutomationController.send(
'webauthn: uv=' +
// This gets the UV bit from the response.
((new Uint8Array(c.response.getAuthenticatorData())[32]&4) != 0)),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kMakeCredentialWithPrf[] = 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,
authenticatorSelection: {
requireResidentKey: true,
userVerification: 'discouraged',
},
extensions: {
prf: {$1},
},
}}).then(c => {
const showValue = (results, key) => {
if (results === undefined) {
return "none";
}
return btoa(String.fromCharCode.apply(
null, new Uint8Array(results[key])));
};
const results = c.getClientExtensionResults().prf.results;
window.domAutomationController.send(
'prf ' +
c.getClientExtensionResults().prf.enabled +
' ' +
showValue(results, 'first') +
' ' +
showValue(results, 'second'));
},
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kMakeCredentialGoogle[] = R"((() => {
return navigator.credentials.create({ publicKey: {
rp: { id: "google.com", name: "google.com" },
user: { id: new Uint8Array([0]), name: "$1", displayName: "" },
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
timeout: 10000,
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: true,
},
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kGetAssertionWithPrf[] = R"((() => {
return navigator.credentials.get({ publicKey: {
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
allowCredentials: [],
extensions: {
prf: {$1},
},
}}).then(c => {
const showValue = (results, key) => {
if (results === undefined) {
return "none";
}
return btoa(String.fromCharCode.apply(
null, new Uint8Array(results[key])));
};
const results = c.getClientExtensionResults().prf.results;
window.domAutomationController.send(
'prf ' +
c.getClientExtensionResults().prf.enabled +
' ' +
showValue(results, 'first') +
' ' +
showValue(results, 'second'));
},
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kGetAssertionUvDiscouraged[] = R"((() => {
return navigator.credentials.get({ publicKey: {
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
allowCredentials: [],
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kGetAssertionCredId1[] = R"((() => {
const credId = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
return navigator.credentials.get({ publicKey: {
challenge: new Uint8Array([0]),
timeout: 10000,
allowCredentials: [{type: 'public-key', id: new Uint8Array(credId)}],
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kGetAssertionUvDiscouragedWithGoogleRp[] = R"((() => {
return navigator.credentials.get({ publicKey: {
rpId: "google.com",
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
allowCredentials: [],
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kAbortableGetAssertion[] = R"((() => {
window.enclaveAbortSignal = new AbortController();
navigator.credentials.get({ publicKey: {
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
allowCredentials: [],
},
signal: window.enclaveAbortSignal.signal,
});
})())";
static constexpr char kAbort[] = R"((() => {
window.enclaveAbortSignal.abort();
})())";
static constexpr char kGetAssertionUvDiscouragedWithCredId[] = R"((() => {
return navigator.credentials.get({ publicKey: {
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
allowCredentials: [{
'type': 'public-key',
'transports': ['internal', 'hybrid', 'usb'],
'id': Uint8Array.from(atob("$1"), c => c.charCodeAt(0)).buffer}],
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char
kGetAssertionUvDiscouragedWithCredIdAndInternalTransport[] = R"((() => {
return navigator.credentials.get({ publicKey: {
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
allowCredentials: [{
'type': 'public-key',
'transports': ['internal'],
'id': Uint8Array.from(atob("$1"), c => c.charCodeAt(0)).buffer}],
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kGetAssertionUvRequired[] = R"((() => {
return navigator.credentials.get({ publicKey: {
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'required',
allowCredentials: [],
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kGetAssertionUvPreferred[] = R"((() => {
return navigator.credentials.get({ publicKey: {
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'preferred',
allowCredentials: [],
}}).then(c => window.domAutomationController.send(
'webauthn: uv=' +
// This gets the UV bit from the response.
((new Uint8Array(c.response.authenticatorData)[32]&4) != 0)),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kGetAssertionConditionalUI[] = R"((() => {
return navigator.credentials.get({
mediation: "conditional",
publicKey: {
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'discouraged',
allowCredentials: [],
}}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kMakeCredentialConditionalCreate[] = R"((() => {
return navigator.credentials.create({
mediation: "conditional",
publicKey: {
rp: { name: "www.example.com" },
user: {
id: new Uint8Array([1]),
name: "test@gmail.com",
displayName: "Foo Bar"
},
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
}
}).then(c => window.domAutomationController.send('webauthn: ' + c.id),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kMakeCredentialConditionalCreateWithExcludeList[] =
R"((() => {
const base64ToArrayBuffer = (base64) => {
const bytes = window.atob(base64);
const len = bytes.length;
const ret = new Uint8Array(len);
for (var i = 0; i < len; i++) {
ret[i] = bytes.charCodeAt(i);
}
return ret.buffer;
}
return navigator.credentials.create({
mediation: "conditional",
publicKey: {
rp: { name: "www.example.com" },
user: {
id: new Uint8Array([1]),
name: "test@gmail.com",
displayName: "Foo Bar"
},
pubKeyCredParams: [{type: "public-key", alg: -7}],
challenge: new Uint8Array([0]),
excludeCredentials: [{type: "public-key",
transports: [],
id: base64ToArrayBuffer("$1")}],
}
}).then(c => window.domAutomationController.send('webauthn: ' + c.id),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kSignalHideTestPasskey[] = R"((() => {
PublicKeyCredential.signalAllAcceptedCredentials({
rpId: "www.example.com",
allAcceptedCredentialIds: [],
userId: "AA",
}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
static constexpr char kSignalRestoreTestPasskey[] = R"((() => {
PublicKeyCredential.signalAllAcceptedCredentials({
rpId: "www.example.com",
allAcceptedCredentialIds: ["SHQCLMWFONoi2Iyv1AUphA"],
userId: "AA",
}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
})())";
#if BUILDFLAG(IS_MAC)
static constexpr char kGetAssertionViaButtonClickImmediateUvPreferred[] = R"(
document.body.innerHTML = '<button id="testButton"">Get Assertion</button>';
function triggerGetAssertion() {
navigator.credentials.get({
mediation: "immediate",
publicKey: {
challenge: new Uint8Array([0]),
timeout: 10000,
userVerification: 'preferred',
}
}).then(c => window.domAutomationController.send('webauthn: OK'),
e => window.domAutomationController.send('error ' + e));
}
const button = document.getElementById('testButton');
button.addEventListener('click', triggerGetAssertion);
)";
#endif // BUILDFLAG(IS_MAC)
bool IsReady(GPMEnclaveController::AccountState state) {
switch (state) {
case GPMEnclaveController::AccountState::kReady:
return true;
default:
LOG(ERROR) << "State " << static_cast<int>(state)
<< " is not a ready state";
return false;
}
}
bool IsMechanismEnclaveCredential(
const AuthenticatorRequestDialogModel::Mechanism& mechanism) {
if (std::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::Credential>(
mechanism.type)) {
return std::get<AuthenticatorRequestDialogModel::Mechanism::Credential>(
mechanism.type)
->source == device::AuthenticatorType::kEnclave;
}
return false;
}
struct TempDir {
public:
TempDir() { CHECK(dir_.CreateUniqueTempDir()); }
base::FilePath GetPath() const { return dir_.GetPath(); }
private:
base::ScopedTempDir dir_;
};
class EnclaveAuthenticatorBrowserTest : public SyncTest {
public:
class DelegateObserver
: public ChromeAuthenticatorRequestDelegate::TestObserver {
public:
explicit DelegateObserver(EnclaveAuthenticatorBrowserTest* test_instance)
: test_instance_(test_instance) {}
virtual ~DelegateObserver() = default;
void WaitForUI() {
ui_shown_run_loop_->Run();
ui_shown_run_loop_ = std::make_unique<base::RunLoop>();
}
void WaitForUIReady() {
ui_ready_run_loop_->Run();
ui_ready_run_loop_ = std::make_unique<base::RunLoop>();
}
void WaitForPreTransportAvailabilityEnumerated() {
pre_tai_run_loop_->Run();
pre_tai_run_loop_ = std::make_unique<base::RunLoop>();
}
void WaitForDelegateDestruction() {
destruction_run_loop_->Run();
destruction_run_loop_ = std::make_unique<base::RunLoop>();
}
void AddAdditionalTransport(device::FidoTransportProtocol transport) {
additional_transport_ = transport;
}
void SetPendingTrustedVaultConnection(
std::unique_ptr<trusted_vault::TrustedVaultConnection> connection) {
pending_connection_ = std::move(connection);
}
void SetUseSyncedDeviceCablePairing(bool use_pairing) {
use_synced_device_cable_pairing_ = use_pairing;
}
bool ui_shown() { return ui_shown_; }
bool on_transport_availability_enumerated_called() {
return on_transport_availability_enumerated_called_;
}
const std::optional<base::flat_set<device::FidoTransportProtocol>>&
transports_observed() const {
return transports_observed_;
}
// ChromeAuthenticatorRequestDelegate::TestObserver:
void Created(ChromeAuthenticatorRequestDelegate* delegate) override {
test_instance_->UpdateRequestDelegate(delegate);
if (pending_connection_) {
delegate->SetTrustedVaultConnectionForTesting(
std::move(pending_connection_));
}
delegate->SetMockTimeForTesting(
test_instance_->timer_task_runner_->GetMockTickClock(),
test_instance_->timer_task_runner_);
transports_observed_ = std::nullopt;
}
void PreStartOver() override {
// Start over creates a new request handler and invokes
// OnTransportAvailabilityEnumerated() again.
transports_observed_ = std::nullopt;
}
void OnDestroy(ChromeAuthenticatorRequestDelegate* delegate) override {
test_instance_->UpdateRequestDelegate(nullptr);
destruction_run_loop_->QuitWhenIdle();
}
std::vector<std::unique_ptr<device::cablev2::Pairing>>
GetCablePairingsFromSyncedDevices() override {
std::vector<std::unique_ptr<device::cablev2::Pairing>> ret;
if (use_synced_device_cable_pairing_) {
ret.emplace_back(TestPhone("phone", /*public_key=*/0,
/*last_updated=*/base::Time::FromTimeT(1),
/*channel_priority=*/1));
}
return ret;
}
void OnTransportAvailabilityEnumerated(
ChromeAuthenticatorRequestDelegate* delegate,
device::FidoRequestHandlerBase::TransportAvailabilityInfo* tai)
override {
CHECK(!transports_observed_);
transports_observed_ = tai->available_transports;
if (additional_transport_.has_value()) {
tai->available_transports.insert(*additional_transport_);
}
on_transport_availability_enumerated_called_ = true;
}
void OnPreTransportAvailabilityEnumerated(
ChromeAuthenticatorRequestDelegate* delegate) override {
pre_tai_run_loop_->QuitWhenIdle();
}
void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {
ui_shown_ = true;
ui_shown_run_loop_->QuitWhenIdle();
}
void CableV2ExtensionSeen(
base::span<const uint8_t> server_link_data) override {}
void AccountSelectorShown(
const std::vector<device::AuthenticatorGetAssertionResponse>& responses)
override {}
private:
raw_ptr<EnclaveAuthenticatorBrowserTest> test_instance_;
std::optional<base::flat_set<device::FidoTransportProtocol>>
transports_observed_;
std::optional<device::FidoTransportProtocol> additional_transport_;
std::unique_ptr<trusted_vault::TrustedVaultConnection> pending_connection_;
bool use_synced_device_cable_pairing_ = false;
bool ui_shown_ = false;
bool on_transport_availability_enumerated_called_ = false;
std::unique_ptr<base::RunLoop> ui_shown_run_loop_ =
std::make_unique<base::RunLoop>();
std::unique_ptr<base::RunLoop> ui_ready_run_loop_ =
std::make_unique<base::RunLoop>();
std::unique_ptr<base::RunLoop> pre_tai_run_loop_ =
std::make_unique<base::RunLoop>();
std::unique_ptr<base::RunLoop> destruction_run_loop_ =
std::make_unique<base::RunLoop>();
};
class ModelObserver : public AuthenticatorRequestDialogModel::Observer {
public:
explicit ModelObserver(AuthenticatorRequestDialogModel* model)
: model_(model) {
model_->observers.AddObserver(this);
}
~ModelObserver() override {
if (model_) {
model_->observers.RemoveObserver(this);
model_ = nullptr;
}
}
// Call this before the state transition you are looking to observe.
void SetStepToObserve(AuthenticatorRequestDialogModel::Step step) {
ASSERT_FALSE(run_loop_);
step_ = step;
run_loop_ = std::make_unique<base::RunLoop>();
}
// Call this to observer the next step change, whatever it might be.
void ObserveNextStep() {
ASSERT_FALSE(run_loop_);
observe_next_step_ = true;
run_loop_ = std::make_unique<base::RunLoop>();
}
// This will return after a transition to the state previously specified by
// `SetStepToObserver`. Returns immediately if the current step matches.
void WaitForStep() {
if (!observe_next_step_ && model_->step() == step_) {
run_loop_.reset();
return;
}
ASSERT_TRUE(run_loop_);
run_loop_->Run();
// When waiting for `kClosed` the model is deleted at this point.
if (!observe_next_step_ &&
step_ != AuthenticatorRequestDialogModel::Step::kClosed) {
CHECK_EQ(step_, model_->step());
}
Reset();
}
// AuthenticatorRequestDialogModel::Observer:
void OnStepTransition() override {
all_steps_.push_back(model_->step());
if (run_loop_ && (observe_next_step_ || step_ == model_->step())) {
run_loop_->QuitWhenIdle();
}
}
void OnLoadingEnclaveTimeout() override {
loading_enclave_timed_out_ = true;
}
void OnModelDestroyed(AuthenticatorRequestDialogModel* model) override {
model_ = nullptr;
}
void Reset() {
step_ = AuthenticatorRequestDialogModel::Step::kNotStarted;
observe_next_step_ = false;
run_loop_.reset();
}
base::span<const AuthenticatorRequestDialogModel::Step> all_steps() const {
return all_steps_;
}
bool loading_enclave_timed_out() { return loading_enclave_timed_out_; }
private:
raw_ptr<AuthenticatorRequestDialogModel> model_;
AuthenticatorRequestDialogModel::Step step_ =
AuthenticatorRequestDialogModel::Step::kNotStarted;
std::vector<AuthenticatorRequestDialogModel::Step> all_steps_;
bool loading_enclave_timed_out_ = false;
bool observe_next_step_ = false;
std::unique_ptr<base::RunLoop> run_loop_;
};
EnclaveAuthenticatorBrowserTest()
: SyncTest(SINGLE_CLIENT),
process_and_port_(StartWebAuthnEnclave(temp_dir_.GetPath())),
enclave_override_(
TestWebAuthnEnclaveIdentity(process_and_port_.second)),
security_domain_service_(
FakeSecurityDomainService::New(kSecretVersion)),
#if BUILDFLAG(IS_WIN)
webauthn_dll_override_(&fake_webauthn_dll_),
#endif
recovery_key_store_(FakeRecoveryKeyStore::New()),
fake_hw_provider_(
std::make_unique<crypto::ScopedFakeUnexportableKeyProvider>()) {
#if BUILDFLAG(IS_WIN)
// Make webauthn.dll unavailable to ensure a consistent test environment on
// Windows. Otherwise the version of webauthn.dll can differ between
// builders causing differences / failures.
fake_webauthn_dll_.set_available(false);
biometrics_override_ =
std::make_unique<device::fido::win::ScopedBiometricsOverride>(false);
#elif BUILDFLAG(IS_MAC)
// By default, Touch ID is disabled in these tests. Specific tests can
// replace this if they need.
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_drive_override_ = OverrideICloudDriveEnabled(false);
#endif
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/{device::kWebAuthnNoAccountTimeout,
device::kWebAuthnSignalApiHidePasskeys},
/*disabled_features=*/{});
OSCryptMocker::SetUp();
// Log call `FIDO_LOG` messages.
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>();
// Disabling Bluetooth significantly speeds up tests on Linux.
bluetooth_values_for_testing_ =
device::BluetoothAdapterFactory::Get()->InitGlobalOverrideValues();
bluetooth_values_for_testing_->SetLESupported(false);
}
~EnclaveAuthenticatorBrowserTest() override {
EnclaveManagerFactory::SetUrlLoaderFactoryForTesting(nullptr);
CHECK(process_and_port_.first.Terminate(/*exit_code=*/1, /*wait=*/true));
OSCryptMocker::TearDown();
}
EnclaveAuthenticatorBrowserTest(const EnclaveAuthenticatorBrowserTest&) =
delete;
EnclaveAuthenticatorBrowserTest& operator=(
const EnclaveAuthenticatorBrowserTest&) = delete;
void SetUpCommandLine(base::CommandLine* command_line) override {
SyncTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
void SetUp() override {
ASSERT_TRUE(https_server_.InitializeAndListen());
EnclaveManagerFactory::SetUrlLoaderFactoryForTesting(
url_loader_factory_.GetSafeWeakWrapper().get());
SyncTest::SetUp();
}
void SetUpInProcessBrowserTestFixture() override {
subscription_ =
BrowserContextDependencyManager::GetInstance()
->RegisterCreateServicesCallbackForTesting(
base::BindRepeating([](content::BrowserContext* context) {
IdentityTestEnvironmentProfileAdaptor::
SetIdentityTestEnvironmentFactoriesOnBrowserContext(
context);
}));
}
void SetUpOnMainThread() override {
SyncTest::SetUpOnMainThread();
identity_test_env_adaptor_ =
std::make_unique<IdentityTestEnvironmentProfileAdaptor>(
browser()->profile());
identity_test_env()->SetAutomaticIssueOfAccessTokens(true);
syncer::SyncServiceImpl* sync_service =
SyncServiceFactory::GetAsSyncServiceImplForProfileForTesting(
browser()->profile());
sync_service->OverrideNetworkForTest(
fake_server::CreateFakeServerHttpPostProviderFactory(
GetFakeServer()->AsWeakPtr()));
sync_harness_ = SyncServiceImplHarness::Create(
browser()->profile(), kEmail, "password",
SyncServiceImplHarness::SigninType::FAKE_SIGNIN);
if (sync_feature_enabled_) {
ASSERT_TRUE(sync_harness_->SetupSync());
} else {
// Sign in without full sync consent, opt into using account passwords.
ASSERT_TRUE(sync_harness_->SignInPrimaryAccount());
}
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");
delegate_observer_ = std::make_unique<DelegateObserver>(this);
ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(
delegate_observer_.get());
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
}
void TearDownOnMainThread() override { identity_test_env_adaptor_.reset(); }
signin::IdentityTestEnvironment* identity_test_env() {
return identity_test_env_adaptor_->identity_test_env();
}
void UpdateRequestDelegate(ChromeAuthenticatorRequestDelegate* delegate) {
request_delegate_ = delegate;
if (request_delegate_) {
model_observer_ = std::make_unique<ModelObserver>(dialog_model());
}
}
ChromeAuthenticatorRequestDelegate* request_delegate() {
return request_delegate_;
}
DelegateObserver* delegate_observer() { return delegate_observer_.get(); }
AuthenticatorRequestDialogModel* dialog_model() {
return request_delegate()->dialog_model();
}
ModelObserver* model_observer() { return model_observer_.get(); }
webauthn::PasskeyModel* passkey_model() {
return PasskeyModelFactory::GetInstance()->GetForProfile(
browser()->profile());
}
void SimulateEnclaveMechanismSelection() {
ASSERT_TRUE(request_delegate_);
for (const auto& mechanism :
request_delegate_->dialog_model()->mechanisms) {
if (mechanism.type ==
AuthenticatorRequestDialogModel::Mechanism::Type(
AuthenticatorRequestDialogModel::Mechanism::Enclave())) {
mechanism.callback.Run();
return;
}
}
EXPECT_TRUE(false) << "No Enclave mechanism found";
}
void AddTestPasskeyToModel() {
sync_pb::WebauthnCredentialSpecifics passkey;
CHECK(passkey.ParseFromArray(kTestProtobuf, sizeof(kTestProtobuf)));
passkey_model()->AddNewPasskeyForTesting(passkey);
}
void SetMockVaultConnectionOnRequestDelegate(
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
result) {
auto connection = std::make_unique<
testing::NiceMock<MockTrustedVaultThrottlingConnection>>();
EXPECT_CALL(*connection, DownloadAuthenticationFactorsRegistrationState(
testing::_, testing::_, testing::_))
.WillOnce(
[result = std::move(result)](
const CoreAccountInfo&,
base::OnceCallback<void(
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult)>
callback,
base::RepeatingClosure _) mutable {
std::move(callback).Run(std::move(result));
return std::make_unique<
trusted_vault::TrustedVaultConnection::Request>();
});
// If the delegate hasn't been created yet, the mock will be assigned upon
// creation.
if (request_delegate_) {
request_delegate_->SetTrustedVaultConnectionForTesting(
std::move(connection));
} else {
delegate_observer_->SetPendingTrustedVaultConnection(
std::move(connection));
}
}
void SetVaultConnectionToTimeout() {
auto connection = std::make_unique<
testing::NiceMock<MockTrustedVaultThrottlingConnection>>();
EXPECT_CALL(*connection, DownloadAuthenticationFactorsRegistrationState(
testing::_, testing::_, testing::_))
.WillOnce(
[](const CoreAccountInfo&,
base::OnceCallback<void(
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult)>
callback,
base::RepeatingClosure _) mutable {
return std::make_unique<
trusted_vault::TrustedVaultConnection::Request>();
});
// If the delegate hasn't been created yet, the mock will be assigned upon
// creation.
if (request_delegate_) {
request_delegate_->SetTrustedVaultConnectionForTesting(
std::move(connection));
} else {
delegate_observer_->SetPendingTrustedVaultConnection(
std::move(connection));
}
}
void CheckRegistrationStateNotRequested() {
auto connection = std::make_unique<
testing::NiceMock<MockTrustedVaultThrottlingConnection>>();
EXPECT_CALL(*connection, DownloadAuthenticationFactorsRegistrationState(
testing::_, testing::_, testing::_))
.WillRepeatedly(
[](const CoreAccountInfo&,
base::OnceCallback<void(
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult)>
callback,
base::RepeatingClosure _)
-> std::unique_ptr<
trusted_vault::TrustedVaultConnection::Request> {
NOTREACHED() << "account state unexpectedly requested";
});
CHECK(!request_delegate_);
delegate_observer_->SetPendingTrustedVaultConnection(std::move(connection));
}
void EnableUVKeySupport() {
fake_uv_provider_.emplace<crypto::ScopedFakeUserVerifyingKeyProvider>();
}
bool IsUVPAA() {
content::WebContents* web_contents =
browser()->tab_strip_model()->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 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
}
protected:
scoped_refptr<base::TestMockTimeTaskRunner> timer_task_runner_ =
base::MakeRefCounted<base::TestMockTimeTaskRunner>();
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
const TempDir temp_dir_;
base::CallbackListSubscription subscription_;
std::unique_ptr<IdentityTestEnvironmentProfileAdaptor>
identity_test_env_adaptor_;
std::unique_ptr<SyncServiceImplHarness> sync_harness_;
const std::pair<base::Process, uint16_t> process_and_port_;
const device::enclave::ScopedEnclaveOverride enclave_override_;
std::unique_ptr<FakeSecurityDomainService> security_domain_service_;
#if BUILDFLAG(IS_WIN)
device::FakeWinWebAuthnApi fake_webauthn_dll_;
device::WinWebAuthnApi::ScopedOverride webauthn_dll_override_;
std::unique_ptr<device::fido::win::ScopedBiometricsOverride>
biometrics_override_;
#elif BUILDFLAG(IS_MAC)
std::unique_ptr<device::fido::mac::ScopedBiometricsOverride>
biometrics_override_;
std::unique_ptr<device::fido::icloud_keychain::Fake> fake_icloud_keychain_;
std::unique_ptr<ScopedICloudDriveOverride> scoped_icloud_drive_override_;
#endif
std::unique_ptr<FakeRecoveryKeyStore> recovery_key_store_;
std::unique_ptr<crypto::ScopedFakeUnexportableKeyProvider> fake_hw_provider_;
network::TestURLLoaderFactory url_loader_factory_;
std::unique_ptr<DelegateObserver> delegate_observer_;
std::unique_ptr<ModelObserver> model_observer_;
raw_ptr<ChromeAuthenticatorRequestDelegate> request_delegate_;
std::unique_ptr<device::BluetoothAdapterFactory::GlobalOverrideValues>
bluetooth_values_for_testing_;
std::variant<crypto::ScopedNullUserVerifyingKeyProvider,
crypto::ScopedFakeUserVerifyingKeyProvider,
crypto::ScopedFailingUserVerifyingKeyProvider>
fake_uv_provider_;
logging::ScopedVmoduleSwitches scoped_vmodule_;
bool sync_feature_enabled_ = true;
base::test::ScopedFeatureList scoped_feature_list_;
};
class EnclaveAuthenticatorWithTimeout : public EnclaveAuthenticatorBrowserTest {
public:
EnclaveAuthenticatorWithTimeout() {
scoped_feature_list_.InitAndDisableFeature(
device::kWebAuthnNoAccountTimeout);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Parses the string resulting from the Javascript snippets that exercise the
// PRF extension.
std::tuple<bool, std::string, std::string> ParsePrfResult(
std::string_view result_view) {
// Javascript strings end up with "" around them. Trim that.
if (!result_view.empty() && result_view[0] == '"') {
result_view.remove_prefix(1);
}
if (!result_view.empty() && result_view.back() == '"') {
result_view.remove_suffix(1);
}
// The string is now a series of space-deliminated tokens:
// * "prf"
// * the `enabled` value: "true" / "false" / "undefined"
// * the base64-encoded `first` result, or "none".
// * the base64-encoded `second` result, or "none".
const std::string result(result_view);
auto tokenizer = base::StringTokenizer(result, " ");
CHECK(tokenizer.GetNext());
CHECK_EQ(tokenizer.token(), "prf");
CHECK(tokenizer.GetNext());
const std::string& enabled_str = tokenizer.token();
CHECK(enabled_str == "true" || enabled_str == "false" ||
enabled_str == "undefined")
<< enabled_str;
const bool enabled = enabled_str == "true";
CHECK(tokenizer.GetNext());
const std::string first = tokenizer.token();
CHECK(tokenizer.GetNext());
const std::string second = tokenizer.token();
return std::make_tuple(enabled, std::move(first), std::move(second));
}
// Parses the output of `kMakeCredentialReturnId` and returns the credential ID
// that was created.
std::optional<std::vector<uint8_t>> ParseCredentialId(
std::string_view result_view) {
// Javascript strings end up with "" around them. Trim that.
if (!result_view.empty() && result_view[0] == '"') {
result_view.remove_prefix(1);
}
if (!result_view.empty() && result_view.back() == '"') {
result_view.remove_suffix(1);
}
// The string is now "webauthn: " followed by the base64url credential ID,
// which is returned from this function.
if (!base::StartsWith(result_view, "webauthn: ")) {
return std::nullopt;
}
std::string_view base64url_cred_id = result_view.substr(10);
return base::Base64UrlDecode(base64url_cred_id,
base::Base64UrlDecodePolicy::IGNORE_PADDING);
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
RegisterDeviceWithGpmPin_MakeCredential_Success) {
/* Test script:
* - Prerequisites:
* Enclave not registered
* No existing security domain secrets
* Existing user account with password sync enabled
* Platform UV unavailable
* 1. Modal MakeCredential request invoked by RP
* 2. Mechanism selection appears; test chooses enclave credential
* 3. UI for creating passkey appears; test chooses create
* 4. UI for creating GPM Pin appears; test selects pin
* 5. Device registration with enclave succeeds
* 6. MakeCredential succeeds
*/
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, NonWebauthnRequest) {
if (!base::FeatureList::IsEnabled(features::kSecurePaymentConfirmation)) {
// SPC is not enabled in this configuration and so the `payment` extension
// in the Javascript will be ignored.
return;
}
CheckRegistrationStateNotRequested();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents,
kMakeCredentialSecurePaymentConfirmation);
delegate_observer()->WaitForUI();
// Non-WebAuthn requests (e.g. Secure Payment Confirmation and credit-card
// confirmation) must not use the enclave. In some cases, they will disable
// the UI, which is not simulated here.
EXPECT_TRUE(
dialog_model()->step() ==
AuthenticatorRequestDialogModel::Step::kCreatePasskey ||
dialog_model()->step() ==
AuthenticatorRequestDialogModel::Step::kErrorNoAvailableTransports);
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, MakeCredentialWithPrf) {
/* Test script:
* - Prerequisites:
* Enclave not registered
* No existing security domain secrets
* Existing user account with password sync enabled
* Platform UV unavailable
* 1. Modal MakeCredential with PRF request invoked by RP
* 2. Mechanism selection appears; test chooses enclave credential
* 3. UI for creating passkey appears; test chooses create
* 4. UI for creating GPM pin appears; test selects pin
* 5. Device registration with enclave succeeds
* 6. MakeCredential succeeds and evaluates PRF.
* 7. Second MakeCredential is made, just enabling PRF.
* 8. Mechanism selection appears; test chooses enclave credential
* 9. User confirms creation.
* 10. MakeCredential succeeds and PRF reports enabled.
*/
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
const std::string kEval =
"eval: { first: new Uint8Array([1]), second: new Uint8Array([2]) }";
content::ExecuteScriptAsync(
web_contents, base::ReplaceStringPlaceholders(
kMakeCredentialWithPrf, {kEval}, /*offsets=*/nullptr));
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
bool enabled;
std::string first, second;
std::tie(enabled, first, second) = ParsePrfResult(script_result);
EXPECT_TRUE(enabled);
// Since the HMAC key is randomly generated the two values are random. But we
// can assert that they are distinct.
EXPECT_NE(first, "none");
EXPECT_NE(second, "none");
EXPECT_NE(first, second);
const std::string kEnable = "enable: true";
content::ExecuteScriptAsync(
web_contents,
base::ReplaceStringPlaceholders(kMakeCredentialWithPrf, {kEnable},
/*offsets=*/nullptr));
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
dialog_model()->OnGPMCreatePasskey();
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
std::tie(enabled, first, second) = ParsePrfResult(script_result);
EXPECT_TRUE(enabled);
EXPECT_EQ(first, "none");
EXPECT_EQ(second, "none");
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, GetAssertionWithPrf) {
/* Test script:
* - Prerequisites:
* Enclave not registered
* Trusted vault state is recoverable
* Existing user account with password sync enabled
* Platform UV unavailable
* Synced passkey for the RP available
* 1. Modal GetAssertion request invoked by RP, includes PRF request.
* 2. Priority mechanism selection for synced passkey appears, test confirms
* 3. Window to recover security domain appears, test simulates reauth
* 4. Test selects a GPM PIN
* 5. Device registration with enclave succeeds
* 6. GetAssertion succeeds with PRF results.
*/
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
const std::string kEval =
"eval: { first: new Uint8Array([1]), second: new Uint8Array([2]) }";
content::ExecuteScriptAsync(
web_contents, base::ReplaceStringPlaceholders(
kGetAssertionWithPrf, {kEval}, /*offsets=*/nullptr));
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
dialog_model()->OnUserConfirmedPriorityMechanism();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
bool enabled;
std::string first, second;
std::tie(enabled, first, second) = ParsePrfResult(script_result);
EXPECT_FALSE(enabled);
EXPECT_EQ(first, "wxrrL9DHkZivKyIp/cg2mRfnB2v4J+M8EevFaBqxpRc=");
EXPECT_EQ(second, "zx7riv8qxdelsyWdRRSZSrzFji35j4fZFnr30gKf8r8=");
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
RegisterDeviceWithGpmPin_MakeCredentialWithUV_Success) {
/* Test script:
* - Prerequisites:
* Enclave not registered
* No existing security domain secrets
* Existing user account with password sync enabled
* Platform UV unavailable
* 1. Modal MakeCredential request invoked by RP, requires UV.
* 2. Mechanism selection appears; test chooses enclave.
* 3. UI for creating passkey appears; test chooses create
* 4. UI for creating GPM pin appears; test selects pin
* 5. Device registration with enclave succeeds
* 6. MakeCredential succeeds
*
* Notably, user verification is asserted without a second GPM PIN prompt.
*/
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
MakeCredential_RecoverWithGPMPIN_Success) {
/* Test script:
* - Prerequisites:
* Enclave not registered
* Security domain exists with GPM PIN.
* Existing user account with password sync enabled
* Platform UV unavailable
* 1. Modal MakeCredential request invoked by RP, requires UV.
* 2. Mechanism selection appears; test chooses enclave.
* 3. UI for onboarding appears; test accepts it
* 4. Test simiulates MagicArch
* 5. Test selects a GPM PIN
* 6. Device registration with enclave succeeds
* 7. MakeCredential succeeds
*
* Notably, user verification is asserted without a second GPM PIN prompt.
*/
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
registration_state_result.gpm_pin_metadata = trusted_vault::GpmPinMetadata(
"public key", trusted_vault::UsableRecoveryPinMetadata(
EnclaveManager::MakeWrappedPINForTesting(
kSecurityDomainSecret, "123456"),
/*expiry=*/base::Time::Now() + base::Seconds(10000)));
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kTrustThisComputerCreation);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
MakeCredential_RecoverWithLSKF_Success) {
/* Test script:
* - Prerequisites:
* Enclave not registered
* Security domain exists with GPM PIN.
* Existing user account with password sync enabled
* Platform UV unavailable
* 1. Modal MakeCredential request invoked by RP, requires UV.
* 2. Mechanism selection appears; test chooses enclave.
* 3. UI for onboarding appears; test accepts it
* 4. Test simiulates MagicArch
* 5. Test selects a GPM PIN
* 6. Device registration with enclave succeeds
* 7. MakeCredential succeeds
*
* Notably, user verification is asserted without a second GPM PIN prompt.
*/
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kTrustThisComputerCreation);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
}
// Tests recovering from an LSKF when there is also a GPM PIN that cannot be
// used for recovery. Regression test for crbug.com/402427390.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
MakeCredential_RecoverWithLSKFAndUnusablePIN) {
// First, register with a PIN.
{
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(
std::move(registration_state_result));
}
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
// Then, have the security domain service mark the PIN as unusable and recover
// from an LSKF.
security_domain_service_->MakePinMemberUnusable();
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->ClearRegistrationForTesting();
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
registration_state_result.gpm_pin_metadata = trusted_vault::GpmPinMetadata(
security_domain_service_->GetPinMemberPublicKey(),
/*pin_metadata=*/std::nullopt);
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kTrustThisComputerCreation);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
CreatingDuplicateGivesInvalidStateError) {
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialReturnId);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kTrustThisComputerCreation);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
std::optional<std::vector<uint8_t>> cred_id =
ParseCredentialId(script_result);
ASSERT_TRUE(cred_id);
content::ExecuteScriptAsync(
web_contents,
base::ReplaceStringPlaceholders(kMakeCredentialWithExcludedCredential,
{base::Base64Encode(*cred_id)},
/*offsets=*/nullptr));
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMEnterPin);
dialog_model()->OnGPMCreatePasskey();
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_THAT(script_result, testing::HasSubstr("InvalidStateError"));
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
RecoverWithLSKF_GetAssertion_Success) {
/* Test script:
* - Prerequisites:
* Enclave not registered
* Trusted vault state is recoverable
* Existing user account with password sync enabled
* Platform UV unavailable
* Synced passkey for the RP available
* 1. Modal GetAssertion request invoked by RP
* 2. Priority mechanism selection for synced passkey appears, test confirms
* 3. Window to recover security domain appears, test simulates reauth
* 4. Test selects a GPM PIN
* 5. Device registration with enclave succeeds
* 6. GetAssertion succeeds
*/
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
class PasskeyModelObserver : public webauthn::PasskeyModel::Observer {
public:
void OnPasskeysChanged(
const std::vector<webauthn::PasskeyModelChange>& changes) override {
for (const auto& change : changes) {
if (change.type() == webauthn::PasskeyModelChange::ChangeType::UPDATE) {
CHECK(!did_update);
did_update = true;
}
}
}
void OnPasskeyModelShuttingDown() override {}
void OnPasskeyModelIsReady(bool is_ready) override {}
bool did_update = false;
};
PasskeyModelObserver passkey_model_observer;
passkey_model()->AddObserver(&passkey_model_observer);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
dialog_model()->OnUserConfirmedPriorityMechanism();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
model_observer()->WaitForStep();
EXPECT_FALSE(passkey_model_observer.did_update);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
passkey_model()->RemoveObserver(&passkey_model_observer);
EXPECT_TRUE(passkey_model_observer.did_update);
auto passkeys = passkey_model()->GetAllPasskeys();
ASSERT_EQ(passkeys.size(), 1u);
// The update time should be in the last 10 minutes.
EXPECT_LT((base::Time::Now() -
base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(
passkeys[0].last_used_time_windows_epoch_micros())))
.InMinutes(),
10);
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
RegisterDeviceWithGpmPin_UVRequestsWithWrongPIN) {
/* Test script:
* - Prerequisites:
* Enclave not registered
* No existing security domain secrets
* Existing user account with password sync enabled
* Platform UV unavailable
* 1. Modal MakeCredential request invoked by RP, requires UV.
* 2. Mechanism selection appears; test chooses enclave.
* 3. UI for creating passkey appears; test chooses create
* 4. UI for creating GPM pin appears; test selects pin
* 5. Device registration with enclave succeeds
* 6. MakeCredential succeeds
* 7. Another modal MakeCredential request is invoked by RP, requiring UV
* 8. Test enters wrong PIN
* 9. PIN entry dialog appears again, test enters correct PIN
* 10. Modal GetAssertion request invoked by RP, requires UV
* 11. Test enters wrong PIN
* 12. PIN entry dialog appears again, test enters correct PIN
*
* Notably, user verification is asserted without a second GPM PIN prompt.
*/
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
// A second MakeCredential, with incorrect PIN and then correct PIN.
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMEnterPin);
dialog_model()->OnGPMCreatePasskey();
model_observer()->WaitForStep();
EXPECT_EQ(browser()->profile()->GetPrefs()->GetInteger(
webauthn::pref_names::kEnclaveFailedPINAttemptsCount),
0);
model_observer()->ObserveNextStep();
dialog_model()->OnGPMPinEntered(u"111111");
model_observer()->WaitForStep();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMEnterPin);
EXPECT_EQ(browser()->profile()->GetPrefs()->GetInteger(
webauthn::pref_names::kEnclaveFailedPINAttemptsCount),
1);
dialog_model()->OnGPMPinEntered(u"123456");
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
// GetAssertion, with incorrect and then correct PIN.
content::ExecuteScriptAsync(web_contents, kGetAssertionUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMEnterPin);
dialog_model()->OnUserConfirmedPriorityMechanism();
model_observer()->WaitForStep();
EXPECT_EQ(browser()->profile()->GetPrefs()->GetInteger(
webauthn::pref_names::kEnclaveFailedPINAttemptsCount),
0);
model_observer()->ObserveNextStep();
dialog_model()->OnGPMPinEntered(u"111111");
model_observer()->WaitForStep();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMEnterPin);
EXPECT_EQ(browser()->profile()->GetPrefs()->GetInteger(
webauthn::pref_names::kEnclaveFailedPINAttemptsCount),
1);
dialog_model()->OnGPMPinEntered(u"123456");
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
EXPECT_EQ(browser()->profile()->GetPrefs()->GetInteger(
webauthn::pref_names::kEnclaveFailedPINAttemptsCount),
0);
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
GpmPinRegistrationPersistAcrossRestart) {
/* Test script:
* - Prerequisites:
* Enclave not registered
* No existing security domain secrets
* Existing user account with password sync enabled
* Platform UV unavailable
* 1. Modal MakeCredential request invoked by RP
* 2. Mechanism selection appears; test chooses enclave credential
* 3. UI for creating passkey appears; test chooses create
* 4. UI for creating GPM pin appears; test selects pin
* 5. Device registration with enclave succeeds
* 6. MakeCredential succeeds
* 7. Test clears the EnclaveManager state to force load from disk
* 8. Modal MakeCredential request invoked by RP
* 9. Mechanism selection appears; test chooses enclave credential
*/
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
delegate_observer()->WaitForDelegateDestruction();
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->ResetForTesting();
EXPECT_EQ(
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->is_loaded(),
false);
// Checks that a following request goes straight to ready state.
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_TRUE(IsReady(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing()));
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, UserCancelsUV) {
EnableUVKeySupport();
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
// Do a get() to ensure that any deferred UV key creation has happened.
content::ExecuteScriptAsync(web_contents, kGetAssertionUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
dialog_model()->OnUserConfirmedPriorityMechanism();
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
// Do a get() where the signing fails, simulating the user canceling the
// request. There should not be any Chrome error UI.
fake_uv_provider_.emplace<crypto::ScopedFailingUserVerifyingKeyProvider>();
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->ClearCachedKeysForTesting();
content::ExecuteScriptAsync(web_contents, kGetAssertionUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kClosed);
dialog_model()->OnUserConfirmedPriorityMechanism();
model_observer()->WaitForStep();
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_THAT(script_result, testing::HasSubstr("\"error NotAllowedError"));
}
// Tests that if the enclave is still loading when the user taps a passkey from
// autofill, Chrome does not jump to the modal loading UI as autofill can
// display that instead. Regression test for crbug.com/343480031.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
ConditionalMediationLoading) {
// Set up a trusted vault connection that lets us control the time it
// resolves.
base::OnceCallback<void(
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult)>
connection_cb;
auto connection = std::make_unique<
testing::NiceMock<MockTrustedVaultThrottlingConnection>>();
EXPECT_CALL(*connection, DownloadAuthenticationFactorsRegistrationState(
testing::_, testing::_, testing::_))
.WillOnce(
[&connection_cb](
const CoreAccountInfo&,
base::OnceCallback<void(
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult)>
callback,
base::RepeatingClosure _) mutable {
connection_cb = std::move(callback);
return std::make_unique<
trusted_vault::TrustedVaultConnection::Request>();
});
delegate_observer_->SetPendingTrustedVaultConnection(std::move(connection));
// Execute a conditional UI request.
AddTestPasskeyToModel();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionConditionalUI);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kPasskeyAutofill);
dialog_model()->OnAccountPreselectedIndex(0);
// The modal UI should not be shown yet.
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kPasskeyAutofill);
// Resolve the connection and wait for the next step.
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
std::move(connection_cb).Run(std::move(registration_state_result));
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
ConditionalAssertionWhileFullySetUp) {
// This test reproduces crbug.com/374366241. It performs a conditional request
// to generate an assertion and then triggers another conditional UI request.
// At one point this crashed.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionConditionalUI);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kPasskeyAutofill);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
dialog_model()->OnAccountPreselectedIndex(0);
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
content::ExecuteScriptAsync(web_contents, kGetAssertionConditionalUI);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kPasskeyAutofill);
// Not crashing here is success.
dialog_model()->OnAccountPreselectedIndex(0);
}
// Tests tapping a passkey from autofill after the trusted vault service times
// out. Regression test for crbug.com/343669719.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorWithTimeout,
SelectPasskeyAfterTimeout) {
SetVaultConnectionToTimeout();
// Execute a conditional UI request.
AddTestPasskeyToModel();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionConditionalUI);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kPasskeyAutofill);
// Have the request time out.
timer_task_runner_->FastForwardBy(
GPMEnclaveController::kDownloadAccountStateTimeout);
ASSERT_TRUE(model_observer()->loading_enclave_timed_out());
// Tap the passkey and expect an error.
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMError);
dialog_model()->OnAccountPreselectedIndex(0);
model_observer()->WaitForStep();
}
// Tests a trusted vault service timeout after tapping a passkey from autofill.
// Regression test for crbug.com/343669719.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorWithTimeout,
SelectPasskeyThenTimeout) {
SetVaultConnectionToTimeout();
// Execute a conditional UI request.
AddTestPasskeyToModel();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionConditionalUI);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kPasskeyAutofill);
// Tap the passkey. The step should be kPasskeyAutofill while autofill shows a
// loading indicator.
dialog_model()->OnAccountPreselectedIndex(0);
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kPasskeyAutofill);
// Wait for the request to time out.
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMError);
timer_task_runner_->FastForwardBy(
GPMEnclaveController::kDownloadAccountStateTimeout);
model_observer()->WaitForStep();
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, GpmEnclaveNeedsReauth) {
// Set the account state to a recoverable signin error.
auto* const identity_manager =
IdentityManagerFactory::GetForProfile(browser()->profile());
CoreAccountId account =
identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSignin);
signin::UpdatePersistentErrorOfRefreshTokenForAccount(
identity_manager, account,
GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
// Add a passkey to make sure it's not shown.
AddTestPasskeyToModel();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionUvDiscouraged);
delegate_observer()->WaitForUI();
// The dialog should always jump to the mechanism selection for signin errors.
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kMechanismSelection);
ASSERT_EQ(browser()->tab_strip_model()->GetTabCount(), 1);
// No credentials should be displayed since tapping on them won't work.
EXPECT_FALSE(
std::ranges::any_of(dialog_model()->mechanisms, [](const auto& m) {
return std::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::Credential>(m.type);
}));
// The button has text indicating the user they need to sign in.
const auto sign_in_again_mech =
std::ranges::find_if(dialog_model()->mechanisms, [](const auto& m) {
return std::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::SignInAgain>(m.type);
});
ASSERT_NE(sign_in_again_mech, dialog_model()->mechanisms.end());
EXPECT_EQ(sign_in_again_mech->name,
l10n_util::GetStringUTF16(IDS_WEBAUTHN_SIGN_IN_AGAIN_TITLE));
std::move(sign_in_again_mech->callback).Run();
// Tapping the button should cancel the request and open a new tab for reauth.
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_TRUE(script_result.starts_with("\"error NotAllowedError"))
<< script_result;
EXPECT_EQ(browser()->tab_strip_model()->GetTabCount(), 2);
}
// Tests that if the enclave is the default, but loading takes too long, the
// user is sent to the mechanism selection screen instead.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
EnclaveIsDefaultButTakesTooLong) {
// Set up a trusted vault connection that lets us control the time it
// resolves.
base::OnceCallback<void(
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult)>
connection_cb;
auto connection = std::make_unique<
testing::NiceMock<MockTrustedVaultThrottlingConnection>>();
EXPECT_CALL(*connection, DownloadAuthenticationFactorsRegistrationState(
testing::_, testing::_, testing::_))
.WillOnce(
[&connection_cb](
const CoreAccountInfo&,
base::OnceCallback<void(
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult)>
callback,
base::RepeatingClosure _) mutable {
connection_cb = std::move(callback);
return std::make_unique<
trusted_vault::TrustedVaultConnection::Request>();
});
delegate_observer_->SetPendingTrustedVaultConnection(std::move(connection));
// Execute a make credential request.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
// The UI should be made ready, but not shown yet.
delegate_observer()->WaitForUI();
ASSERT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kNotStarted);
// Wait for time it takes to decide to jump to the mechanism selection screen.
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kMechanismSelection);
timer_task_runner_->FastForwardBy(GPMEnclaveController::kLoadingTimeout);
model_observer()->WaitForStep();
EXPECT_FALSE(dialog_model()->ui_disabled_);
// Select Google Password Manager. This should trigger the loading UI.
dialog_model()->OnGPMSelected();
EXPECT_TRUE(dialog_model()->ui_disabled_);
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kMechanismSelection);
// Resolve the connection.
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerCreation);
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
std::move(connection_cb).Run(std::move(registration_state_result));
model_observer()->WaitForStep();
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
GpmEnclaveNeedsReauthOnGoogleCom) {
// Set the account state to a recoverable signin error.
auto* const identity_manager =
IdentityManagerFactory::GetForProfile(browser()->profile());
CoreAccountId account =
identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSignin);
signin::UpdatePersistentErrorOfRefreshTokenForAccount(
identity_manager, account,
GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
// Add a passkey to make sure it's not shown.
AddTestPasskeyToModel();
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("accounts.google.com", "/title1.html")));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents,
kGetAssertionUvDiscouragedWithGoogleRp);
delegate_observer()->WaitForUI();
ASSERT_EQ(browser()->tab_strip_model()->GetTabCount(), 1);
// The sign in button is not visible.
const auto sign_in_again_mech =
std::ranges::find_if(dialog_model()->mechanisms, [](const auto& m) {
return std::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::SignInAgain>(m.type);
});
ASSERT_EQ(sign_in_again_mech, dialog_model()->mechanisms.end());
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
UserResetsSecurityDomain) {
EnableUVKeySupport();
// Setup the EnclaveManager with a security domain secret.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
// Now simulate a security domain reset by increasing the epoch. Chrome must
// not create a credential encrypted to the previous security domain secret.
// Instead it should notice the reset and try to set up the security domain
// again.
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
registration_state_result.key_version = kSecretVersion + 1;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorWithTimeout,
SecurityDomainCheckTimesOut) {
EnableUVKeySupport();
// Setup the EnclaveManager with a security domain secret.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
// Now set the security domain check to timeout. Chrome should operate
// normally.
SetVaultConnectionToTimeout();
// Wait for the transport availability to be enumerated. The UI won't be shown
// yet because the enclave is not ready.
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMConnecting);
delegate_observer()->WaitForPreTransportAvailabilityEnumerated();
// Make the enclave ready by having the account state download time out.
timer_task_runner_->FastForwardBy(
GPMEnclaveController::kDownloadAccountStateTimeout);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
}
// Tests that receiving partial data from the security domain server resets the
// timeout timer.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorWithTimeout,
SecurityDomainKeepAlive) {
// Set up a trusted vault connection that lets us control the time it
// resolves.
base::OnceCallback<void(
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult)>
connection_cb;
base::RepeatingClosure keep_alive_cb;
auto connection = std::make_unique<
testing::NiceMock<MockTrustedVaultThrottlingConnection>>();
EXPECT_CALL(*connection, DownloadAuthenticationFactorsRegistrationState(
testing::_, testing::_, testing::_))
.WillOnce(
[&connection_cb, &keep_alive_cb](
const CoreAccountInfo&,
base::OnceCallback<void(
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult)>
callback,
base::RepeatingClosure keep_alive) mutable {
connection_cb = std::move(callback);
keep_alive_cb = std::move(keep_alive);
return std::make_unique<
trusted_vault::TrustedVaultConnection::Request>();
});
delegate_observer_->SetPendingTrustedVaultConnection(std::move(connection));
// Execute a make credential request.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
// Wait for the transport availability to be enumerated. The UI won't be shown
// yet because the enclave is not ready.
delegate_observer()->WaitForPreTransportAvailabilityEnumerated();
// Wait for 75% of the time it takes to time out. We should still be waiting.
timer_task_runner_->FastForwardBy(
GPMEnclaveController::kDownloadAccountStateTimeout * 0.75);
// Pretend we downloaded data but there's more to receive.
keep_alive_cb.Run();
ASSERT_FALSE(delegate_observer()->ui_shown());
// Wait for 75% of the time again. If everything goes right, we should not
// have timed out yet.
timer_task_runner_->FastForwardBy(
GPMEnclaveController::kDownloadAccountStateTimeout * 0.75);
ASSERT_FALSE(delegate_observer()->ui_shown());
// Resolve the connection.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
std::move(connection_cb).Run(std::move(registration_state_result));
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kTrustThisComputerCreation);
}
// Verifies that if the enclave activation takes a while and transport
// enumeration completes, the request is still successful.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorWithTimeout,
DelayedEnclaveActivation) {
// Set up a trusted vault connection that lets us control the time it
// resolves, so enclave manager initialization can be delayed.
base::OnceCallback<void(
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult)>
connection_cb;
auto connection = std::make_unique<
testing::NiceMock<MockTrustedVaultThrottlingConnection>>();
EXPECT_CALL(*connection, DownloadAuthenticationFactorsRegistrationState(
testing::_, testing::_, testing::_))
.WillOnce(
[&connection_cb](
const CoreAccountInfo&,
base::OnceCallback<void(
trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult)>
callback,
base::RepeatingClosure _) mutable {
connection_cb = std::move(callback);
return std::make_unique<
trusted_vault::TrustedVaultConnection::Request>();
});
delegate_observer_->SetPendingTrustedVaultConnection(std::move(connection));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
// Wait for the transport availability to be enumerated. The UI won't be shown
// yet because the enclave is not ready.
delegate_observer()->WaitForPreTransportAvailabilityEnumerated();
EXPECT_FALSE(
delegate_observer()->on_transport_availability_enumerated_called());
EXPECT_FALSE(delegate_observer()->ui_shown());
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
std::move(connection_cb).Run(std::move(registration_state_result));
delegate_observer()->WaitForUI();
}
#if BUILDFLAG(IS_MAC)
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, BiometricsInPWA) {
// When requesting biometrics in a PWA, Touch ID should never be used.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
EnableUVKeySupport();
SetBiometricsEnabled(true);
// Create a Browser of type `TYPE_APP`, like a PWA.
Browser* app_browser = Browser::Create(Browser::CreateParams::CreateForApp(
"appname", /*trusted_source=*/true, gfx::Rect(0, 0, 500, 500),
browser()->profile(),
/*user_gesture=*/true));
ASSERT_EQ(app_browser->type(), Browser::Type::TYPE_APP);
app_browser->window()->Show();
ASSERT_TRUE(NavigateToURLWithDisposition(
app_browser, https_server_.GetURL("www.example.com", "/title1.html"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
content::WebContents* web_contents =
app_browser->tab_strip_model()->GetActiveWebContents();
// Trigger a get() call to initialize the enclave. UV will be satisfied by
// entering the PIN.
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionUvRequired);
delegate_observer()->WaitForUI();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
// Do a get() call that uses biometrics. Check that Touch ID wasn't used.
content::ExecuteScriptAsync(web_contents, kGetAssertionUvRequired);
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
EXPECT_FALSE(
base::Contains(model_observer()->all_steps(),
AuthenticatorRequestDialogModel::Step::kGPMTouchID));
}
#endif
// Without a Windows-on-ARM device we've been unable to debug why these
// tests fail in that that context.
#if BUILDFLAG(IS_WIN) && defined(ARCH_CPU_ARM64)
#define MAYBE_NoGpmForCrossPlatformAttachment \
DISABLED_NoGpmForCrossPlatformAttachment
#else
#define MAYBE_NoGpmForCrossPlatformAttachment NoGpmForCrossPlatformAttachment
#endif
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
MAYBE_NoGpmForCrossPlatformAttachment) {
EnableUVKeySupport();
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialCrossPlatform);
delegate_observer()->WaitForUI();
EXPECT_TRUE(
std::ranges::none_of(dialog_model()->mechanisms, [](const auto& m) {
return std::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::Enclave>(m.type);
}));
EXPECT_TRUE(
request_delegate()->enclave_controller_for_testing()->is_active());
}
#if BUILDFLAG(IS_WIN) && defined(ARCH_CPU_ARM64)
#define MAYBE_NoGpmCreationIfPasswordManagerDisabled \
DISABLED_NoGpmCreationIfPasswordManagerDisabled
#else
#define MAYBE_NoGpmCreationIfPasswordManagerDisabled \
NoGpmCreationIfPasswordManagerDisabled
#endif
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
MAYBE_NoGpmCreationIfPasswordManagerDisabled) {
EnableUVKeySupport();
CheckRegistrationStateNotRequested();
browser()->profile()->GetPrefs()->SetBoolean(
password_manager::prefs::kCredentialsEnableService, false);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_TRUE(
std::ranges::none_of(dialog_model()->mechanisms, [](const auto& m) {
return std::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::Enclave>(m.type);
}));
EXPECT_FALSE(request_delegate()->enclave_controller_for_testing());
}
#if BUILDFLAG(IS_WIN) && defined(ARCH_CPU_ARM64)
#define MAYBE_NoGpmCreationIfPasswordManagerPasskeysDisabled \
DISABLED_NoGpmCreationIfPasswordManagerPasskeysDisabled
#else
#define MAYBE_NoGpmCreationIfPasswordManagerPasskeysDisabled \
NoGpmCreationIfPasswordManagerPasskeysDisabled
#endif
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
MAYBE_NoGpmCreationIfPasswordManagerPasskeysDisabled) {
EnableUVKeySupport();
CheckRegistrationStateNotRequested();
browser()->profile()->GetPrefs()->SetBoolean(
password_manager::prefs::kCredentialsEnablePasskeys, false);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_TRUE(
std::ranges::none_of(dialog_model()->mechanisms, [](const auto& m) {
return std::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::Enclave>(m.type);
}));
EXPECT_FALSE(request_delegate()->enclave_controller_for_testing());
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, EnrollAndCreate) {
EnableUVKeySupport();
security_domain_service_->pretend_there_are_members();
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kTrustThisComputerCreation);
EXPECT_TRUE(
request_delegate()->enclave_controller_for_testing()->is_active());
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
GetAssertionWithPlatformUV) {
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
EnableUVKeySupport();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
dialog_model()->OnUserConfirmedPriorityMechanism();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
}
// Tests hiding a passkey using the Signal API, then restoring it.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
SignalApiHideAndRestorePasskey) {
AddTestPasskeyToModel();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
// Make a request and expect to see the credential listed as a mechanism.
content::ExecuteScriptAsync(web_contents, kGetAssertionUvDiscouraged);
delegate_observer()->WaitForUI();
std::vector<uint8_t> user_id =
std::get<AuthenticatorRequestDialogModel::Mechanism::Credential>(
dialog_model()
->mechanisms.at(*dialog_model()->priority_mechanism_index)
.type)
->user_id;
EXPECT_EQ(base::span(user_id), TestProtobufUserId());
dialog_model()->CancelAuthenticatorRequest();
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
// Hide the passkey.
content::ExecuteScriptAsync(web_contents, kSignalHideTestPasskey);
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ("\"webauthn: OK\"", script_result);
// The credential should not be offered in the next request.
content::ExecuteScriptAsync(web_contents, kGetAssertionUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_TRUE(
std::ranges::none_of(dialog_model()->mechanisms, [](const auto& mech) {
return std::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::Credential>(mech.type);
}));
dialog_model()->CancelAuthenticatorRequest();
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
// Restore the passkey.
content::ExecuteScriptAsync(web_contents, kSignalRestoreTestPasskey);
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ("\"webauthn: OK\"", script_result);
// Make a request and expect to see the credential listed again.
content::ExecuteScriptAsync(web_contents, kGetAssertionUvDiscouraged);
delegate_observer()->WaitForUI();
user_id = std::get<AuthenticatorRequestDialogModel::Mechanism::Credential>(
dialog_model()
->mechanisms.at(*dialog_model()->priority_mechanism_index)
.type)
->user_id;
EXPECT_EQ(base::span(user_id), TestProtobufUserId());
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
NotForSameGoogleAccount) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("accounts.google.com", "/title1.html")));
EnableUVKeySupport();
// The enclave should not appear when attempting to create a Google passkey
// for the same account.
{
CheckRegistrationStateNotRequested();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(
web_contents,
base::ReplaceStringPlaceholders(kMakeCredentialGoogle, {kEmail},
/*offsets=*/nullptr));
delegate_observer()->WaitForUI();
EXPECT_TRUE(
std::ranges::none_of(dialog_model()->mechanisms, [](const auto& m) {
return std::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::Enclave>(m.type);
}));
dialog_model()->CancelAuthenticatorRequest();
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
delegate_observer()->WaitForDelegateDestruction();
}
// For Google-internal users, the username in the create request is just the
// local part of the email address. Enclave should not appear for those cases
// either.
{
CheckRegistrationStateNotRequested();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(
web_contents, base::ReplaceStringPlaceholders(kMakeCredentialGoogle,
{kEmailLocalPartOnly},
/*offsets=*/nullptr));
delegate_observer()->WaitForUI();
EXPECT_TRUE(
std::ranges::none_of(dialog_model()->mechanisms, [](const auto& m) {
return std::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::Enclave>(m.type);
}));
dialog_model()->CancelAuthenticatorRequest();
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
delegate_observer()->WaitForDelegateDestruction();
}
// But trying to create a passkey for a different account is fine.
{
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state =
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::
State::kRecoverable;
SetMockVaultConnectionOnRequestDelegate(
std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(
web_contents,
base::ReplaceStringPlaceholders(kMakeCredentialGoogle,
{std::string(kEmail) + "_different"},
/*offsets=*/nullptr));
delegate_observer()->WaitForUI();
EXPECT_FALSE(
std::ranges::none_of(dialog_model()->mechanisms, [](const auto& m) {
return std::holds_alternative<
AuthenticatorRequestDialogModel::Mechanism::Enclave>(m.type);
}));
dialog_model()->CancelAuthenticatorRequest();
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
delegate_observer()->WaitForDelegateDestruction();
}
}
// Tests that an allow list filters the available GPM credentials.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
GetAssertionWithAllowList) {
const std::vector<uint8_t> kCredId1 = {1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16};
constexpr char kUserName1[] = "ruby";
const std::vector<uint8_t> kCredId2 = {16, 15, 14, 13, 12, 11, 10, 9,
8, 7, 6, 5, 4, 3, 2, 1};
constexpr char kUserName2[] = "yang";
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
sync_pb::WebauthnCredentialSpecifics passkey1;
CHECK(passkey1.ParseFromArray(kTestProtobuf, sizeof(kTestProtobuf)));
passkey1.set_sync_id(kCredId1.data(), kCredId1.size());
passkey1.set_credential_id(kCredId1.data(), kCredId1.size());
passkey1.set_user_id(kCredId1.data(), kCredId1.size());
passkey1.set_user_name(kUserName1);
passkey1.set_user_display_name(kUserName1);
passkey_model()->AddNewPasskeyForTesting(passkey1);
sync_pb::WebauthnCredentialSpecifics passkey2;
CHECK(passkey2.ParseFromArray(kTestProtobuf, sizeof(kTestProtobuf)));
passkey1.set_sync_id(kCredId2.data(), kCredId2.size());
passkey2.set_credential_id(kCredId2.data(), kCredId2.size());
passkey2.set_user_id(kCredId2.data(), kCredId2.size());
passkey2.set_user_name(kUserName2);
passkey1.set_user_display_name(kUserName2);
passkey_model()->AddNewPasskeyForTesting(passkey2);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionCredId1);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
// Only the first passkey should be included in the request.
std::optional<int> found;
for (size_t i = 0; i < dialog_model()->mechanisms.size(); ++i) {
if (IsMechanismEnclaveCredential(dialog_model()->mechanisms[i])) {
ASSERT_EQ(
std::get<AuthenticatorRequestDialogModel::Mechanism::Credential>(
dialog_model()->mechanisms[i].type)
->user_id,
kCredId1);
found = i;
}
}
ASSERT_TRUE(found);
EXPECT_EQ(found, dialog_model()->priority_mechanism_index);
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
IncognitoModeMakeCredential) {
Browser* otr_browser = OpenURLOffTheRecord(
browser()->profile(),
https_server_.GetURL("www.example.com", "/title1.html"));
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
// Initially bootstrap from LSKF, ensuring the incognito warning is shown.
content::WebContents* web_contents =
otr_browser->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(
dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMConfirmOffTheRecordCreate);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerCreation);
dialog_model()->OnGPMConfirmOffTheRecordCreate();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
delegate_observer()->WaitForDelegateDestruction();
// Ensure the incognito warning is also shown in the non-bootstrapping flow.
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(
dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMConfirmOffTheRecordCreate);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
dialog_model()->OnGPMConfirmOffTheRecordCreate();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMEnterPin);
dialog_model()->OnGPMCreatePasskey();
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
IncognitoModeGetAssertion) {
Browser* otr_browser = OpenURLOffTheRecord(
browser()->profile(),
https_server_.GetURL("www.example.com", "/title1.html"));
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
content::WebContents* web_contents =
otr_browser->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
dialog_model()->OnUserConfirmedPriorityMechanism();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
}
#if BUILDFLAG(IS_MAC)
bool MacBiometricApisAvailable() {
if (__builtin_available(macOS 12, *)) {
return true;
}
return false;
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
BiometricsDisabledDuringRequest) {
if (!MacBiometricApisAvailable()) {
GTEST_SKIP() << "Need macOS >= 12";
}
// If Touch ID is disabled during the course of a request, the UV disposition
// shouldn't also change. I.e. if we started with the expectation of doing
// UV=true, the UI expects that to continue, even if we need macOS to prompt
// for the system password.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
EnableUVKeySupport();
SetBiometricsEnabled(true);
// The first get() request is satisfied implicitly because recovery was done.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionUvPreferred);
delegate_observer()->WaitForUI();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
// During this second get() request, Touch ID will be disabled.
content::ExecuteScriptAsync(web_contents, kGetAssertionUvPreferred);
delegate_observer()->WaitForUI();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMTouchID);
model_observer()->WaitForStep();
SetBiometricsEnabled(false);
// Disable Touch ID. The request should still resolve with uv=true.
request_delegate()->dialog_model()->OnTouchIDComplete(false);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
}
constexpr char kICloudKeychainRecoveryKeyAccessGroup[] =
MAC_TEAM_IDENTIFIER_STRING ".com.google.common.folsom";
class EnclaveICloudRecoveryKeyTest : public EnclaveAuthenticatorBrowserTest {
protected:
crypto::ScopedFakeAppleKeychainV2 scoped_fake_apple_keychain_{
kICloudKeychainRecoveryKeyAccessGroup};
};
// Tests enrolling an iCloud recovery key when there are no keys already
// enrolled with the recovery service or present in iCloud keychain.
IN_PROC_BROWSER_TEST_F(EnclaveICloudRecoveryKeyTest, Enroll) {
// Do a make credential request and enroll a PIN.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
delegate_observer()->WaitForDelegateDestruction();
// Find the iCloud recovery key member.
const auto icloud_member = std::ranges::find_if(
security_domain_service_->members(),
[](const trusted_vault_pb::SecurityDomainMember& member) {
return member.member_type() == trusted_vault_pb::SecurityDomainMember::
MEMBER_TYPE_ICLOUD_KEYCHAIN;
});
ASSERT_NE(icloud_member, security_domain_service_->members().end());
// Find the recovery key on iCloud keychain.
base::test::TestFuture<
std::vector<std::unique_ptr<trusted_vault::ICloudRecoveryKey>>>
future;
trusted_vault::ICloudRecoveryKey::Retrieve(
future.GetCallback(), trusted_vault::SecurityDomainId::kPasskeys,
kICloudKeychainRecoveryKeyAccessGroup);
EXPECT_TRUE(future.Wait());
std::vector<std::unique_ptr<trusted_vault::ICloudRecoveryKey>> recovery_keys =
future.Take();
ASSERT_EQ(recovery_keys.size(), 1u);
std::unique_ptr<trusted_vault::ICloudRecoveryKey> icloud_key =
std::move(recovery_keys.at(0));
// Make sure they match.
EXPECT_EQ(trusted_vault::ProtoStringToBytes(icloud_member->public_key()),
icloud_key->key()->public_key().ExportToBytes());
}
// Tests enrolling an iCloud recovery key when there is already a recovery key
// stored in iCloud keychain. A new key should be created.
// Regression test for https://crbug.com/360321350.
IN_PROC_BROWSER_TEST_F(EnclaveICloudRecoveryKeyTest,
EnrollWithExistingKeyInICloud) {
// Create an iCloud recovery key.
base::test::TestFuture<std::unique_ptr<trusted_vault::ICloudRecoveryKey>>
future;
trusted_vault::ICloudRecoveryKey::Create(
future.GetCallback(), trusted_vault::SecurityDomainId::kPasskeys,
kICloudKeychainRecoveryKeyAccessGroup);
EXPECT_TRUE(future.Wait());
std::unique_ptr<trusted_vault::ICloudRecoveryKey> existing_icloud_key =
future.Take();
ASSERT_TRUE(existing_icloud_key);
// Do a make credential request and enroll a PIN.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
delegate_observer()->WaitForDelegateDestruction();
// Find the iCloud recovery key member.
const auto icloud_member = std::ranges::find_if(
security_domain_service_->members(),
[](const trusted_vault_pb::SecurityDomainMember& member) {
return member.member_type() == trusted_vault_pb::SecurityDomainMember::
MEMBER_TYPE_ICLOUD_KEYCHAIN;
});
ASSERT_NE(icloud_member, security_domain_service_->members().end());
// Make sure it does not match the existing key.
EXPECT_NE(trusted_vault::ProtoStringToBytes(icloud_member->public_key()),
existing_icloud_key->key()->public_key().ExportToBytes());
// Instead, a new key should have been created.
base::test::TestFuture<
std::vector<std::unique_ptr<trusted_vault::ICloudRecoveryKey>>>
list_future;
trusted_vault::ICloudRecoveryKey::Retrieve(
list_future.GetCallback(), trusted_vault::SecurityDomainId::kPasskeys,
kICloudKeychainRecoveryKeyAccessGroup);
EXPECT_TRUE(list_future.Wait());
std::vector<std::unique_ptr<trusted_vault::ICloudRecoveryKey>> recovery_keys =
list_future.Take();
EXPECT_EQ(recovery_keys.size(), 2u);
}
// TODO(crbug.com/368799197): The test is flaky.
// Tests enrolling an iCloud recovery key, then recovering from it.
IN_PROC_BROWSER_TEST_F(EnclaveICloudRecoveryKeyTest, DISABLED_Recovery) {
{
// Do a make credential request and enroll a PIN.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(
std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
delegate_observer()->WaitForDelegateDestruction();
// Make sure a new recovery key was enrolled.
base::test::TestFuture<
std::vector<std::unique_ptr<trusted_vault::ICloudRecoveryKey>>>
future;
trusted_vault::ICloudRecoveryKey::Retrieve(
future.GetCallback(), trusted_vault::SecurityDomainId::kPasskeys,
kICloudKeychainRecoveryKeyAccessGroup);
EXPECT_TRUE(future.Wait());
ASSERT_EQ(future.Get().size(), 1u);
}
// Unenroll the current device from the enclave.
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->ClearRegistrationForTesting();
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->ResetForTesting();
// Do a make credential request and recover with the iCloud key.
{
// Set up the mock trusted vault connection to download the iCloud recovery
// factor that should have been added.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
const auto pin_member = std::ranges::find_if(
security_domain_service_->members(),
[](const trusted_vault_pb::SecurityDomainMember& member) {
return member.member_type() ==
trusted_vault_pb::SecurityDomainMember::
MEMBER_TYPE_GOOGLE_PASSWORD_MANAGER_PIN;
});
const auto& pin_metadata =
pin_member->member_metadata().google_password_manager_pin_metadata();
registration_state_result.gpm_pin_metadata = trusted_vault::GpmPinMetadata(
pin_member->public_key(),
trusted_vault::UsableRecoveryPinMetadata(
pin_metadata.encrypted_pin_hash(),
base::Time::FromSecondsSinceUnixEpoch(
pin_metadata.expiration_time().seconds())));
registration_state_result.state =
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::
State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
const auto icloud_member = std::ranges::find_if(
security_domain_service_->members(),
[](const trusted_vault_pb::SecurityDomainMember& member) {
return member.member_type() ==
trusted_vault_pb::SecurityDomainMember::
MEMBER_TYPE_ICLOUD_KEYCHAIN;
});
ASSERT_NE(icloud_member, security_domain_service_->members().end());
std::vector<trusted_vault::MemberKeys> member_keys;
auto member_key = icloud_member->memberships().at(0).keys().at(0);
member_keys.emplace_back(
member_key.epoch(),
std::vector<uint8_t>(member_key.wrapped_key().begin(),
member_key.wrapped_key().end()),
std::vector<uint8_t>(member_key.member_proof().begin(),
member_key.member_proof().end()));
registration_state_result.icloud_keys.emplace_back(
trusted_vault::SecureBoxPublicKey::CreateByImport(
std::vector<uint8_t>(icloud_member->public_key().begin(),
icloud_member->public_key().end())),
std::move(member_keys));
SetMockVaultConnectionOnRequestDelegate(
std::move(registration_state_result));
// Running the request should result in recovering automatically after the
// "Trust this computer" screen.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(
dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kTrustThisComputerCreation);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
// User verification must not be skipped when recovering from an iCloud key.
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMEnterPin);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
delegate_observer()->WaitForDelegateDestruction();
// Make sure no new recovery key was enrolled.
base::test::TestFuture<
std::vector<std::unique_ptr<trusted_vault::ICloudRecoveryKey>>>
future;
trusted_vault::ICloudRecoveryKey::Retrieve(
future.GetCallback(), trusted_vault::SecurityDomainId::kPasskeys,
kICloudKeychainRecoveryKeyAccessGroup);
EXPECT_TRUE(future.Wait());
ASSERT_EQ(future.Get().size(), 1u);
}
}
#endif // BUILDFLAG(IS_MAC)
#if BUILDFLAG(IS_MAC)
#define MAYBE_MakeCredentialDeclineGPM DISABLED_MakeCredentialDeclineGPM
#else
#define MAYBE_MakeCredentialDeclineGPM MakeCredentialDeclineGPM
#endif
// TODO(crbug.com/345308672): Failing on various Mac bots.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
MAYBE_MakeCredentialDeclineGPM) {
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
delegate_observer()->AddAdditionalTransport(
device::FidoTransportProtocol::kInternal);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
delegate_observer()->WaitForDelegateDestruction();
// With the enclave configured, the next request should offer GPM as a
// priority mechanism for an attachment=platform request.
content::ExecuteScriptAsync(web_contents, kMakeCredentialAttachmentPlatform);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kMechanismSelection);
dialog_model()->StartOver();
model_observer()->WaitForStep();
dialog_model()->CancelAuthenticatorRequest();
delegate_observer()->WaitForDelegateDestruction();
content::ExecuteScriptAsync(web_contents, kMakeCredentialAttachmentPlatform);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kMechanismSelection);
dialog_model()->StartOver();
model_observer()->WaitForStep();
dialog_model()->CancelAuthenticatorRequest();
delegate_observer()->WaitForDelegateDestruction();
// After backing out of GPM twice, the next attempt should default to
// either mechanism selection or, on Mac, the custom platform authenticator
// passkey creation dialog.
content::ExecuteScriptAsync(web_contents, kMakeCredentialAttachmentPlatform);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kMechanismSelection);
dialog_model()->CancelAuthenticatorRequest();
delegate_observer()->WaitForDelegateDestruction();
}
class EnclaveAuthenticatorIncognitoBrowserTest
: public EnclaveAuthenticatorBrowserTest,
public testing::WithParamInterface<bool> {};
// Attempt a GetAssertion multiple times with GPM passkey bootstrapping
// offered, and decline each time. The default should change away from GPM after
// two times declined.
IN_PROC_BROWSER_TEST_P(EnclaveAuthenticatorIncognitoBrowserTest,
MultipleDeclinedBootstrappings) {
content::WebContents* web_contents;
if (GetParam()) {
Browser* otr_browser = OpenURLOffTheRecord(
browser()->profile(),
https_server_.GetURL("www.example.com", "/title1.html"));
web_contents = otr_browser->tab_strip_model()->GetActiveWebContents();
} else {
web_contents = browser()->tab_strip_model()->GetActiveWebContents();
}
EnableUVKeySupport();
delegate_observer()->SetUseSyncedDeviceCablePairing(/*use_pairing=*/true);
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
AddTestPasskeyToModel();
content::ExecuteScriptAsync(web_contents, kGetAssertionUvRequired);
delegate_observer()->WaitForUI();
// Enclave will be the priority mechanism. Select it.
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
dialog_model()->OnUserConfirmedPriorityMechanism();
model_observer()->WaitForStep();
// Now cancel the request...
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kMechanismSelection);
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
dialog_model()->StartOver();
model_observer()->WaitForStep();
// ...and select it again from the mechanism list.
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
EXPECT_TRUE(std::ranges::any_of(
dialog_model()->mechanisms,
[](const auto& m) { return IsMechanismEnclaveCredential(m); }));
for (auto& mechanism : request_delegate_->dialog_model()->mechanisms) {
if (IsMechanismEnclaveCredential(mechanism)) {
mechanism.callback.Run();
break;
}
}
model_observer()->WaitForStep();
// Cancel the request once more.
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kMechanismSelection);
dialog_model()->StartOver();
model_observer()->WaitForStep();
// Terminate the request and send a new one so newly-enumerated credentials
// will be used.
dialog_model()->CancelAuthenticatorRequest();
delegate_observer()->WaitForDelegateDestruction();
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::ExecuteScriptAsync(web_contents, kGetAssertionUvRequired);
delegate_observer()->WaitForUI();
// Passkeys from GPM should still be present, but they should not be the
// default.
EXPECT_TRUE(std::ranges::any_of(
dialog_model()->mechanisms,
[](const auto& m) { return IsMechanismEnclaveCredential(m); }));
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kMechanismSelection);
// Finally, if the user manually chooses the enclave, it should be the default
// again. Attempting to bootstrap should be enough.
dialog_model()->OnGPMSelected();
if (GetParam()) {
EXPECT_EQ(
dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMConfirmOffTheRecordCreate);
dialog_model()->OnGPMConfirmOffTheRecordCreate();
}
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kTrustThisComputerCreation);
dialog_model()->OnTrustThisComputer();
// Terminate the request and send a new one. The enclave should once again be
// the default.
dialog_model()->CancelAuthenticatorRequest();
delegate_observer()->WaitForDelegateDestruction();
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
content::ExecuteScriptAsync(web_contents, kGetAssertionUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
}
INSTANTIATE_TEST_SUITE_P(Incognito,
EnclaveAuthenticatorIncognitoBrowserTest,
testing::Values(false, true));
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
ChangedPINDetectedWhenDoingUV) {
// Set up an account with a GPM PIN and create a credential. Then create a
// second `EnclaveManager` to change the PIN. Lastly, assert that credential
// with the updated GPM PIN for UV. This tests that the updated PIN is used
// for the UV.
const std::string pin = "123456";
const std::string newpin = "111111";
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(base::UTF8ToUTF16(pin));
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
EXPECT_EQ(security_domain_service_->num_physical_members(), 1u);
EXPECT_EQ(security_domain_service_->num_pin_members(), 1u);
const std::optional<std::vector<uint8_t>> security_domain_secret =
FakeMagicArch::RecoverWithPIN(pin, *security_domain_service_,
*recovery_key_store_);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMEnterPin);
dialog_model()->OnGPMCreatePasskey();
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(base::UTF8ToUTF16(pin));
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
{
Profile* const profile = browser()->profile();
EnclaveManager second_manager(
temp_dir_.GetPath(),
IdentityManagerFactory::GetForProfile(browser()->profile()),
base::BindRepeating(
[](base::WeakPtr<Profile> profile)
-> network::mojom::NetworkContext* {
if (!profile) {
return nullptr;
}
return profile->GetDefaultStoragePartition()->GetNetworkContext();
},
profile->GetWeakPtr()),
url_loader_factory_.GetSafeWeakWrapper());
second_manager.StoreKeys(kGaiaId, {*security_domain_secret},
/*last_key_version=*/kSecretVersion);
base::test::TestFuture<bool> add_future;
second_manager.AddDeviceToAccount(std::nullopt, add_future.GetCallback());
EXPECT_TRUE(add_future.Wait());
EXPECT_TRUE(add_future.Get());
base::test::TestFuture<bool> change_future;
second_manager.ChangePIN(newpin, "rapt", change_future.GetCallback());
EXPECT_TRUE(change_future.Wait());
ASSERT_TRUE(change_future.Get());
}
content::ExecuteScriptAsync(web_contents, kGetAssertionUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMEnterPin);
dialog_model()->OnUserConfirmedPriorityMechanism();
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(base::UTF8ToUTF16(newpin));
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
}
#if BUILDFLAG(IS_LINUX)
// These tests are run on Linux because Linux has no platform authenticator
// that can effect whether IsUVPAA returns true or not.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, IsUVPAA) {
// With the enclave authenticator in place, IsUVPAA should return true.
EXPECT_TRUE(IsUVPAA());
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, IsUVPAA_GoogleSite) {
// With the enclave authenticator in place, IsUVPAA should return false for
// google.com sites because we won't create a credential for an account in
// that same account. But since we don't know the user.id value at IsUVPAA
// time, the result has to be conservative.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("accounts.google.com", "/title1.html")));
EXPECT_FALSE(IsUVPAA());
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
IsUVPAA_NoUnexportableKeys) {
// Without support for unexportable keys, IsUVPAA should return false because
// the enclave cannot be used.
fake_hw_provider_.reset();
crypto::ScopedNullUnexportableKeyProvider no_hw_key_support;
EXPECT_FALSE(IsUVPAA());
}
#endif // IS_LINUX
// Verify that GPM will do UV on a uv=preferred request if and only if
// biometrics are available.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest,
UserVerificationPolicy) {
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
EnableUVKeySupport();
// The first get() request is satisfied implicitly because recovery was done.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionUvPreferred);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
dialog_model()->OnUserConfirmedPriorityMechanism();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
SetBiometricsEnabled(false);
content::ExecuteScriptAsync(web_contents, kGetAssertionUvPreferred);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
dialog_model()->OnUserConfirmedPriorityMechanism();
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=false\"");
// On Linux biometrics is not available so the test is done.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_MAC)
if (!MacBiometricApisAvailable()) {
return;
}
#endif
SetBiometricsEnabled(true);
content::ExecuteScriptAsync(web_contents, kGetAssertionUvPreferred);
delegate_observer()->WaitForUI();
#if BUILDFLAG(IS_MAC)
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMTouchID);
dialog_model()->OnTouchIDComplete(true);
#else
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
dialog_model()->OnUserConfirmedPriorityMechanism();
#endif
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
#endif
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, Bug_354083161) {
// Reproduces the crash from b/354083161
// Do an assertion to set up the enclave.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
dialog_model()->OnUserConfirmedPriorityMechanism();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
// Do an assertion to trigger the UI pattern that caused the crash.
content::ExecuteScriptAsync(
web_contents,
base::ReplaceStringPlaceholders(
kGetAssertionUvDiscouragedWithCredId,
{base::Base64Encode(TestProtobufCredId())}, /*offsets=*/nullptr));
delegate_observer()->WaitForUI();
ASSERT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
dialog_model()->OnUserConfirmedPriorityMechanism();
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, NoSilentOperations) {
// Check that the enclave doesn't allow silent operations.
// Do an assertion to set up the enclave.
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionUvDiscouraged);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kRecoverable);
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
dialog_model()->OnUserConfirmedPriorityMechanism();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
model_observer()->WaitForStep();
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: OK\"");
// Do an assertion with an allowlist and check that the assertion isn't
// immediately run.
content::ExecuteScriptAsync(
web_contents,
base::ReplaceStringPlaceholders(
// Setting the transport hint to just `internal` means that there are
// no other mechanisms and so the credential will be the priority
// mechanism.
kGetAssertionUvDiscouragedWithCredIdAndInternalTransport,
{base::Base64Encode(TestProtobufCredId())}, /*offsets=*/nullptr));
delegate_observer()->WaitForUI();
// The UI must not be, e.g., kGPMConnecting as that indicates that the
// operation is happening without any UI.
ASSERT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kSelectPriorityMechanism);
}
// Allows a `BlockingUnexportableKeyProvider` to block inside a thread-pool
// thread so that the main test can synchronize with it on the UI thread.
class BlockingUnexportableKeyProviderRendezvous {
public:
void Block() {
base::ScopedAllowBaseSyncPrimitivesForTesting locks_allowed;
base::AutoLock locked(lock_);
blocked_ = true;
while (!ready_to_continue_) {
condition_.Wait();
}
}
bool IsBlocked() {
base::AutoLock locked(lock_);
return blocked_;
}
void Continue() {
base::AutoLock locked(lock_);
CHECK(!ready_to_continue_);
ready_to_continue_ = true;
condition_.Broadcast();
}
private:
base::Lock lock_;
base::ConditionVariable condition_{&lock_};
bool blocked_ GUARDED_BY(lock_) = false;
bool ready_to_continue_ GUARDED_BY(lock_) = false;
};
BlockingUnexportableKeyProviderRendezvous&
GetBlockingUnexportableKeyProviderRendezvous() {
static base::NoDestructor<BlockingUnexportableKeyProviderRendezvous> instance;
return *instance;
}
// An `UnexportableKeyProvider` that blocks inside `SelectAlgorithm` and waits
// for the UI thread to synchronize with it. It doesn't implement any other
// functions.
class BlockingUnexportableKeyProvider : public crypto::UnexportableKeyProvider {
public:
std::optional<crypto::SignatureVerifier::SignatureAlgorithm> SelectAlgorithm(
base::span<const crypto::SignatureVerifier::SignatureAlgorithm>
acceptable_algorithms) override {
CHECK(!acceptable_algorithms.empty());
// This function runs in a thread-pool thread.
GetBlockingUnexportableKeyProviderRendezvous().Block();
return acceptable_algorithms[0];
}
std::unique_ptr<crypto::UnexportableSigningKey> GenerateSigningKeySlowly(
base::span<const crypto::SignatureVerifier::SignatureAlgorithm>
acceptable_algorithms) override {
NOTREACHED();
}
std::unique_ptr<crypto::UnexportableSigningKey> FromWrappedSigningKeySlowly(
base::span<const uint8_t> wrapped_key) override {
NOTREACHED();
}
bool DeleteSigningKeySlowly(base::span<const uint8_t> wrapped_key) override {
NOTREACHED();
}
};
std::unique_ptr<crypto::UnexportableKeyProvider>
BlockingUnexportableKeyProviderFactory() {
return std::make_unique<BlockingUnexportableKeyProvider>();
}
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, CancelRacesTPMCheck) {
// https://crbug.com/352532554
// Set the UnexportableKeyProvider to one that will block inside
// `SelectAlgorithm` so that we can simulate a slow TPM check.
fake_hw_provider_.reset();
crypto::internal::SetUnexportableKeyProviderForTesting(
BlockingUnexportableKeyProviderFactory);
// Start a WebAuthn request. It'll block when checking the TPM.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
EXPECT_TRUE(content::ExecJs(web_contents, kAbortableGetAssertion));
// Wait until the request is blocked on checking the TPM.
auto run_loop = std::make_unique<base::RunLoop>();
while (!GetBlockingUnexportableKeyProviderRendezvous().IsBlocked()) {
run_loop->RunUntilIdle();
}
// Cancel the outstanding request.
EXPECT_TRUE(content::ExecJs(web_contents, kAbort));
// Let the TPM check complete.
GetBlockingUnexportableKeyProviderRendezvous().Continue();
run_loop->RunUntilIdle();
// This test is successful if it doesn't crash. It reliably crashed prior to
// the fix for https://crbug.com/352532554.
}
// Regression test for crbug.com/399937685.
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorBrowserTest, SelectDeletedPasskey) {
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
// Set up a conditional UI request.
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kGetAssertionConditionalUI);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kPasskeyAutofill);
// Delete the credential during the request. Websites could do this through
// the signal API, and users through the password manager.
passkey_model()->DeleteAllPasskeys();
// Go through all the steps to get the enclave set up.
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
dialog_model()->OnAccountPreselectedIndex(0);
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMError);
dialog_model()->OnGPMPinEntered(u"123456");
model_observer()->WaitForStep();
}
class EnclaveAuthenticatorConditionalCreateBrowserTest
: public EnclaveAuthenticatorBrowserTest,
public testing::WithParamInterface<bool> {
protected:
EnclaveAuthenticatorConditionalCreateBrowserTest() {
sync_feature_enabled_ = GetParam();
scoped_feature_list_.InitAndEnableFeature(device::kWebAuthnPasskeyUpgrade);
CHECK(base::FeatureList::IsEnabled(device::kWebAuthnPasskeyUpgrade));
}
bool use_account_password_store() { return !sync_feature_enabled_; }
password_manager::PasswordStoreInterface* password_store() {
return use_account_password_store()
? AccountPasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get()
: ProfilePasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get();
}
// Creates a credential to ensure the enclave authenticator is in a usable
// state prior to making a conditional create request.
void BootstrapEnclave() {
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
SetMockVaultConnectionOnRequestDelegate(
std::move(registration_state_result));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
delegate_observer()->WaitForUI();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
EXPECT_EQ(request_delegate()
->enclave_controller_for_testing()
->account_state_for_testing(),
GPMEnclaveController::AccountState::kEmpty);
dialog_model()->OnGPMCreatePasskey();
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
dialog_model()->OnGPMPinEntered(u"123456");
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
}
void InjectPassword(base::Time last_used) {
password_manager::PasswordForm saved_form;
saved_form.signon_realm = https_server_.GetURL("example.com", "/").spec();
saved_form.url = https_server_.GetURL("example.com",
"/password/prefilled_username.html");
saved_form.username_value = base::UTF8ToUTF16(std::string(kEmail));
saved_form.password_value = u"hunter1";
saved_form.date_last_used = last_used;
password_store()->AddLogin(saved_form);
}
sync_pb::WebauthnCredentialSpecifics InjectPasskey() {
sync_pb::WebauthnCredentialSpecifics passkey;
CHECK(passkey.ParseFromArray(kTestProtobuf, sizeof(kTestProtobuf)));
// Sync ID and credential ID must be 16 bytes long.
passkey.set_sync_id(base::RandBytesAsString(16));
passkey.set_credential_id(base::RandBytesAsString(16));
passkey.set_user_id(base::RandBytesAsString(16));
passkey.set_user_name(kEmail);
passkey.set_user_display_name(kEmail);
passkey_model()->AddNewPasskeyForTesting(passkey);
return passkey;
}
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(WithSyncFeatureEnabled,
EnclaveAuthenticatorConditionalCreateBrowserTest,
testing::Bool());
IN_PROC_BROWSER_TEST_P(EnclaveAuthenticatorConditionalCreateBrowserTest,
ConditionalCreate) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
BootstrapEnclave();
InjectPassword(base::Time::Now());
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialConditionalCreate);
delegate_observer()->WaitForUI();
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
std::optional<std::vector<uint8_t>> cred_id =
ParseCredentialId(script_result);
EXPECT_TRUE(cred_id);
// The request should not have instantiated non-enclave discoveries.
ASSERT_TRUE(
delegate_observer()->on_transport_availability_enumerated_called());
EXPECT_TRUE(delegate_observer()->transports_observed()->empty());
}
IN_PROC_BROWSER_TEST_P(EnclaveAuthenticatorConditionalCreateBrowserTest,
ConditionalCreate_FailsWithSettingDisabled) {
browser()->profile()->GetPrefs()->SetBoolean(
password_manager::prefs::kAutomaticPasskeyUpgrades, false);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
BootstrapEnclave();
InjectPassword(base::Time::Now());
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialConditionalCreate);
delegate_observer()->WaitForUI();
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_THAT(script_result, testing::HasSubstr("NotAllowedError"));
}
// Regression test for crbug.com/414750307.
IN_PROC_BROWSER_TEST_P(EnclaveAuthenticatorConditionalCreateBrowserTest,
ConditionalCreate_FailsWithGPMDisabledByPolicy) {
// Disabling GPM via policy should cause upgrade requests to fail.
browser()->profile()->GetPrefs()->SetBoolean(
password_manager::prefs::kCredentialsEnableService, false);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
InjectPassword(base::Time::Now());
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialConditionalCreate);
delegate_observer()->WaitForUI();
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_THAT(script_result, testing::HasSubstr("NotAllowedError"));
}
// Regression test for crbug.com/414750307.
IN_PROC_BROWSER_TEST_P(EnclaveAuthenticatorConditionalCreateBrowserTest,
ConditionalCreate_FailsWithGPMPasskeysDisabledByPolicy) {
// Disabling GPM passkeys via policy should cause upgrade requests to fail.
browser()->profile()->GetPrefs()->SetBoolean(
password_manager::prefs::kCredentialsEnablePasskeys, false);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
InjectPassword(base::Time::Now());
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialConditionalCreate);
delegate_observer()->WaitForUI();
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_THAT(script_result, testing::HasSubstr("NotAllowedError"));
}
IN_PROC_BROWSER_TEST_P(EnclaveAuthenticatorConditionalCreateBrowserTest,
ConditionalCreate_FailsWithoutBootstrappedEnclave) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
InjectPassword(base::Time::Now());
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialConditionalCreate);
delegate_observer()->WaitForUI();
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_THAT(script_result, testing::HasSubstr("NotAllowedError"));
}
IN_PROC_BROWSER_TEST_P(EnclaveAuthenticatorConditionalCreateBrowserTest,
ConditionalCreate_FailsWithoutMatchingPassword) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
BootstrapEnclave();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
content::ExecuteScriptAsync(web_contents, kMakeCredentialConditionalCreate);
delegate_observer()->WaitForUI();
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_THAT(script_result, testing::HasSubstr("NotAllowedError"));
}
IN_PROC_BROWSER_TEST_P(EnclaveAuthenticatorConditionalCreateBrowserTest,
ConditionalCreate_ExcludeListMatch) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
BootstrapEnclave();
sync_pb::WebauthnCredentialSpecifics passkey = InjectPasskey();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
std::string script = base::ReplaceStringPlaceholders(
kMakeCredentialConditionalCreateWithExcludeList,
{base::Base64Encode(passkey.credential_id())},
/*offsets=*/nullptr);
content::ExecuteScriptAsync(web_contents, script);
delegate_observer()->WaitForUI();
// It shouldn't be possible to test the matches on an exclude list without
// also having an upgrade eligible password for the request. I.e., without an
// upgrade-eligible password, this create() request results in the generic
// NotAllowedError, rather than the exclude-list specific InvalidStateError.
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_THAT(script_result, testing::HasSubstr("NotAllowedError"));
// With an upgrade eligible password, we signal the exclude list match with an
// InvalidStateError.
InjectPassword(base::Time::Now());
content::ExecuteScriptAsync(web_contents, script);
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_THAT(script_result, testing::HasSubstr("InvalidStateError"));
}
#if BUILDFLAG(IS_MAC)
class EnclaveAuthenticatorImmediateMediationBrowserTest
: public EnclaveAuthenticatorBrowserTest {
base::test::ScopedFeatureList scoped_feature_list_{
device::kWebAuthnImmediateGet};
};
IN_PROC_BROWSER_TEST_F(
EnclaveAuthenticatorImmediateMediationBrowserTest,
GivenOnlyOneGpmPasskeyWithBiometricsEnabled_WhenImmediateRequestWithUv_TouchIdShown) {
if (!MacBiometricApisAvailable()) {
GTEST_SKIP() << "Need macOS biometric support for this test.";
}
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::DOMMessageQueue message_queue(web_contents);
// GPM setup with one passkey
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
registration_state_result;
registration_state_result.state = trusted_vault::
DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable;
registration_state_result.key_version = kSecretVersion;
SetMockVaultConnectionOnRequestDelegate(std::move(registration_state_result));
security_domain_service_->pretend_there_are_members();
AddTestPasskeyToModel();
EnableUVKeySupport();
SetBiometricsEnabled(true);
// The first get() request is satisfied implicitly because recovery was done.
content::ExecuteScriptAsync(web_contents, kGetAssertionUvPreferred);
delegate_observer()->WaitForUI();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kTrustThisComputerAssertion);
model_observer()->WaitForStep();
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kRecoverSecurityDomain);
dialog_model()->OnTrustThisComputer();
model_observer()->WaitForStep();
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(browser()->profile())
->StoreKeys(kGaiaId,
{std::vector<uint8_t>(std::begin(kSecurityDomainSecret),
std::end(kSecurityDomainSecret))},
kSecretVersion);
std::string script_result;
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
// Setup page for immediate request:
ASSERT_TRUE(content::ExecJs(web_contents,
kGetAssertionViaButtonClickImmediateUvPreferred));
// Simulate button click to trigger the navigator.credentials.get() call
content::ExecuteScriptAsync(web_contents,
"document.getElementById('testButton').click();");
// Wait for TAI to be processed. This ensures SetUIPresentation has been
// called with kModalImmediate.
delegate_observer()->WaitForPreTransportAvailabilityEnumerated();
// Wait for the UI to be shown. For kModalImmediate, this means a specific
// sheet (like bootstrapping or Touch ID) is shown.
delegate_observer()->WaitForUI();
// Simulate successful recovery/enrollment by storing keys.
// This should lead to OnEnclaveAccountSetUpComplete, which then picks
// kUVKeyWithChromeUI and sets step to kGPMTouchID.
model_observer()->SetStepToObserve(
AuthenticatorRequestDialogModel::Step::kGPMTouchID);
model_observer()->WaitForStep();
// At this point, step should be kGPMTouchID
EXPECT_EQ(dialog_model()->step(),
AuthenticatorRequestDialogModel::Step::kGPMTouchID);
}
#endif // BUILDFLAG(IS_MAC)
} // namespace
#endif // !defined(MEMORY_SANITIZER)