| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/webauthn/enclave_manager.h" |
| |
| #include <array> |
| #include <cstddef> |
| #include <cstdint> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <tuple> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/command_line.h" |
| #include "base/containers/flat_set.h" |
| #include "base/files/file_path.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/json/json_reader.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/notimplemented.h" |
| #include "base/notreached.h" |
| #include "base/process/process.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/task/current_thread.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/time/time.h" |
| #include "base/types/expected.h" |
| #include "build/build_config.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/proto/enclave_local_state.pb.h" |
| #include "chrome/browser/webauthn/test_util.h" |
| #include "chrome/browser/webauthn/unexportable_key_utils.h" |
| #include "components/cbor/reader.h" |
| #include "components/cbor/writer.h" |
| #include "components/os_crypt/sync/os_crypt_mocker.h" |
| #include "components/signin/public/base/consent_level.h" |
| #include "components/signin/public/identity_manager/identity_test_environment.h" |
| #include "components/sync/protocol/webauthn_credential_specifics.pb.h" |
| #include "components/trusted_vault/command_line_switches.h" |
| #include "components/trusted_vault/proto/vault.pb.h" |
| #include "components/trusted_vault/trusted_vault_connection.h" |
| #include "crypto/aead.h" |
| #include "crypto/hkdf.h" |
| #include "crypto/scoped_fake_unexportable_key_provider.h" |
| #include "crypto/scoped_fake_user_verifying_key_provider.h" |
| #include "crypto/user_verifying_key.h" |
| #include "device/fido/authenticator_get_assertion_response.h" |
| #include "device/fido/authenticator_make_credential_response.h" |
| #include "device/fido/ctap_get_assertion_request.h" |
| #include "device/fido/ctap_make_credential_request.h" |
| #include "device/fido/enclave/constants.h" |
| #include "device/fido/enclave/enclave_authenticator.h" |
| #include "device/fido/enclave/types.h" |
| #include "device/fido/features.h" |
| #include "device/fido/fido_authenticator.h" |
| #include "device/fido/fido_constants.h" |
| #include "device/fido/fido_types.h" |
| #include "device/fido/json_request.h" |
| #include "device/fido/public_key_credential_descriptor.h" |
| #include "device/fido/public_key_credential_params.h" |
| #include "google_apis/gaia/gaia_id.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/http/http_status_code.h" |
| #include "services/network/network_service.h" |
| #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "services/network/test/fake_test_cert_verifier_params_factory.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "components/trusted_vault/icloud_recovery_key_mac.h" |
| #include "crypto/apple/scoped_fake_keychain_v2.h" |
| #include "device/fido/mac/scoped_touch_id_test_environment.h" |
| #include "third_party/boringssl/src/include/openssl/hmac.h" |
| #include "third_party/boringssl/src/include/openssl/sha.h" |
| #endif // BUILDFLAG(IS_MAC) |
| |
| #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) |
| #include "base/run_loop.h" |
| #include "components/trusted_vault/proto/recovery_key_store.pb.h" |
| #include "components/trusted_vault/proto/vault.pb.h" |
| #include "components/trusted_vault/proto_string_bytes_conversion.h" |
| #include "components/trusted_vault/securebox.h" |
| #include "crypto/signature_verifier.h" |
| #endif // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) |
| |
| // These tests are also disabled under MSAN. The enclave subprocess is written |
| // in Rust and FFI from Rust to C++ doesn't work in Chromium at this time |
| // (crbug.com/1369167). |
| #if !defined(MEMORY_SANITIZER) |
| |
| namespace enclave = device::enclave; |
| using NoArgFuture = base::test::TestFuture<void>; |
| using BoolFuture = base::test::TestFuture<bool>; |
| |
| namespace { |
| |
| constexpr std::array<uint8_t, 32> kTestKey = { |
| 0xc4, 0xdf, 0xa4, 0xed, 0xfc, 0xf9, 0x7c, 0xc0, 0x3a, 0xb1, 0xcb, |
| 0x3c, 0x03, 0x02, 0x9b, 0x5a, 0x05, 0xec, 0x88, 0x48, 0x54, 0x42, |
| 0xf1, 0x20, 0xb4, 0x75, 0x01, 0xde, 0x61, 0xf1, 0x39, 0x5d, |
| }; |
| constexpr uint8_t kTestProtobuf[] = { |
| 0x0a, 0x10, 0x71, 0xfd, 0xf9, 0x65, 0xa8, 0x7c, 0x61, 0xe2, 0xff, 0x27, |
| 0x0c, 0x76, 0x25, 0x23, 0xe0, 0xa4, 0x12, 0x10, 0x77, 0xf2, 0x3c, 0x31, |
| 0x3c, 0xe8, 0x94, 0x9a, 0x9f, 0xbc, 0xdf, 0x44, 0xfc, 0xf5, 0x41, 0x97, |
| 0x1a, 0x0b, 0x77, 0x65, 0x62, 0x61, 0x75, 0x74, 0x68, 0x6e, 0x2e, 0x69, |
| 0x6f, 0x22, 0x06, 0x56, 0x47, 0x56, 0x7a, 0x64, 0x41, 0x2a, 0x10, 0x60, |
| 0x07, 0x19, 0x5b, 0x4e, 0x19, 0xf9, 0x6e, 0xc1, 0xfc, 0xfd, 0x0a, 0xf6, |
| 0x0c, 0x00, 0x7e, 0x30, 0xf9, 0xa0, 0xea, 0xf3, 0xc8, 0x31, 0x3a, 0x04, |
| 0x54, 0x65, 0x73, 0x74, 0x42, 0x04, 0x54, 0x65, 0x73, 0x74, 0x4a, 0xa6, |
| 0x01, 0xdc, 0xc5, 0x16, 0x15, 0x91, 0x24, 0xd2, 0x31, 0xfc, 0x85, 0x8b, |
| 0xe2, 0xec, 0x22, 0x09, 0x8f, 0x8d, 0x0f, 0xbe, 0x9b, 0x59, 0x71, 0x04, |
| 0xcd, 0xaa, 0x3d, 0x32, 0x23, 0xbd, 0x25, 0x46, 0x14, 0x86, 0x9c, 0xfe, |
| 0x74, 0xc8, 0xd3, 0x37, 0x70, 0xed, 0xb0, 0x25, 0xd4, 0x1b, 0xdd, 0xa4, |
| 0x3c, 0x02, 0x13, 0x8c, 0x69, 0x03, 0xff, 0xd1, 0xb0, 0x72, 0x00, 0x29, |
| 0xcf, 0x5f, 0x06, 0xb3, 0x94, 0xe2, 0xea, 0xca, 0x68, 0xdd, 0x0b, 0x07, |
| 0x98, 0x7a, 0x2c, 0x8f, 0x08, 0xee, 0x7d, 0xad, 0x16, 0x35, 0xc7, 0x10, |
| 0xf3, 0xa4, 0x90, 0x84, 0xd1, 0x8e, 0x2e, 0xdb, 0xb9, 0xfa, 0x72, 0x9a, |
| 0xcf, 0x12, 0x1b, 0x3c, 0xca, 0xfa, 0x79, 0x4a, 0x1e, 0x1b, 0xe1, 0x15, |
| 0xdf, 0xab, 0xee, 0x75, 0xbb, 0x5c, 0x5a, 0x94, 0x14, 0xeb, 0x72, 0xae, |
| 0x37, 0x97, 0x03, 0xa8, 0xe7, 0x62, 0x9d, 0x2e, 0xfd, 0x28, 0xce, 0x03, |
| 0x34, 0x20, 0xa7, 0xa2, 0x7b, 0x00, 0xc8, 0x12, 0x62, 0x12, 0x7f, 0x54, |
| 0x73, 0x8c, 0x21, 0xc8, 0x85, 0x15, 0xce, 0x36, 0x14, 0xd9, 0x41, 0x22, |
| 0xe8, 0xbf, 0x88, 0xf9, 0x45, 0xe4, 0x1c, 0x89, 0x7d, 0xa4, 0x23, 0x58, |
| 0x00, 0x68, 0x98, 0xf5, 0x81, 0xef, 0xad, 0xf4, 0xda, 0x17, 0x70, 0xab, |
| 0x03, |
| }; |
| constexpr std::string_view kTestPINPublicKey = |
| "\x04\xe4\x72\x4c\x87\xf9\x42\xbe\x2a\xd1\xe6\xac\xa3\x52\x85\xea\x08\xf7" |
| "\xe9\x6d\xea\xf2\xf0\x7f\xa9\xde\x89\xe2\x9e\x69\x36\xc4\x4c\xf9\x56\xe9" |
| "\xa1\x1f\x08\xfe\x55\xca\x1b\x84\xb9\xe5\x1e\xc3\x26\x69\x16\xa0\x6b\x03" |
| "\xfa\x42\x08\xa8\xaf\x7d\xd9\x14\xb4\xfc\x1a"; |
| |
| std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> GetTestEntity() { |
| auto ret = std::make_unique<sync_pb::WebauthnCredentialSpecifics>(); |
| CHECK(ret->ParseFromArray(kTestProtobuf, sizeof(kTestProtobuf))); |
| return ret; |
| } |
| |
| std::string StringOfZeros(size_t len) { |
| return std::string(len, '0'); |
| } |
| |
| enclave::SigningCallback AlwaysFailsSigningCallback() { |
| return base::BindOnce( |
| [](enclave::SignedMessage, |
| base::OnceCallback<void(std::optional<enclave::ClientSignature>)> |
| callback) { std::move(callback).Run(std::nullopt); }); |
| } |
| |
| webauthn_pb::EnclaveLocalState::WrappedPIN GetTestWrappedPIN() { |
| webauthn_pb::EnclaveLocalState::WrappedPIN wrapped_pin; |
| wrapped_pin.set_wrapped_pin(StringOfZeros(30)); |
| wrapped_pin.set_claim_key(StringOfZeros(32)); |
| wrapped_pin.set_form(wrapped_pin.FORM_SIX_DIGITS); |
| wrapped_pin.set_hash(wrapped_pin.HASH_SCRYPT); |
| wrapped_pin.set_hash_difficulty(1 << 12); |
| wrapped_pin.set_hash_salt(StringOfZeros(16)); |
| |
| return wrapped_pin; |
| } |
| |
| struct TempDir { |
| public: |
| TempDir() { CHECK(dir_.CreateUniqueTempDir()); } |
| |
| base::FilePath GetPath() const { return dir_.GetPath(); } |
| |
| private: |
| base::ScopedTempDir dir_; |
| }; |
| |
| std::unique_ptr<network::NetworkService> CreateNetwork( |
| mojo::Remote<network::mojom::NetworkContext>* network_context) { |
| network::mojom::NetworkContextParamsPtr params = |
| network::mojom::NetworkContextParams::New(); |
| params->cert_verifier_params = |
| network::FakeTestCertVerifierParamsFactory::GetCertVerifierParams(); |
| |
| auto service = network::NetworkService::CreateForTesting(); |
| service->CreateNetworkContext(network_context->BindNewPipeAndPassReceiver(), |
| std::move(params)); |
| |
| return service; |
| } |
| |
| scoped_refptr<device::JSONRequest> JSONFromString(std::string_view json_str) { |
| base::Value json_request = base::JSONReader::Read(json_str).value(); |
| return base::MakeRefCounted<device::JSONRequest>(std::move(json_request)); |
| } |
| |
| std::vector<uint8_t> DecryptWrappedPin( |
| base::span<const uint8_t> security_domain_secret, |
| base::span<const uint8_t> wrapped_pin) { |
| base::span<const uint8_t> nonce = wrapped_pin.first(12u); |
| base::span<const uint8_t> encrypted_pin = wrapped_pin.subspan(12u); |
| // This is "KeychainApplicationKey:chrome:GPM PIN data wrapping key". |
| static constexpr uint8_t kKeyPurposePinDataKey[] = { |
| 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x41, 0x70, 0x70, |
| 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, |
| 0x3a, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x3a, 0x47, 0x50, 0x4d, |
| 0x20, 0x50, 0x49, 0x4e, 0x20, 0x64, 0x61, 0x74, 0x61, 0x20, 0x77, |
| 0x72, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x20, 0x6b, 0x65, 0x79}; |
| const std::array<uint8_t, 32> derived_key = crypto::HkdfSha256<32>( |
| security_domain_secret, /*salt=*/base::span<const uint8_t>(), |
| kKeyPurposePinDataKey); |
| crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM); |
| aead.Init(derived_key); |
| std::optional<std::vector<uint8_t>> pin = aead.Open( |
| encrypted_pin, nonce, /*additional_data=*/base::span<const uint8_t>()); |
| CHECK(pin.has_value()); |
| return *pin; |
| } |
| |
| } // namespace |
| |
| class EnclaveManagerTest : public testing::Test, EnclaveManager::Observer { |
| public: |
| EnclaveManagerTest() |
| : EnclaveManagerTest( |
| base::test::TaskEnvironment::TimeSource::SYSTEM_TIME) {} |
| |
| explicit EnclaveManagerTest( |
| base::test::TaskEnvironment::TimeSource time_source) |
| // `IdentityTestEnvironment` wants to run on an IO thread. |
| : task_env_(base::test::TaskEnvironment::MainThreadType::IO, time_source), |
| temp_dir_(), |
| process_and_port_(StartWebAuthnEnclave(temp_dir_.GetPath())), |
| enclave_override_( |
| TestWebAuthnEnclaveIdentity(process_and_port_.second)), |
| network_service_(CreateNetwork(&network_context_)), |
| security_domain_service_( |
| FakeSecurityDomainService::New(kSecretVersion)), |
| recovery_key_store_(FakeRecoveryKeyStore::New()), |
| manager_(temp_dir_.GetPath(), |
| identity_test_env_.identity_manager(), |
| base::BindLambdaForTesting( |
| [&]() -> network::mojom::NetworkContext* { |
| return network_context_.get(); |
| }), |
| url_loader_factory_.GetSafeWeakWrapper()) { |
| OSCryptMocker::SetUp(); |
| |
| identity_test_env_.MakePrimaryAccountAvailable( |
| "test@gmail.com", signin::ConsentLevel::kSignin); |
| gaia_id_ = identity_test_env_.identity_manager() |
| ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin) |
| .gaia; |
| identity_test_env_.SetAutomaticIssueOfAccessTokens(true); |
| manager_.AddObserver(this); |
| |
| 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_hw_provider_ = |
| std::make_unique<crypto::ScopedFakeUnexportableKeyProvider>(); |
| } |
| |
| ~EnclaveManagerTest() override { |
| if (manager_.RunWhenStoppedForTesting(task_env_.QuitClosure())) { |
| task_env_.RunUntilQuit(); |
| } |
| CHECK(process_and_port_.first.Terminate(/*exit_code=*/1, /*wait=*/true)); |
| OSCryptMocker::TearDown(); |
| } |
| |
| protected: |
| base::flat_set<std::string> GaiaAccountsInState() { |
| const webauthn_pb::EnclaveLocalState& state = |
| manager_.local_state_for_testing(); |
| base::flat_set<std::string> ret; |
| for (const auto& it : state.users()) { |
| ret.insert(it.first); |
| } |
| return ret; |
| } |
| |
| void OnKeysStored() override { stored_count_++; } |
| |
| void DoCreate( |
| std::unique_ptr<enclave::ClaimedPIN> claimed_pin, |
| std::unique_ptr<sync_pb::WebauthnCredentialSpecifics>* out_specifics) { |
| auto ui_request = std::make_unique<enclave::CredentialRequest>(); |
| ui_request->signing_callback = manager_.IdentityKeySigningCallback(); |
| int32_t secret_version; |
| std::vector<uint8_t> wrapped_secret; |
| std::tie(secret_version, wrapped_secret) = |
| manager_.GetCurrentWrappedSecret(); |
| EXPECT_EQ(secret_version, kSecretVersion); |
| ui_request->wrapped_secret = std::move(wrapped_secret); |
| ui_request->key_version = kSecretVersion; |
| ui_request->claimed_pin = std::move(claimed_pin); |
| |
| std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> specifics; |
| ui_request->save_passkey_callback = base::BindLambdaForTesting( |
| [&specifics](sync_pb::WebauthnCredentialSpecifics in_specifics) { |
| specifics = std::make_unique<sync_pb::WebauthnCredentialSpecifics>( |
| std::move(in_specifics)); |
| }); |
| |
| enclave::EnclaveAuthenticator authenticator( |
| std::move(ui_request), /*network_context_factory=*/ |
| base::BindLambdaForTesting([&]() -> network::mojom::NetworkContext* { |
| return network_context_.get(); |
| })); |
| |
| std::vector<device::PublicKeyCredentialParams::CredentialInfo> |
| pub_key_params; |
| pub_key_params.emplace_back(); |
| |
| device::MakeCredentialOptions ctap_options; |
| ctap_options.json = JSONFromString(R"({ |
| "attestation": "none", |
| "authenticatorSelection": { |
| "residentKey": "preferred", |
| "userVerification": "preferred" |
| }, |
| "challenge": "xHyLYEorFsaL6vb", |
| "extensions": { "credProps": true }, |
| "pubKeyCredParams": [ |
| { "alg": -7, "type": "public-key" }, |
| { "alg": -257, "type": "public-key" } |
| ], |
| "rp": { |
| "id": "webauthn.io", |
| "name": "webauthn.io" |
| }, |
| "user": { |
| "displayName": "test", |
| "id": "ZEdWemRB", |
| "name": "test" |
| } |
| })"); |
| |
| auto quit_closure = task_env_.QuitClosure(); |
| std::optional<device::MakeCredentialStatus> status; |
| std::optional<device::AuthenticatorMakeCredentialResponse> response; |
| authenticator.MakeCredential( |
| /*request=*/{R"({"foo": "bar"})", |
| /*rp=*/{"rpid", "rpname"}, |
| /*user=*/{{'u', 'i', 'd'}, "user", "display name"}, |
| device::PublicKeyCredentialParams( |
| std::move(pub_key_params))}, |
| std::move(ctap_options), |
| base::BindLambdaForTesting( |
| [&quit_closure, &status, &response]( |
| device::MakeCredentialStatus in_status, |
| std::optional<device::AuthenticatorMakeCredentialResponse> |
| in_responses) { |
| status = in_status; |
| response = std::move(in_responses); |
| quit_closure.Run(); |
| })); |
| task_env_.RunUntilQuit(); |
| |
| ASSERT_TRUE(status.has_value()); |
| ASSERT_EQ(status, device::MakeCredentialStatus::kSuccess); |
| ASSERT_TRUE(response.has_value()); |
| ASSERT_TRUE(specifics); |
| EXPECT_EQ(specifics->rp_id(), "rpid"); |
| EXPECT_EQ(specifics->user_id(), "uid"); |
| EXPECT_EQ(specifics->user_name(), "user"); |
| EXPECT_EQ(specifics->user_display_name(), "display name"); |
| EXPECT_EQ(specifics->key_version(), kSecretVersion); |
| |
| if (out_specifics) { |
| *out_specifics = std::move(specifics); |
| } |
| } |
| |
| struct GetAssertionResponseExpectation { |
| device::GetAssertionStatus result = device::GetAssertionStatus::kSuccess; |
| uint32_t size = 1; |
| }; |
| |
| std::optional<base::Time> LastPINRenewalTime() { |
| std::optional<base::Time> ret; |
| webauthn_pb::EnclaveLocalState& state = manager_.local_state_for_testing(); |
| if (state.users().size() == 0) { |
| return ret; |
| } |
| CHECK_EQ(state.users().size(), 1u); |
| if (!state.users().begin()->second.has_last_refreshed_pin_epoch_secs()) { |
| return ret; |
| } |
| return base::Time::FromSecondsSinceUnixEpoch( |
| state.users().begin()->second.last_refreshed_pin_epoch_secs()); |
| } |
| |
| void DoAssertion( |
| std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> entity, |
| std::unique_ptr<enclave::ClaimedPIN> claimed_pin, |
| GetAssertionResponseExpectation expected_response, |
| std::unique_ptr<enclave::CredentialRequest> custom_ui_request = nullptr) { |
| std::unique_ptr<enclave::CredentialRequest> ui_request; |
| if (custom_ui_request) { |
| ui_request = std::move(custom_ui_request); |
| } else { |
| ui_request = std::make_unique<enclave::CredentialRequest>(); |
| ui_request->signing_callback = manager_.IdentityKeySigningCallback(); |
| ui_request->wrapped_secret = |
| *manager_.GetWrappedSecret(/*version=*/kSecretVersion); |
| ui_request->entity = std::move(entity); |
| ui_request->claimed_pin = std::move(claimed_pin); |
| ui_request->save_passkey_callback = base::BindOnce( |
| [](sync_pb::WebauthnCredentialSpecifics) { NOTREACHED(); }); |
| } |
| |
| enclave::EnclaveAuthenticator authenticator( |
| std::move(ui_request), /*network_context_factory=*/ |
| base::BindLambdaForTesting([&]() -> network::mojom::NetworkContext* { |
| return network_context_.get(); |
| })); |
| |
| device::CtapGetAssertionRequest ctap_request("test.com", |
| R"({"foo": "bar"})"); |
| ctap_request.allow_list.emplace_back(device::PublicKeyCredentialDescriptor( |
| device::CredentialType::kPublicKey, /*id=*/{1, 2, 3, 4})); |
| |
| device::CtapGetAssertionOptions ctap_options; |
| ctap_options.json = JSONFromString(R"({ |
| "allowCredentials": [ ], |
| "challenge": "CYO8B30gOPIOVFAaU61J7PvoETG_sCZQ38Gzpu", |
| "rpId": "webauthn.io", |
| "userVerification": "preferred" |
| })"); |
| |
| auto quit_closure = task_env_.QuitClosure(); |
| std::optional<device::GetAssertionStatus> status; |
| std::vector<device::AuthenticatorGetAssertionResponse> responses; |
| authenticator.GetAssertion( |
| std::move(ctap_request), std::move(ctap_options), |
| base::BindLambdaForTesting( |
| [&quit_closure, &status, &responses]( |
| device::GetAssertionStatus in_status, |
| std::vector<device::AuthenticatorGetAssertionResponse> |
| in_responses) { |
| status = in_status; |
| responses = std::move(in_responses); |
| quit_closure.Run(); |
| })); |
| task_env_.RunUntilQuit(); |
| |
| ASSERT_TRUE(status.has_value()); |
| ASSERT_TRUE(true); |
| ASSERT_EQ(status, expected_response.result); |
| ASSERT_EQ(responses.size(), expected_response.size); |
| } |
| |
| bool Register() { |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| EXPECT_TRUE(register_future.Wait()); |
| return register_future.Get(); |
| } |
| |
| void CorruptDeviceId() { |
| webauthn_pb::EnclaveLocalState& state = manager_.local_state_for_testing(); |
| ASSERT_EQ(state.users().size(), 1u); |
| state.mutable_users()->begin()->second.set_device_id("corrupted value"); |
| } |
| |
| base::test::TaskEnvironment task_env_; |
| unsigned stored_count_ = 0; |
| const TempDir temp_dir_; |
| const std::pair<base::Process, uint16_t> process_and_port_; |
| const enclave::ScopedEnclaveOverride enclave_override_; |
| network::TestURLLoaderFactory url_loader_factory_; |
| mojo::Remote<network::mojom::NetworkContext> network_context_; |
| std::unique_ptr<network::NetworkService> network_service_; |
| signin::IdentityTestEnvironment identity_test_env_; |
| GaiaId gaia_id_; |
| std::unique_ptr<FakeSecurityDomainService> security_domain_service_; |
| std::unique_ptr<FakeRecoveryKeyStore> recovery_key_store_; |
| std::unique_ptr<crypto::ScopedFakeUnexportableKeyProvider> fake_hw_provider_; |
| EnclaveManager manager_; |
| base::test::ScopedFeatureList scoped_feature_list_{ |
| device::kWebAuthnWrapCohortData}; |
| }; |
| |
| TEST_F(EnclaveManagerTest, TestInfrastructure) { |
| // Tests that the enclave starts up. |
| } |
| |
| TEST_F(EnclaveManagerTest, Basic) { |
| security_domain_service_->pretend_there_are_members(); |
| |
| ASSERT_FALSE(manager_.is_loaded()); |
| ASSERT_FALSE(manager_.is_registered()); |
| ASSERT_FALSE(manager_.is_ready()); |
| |
| NoArgFuture loaded_future; |
| manager_.Load(loaded_future.GetCallback()); |
| EXPECT_TRUE(loaded_future.Wait()); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.is_loaded()); |
| ASSERT_FALSE(manager_.is_registered()); |
| ASSERT_FALSE(manager_.is_ready()); |
| |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(register_future.Wait()); |
| ASSERT_TRUE(register_future.Get()); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.is_loaded()); |
| ASSERT_TRUE(manager_.is_registered()); |
| ASSERT_FALSE(manager_.is_ready()); |
| EXPECT_TRUE(manager_.local_state_for_testing() |
| .users() |
| .find(gaia_id_.ToString()) |
| ->second.identity_key_is_software_backed()); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| EXPECT_EQ(stored_count_, 1u); |
| |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(add_future.Wait()); |
| ASSERT_TRUE(add_future.Get()); |
| |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.is_loaded()); |
| ASSERT_TRUE(manager_.is_registered()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| ASSERT_TRUE(manager_.TakeSecret()); |
| ASSERT_FALSE(manager_.TakeSecret()); |
| EXPECT_EQ(security_domain_service_->num_physical_members(), 1u); |
| EXPECT_EQ(security_domain_service_->num_pin_members(), 0u); |
| |
| base::HistogramTester histogram_tester; |
| DoCreate(/*claimed_pin=*/nullptr, /*out_specifics=*/nullptr); |
| DoAssertion(GetTestEntity(), /*claimed_pin=*/nullptr, |
| GetAssertionResponseExpectation()); |
| histogram_tester.ExpectBucketCount( |
| "WebAuthentication.EnclaveTransactionResult", |
| device::enclave::EnclaveTransactionResult::kSuccess, 2); |
| } |
| |
| TEST_F(EnclaveManagerTest, SecretsArriveBeforeRegistrationRequested) { |
| security_domain_service_->pretend_there_are_members(); |
| ASSERT_FALSE(manager_.is_registered()); |
| |
| // If secrets are provided before `RegisterIfNeeded` is called, the state |
| // machine should still trigger registration. |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/417); |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| EXPECT_TRUE(add_future.Wait()); |
| |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.is_loaded()); |
| ASSERT_TRUE(manager_.is_registered()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.TakeSecret()); |
| } |
| |
| TEST_F(EnclaveManagerTest, SecretsArriveBeforeRegistrationCompleted) { |
| security_domain_service_->pretend_there_are_members(); |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| ASSERT_FALSE(manager_.is_registered()); |
| |
| // Provide the domain secrets before the registration has completed. The |
| // system should still end up in the correct state. |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/417); |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| EXPECT_TRUE(add_future.Wait()); |
| EXPECT_TRUE(register_future.Wait()); |
| |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.is_loaded()); |
| ASSERT_TRUE(manager_.is_registered()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.TakeSecret()); |
| } |
| |
| TEST_F(EnclaveManagerTest, RegistrationFailureAndRetry) { |
| const std::string gaia = |
| identity_test_env_.identity_manager() |
| ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin) |
| .gaia.ToString(); |
| |
| // Override the enclave with port=100, which will cause connection failures. |
| { |
| device::enclave::ScopedEnclaveOverride override( |
| TestWebAuthnEnclaveIdentity(/*port=*/100)); |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| EXPECT_TRUE(register_future.Wait()); |
| ASSERT_FALSE(register_future.Get()); |
| } |
| ASSERT_FALSE(manager_.is_registered()); |
| const std::string public_key = manager_.local_state_for_testing() |
| .users() |
| .find(gaia) |
| ->second.identity_public_key(); |
| ASSERT_FALSE(public_key.empty()); |
| |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| EXPECT_TRUE(register_future.Wait()); |
| ASSERT_TRUE(manager_.is_registered()); |
| ASSERT_TRUE(register_future.Get()); |
| |
| // The public key should not have changed because re-registration attempts |
| // must try the same public key again in case they actually worked the first |
| // time. |
| ASSERT_TRUE(public_key == manager_.local_state_for_testing() |
| .users() |
| .find(gaia) |
| ->second.identity_public_key()); |
| } |
| |
| TEST_F(EnclaveManagerTest, PrimaryUserChange) { |
| const std::string gaia1 = |
| identity_test_env_.identity_manager() |
| ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin) |
| .gaia.ToString(); |
| |
| { |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| EXPECT_TRUE(register_future.Wait()); |
| } |
| ASSERT_TRUE(manager_.is_registered()); |
| EXPECT_THAT(GaiaAccountsInState(), testing::UnorderedElementsAre(gaia1)); |
| |
| identity_test_env_.MakePrimaryAccountAvailable("test2@gmail.com", |
| signin::ConsentLevel::kSignin); |
| const std::string gaia2 = |
| identity_test_env_.identity_manager() |
| ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin) |
| .gaia.ToString(); |
| ASSERT_FALSE(manager_.is_registered()); |
| { |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| EXPECT_TRUE(register_future.Wait()); |
| } |
| ASSERT_TRUE(manager_.is_registered()); |
| EXPECT_THAT(GaiaAccountsInState(), |
| testing::UnorderedElementsAre(gaia1, gaia2)); |
| |
| // Remove all accounts from the cookie jar. The primary account should be |
| // retained. |
| identity_test_env_.SetCookieAccounts({}); |
| EXPECT_THAT(GaiaAccountsInState(), testing::UnorderedElementsAre(gaia2)); |
| |
| // When the primary account changes, the second account should be dropped |
| // because it was removed from the cookie jar. |
| identity_test_env_.MakePrimaryAccountAvailable("test3@gmail.com", |
| signin::ConsentLevel::kSignin); |
| const std::string gaia3 = |
| identity_test_env_.identity_manager() |
| ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin) |
| .gaia.ToString(); |
| EXPECT_THAT(GaiaAccountsInState(), testing::UnorderedElementsAre(gaia3)); |
| } |
| |
| TEST_F(EnclaveManagerTest, PrimaryUserChangeDiscardsActions) { |
| security_domain_service_->pretend_there_are_members(); |
| const std::string gaia1 = |
| identity_test_env_.identity_manager() |
| ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin) |
| .gaia.ToString(); |
| |
| NoArgFuture loaded_future; |
| manager_.Load(loaded_future.GetCallback()); |
| EXPECT_TRUE(loaded_future.Wait()); |
| |
| BoolFuture register_future1; |
| manager_.RegisterIfNeeded(register_future1.GetCallback()); |
| BoolFuture register_future2; |
| manager_.RegisterIfNeeded(register_future2.GetCallback()); |
| |
| identity_test_env_.MakePrimaryAccountAvailable("test2@gmail.com", |
| signin::ConsentLevel::kSignin); |
| // `MakePrimaryAccountAvailable` should have canceled any actions. |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| ASSERT_FALSE(manager_.is_registered()); |
| ASSERT_FALSE(manager_.is_ready()); |
| |
| EXPECT_TRUE(register_future1.Wait()); |
| ASSERT_FALSE(register_future1.Get()); |
| EXPECT_TRUE(register_future2.Wait()); |
| ASSERT_FALSE(register_future2.Get()); |
| } |
| |
| TEST_F(EnclaveManagerTest, AddWithExistingPIN) { |
| security_domain_service_->pretend_there_are_members(); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/417); |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| trusted_vault::GpmPinMetadata(std::string(kTestPINPublicKey), |
| trusted_vault::UsableRecoveryPinMetadata( |
| GetTestWrappedPIN().SerializeAsString(), |
| /*expiry=*/base::Time())), |
| add_future.GetCallback())); |
| EXPECT_TRUE(add_future.Wait()); |
| |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.is_loaded()); |
| ASSERT_TRUE(manager_.is_registered()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.TakeSecret()); |
| |
| EXPECT_EQ(security_domain_service_->num_physical_members(), 1u); |
| // The PIN should not have been added to the account. Instead this test is |
| // pretending that it was already there. |
| EXPECT_EQ(security_domain_service_->num_pin_members(), 0u); |
| EXPECT_TRUE(manager_.has_wrapped_pin()); |
| } |
| |
| TEST_F(EnclaveManagerTest, InvalidWrappedPIN) { |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/417); |
| |
| BoolFuture add_future; |
| // A wrapped PIN that isn't a valid protobuf should be rejected. |
| EXPECT_FALSE(manager_.AddDeviceToAccount( |
| trusted_vault::GpmPinMetadata( |
| std::string(kTestPINPublicKey), |
| trusted_vault::UsableRecoveryPinMetadata("nonsense wrapped PIN", |
| /*expiry=*/base::Time())), |
| add_future.GetCallback())); |
| |
| // A valid protobuf, but which fails invariants, should be rejected. |
| webauthn_pb::EnclaveLocalState::WrappedPIN wrapped_pin = GetTestWrappedPIN(); |
| wrapped_pin.set_wrapped_pin("too short"); |
| EXPECT_FALSE(manager_.AddDeviceToAccount( |
| trusted_vault::GpmPinMetadata(std::string(kTestPINPublicKey), |
| trusted_vault::UsableRecoveryPinMetadata( |
| wrapped_pin.SerializeAsString(), |
| /*expiry=*/base::Time())), |
| add_future.GetCallback())); |
| } |
| |
| TEST_F(EnclaveManagerTest, SetupWithPIN) { |
| const std::string pin = "123456"; |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(pin, setup_future.GetCallback()); |
| EXPECT_TRUE(setup_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.has_wrapped_pin()); |
| EXPECT_FALSE(manager_.wrapped_pin_is_arbitrary()); |
| EXPECT_TRUE(LastPINRenewalTime().has_value()); |
| |
| 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_); |
| CHECK(security_domain_secret.has_value()); |
| EXPECT_EQ(manager_.TakeSecret()->second, *security_domain_secret); |
| |
| // Verify that the wrapped PIN Chrome generated contains the cohort details. |
| std::vector<uint8_t> wrapped_pin = DecryptWrappedPin( |
| *security_domain_secret, |
| base::as_byte_span(manager_.GetWrappedPIN()->wrapped_pin())); |
| std::optional<cbor::Value> cbor = cbor::Reader::Read(wrapped_pin); |
| const cbor::Value::MapValue& wrapped_pin_cbor = cbor->GetMap(); |
| int cert_xml_serial_number = |
| wrapped_pin_cbor.find(cbor::Value(6))->second.GetInteger(); |
| EXPECT_EQ(cert_xml_serial_number, FakeRecoveryKeyStore::kTestSerialNumber); |
| const std::vector<uint8_t> cohort_public_key = |
| wrapped_pin_cbor.find(cbor::Value(7))->second.GetBytestring(); |
| EXPECT_EQ(cohort_public_key, |
| recovery_key_store_->endpoint_public_key_bytes()); |
| |
| // Verify we can use the PIN to create a passkey and assert it. |
| std::unique_ptr<device::enclave::ClaimedPIN> claimed_pin = |
| EnclaveManager::MakeClaimedPINSlowly(pin, manager_.GetWrappedPIN()); |
| std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> entity; |
| DoCreate(/*claimed_pin=*/nullptr, &entity); |
| DoAssertion(std::move(entity), std::move(claimed_pin), |
| GetAssertionResponseExpectation()); |
| } |
| |
| TEST_F(EnclaveManagerTest, SetupWithPIN_SecurityDomainFailure) { |
| security_domain_service_->fail_all_requests(); |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN("123456", setup_future.GetCallback()); |
| EXPECT_TRUE(setup_future.Wait()); |
| ASSERT_FALSE(setup_future.Get()); |
| ASSERT_FALSE(manager_.is_ready()); |
| } |
| |
| TEST_F(EnclaveManagerTest, SetupWithPIN_CertXMLFailure) { |
| recovery_key_store_->break_cert_xml_file(); |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN("123456", setup_future.GetCallback()); |
| // This test primarily shouldn't crash or hang. |
| EXPECT_TRUE(setup_future.Wait()); |
| ASSERT_FALSE(setup_future.Get()); |
| ASSERT_FALSE(manager_.is_ready()); |
| } |
| |
| TEST_F(EnclaveManagerTest, SetupWithPIN_SigXMLFailure) { |
| recovery_key_store_->break_sig_xml_file(); |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN("123456", setup_future.GetCallback()); |
| // This test primarily shouldn't crash or hang. |
| EXPECT_TRUE(setup_future.Wait()); |
| ASSERT_FALSE(setup_future.Get()); |
| ASSERT_FALSE(manager_.is_ready()); |
| } |
| |
| TEST_F(EnclaveManagerTest, AddDeviceAndPINToAccount) { |
| security_domain_service_->pretend_there_are_members(); |
| const std::string pin = "pin"; |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| manager_.AddDeviceAndPINToAccount( |
| pin, /*previous_pin_public_key=*/std::nullopt, add_future.GetCallback()); |
| EXPECT_TRUE(add_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.has_wrapped_pin()); |
| EXPECT_TRUE(manager_.wrapped_pin_is_arbitrary()); |
| |
| 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_); |
| CHECK(security_domain_secret.has_value()); |
| EXPECT_EQ(manager_.TakeSecret()->second, *security_domain_secret); |
| |
| // Verify that the wrapped PIN the enclave generated contains the cohort |
| // details. |
| std::vector<uint8_t> wrapped_pin = DecryptWrappedPin( |
| *security_domain_secret, |
| base::as_byte_span(manager_.GetWrappedPIN()->wrapped_pin())); |
| std::optional<cbor::Value> cbor = cbor::Reader::Read(wrapped_pin); |
| const cbor::Value::MapValue& wrapped_pin_cbor = cbor->GetMap(); |
| int cert_xml_serial_number = |
| wrapped_pin_cbor.find(cbor::Value(6))->second.GetInteger(); |
| EXPECT_EQ(cert_xml_serial_number, FakeRecoveryKeyStore::kTestSerialNumber); |
| const std::vector<uint8_t> cohort_public_key = |
| wrapped_pin_cbor.find(cbor::Value(7))->second.GetBytestring(); |
| EXPECT_EQ(cohort_public_key, |
| recovery_key_store_->endpoint_public_key_bytes()); |
| |
| std::unique_ptr<device::enclave::ClaimedPIN> claimed_pin = |
| EnclaveManager::MakeClaimedPINSlowly(pin, manager_.GetWrappedPIN()); |
| std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> entity; |
| DoCreate(/*claimed_pin=*/nullptr, &entity); |
| DoAssertion(std::move(entity), std::move(claimed_pin), |
| GetAssertionResponseExpectation()); |
| } |
| |
| TEST_F(EnclaveManagerTest, AddDeviceAndPINToAccountWithPreviouslyInvalidPIN) { |
| // First, set up with a PIN. |
| security_domain_service_->pretend_there_are_members(); |
| const std::string pin = "pin"; |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| manager_.StoreKeys(gaia_id_, {key}, |
| /*last_key_version=*/kSecretVersion); |
| { |
| BoolFuture add_future; |
| manager_.AddDeviceAndPINToAccount(pin, |
| /*previous_pin_public_key=*/std::nullopt, |
| add_future.GetCallback()); |
| ASSERT_TRUE(add_future.Wait()); |
| ASSERT_TRUE(add_future.Get()); |
| } |
| // Then, make the PIN unusable and reset the registration. |
| security_domain_service_->MakePinMemberUnusable(); |
| manager_.ClearRegistrationForTesting(); |
| manager_.StoreKeys(gaia_id_, {key}, |
| /*last_key_version=*/kSecretVersion); |
| { |
| // Verify that attempting to register with a PIN succeeds when the public |
| // key of the obsolete PIN is set. |
| BoolFuture add_future; |
| manager_.AddDeviceAndPINToAccount( |
| pin, security_domain_service_->GetPinMemberPublicKey(), |
| add_future.GetCallback()); |
| ASSERT_TRUE(add_future.Wait()); |
| EXPECT_TRUE(add_future.Get()); |
| } |
| } |
| |
| class EnclaveManagerChangePINTest : public EnclaveManagerTest, |
| public testing::WithParamInterface<bool> { |
| public: |
| void SetUp() override { |
| scoped_feature_list_.InitWithFeatureState(device::kWebAuthnWrapCohortData, |
| GetParam()); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(, |
| EnclaveManagerChangePINTest, |
| testing::Values(false, true)); |
| |
| TEST_P(EnclaveManagerChangePINTest, ChangePIN) { |
| security_domain_service_->pretend_there_are_members(); |
| const std::string pin = "pin"; |
| const std::string new_pin = "newpin"; |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| manager_.AddDeviceAndPINToAccount( |
| pin, /*previous_pin_public_key=*/std::nullopt, add_future.GetCallback()); |
| EXPECT_TRUE(add_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.has_wrapped_pin()); |
| EXPECT_TRUE(manager_.wrapped_pin_is_arbitrary()); |
| const std::vector<uint8_t> security_domain_secret = |
| std::move(manager_.TakeSecret()->second); |
| |
| BoolFuture change_future; |
| manager_.ChangePIN(new_pin, "rapt", change_future.GetCallback()); |
| EXPECT_TRUE(change_future.Wait()); |
| ASSERT_TRUE(change_future.Get()); |
| |
| EXPECT_EQ(security_domain_service_->num_physical_members(), 1u); |
| EXPECT_EQ(security_domain_service_->num_pin_members(), 1u); |
| EXPECT_EQ(recovery_key_store_->vaults().size(), 2u); |
| const std::optional<std::vector<uint8_t>> recovered_security_domain_secret = |
| FakeMagicArch::RecoverWithPIN(new_pin, *security_domain_service_, |
| *recovery_key_store_); |
| CHECK(recovered_security_domain_secret.has_value()); |
| EXPECT_EQ(*recovered_security_domain_secret, security_domain_secret); |
| |
| std::unique_ptr<device::enclave::ClaimedPIN> claimed_pin = |
| EnclaveManager::MakeClaimedPINSlowly(new_pin, manager_.GetWrappedPIN()); |
| std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> entity; |
| DoCreate(/*claimed_pin=*/nullptr, &entity); |
| DoAssertion(std::move(entity), std::move(claimed_pin), |
| GetAssertionResponseExpectation()); |
| } |
| |
| TEST_P(EnclaveManagerChangePINTest, AddPINToExistingAccount) { |
| security_domain_service_->pretend_there_are_members(); |
| const std::string new_pin = "newpin"; |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| manager_.AddDeviceToAccount(std::nullopt, add_future.GetCallback()); |
| EXPECT_TRUE(add_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| const std::vector<uint8_t> security_domain_secret = |
| std::move(manager_.TakeSecret()->second); |
| |
| BoolFuture set_pin_future; |
| manager_.SetPIN(new_pin, "rapt", set_pin_future.GetCallback()); |
| EXPECT_TRUE(set_pin_future.Wait()); |
| ASSERT_TRUE(set_pin_future.Get()); |
| |
| EXPECT_EQ(security_domain_service_->num_physical_members(), 1u); |
| EXPECT_EQ(security_domain_service_->num_pin_members(), 1u); |
| EXPECT_EQ(recovery_key_store_->vaults().size(), 1u); |
| const std::optional<std::vector<uint8_t>> recovered_security_domain_secret = |
| FakeMagicArch::RecoverWithPIN(new_pin, *security_domain_service_, |
| *recovery_key_store_); |
| CHECK(recovered_security_domain_secret.has_value()); |
| EXPECT_EQ(*recovered_security_domain_secret, security_domain_secret); |
| |
| std::unique_ptr<device::enclave::ClaimedPIN> claimed_pin = |
| EnclaveManager::MakeClaimedPINSlowly(new_pin, manager_.GetWrappedPIN()); |
| std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> entity; |
| DoCreate(/*claimed_pin=*/nullptr, &entity); |
| DoAssertion(std::move(entity), std::move(claimed_pin), |
| GetAssertionResponseExpectation()); |
| } |
| |
| TEST_P(EnclaveManagerChangePINTest, |
| AddPINToExistingAccountButTheresAlreadyOne) { |
| security_domain_service_->pretend_there_are_members(); |
| const std::string pin = "pin"; |
| const std::string new_pin = "newpin"; |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| manager_.AddDeviceAndPINToAccount( |
| pin, /*previous_pin_public_key=*/std::nullopt, add_future.GetCallback()); |
| EXPECT_TRUE(add_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| const std::vector<uint8_t> security_domain_secret = |
| std::move(manager_.TakeSecret()->second); |
| |
| BoolFuture set_pin_future; |
| manager_.SetPIN(new_pin, "rapt", set_pin_future.GetCallback()); |
| EXPECT_TRUE(set_pin_future.Wait()); |
| |
| // This should fail because there's already a PIN set. |
| ASSERT_FALSE(set_pin_future.Get()); |
| } |
| |
| TEST_P(EnclaveManagerChangePINTest, ChangePINWithTwoDevices) { |
| security_domain_service_->pretend_there_are_members(); |
| const std::string pin = "pin"; |
| const std::string intermediate_pin = "intermediate_pin"; |
| const std::string new_pin = "newpin"; |
| |
| const TempDir temp_dir_2; |
| EnclaveManager second_manager( |
| temp_dir_2.GetPath(), identity_test_env_.identity_manager(), |
| base::BindLambdaForTesting([&]() -> network::mojom::NetworkContext* { |
| return network_context_.get(); |
| }), |
| url_loader_factory_.GetSafeWeakWrapper()); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| manager_.StoreKeys(gaia_id_, {key}, |
| /*last_key_version=*/kSecretVersion); |
| second_manager.StoreKeys(gaia_id_, {key}, |
| /*last_key_version=*/kSecretVersion); |
| |
| { |
| BoolFuture add_future; |
| manager_.AddDeviceAndPINToAccount(pin, |
| /*previous_pin_public_key=*/std::nullopt, |
| add_future.GetCallback()); |
| EXPECT_TRUE(add_future.Wait()); |
| ASSERT_TRUE(add_future.Get()); |
| } |
| const std::vector<uint8_t> security_domain_secret = |
| std::move(manager_.TakeSecret()->second); |
| |
| { |
| BoolFuture add_future; |
| second_manager.AddDeviceToAccount(std::nullopt, add_future.GetCallback()); |
| EXPECT_TRUE(add_future.Wait()); |
| } |
| |
| { |
| BoolFuture change_future; |
| // `second_manager` must fetch PIN information from the security domain in |
| // order to change it. |
| second_manager.ChangePIN(intermediate_pin, "rapt", |
| change_future.GetCallback()); |
| EXPECT_TRUE(change_future.Wait()); |
| ASSERT_TRUE(change_future.Get()); |
| } |
| |
| { |
| BoolFuture change_future; |
| manager_.ChangePIN(new_pin, "rapt", change_future.GetCallback()); |
| EXPECT_TRUE(change_future.Wait()); |
| ASSERT_TRUE(change_future.Get()); |
| } |
| |
| EXPECT_EQ(security_domain_service_->num_physical_members(), 2u); |
| EXPECT_EQ(security_domain_service_->num_pin_members(), 1u); |
| EXPECT_EQ(recovery_key_store_->vaults().size(), 3u); |
| const std::optional<std::vector<uint8_t>> recovered_security_domain_secret = |
| FakeMagicArch::RecoverWithPIN(new_pin, *security_domain_service_, |
| *recovery_key_store_); |
| CHECK(recovered_security_domain_secret.has_value()); |
| EXPECT_EQ(*recovered_security_domain_secret, security_domain_secret); |
| |
| std::unique_ptr<device::enclave::ClaimedPIN> claimed_pin = |
| EnclaveManager::MakeClaimedPINSlowly(new_pin, manager_.GetWrappedPIN()); |
| std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> entity; |
| DoCreate(/*claimed_pin=*/nullptr, &entity); |
| DoAssertion(std::move(entity), std::move(claimed_pin), |
| GetAssertionResponseExpectation()); |
| } |
| |
| TEST_F(EnclaveManagerTest, EnclaveForgetsClient_SetupWithPIN) { |
| ASSERT_TRUE(Register()); |
| CorruptDeviceId(); |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN("1234", setup_future.GetCallback()); |
| EXPECT_TRUE(setup_future.Wait()); |
| EXPECT_FALSE(setup_future.Get()); |
| } |
| |
| TEST_F(EnclaveManagerTest, EnclaveForgetsClient_AddDeviceToAccount) { |
| ASSERT_TRUE(Register()); |
| CorruptDeviceId(); |
| security_domain_service_->pretend_there_are_members(); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/417); |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| trusted_vault::GpmPinMetadata(std::string(kTestPINPublicKey), |
| trusted_vault::UsableRecoveryPinMetadata( |
| GetTestWrappedPIN().SerializeAsString(), |
| /*expiry=*/base::Time())), |
| add_future.GetCallback())); |
| EXPECT_TRUE(add_future.Wait()); |
| EXPECT_FALSE(add_future.Get()); |
| } |
| |
| TEST_F(EnclaveManagerTest, EnclaveForgetsClient_AddDeviceAndPINToAccount) { |
| ASSERT_TRUE(Register()); |
| CorruptDeviceId(); |
| |
| security_domain_service_->pretend_there_are_members(); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/417); |
| BoolFuture add_future; |
| manager_.AddDeviceAndPINToAccount("1234", |
| /*previous_pin_public_key=*/std::nullopt, |
| add_future.GetCallback()); |
| EXPECT_TRUE(add_future.Wait()); |
| EXPECT_FALSE(add_future.Get()); |
| } |
| |
| TEST_F(EnclaveManagerTest, RenewPIN) { |
| ASSERT_TRUE(Register()); |
| |
| const std::string pin = "123456"; |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(pin, setup_future.GetCallback()); |
| EXPECT_TRUE(setup_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.has_wrapped_pin()); |
| |
| EXPECT_EQ(security_domain_service_->num_physical_members(), 1u); |
| EXPECT_EQ(security_domain_service_->num_pin_members(), 1u); |
| |
| const std::optional<base::Time> initial_time = LastPINRenewalTime(); |
| ASSERT_TRUE(initial_time.has_value()); |
| |
| BoolFuture renew_future; |
| manager_.RenewPIN(renew_future.GetCallback()); |
| EXPECT_TRUE(renew_future.Wait()); |
| EXPECT_TRUE(renew_future.Get()); |
| |
| // The number of PIN members must not have increased because the upload should |
| // have reused the vault handle etc of the original. |
| 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_); |
| CHECK(security_domain_secret.has_value()); |
| EXPECT_EQ(manager_.TakeSecret()->second, *security_domain_secret); |
| EXPECT_TRUE(*LastPINRenewalTime() > *initial_time); |
| } |
| |
| // Tests that renewing a PIN that didn't have cohort details (because it was |
| // wrapped on an older version of Chrome) results in the enclave re-wrapping it |
| // with the details. |
| TEST_F(EnclaveManagerTest, RenewPINAddsCohortDetails) { |
| // Set up with a PIN. |
| ASSERT_TRUE(Register()); |
| const std::string pin = "123456"; |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(pin, setup_future.GetCallback()); |
| EXPECT_TRUE(setup_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.has_wrapped_pin()); |
| |
| const std::vector<uint8_t> security_domain_secret = |
| manager_.TakeSecret()->second; |
| { |
| // Delete the wrapped PIN cohort details from the enclave manager and |
| // security domain service, simulating an older version of Chrome. |
| std::unique_ptr<webauthn_pb::EnclaveLocalState_WrappedPIN> |
| wrapped_pin_proto = manager_.GetWrappedPIN(); |
| std::vector<uint8_t> wrapped_pin = |
| DecryptWrappedPin(security_domain_secret, |
| base::as_byte_span(wrapped_pin_proto->wrapped_pin())); |
| std::optional<cbor::Value> cbor = cbor::Reader::Read(wrapped_pin); |
| cbor::Value::MapValue& wrapped_pin_cbor = |
| const_cast<cbor::Value::MapValue&>(cbor->GetMap()); |
| wrapped_pin_cbor.erase(wrapped_pin_cbor.find(cbor::Value(6))); |
| wrapped_pin_cbor.erase(wrapped_pin_cbor.find(cbor::Value(7))); |
| std::vector<uint8_t> encrypted_pin = EnclaveManager::EncryptWrappedPIN( |
| security_domain_secret, |
| *cbor::Writer::Write(cbor::Value(wrapped_pin_cbor))); |
| wrapped_pin_proto->set_wrapped_pin( |
| std::string(base::as_string_view(encrypted_pin))); |
| security_domain_service_->SetPinMemberWrappedPin( |
| wrapped_pin_proto->SerializeAsString()); |
| manager_.SetWrappedPINDataForTesting(std::move(encrypted_pin)); |
| } |
| |
| // Renew the PIN. |
| BoolFuture renew_future; |
| manager_.RenewPIN(renew_future.GetCallback()); |
| EXPECT_TRUE(renew_future.Wait()); |
| EXPECT_TRUE(renew_future.Get()); |
| |
| // Verify that the wrapped PIN that is now present in the security domain |
| // service contains the cohort details. |
| webauthn_pb::EnclaveLocalState_WrappedPIN wrapped_pin_proto; |
| wrapped_pin_proto.ParseFromString(security_domain_service_->GetPinMetadata() |
| .usable_pin_metadata->wrapped_pin); |
| std::vector<uint8_t> wrapped_pin = |
| DecryptWrappedPin(security_domain_secret, |
| base::as_byte_span(wrapped_pin_proto.wrapped_pin())); |
| |
| std::optional<cbor::Value> cbor = cbor::Reader::Read(wrapped_pin); |
| const cbor::Value::MapValue& wrapped_pin_cbor = cbor->GetMap(); |
| |
| auto cert_xml_serial_number_it = wrapped_pin_cbor.find(cbor::Value(6)); |
| ASSERT_NE(cert_xml_serial_number_it, wrapped_pin_cbor.end()); |
| ASSERT_TRUE(cert_xml_serial_number_it->second.is_integer()); |
| int cert_xml_serial_number = cert_xml_serial_number_it->second.GetInteger(); |
| EXPECT_EQ(cert_xml_serial_number, FakeRecoveryKeyStore::kTestSerialNumber); |
| |
| auto cohort_public_key_it = wrapped_pin_cbor.find(cbor::Value(7)); |
| ASSERT_NE(cohort_public_key_it, wrapped_pin_cbor.end()); |
| ASSERT_TRUE(cohort_public_key_it->second.is_bytestring()); |
| const std::vector<uint8_t> cohort_public_key = |
| cohort_public_key_it->second.GetBytestring(); |
| EXPECT_EQ(cohort_public_key, |
| recovery_key_store_->endpoint_public_key_bytes()); |
| } |
| |
| // Regression test for crbug.com/403218779. |
| // Simulates two chrome clients by standing up a second Enclave Manager. The |
| // second Enclave Manager will renew a PIN. This invalidates the data from the |
| // first manager. Then, the first manager will attempt renewing the PIN. This |
| // used to be broken because the first manager would not download the updated |
| // PIN data, causing a public key mismatch on the join security domain query. |
| TEST_F(EnclaveManagerTest, RenewPINWithStaleDataFromAnotherClient) { |
| const std::string kPin = "123456"; |
| |
| // Set up the first manager with the PIN. |
| ASSERT_TRUE(Register()); |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(kPin, setup_future.GetCallback()); |
| ASSERT_TRUE(setup_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.has_wrapped_pin()); |
| ASSERT_EQ(security_domain_service_->num_physical_members(), 1u); |
| ASSERT_EQ(security_domain_service_->num_pin_members(), 1u); |
| std::optional<std::pair<int32_t, std::vector<uint8_t>>> secret = |
| manager_.TakeSecret(); |
| ASSERT_TRUE(secret); |
| const std::string initial_pin_key = |
| security_domain_service_->GetPinMemberPublicKey(); |
| |
| // Set up the second manager. |
| const TempDir temp_dir_2; |
| EnclaveManager second_manager( |
| temp_dir_2.GetPath(), identity_test_env_.identity_manager(), |
| base::BindLambdaForTesting([&]() -> network::mojom::NetworkContext* { |
| return network_context_.get(); |
| }), |
| url_loader_factory_.GetSafeWeakWrapper()); |
| second_manager.StoreKeys(gaia_id_, {secret->second}, secret->first); |
| BoolFuture add_future; |
| second_manager.AddDeviceToAccount(security_domain_service_->GetPinMetadata(), |
| add_future.GetCallback()); |
| ASSERT_TRUE(add_future.Wait()); |
| ASSERT_TRUE(add_future.Get()); |
| ASSERT_EQ(security_domain_service_->num_physical_members(), 2u); |
| ASSERT_EQ(security_domain_service_->num_pin_members(), 1u); |
| |
| // Renew the PIN with the second manager. |
| { |
| BoolFuture renew_future; |
| second_manager.RenewPIN(renew_future.GetCallback()); |
| ASSERT_TRUE(renew_future.Wait()); |
| ASSERT_TRUE(renew_future.Get()); |
| } |
| const std::string second_pin_key = |
| security_domain_service_->GetPinMemberPublicKey(); |
| ASSERT_NE(initial_pin_key, second_pin_key); |
| |
| // Attempt renewing the PIN with the first enclave. |
| { |
| BoolFuture renew_future; |
| manager_.RenewPIN(renew_future.GetCallback()); |
| ASSERT_TRUE(renew_future.Wait()); |
| ASSERT_TRUE(renew_future.Get()); |
| } |
| const std::string third_pin_key = |
| security_domain_service_->GetPinMemberPublicKey(); |
| EXPECT_NE(second_pin_key, third_pin_key); |
| } |
| |
| // Regression test for crbug.com/402425846. |
| // Attempts renewing a PIN from local data when the security domain indicates |
| // that the current PIN changed, and is also not usable for recovery. |
| TEST_F(EnclaveManagerTest, RenewUnusablePINFromLocalData) { |
| const std::string kPin = "123456"; |
| |
| // Set up the manager with the PIN. |
| ASSERT_TRUE(Register()); |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(kPin, setup_future.GetCallback()); |
| ASSERT_TRUE(setup_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.has_wrapped_pin()); |
| ASSERT_EQ(security_domain_service_->num_physical_members(), 1u); |
| ASSERT_EQ(security_domain_service_->num_pin_members(), 1u); |
| const std::string initial_pin_key = |
| security_domain_service_->GetPinMemberPublicKey(); |
| |
| // Update the joined PIN to one that has a different public key and is not |
| // usable for recovery. |
| security_domain_service_->MakePinMemberUnusable(); |
| security_domain_service_->SetPinMemberPublicKey("Bad PK"); |
| |
| // Renew the PIN. |
| BoolFuture renew_future; |
| manager_.RenewPIN(renew_future.GetCallback()); |
| ASSERT_TRUE(renew_future.Wait()); |
| EXPECT_TRUE(renew_future.Get()); |
| EXPECT_NE(security_domain_service_->GetPinMemberPublicKey(), "Bad PK"); |
| } |
| |
| // Regression test for crbug.com/407171373. |
| // Attempts renewing a PIN after the security domain has been reset. |
| TEST_F(EnclaveManagerTest, RenewPINAfterSecurityDomainReset) { |
| base::HistogramTester histogram_tester; |
| const std::string kPin = "123456"; |
| |
| // Set up the manager with the PIN. |
| ASSERT_TRUE(Register()); |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(kPin, setup_future.GetCallback()); |
| ASSERT_TRUE(setup_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.has_wrapped_pin()); |
| ASSERT_TRUE(manager_.is_registered()); |
| ASSERT_EQ(security_domain_service_->num_physical_members(), 1u); |
| ASSERT_EQ(security_domain_service_->num_pin_members(), 1u); |
| const std::string initial_pin_key = |
| security_domain_service_->GetPinMemberPublicKey(); |
| |
| // Reset the security domain. |
| security_domain_service_->ResetSecurityDomain(); |
| |
| // Attempt to renew the PIN. This should clear the registration data. |
| BoolFuture renew_future; |
| manager_.RenewPIN(renew_future.GetCallback()); |
| ASSERT_TRUE(renew_future.Wait()); |
| EXPECT_FALSE(renew_future.Get()); |
| EXPECT_EQ(security_domain_service_->num_pin_members(), 0u); |
| EXPECT_FALSE(manager_.is_registered()); |
| histogram_tester.ExpectUniqueSample( |
| "WebAuthentication.PinRenewalFailureCause", |
| EnclaveManager::PinRenewalFailureCause::kSecurityDomainReset, 1); |
| } |
| |
| // Regression test for crbug.com/407171373. |
| // Attempts renewing a PIN when the security domain reports that the user |
| // doesn't have a GPM PIN at all. |
| TEST_F(EnclaveManagerTest, RenewPINAfterSecurityDomainReportsNoPin) { |
| base::HistogramTester histogram_tester; |
| const std::string kPin = "123456"; |
| |
| // Set up the manager with the PIN. |
| ASSERT_TRUE(Register()); |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(kPin, setup_future.GetCallback()); |
| ASSERT_TRUE(setup_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.has_wrapped_pin()); |
| ASSERT_EQ(security_domain_service_->num_physical_members(), 1u); |
| ASSERT_EQ(security_domain_service_->num_pin_members(), 1u); |
| const std::string initial_pin_key = |
| security_domain_service_->GetPinMemberPublicKey(); |
| |
| // Remove the PIN member from the security domain. |
| security_domain_service_->RemovePinMember(); |
| |
| // Try to renew the PIN. This shouldn't do anything. |
| BoolFuture renew_future; |
| manager_.RenewPIN(renew_future.GetCallback()); |
| ASSERT_TRUE(renew_future.Wait()); |
| EXPECT_FALSE(renew_future.Get()); |
| EXPECT_EQ(security_domain_service_->num_pin_members(), 0u); |
| histogram_tester.ExpectUniqueSample( |
| "WebAuthentication.PinRenewalFailureCause", |
| EnclaveManager::PinRenewalFailureCause::kSecurityDomainReportsNoPin, 1); |
| } |
| |
| TEST_F(EnclaveManagerTest, EpochChanged) { |
| ASSERT_TRUE(Register()); |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN("123456", setup_future.GetCallback()); |
| EXPECT_TRUE(setup_future.Wait()); |
| EXPECT_TRUE(manager_.is_ready()); |
| |
| trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult state; |
| state.state = trusted_vault:: |
| DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable; |
| state.key_version = kSecretVersion; |
| |
| EXPECT_TRUE(manager_.ConsiderSecurityDomainState(state, base::DoNothing())); |
| EXPECT_TRUE(manager_.is_idle()); |
| |
| BoolFuture update_future; |
| state.key_version = kSecretVersion + 1; |
| EXPECT_FALSE( |
| manager_.ConsiderSecurityDomainState(state, update_future.GetCallback())); |
| EXPECT_TRUE(update_future.Wait()); |
| EXPECT_FALSE(manager_.is_ready()); |
| } |
| |
| TEST_F(EnclaveManagerTest, PINChanged) { |
| ASSERT_TRUE(Register()); |
| constexpr std::string_view kNewWrappedPin = "dummy wrapped pin >= 29 chars"; |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN("123456", setup_future.GetCallback()); |
| EXPECT_TRUE(setup_future.Wait()); |
| EXPECT_TRUE(manager_.is_ready()); |
| |
| const webauthn_pb::EnclaveLocalState::User& user = |
| manager_.local_state_for_testing().users().begin()->second; |
| webauthn_pb::EnclaveLocalState::WrappedPIN wrapped_pin = user.wrapped_pin(); |
| wrapped_pin.set_wrapped_pin(kNewWrappedPin); |
| |
| trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult state; |
| state.state = trusted_vault:: |
| DownloadAuthenticationFactorsRegistrationStateResult::State::kRecoverable; |
| state.key_version = kSecretVersion; |
| state.gpm_pin_metadata = trusted_vault::GpmPinMetadata( |
| "new public key", trusted_vault::UsableRecoveryPinMetadata( |
| wrapped_pin.SerializeAsString(), |
| /*expiry=*/base::Time::FromTimeT(1))); |
| |
| BoolFuture update_future; |
| EXPECT_TRUE( |
| manager_.ConsiderSecurityDomainState(state, update_future.GetCallback())); |
| EXPECT_TRUE(update_future.Wait()); |
| EXPECT_TRUE(manager_.is_ready()); |
| const webauthn_pb::EnclaveLocalState::User& updated_user = |
| manager_.local_state_for_testing().users().begin()->second; |
| EXPECT_EQ(updated_user.wrapped_pin().wrapped_pin(), kNewWrappedPin); |
| } |
| |
| TEST_F(EnclaveManagerTest, SigningFails) { |
| auto ui_request = std::make_unique<enclave::CredentialRequest>(); |
| ui_request->signing_callback = AlwaysFailsSigningCallback(); |
| ui_request->wrapped_secret = {1, 2, 3}; |
| ui_request->key_version = 1; |
| |
| enclave::EnclaveAuthenticator authenticator( |
| std::move(ui_request), /*network_context_factory=*/ |
| base::BindLambdaForTesting([&]() -> network::mojom::NetworkContext* { |
| return network_context_.get(); |
| })); |
| |
| std::vector<device::PublicKeyCredentialParams::CredentialInfo> pub_key_params; |
| pub_key_params.emplace_back(); |
| |
| device::MakeCredentialOptions ctap_options; |
| ctap_options.json = JSONFromString(R"({ |
| "attestation": "none", |
| "challenge": "xHyLYEorFsaL6vb", |
| "extensions": { "credProps": true } |
| })"); |
| |
| auto quit_closure = task_env_.QuitClosure(); |
| std::optional<device::MakeCredentialStatus> status; |
| std::optional<device::AuthenticatorMakeCredentialResponse> response; |
| authenticator.MakeCredential( |
| /*request=*/{R"({"foo": "bar"})", |
| /*rp=*/{"rpid", "rpname"}, |
| /*user=*/{{'u', 'i', 'd'}, "user", "display name"}, |
| device::PublicKeyCredentialParams( |
| std::move(pub_key_params))}, |
| std::move(ctap_options), |
| base::BindLambdaForTesting( |
| [&quit_closure, &status, &response]( |
| device::MakeCredentialStatus in_status, |
| std::optional<device::AuthenticatorMakeCredentialResponse> |
| in_responses) { |
| status = in_status; |
| response = std::move(in_responses); |
| quit_closure.Run(); |
| })); |
| task_env_.RunUntilQuit(); |
| |
| ASSERT_TRUE(status.has_value()); |
| ASSERT_EQ(status, device::MakeCredentialStatus::kEnclaveCancel); |
| ASSERT_FALSE(response.has_value()); |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| TEST_F(EnclaveManagerTest, AddICloudRecoveryKey) { |
| ASSERT_TRUE(Register()); |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN("123456", setup_future.GetCallback()); |
| EXPECT_TRUE(setup_future.Wait()); |
| ASSERT_TRUE(manager_.is_ready()); |
| |
| std::unique_ptr<trusted_vault::ICloudRecoveryKey> icloud_key = |
| trusted_vault::ICloudRecoveryKey::CreateForTest(); |
| std::unique_ptr<trusted_vault::SecureBoxKeyPair> key = |
| trusted_vault::SecureBoxKeyPair::CreateByPrivateKeyImport( |
| icloud_key->key()->private_key().ExportToBytes()); |
| BoolFuture icloud_future; |
| manager_.AddICloudRecoveryKey(std::move(icloud_key), |
| icloud_future.GetCallback()); |
| EXPECT_TRUE(icloud_future.Wait()); |
| EXPECT_TRUE(icloud_future.Get()); |
| |
| EXPECT_EQ(security_domain_service_->num_physical_members(), 1u); |
| EXPECT_EQ(security_domain_service_->num_pin_members(), 1u); |
| |
| // 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()); |
| ASSERT_EQ(trusted_vault::ProtoStringToBytes(icloud_member->public_key()), |
| key->public_key().ExportToBytes()); |
| |
| // Use the iCloud recovery key to recover the security domain secret. |
| const trusted_vault_pb::SharedMemberKey& shared_member_key = |
| icloud_member->memberships().at(0).keys().at(0); |
| const std::optional<std::vector<uint8_t>> security_domain_secret = |
| key->private_key().Decrypt( |
| base::span<const uint8_t>(), |
| base::byte_span_from_cstring("V1 shared_key"), |
| base::as_byte_span(shared_member_key.wrapped_key())); |
| ASSERT_TRUE(security_domain_secret); |
| EXPECT_EQ(manager_.TakeSecret()->second, *security_domain_secret); |
| |
| std::array<uint8_t, SHA256_DIGEST_LENGTH> expected_proof; |
| unsigned expected_proof_len; |
| HMAC(EVP_sha256(), security_domain_secret->data(), |
| security_domain_secret->size(), |
| reinterpret_cast<const uint8_t*>(icloud_member->public_key().data()), |
| icloud_member->public_key().size(), expected_proof.data(), |
| &expected_proof_len); |
| ASSERT_EQ(expected_proof_len, expected_proof.size()); |
| EXPECT_EQ(base::span<const uint8_t>(expected_proof), |
| base::as_byte_span(shared_member_key.member_proof())); |
| } |
| #endif // BUILDFLAG(IS_MAC) |
| |
| TEST_F(EnclaveManagerTest, Unenroll) { |
| ASSERT_TRUE(Register()); |
| |
| ASSERT_TRUE(manager_.is_registered()); |
| BoolFuture unenroll_future; |
| manager_.Unenroll(unenroll_future.GetCallback()); |
| EXPECT_TRUE(unenroll_future.Wait()); |
| EXPECT_TRUE(unenroll_future.Get()); |
| ASSERT_FALSE(manager_.is_registered()); |
| |
| // Things should be in a good state such that we can register again. |
| ASSERT_TRUE(Register()); |
| ASSERT_TRUE(manager_.is_registered()); |
| } |
| |
| TEST_F(EnclaveManagerTest, UnenrollRace) { |
| ASSERT_TRUE(Register()); |
| |
| // Should be safe to race multiple unenroll requests. The ones after the first |
| // will fail when pending requests are cancelled. |
| ASSERT_TRUE(manager_.is_registered()); |
| BoolFuture unenroll_future1; |
| BoolFuture unenroll_future2; |
| BoolFuture unenroll_future3; |
| manager_.Unenroll(unenroll_future1.GetCallback()); |
| manager_.Unenroll(unenroll_future2.GetCallback()); |
| manager_.Unenroll(unenroll_future3.GetCallback()); |
| EXPECT_TRUE(unenroll_future1.Wait()); |
| EXPECT_TRUE(unenroll_future2.Wait()); |
| EXPECT_TRUE(unenroll_future3.Wait()); |
| EXPECT_TRUE(unenroll_future1.Get()); |
| EXPECT_FALSE(unenroll_future2.Get()); |
| EXPECT_FALSE(unenroll_future3.Get()); |
| ASSERT_FALSE(manager_.is_registered()); |
| } |
| |
| TEST_F(EnclaveManagerTest, UnenrollWithoutRegistering) { |
| ASSERT_FALSE(manager_.is_registered()); |
| BoolFuture unenroll_future; |
| manager_.Unenroll(unenroll_future.GetCallback()); |
| EXPECT_TRUE(unenroll_future.Wait()); |
| EXPECT_TRUE(unenroll_future.Get()); |
| ASSERT_FALSE(manager_.is_registered()); |
| } |
| |
| TEST_F(EnclaveManagerTest, LockPINThenChange) { |
| const std::string pin = "123456"; |
| const std::string wrong_pin = "654321"; |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(pin, setup_future.GetCallback()); |
| EXPECT_TRUE(setup_future.Wait()); |
| |
| std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> entity; |
| DoCreate(/*claimed_pin=*/nullptr, &entity); |
| |
| // Use the wrong PIN until it's locked at the enclave. |
| for (int i = 0; i < 5; i++) { |
| std::unique_ptr<device::enclave::ClaimedPIN> wrong_claimed_pin = |
| EnclaveManager::MakeClaimedPINSlowly(wrong_pin, |
| manager_.GetWrappedPIN()); |
| DoAssertion( |
| std::make_unique<sync_pb::WebauthnCredentialSpecifics>(*entity.get()), |
| std::move(wrong_claimed_pin), |
| GetAssertionResponseExpectation{ |
| .result = device::GetAssertionStatus::kUserConsentDenied, |
| .size = 0}); |
| } |
| |
| // Even the correct PIN should fail now. |
| std::unique_ptr<device::enclave::ClaimedPIN> correct_claimed_pin = |
| EnclaveManager::MakeClaimedPINSlowly(pin, manager_.GetWrappedPIN()); |
| DoAssertion( |
| std::make_unique<sync_pb::WebauthnCredentialSpecifics>(*entity.get()), |
| std::move(correct_claimed_pin), |
| GetAssertionResponseExpectation{ |
| .result = device::GetAssertionStatus::kUserConsentDenied, .size = 0}); |
| |
| // Change the PIN. |
| const std::string new_pin = "123123"; |
| BoolFuture change_future; |
| manager_.ChangePIN(new_pin, "rapt", change_future.GetCallback()); |
| ASSERT_TRUE(change_future.Get()); |
| |
| // The new PIN should work. |
| std::unique_ptr<device::enclave::ClaimedPIN> new_correct_claimed_pin = |
| EnclaveManager::MakeClaimedPINSlowly(new_pin, manager_.GetWrappedPIN()); |
| DoAssertion(std::move(entity), std::move(new_correct_claimed_pin), |
| GetAssertionResponseExpectation()); |
| } |
| |
| // Tests that rely on `ScopedFakeUnexportableKeyProvider` only work on |
| // platforms where EnclaveManager uses `GetUnexportableKeyProvider`, as opposed |
| // to `GetSoftwareUnsecureUnexportableKeyProvider`. |
| #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) |
| #define MAYBE_HardwareKeyLost HardwareKeyLost |
| #else |
| #define MAYBE_HardwareKeyLost DISABLED_HardwareKeyLost |
| #endif |
| TEST_F(EnclaveManagerTest, MAYBE_HardwareKeyLost) { |
| crypto::ScopedFakeUserVerifyingKeyProvider scoped_uv_key_provider; |
| security_domain_service_->pretend_there_are_members(); |
| NoArgFuture loaded_future; |
| manager_.Load(loaded_future.GetCallback()); |
| EXPECT_TRUE(loaded_future.Wait()); |
| |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(register_future.Wait()); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(add_future.Wait()); |
| |
| base::RepeatingClosure quit_closure; |
| #if BUILDFLAG(IS_WIN) |
| // Windows does deferred UV key creation. This test has to trigger the actual |
| // create before testing that it is later deleted. |
| EXPECT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/false), |
| EnclaveManager::UvKeyState::kUsesSystemUIDeferredCreation); |
| std::unique_ptr<EnclaveManager::UvKeyCreationLock> uv_creation_lock; |
| device::enclave::UVKeyCreationCallback key_creation_callback; |
| std::tie(uv_creation_lock, key_creation_callback) = |
| manager_.UserVerifyingKeyCreationCallback(); |
| quit_closure = task_env_.QuitClosure(); |
| std::move(key_creation_callback) |
| .Run(base::BindLambdaForTesting( |
| [&quit_closure](base::span<const uint8_t> uv_public_key) { |
| EXPECT_FALSE(uv_public_key.empty()); |
| quit_closure.Run(); |
| })); |
| task_env_.RunUntilQuit(); |
| #endif |
| |
| fake_hw_provider_.reset(); |
| manager_.ClearCachedKeysForTesting(); |
| |
| // Verify a UV key was created as well. |
| std::string uv_key_label = manager_.local_state_for_testing() |
| .mutable_users() |
| ->begin() |
| ->second.wrapped_uv_private_key(); |
| auto uv_key_provider = crypto::GetUserVerifyingKeyProvider( |
| crypto::UserVerifyingKeyProvider::Config()); |
| base::test::TestFuture< |
| base::expected<std::unique_ptr<crypto::UserVerifyingSigningKey>, |
| crypto::UserVerifyingKeyCreationError>> |
| key_future_present; |
| uv_key_provider->GetUserVerifyingSigningKey(uv_key_label, |
| key_future_present.GetCallback()); |
| EXPECT_TRUE(key_future_present.Wait()); |
| EXPECT_TRUE(key_future_present.Get().has_value()); |
| |
| crypto::ScopedNullUnexportableKeyProvider null_hw_provider; |
| auto signing_callback = manager_.IdentityKeySigningCallback(); |
| quit_closure = task_env_.QuitClosure(); |
| std::move(signing_callback) |
| .Run({1, 2, 3, 4}, |
| base::BindLambdaForTesting( |
| [&quit_closure]( |
| std::optional<enclave::ClientSignature> signature) { |
| EXPECT_EQ(signature, std::nullopt); |
| quit_closure.Run(); |
| })); |
| task_env_.RunUntilQuit(); |
| EXPECT_FALSE(manager_.is_registered()); |
| |
| // Verify that the UV key was deleted when the HW key was lost. |
| base::test::TestFuture< |
| base::expected<std::unique_ptr<crypto::UserVerifyingSigningKey>, |
| crypto::UserVerifyingKeyCreationError>> |
| key_future_deleted; |
| uv_key_provider->GetUserVerifyingSigningKey(uv_key_label, |
| key_future_deleted.GetCallback()); |
| EXPECT_TRUE(key_future_deleted.Wait()); |
| EXPECT_FALSE(key_future_deleted.Get().has_value()); |
| } |
| |
| // Tests that Chrome resets the local state if joining the physical device to |
| // the security domain failed. |
| // Regression test for crbug.com/404563934. |
| TEST_F(EnclaveManagerTest, JoiningSecurityDomainFailed) { |
| const std::string kPin = "123456"; |
| ASSERT_TRUE(Register()); |
| |
| // Fail the request to join the physical device. |
| security_domain_service_->fail_join_requests_matching(base::BindRepeating( |
| [](const trusted_vault_pb::JoinSecurityDomainsRequest& request) { |
| return request.security_domain_member().member_type() == |
| trusted_vault_pb::SecurityDomainMember:: |
| MEMBER_TYPE_PHYSICAL_DEVICE; |
| })); |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(kPin, setup_future.GetCallback()); |
| ASSERT_TRUE(setup_future.Wait()); |
| ASSERT_FALSE(setup_future.Get()); |
| |
| const auto& local_state = manager_.local_state_for_testing() |
| .mutable_users() |
| ->find(gaia_id_.ToString()) |
| ->second; |
| EXPECT_FALSE(local_state.has_wrapped_pin()); |
| EXPECT_FALSE(local_state.registered()); |
| EXPECT_FALSE(local_state.joined()); |
| } |
| |
| // Tests that attempting to renew a PIN does not make Chrome crash if joining |
| // the physical device to the security domain failed. |
| // Regression test for crbug.com/404563934. |
| TEST_F(EnclaveManagerTest, RenewPinAfterJoiningFailed) { |
| const std::string kPin = "123456"; |
| ASSERT_TRUE(Register()); |
| |
| // Fail the request to join the physical device. This used to leave Chrome in |
| // an inconsistent state with a wrapped PIN but no wrapped security domain |
| // secret. |
| security_domain_service_->fail_join_requests_matching(base::BindRepeating( |
| [](const trusted_vault_pb::JoinSecurityDomainsRequest& request) { |
| return request.security_domain_member().member_type() == |
| trusted_vault_pb::SecurityDomainMember:: |
| MEMBER_TYPE_PHYSICAL_DEVICE; |
| })); |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(kPin, setup_future.GetCallback()); |
| ASSERT_TRUE(setup_future.Wait()); |
| ASSERT_FALSE(setup_future.Get()); |
| |
| // Force considering a PIN renewal. |
| manager_.local_state_for_testing() |
| .mutable_users() |
| ->find(gaia_id_.ToString()) |
| ->second.set_last_refreshed_pin_epoch_secs(0); |
| manager_.ConsiderPinRenewalForTesting(); |
| base::test::RunUntil([this]() { return manager_.is_idle(); }); |
| |
| // Chrome should not have attempted to renew the PIN. |
| EXPECT_EQ(manager_.local_state_for_testing() |
| .mutable_users() |
| ->find(gaia_id_.ToString()) |
| ->second.last_refreshed_pin_epoch_secs(), |
| 0); |
| } |
| |
| // Tests that attempting to renew a PIN does not make Chrome crash if there is |
| // no wrapped security domain secret. It used to be possible to end up in this |
| // state, so we need to make sure clients who got into it don't crash after |
| // startup. |
| // Regression test for crbug.com/404563934. |
| TEST_F(EnclaveManagerTest, RenewPinWithoutWrappedSecurityDomainSecret) { |
| const std::string kPin = "123456"; |
| ASSERT_TRUE(Register()); |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(kPin, setup_future.GetCallback()); |
| ASSERT_TRUE(setup_future.Wait()); |
| ASSERT_TRUE(setup_future.Get()); |
| |
| // Remove the security domain secrets. |
| manager_.local_state_for_testing() |
| .mutable_users() |
| ->find(gaia_id_.ToString()) |
| ->second.clear_wrapped_security_domain_secrets(); |
| |
| // Force considering a PIN renewal. |
| manager_.local_state_for_testing() |
| .mutable_users() |
| ->find(gaia_id_.ToString()) |
| ->second.set_last_refreshed_pin_epoch_secs(0); |
| manager_.ConsiderPinRenewalForTesting(); |
| base::test::RunUntil([this]() { return manager_.is_idle(); }); |
| |
| // Chrome should not have attempted to renew the PIN. |
| EXPECT_EQ(manager_.local_state_for_testing() |
| .mutable_users() |
| ->find(gaia_id_.ToString()) |
| ->second.last_refreshed_pin_epoch_secs(), |
| 0); |
| } |
| |
| class EnclaveManagerMockTimeTest : public EnclaveManagerTest { |
| public: |
| EnclaveManagerMockTimeTest() |
| : EnclaveManagerTest(base::test::TaskEnvironment::TimeSource::MOCK_TIME) { |
| } |
| }; |
| |
| TEST_F(EnclaveManagerMockTimeTest, AutomaticRenewal) { |
| const std::string pin = "123456"; |
| |
| BoolFuture setup_future; |
| manager_.SetupWithPIN(pin, setup_future.GetCallback()); |
| |
| // Because this test runs under MOCK_TIME, waiting for `setup_future` causes |
| // all events to run immediately, including the timeout timer for the |
| // WebSocket. Thus we have to step time forwards incrementally so that the |
| // enclave process (which isn't under MOCK_TIME) gets a chance to do |
| // something. |
| const base::TimeDelta time_step = base::Milliseconds(1); |
| while (!setup_future.IsReady()) { |
| base::PlatformThread::Sleep(time_step); |
| task_env_.FastForwardBy(time_step); |
| } |
| ASSERT_TRUE(manager_.is_ready()); |
| ASSERT_TRUE(manager_.has_wrapped_pin()); |
| |
| // When using MOCK_TIME, requests to the enclave will likely timeout as noted |
| // just above. But to avoid flakes, this ensures that the requests will always |
| // fail. |
| device::enclave::ScopedEnclaveOverride override( |
| TestWebAuthnEnclaveIdentity(/*port=*/100)); |
| |
| EXPECT_EQ(security_domain_service_->num_physical_members(), 1u); |
| EXPECT_EQ(security_domain_service_->num_pin_members(), 1u); |
| EXPECT_EQ(recovery_key_store_->vaults().size(), 1u); |
| |
| // Renewal will have been checked as soon as the state was loaded. |
| EXPECT_EQ(manager_.renewal_checks_for_testing(), 1u); |
| EXPECT_EQ(manager_.renewal_attempts_for_testing(), 0u); |
| |
| // After a day, another check should have been done. |
| task_env_.FastForwardBy(base::Hours(24)); |
| EXPECT_EQ(manager_.renewal_checks_for_testing(), 2u); |
| EXPECT_EQ(manager_.renewal_attempts_for_testing(), 0u); |
| |
| // After 30 days, there should be a renewal attempt. |
| task_env_.FastForwardBy(base::Days(30)); |
| EXPECT_EQ(manager_.renewal_checks_for_testing(), 32u); |
| EXPECT_EQ(manager_.renewal_attempts_for_testing(), 1u); |
| |
| // The renewal attempts will fail so an attempt should be made every day. |
| task_env_.FastForwardBy(base::Days(1)); |
| EXPECT_EQ(manager_.renewal_checks_for_testing(), 33u); |
| EXPECT_EQ(manager_.renewal_attempts_for_testing(), 2u); |
| |
| // Ensure that no operation is outstanding. |
| task_env_.FastForwardBy(base::Hours(1)); |
| } |
| |
| // UV keys are only supported on Windows macOS, and ChromeOS at this time. |
| #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS) |
| |
| std::string ToString(base::span<const uint8_t> v) { |
| return std::string(v.begin(), v.end()); |
| } |
| |
| class EnclaveUVTest : public EnclaveManagerTest { |
| protected: |
| void SetUp() override { |
| #if BUILDFLAG(IS_MAC) |
| scoped_fake_keychain_.SetUVMethod( |
| crypto::apple::ScopedFakeKeychainV2::UVMethod::kPasswordOnly); |
| #endif // BUILDFLAG(IS_MAC) |
| } |
| |
| void TearDown() override { |
| #if BUILDFLAG(IS_CHROMEOS) |
| OverrideWebAuthnChromeosUserVerifyingKeyProviderForTesting(nullptr); |
| #endif |
| } |
| |
| void DisableUVKeySupport() { |
| fake_provider_.emplace<crypto::ScopedNullUserVerifyingKeyProvider>(); |
| #if BUILDFLAG(IS_CHROMEOS) |
| // The scoped fake provider doesn't cover ChromeOS. |
| OverrideWebAuthnChromeosUserVerifyingKeyProviderForTesting([]() { |
| return std::unique_ptr<crypto::UserVerifyingKeyProvider>(nullptr); |
| }); |
| #endif |
| } |
| |
| void UseFailingUVKeySupport() { |
| fake_provider_.emplace<crypto::ScopedFailingUserVerifyingKeyProvider>(); |
| #if BUILDFLAG(IS_CHROMEOS) |
| // The scoped fake provider doesn't cover ChromeOS. |
| NOTIMPLEMENTED(); |
| #endif |
| } |
| |
| std::variant<crypto::ScopedFakeUserVerifyingKeyProvider, |
| crypto::ScopedNullUserVerifyingKeyProvider, |
| crypto::ScopedFailingUserVerifyingKeyProvider> |
| fake_provider_; |
| |
| #if BUILDFLAG(IS_MAC) |
| crypto::apple::ScopedFakeKeychainV2 scoped_fake_keychain_{ |
| "test-keychain-access-group"}; |
| #endif // BUILDFLAG(IS_MAC) |
| }; |
| |
| TEST_F(EnclaveUVTest, UserVerifyingKeyAvailable) { |
| security_domain_service_->pretend_there_are_members(); |
| NoArgFuture loaded_future; |
| manager_.Load(loaded_future.GetCallback()); |
| EXPECT_TRUE(loaded_future.Wait()); |
| |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(register_future.Wait()); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(add_future.Wait()); |
| |
| #if BUILDFLAG(IS_WIN) |
| EXPECT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/false), |
| EnclaveManager::UvKeyState::kUsesSystemUIDeferredCreation); |
| #else |
| EXPECT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/false), |
| EnclaveManager::UvKeyState::kUsesSystemUI); |
| #endif |
| } |
| |
| TEST_F(EnclaveUVTest, UserVerifyingKeyUnavailable) { |
| DisableUVKeySupport(); |
| security_domain_service_->pretend_there_are_members(); |
| NoArgFuture loaded_future; |
| manager_.Load(loaded_future.GetCallback()); |
| EXPECT_TRUE(loaded_future.Wait()); |
| |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(register_future.Wait()); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(add_future.Wait()); |
| ASSERT_TRUE(manager_.is_registered()); |
| EXPECT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/false), |
| EnclaveManager::UvKeyState::kNone); |
| } |
| |
| TEST_F(EnclaveUVTest, UserVerifyingKeyLost) { |
| security_domain_service_->pretend_there_are_members(); |
| NoArgFuture loaded_future; |
| manager_.Load(loaded_future.GetCallback()); |
| EXPECT_TRUE(loaded_future.Wait()); |
| |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(register_future.Wait()); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(add_future.Wait()); |
| |
| base::RepeatingClosure quit_closure; |
| #if BUILDFLAG(IS_WIN) |
| // Windows does deferred UV key creation. This test has to trigger the actual |
| // create before testing that it is later deleted. |
| EXPECT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/false), |
| EnclaveManager::UvKeyState::kUsesSystemUIDeferredCreation); |
| std::unique_ptr<EnclaveManager::UvKeyCreationLock> uv_creation_lock; |
| device::enclave::UVKeyCreationCallback key_creation_callback; |
| std::tie(uv_creation_lock, key_creation_callback) = |
| manager_.UserVerifyingKeyCreationCallback(); |
| quit_closure = task_env_.QuitClosure(); |
| std::move(key_creation_callback) |
| .Run(base::BindLambdaForTesting( |
| [&quit_closure](base::span<const uint8_t> uv_public_key) { |
| EXPECT_FALSE(uv_public_key.empty()); |
| quit_closure.Run(); |
| })); |
| task_env_.RunUntilQuit(); |
| #else |
| ASSERT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/false), |
| EnclaveManager::UvKeyState::kUsesSystemUI); |
| #endif |
| manager_.ClearCachedKeysForTesting(); |
| DisableUVKeySupport(); |
| auto signing_callback = |
| manager_.UserVerifyingKeySigningCallback(/*options=*/{}); |
| quit_closure = task_env_.QuitClosure(); |
| std::move(signing_callback) |
| .Run({1, 2, 3, 4}, |
| base::BindLambdaForTesting( |
| [&quit_closure]( |
| std::optional<enclave::ClientSignature> signature) { |
| EXPECT_EQ(signature, std::nullopt); |
| quit_closure.Run(); |
| })); |
| task_env_.RunUntilQuit(); |
| EXPECT_FALSE(manager_.is_registered()); |
| } |
| |
| TEST_F(EnclaveUVTest, UserVerifyingKeyUseExisting) { |
| security_domain_service_->pretend_there_are_members(); |
| NoArgFuture loaded_future; |
| manager_.Load(loaded_future.GetCallback()); |
| EXPECT_TRUE(loaded_future.Wait()); |
| |
| base::test::TestFuture< |
| base::expected<std::unique_ptr<crypto::UserVerifyingSigningKey>, |
| crypto::UserVerifyingKeyCreationError>> |
| key_future; |
| std::unique_ptr<crypto::UserVerifyingKeyProvider> key_provider = |
| crypto::GetUserVerifyingKeyProvider(/*config=*/{}); |
| key_provider->GenerateUserVerifyingSigningKey( |
| std::array{crypto::SignatureVerifier::ECDSA_SHA256}, |
| key_future.GetCallback()); |
| EXPECT_TRUE(key_future.Wait()); |
| manager_.local_state_for_testing() |
| .mutable_users() |
| ->begin() |
| ->second.set_uv_public_key( |
| ToString(key_future.Get().value()->GetPublicKey())); |
| manager_.local_state_for_testing() |
| .mutable_users() |
| ->begin() |
| ->second.set_wrapped_uv_private_key( |
| key_future.Get().value()->GetKeyLabel()); |
| |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(register_future.Wait()); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(add_future.Wait()); |
| |
| ASSERT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/false), |
| EnclaveManager::UvKeyState::kUsesSystemUI); |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| // Tests that if biometrics are available on macOS, Chrome will handle prompting |
| // the user for biometrics. |
| TEST_F(EnclaveUVTest, ChromeHandlesBiometrics) { |
| security_domain_service_->pretend_there_are_members(); |
| NoArgFuture loaded_future; |
| manager_.Load(loaded_future.GetCallback()); |
| EXPECT_TRUE(loaded_future.Wait()); |
| |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(register_future.Wait()); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(add_future.Wait()); |
| |
| scoped_fake_keychain_.SetUVMethod( |
| crypto::apple::ScopedFakeKeychainV2::UVMethod::kBiometrics); |
| EXPECT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/true), |
| EnclaveManager::UvKeyState::kUsesChromeUI); |
| |
| scoped_fake_keychain_.SetUVMethod( |
| crypto::apple::ScopedFakeKeychainV2::UVMethod::kPasswordOnly); |
| EXPECT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/false), |
| EnclaveManager::UvKeyState::kUsesSystemUI); |
| } |
| #endif // BUILDFLAG(IS_MAC) |
| |
| #if BUILDFLAG(IS_WIN) |
| TEST_F(EnclaveUVTest, DeferredUVKeyCreation) { |
| security_domain_service_->pretend_there_are_members(); |
| NoArgFuture loaded_future; |
| manager_.Load(loaded_future.GetCallback()); |
| EXPECT_TRUE(loaded_future.Wait()); |
| |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(register_future.Wait()); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(add_future.Wait()); |
| |
| EXPECT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/false), |
| EnclaveManager::UvKeyState::kUsesSystemUIDeferredCreation); |
| const auto& user_state = manager_.local_state_for_testing() |
| .users() |
| .find(gaia_id_.ToString()) |
| ->second; |
| EXPECT_TRUE(user_state.has_deferred_uv_key_creation() && |
| user_state.deferred_uv_key_creation()); |
| EXPECT_TRUE(user_state.wrapped_uv_private_key().empty()); |
| |
| std::unique_ptr<EnclaveManager::UvKeyCreationLock> uv_creation_lock; |
| device::enclave::UVKeyCreationCallback key_creation_callback; |
| std::tie(uv_creation_lock, key_creation_callback) = |
| manager_.UserVerifyingKeyCreationCallback(); |
| auto quit_closure = task_env_.QuitClosure(); |
| std::move(key_creation_callback) |
| .Run(base::BindLambdaForTesting( |
| [&quit_closure](base::span<const uint8_t> uv_public_key) { |
| EXPECT_FALSE(uv_public_key.empty()); |
| quit_closure.Run(); |
| })); |
| task_env_.RunUntilQuit(); |
| |
| EXPECT_FALSE(user_state.deferred_uv_key_creation()); |
| EXPECT_FALSE(user_state.wrapped_uv_private_key().empty()); |
| } |
| |
| TEST_F(EnclaveUVTest, UnregisterOnFailedDeferredUVKeyCreation) { |
| security_domain_service_->pretend_there_are_members(); |
| NoArgFuture loaded_future; |
| manager_.Load(loaded_future.GetCallback()); |
| EXPECT_TRUE(loaded_future.Wait()); |
| |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(register_future.Wait()); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(add_future.Wait()); |
| |
| EXPECT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/false), |
| EnclaveManager::UvKeyState::kUsesSystemUIDeferredCreation); |
| const auto& user_state = manager_.local_state_for_testing() |
| .users() |
| .find(gaia_id_.ToString()) |
| ->second; |
| EXPECT_TRUE(user_state.deferred_uv_key_creation()); |
| EXPECT_TRUE(user_state.wrapped_uv_private_key().empty()); |
| |
| UseFailingUVKeySupport(); |
| EnclaveManager::EnableInvariantChecksForTesting(false); |
| |
| base::RunLoop run_loop; |
| auto ui_request = std::make_unique<enclave::CredentialRequest>(); |
| ui_request->signing_callback = manager_.IdentityKeySigningCallback(); |
| ui_request->wrapped_secret = |
| *manager_.GetWrappedSecret(/*version=*/kSecretVersion); |
| ui_request->entity = GetTestEntity(); |
| ui_request->claimed_pin = nullptr; |
| ui_request->save_passkey_callback = base::BindOnce( |
| [](sync_pb::WebauthnCredentialSpecifics) { NOTREACHED(); }); |
| ui_request->up_and_uv_bits = |
| device::enclave::UserPresentAndVerifiedBits::kPresentAndVerified; |
| std::unique_ptr<EnclaveManager::UvKeyCreationLock> uv_creation_lock; |
| std::tie(uv_creation_lock, ui_request->uv_key_creation_callback) = |
| manager_.UserVerifyingKeyCreationCallback(); |
| ui_request->unregister_callback = |
| base::BindOnce(&EnclaveManager::Unenroll, manager_.GetWeakPtr(), |
| base::BindLambdaForTesting( |
| [&run_loop](bool) { run_loop.QuitWhenIdle(); })); |
| |
| GetAssertionResponseExpectation expected_response; |
| expected_response.result = device::GetAssertionStatus::kEnclaveError; |
| expected_response.size = 0; |
| DoAssertion(GetTestEntity(), /*claimed_pin=*/nullptr, expected_response, |
| std::move(ui_request)); |
| run_loop.Run(); |
| |
| EXPECT_FALSE(manager_.is_registered()); |
| } |
| |
| // Test that signing with a key that is unknown to the service unregisters |
| // the local client. |
| TEST_F(EnclaveUVTest, UnregisterOnMissingUserVerifyingKey) { |
| base::HistogramTester histogram_tester; |
| security_domain_service_->pretend_there_are_members(); |
| NoArgFuture loaded_future; |
| manager_.Load(loaded_future.GetCallback()); |
| EXPECT_TRUE(loaded_future.Wait()); |
| |
| BoolFuture register_future; |
| manager_.RegisterIfNeeded(register_future.GetCallback()); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(register_future.Wait()); |
| |
| std::vector<uint8_t> key(kTestKey.begin(), kTestKey.end()); |
| ASSERT_FALSE(manager_.has_pending_keys()); |
| manager_.StoreKeys(gaia_id_, {std::move(key)}, |
| /*last_key_version=*/kSecretVersion); |
| ASSERT_TRUE(manager_.is_idle()); |
| ASSERT_TRUE(manager_.has_pending_keys()); |
| |
| BoolFuture add_future; |
| ASSERT_TRUE(manager_.AddDeviceToAccount( |
| /*pin_metadata=*/std::nullopt, add_future.GetCallback())); |
| ASSERT_FALSE(manager_.is_idle()); |
| EXPECT_TRUE(add_future.Wait()); |
| |
| base::RepeatingClosure quit_closure; |
| |
| EXPECT_EQ(manager_.uv_key_state(/*platform_has_biometrics=*/false), |
| EnclaveManager::UvKeyState::kUsesSystemUIDeferredCreation); |
| |
| // Generate a UV key and reset the deferred UV key flag, without sending a |
| // public key to the service. |
| base::test::TestFuture< |
| base::expected<std::unique_ptr<crypto::UserVerifyingSigningKey>, |
| crypto::UserVerifyingKeyCreationError>> |
| key_future; |
| std::unique_ptr<crypto::UserVerifyingKeyProvider> key_provider = |
| crypto::GetUserVerifyingKeyProvider(/*config=*/{}); |
| key_provider->GenerateUserVerifyingSigningKey( |
| std::array{crypto::SignatureVerifier::ECDSA_SHA256}, |
| key_future.GetCallback()); |
| EXPECT_TRUE(key_future.Wait()); |
| manager_.local_state_for_testing() |
| .mutable_users() |
| ->begin() |
| ->second.set_uv_public_key( |
| ToString(key_future.Get().value()->GetPublicKey())); |
| manager_.local_state_for_testing() |
| .mutable_users() |
| ->begin() |
| ->second.set_wrapped_uv_private_key( |
| key_future.Get().value()->GetKeyLabel()); |
| manager_.local_state_for_testing() |
| .mutable_users() |
| ->begin() |
| ->second.set_deferred_uv_key_creation(false); |
| std::unique_ptr<crypto::UserVerifyingSigningKey> key_temp = |
| std::move(key_future.Take().value()); |
| manager_.user_verifying_key_ = |
| base::MakeRefCounted<crypto::RefCountedUserVerifyingSigningKey>( |
| std::move(key_temp)); |
| |
| base::RunLoop run_loop; |
| auto ui_request = std::make_unique<enclave::CredentialRequest>(); |
| ui_request->signing_callback = |
| manager_.UserVerifyingKeySigningCallback(/*options=*/{}); |
| ui_request->wrapped_secret = |
| *manager_.GetWrappedSecret(/*version=*/kSecretVersion); |
| ui_request->entity = GetTestEntity(); |
| ui_request->claimed_pin = nullptr; |
| ui_request->save_passkey_callback = base::BindOnce( |
| [](sync_pb::WebauthnCredentialSpecifics) { NOTREACHED(); }); |
| ui_request->up_and_uv_bits = |
| device::enclave::UserPresentAndVerifiedBits::kPresentAndVerified; |
| ui_request->unregister_callback = |
| base::BindOnce(&EnclaveManager::Unenroll, manager_.GetWeakPtr(), |
| base::BindLambdaForTesting( |
| [&run_loop](bool) { run_loop.QuitWhenIdle(); })); |
| |
| GetAssertionResponseExpectation expected_response; |
| expected_response.result = device::GetAssertionStatus::kEnclaveError; |
| expected_response.size = 0; |
| DoAssertion(GetTestEntity(), /*claimed_pin=*/nullptr, expected_response, |
| std::move(ui_request)); |
| run_loop.Run(); |
| |
| EXPECT_FALSE(manager_.is_registered()); |
| histogram_tester.ExpectBucketCount( |
| "WebAuthentication.EnclaveTransactionResult", |
| device::enclave::EnclaveTransactionResult::kMissingKey, 1); |
| } |
| |
| #endif // BUILDFLAG(IS_WIN) |
| |
| #endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS) |
| |
| #endif // !defined(MEMORY_SANITIZER) |