| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/webauth/authenticator_impl.h" |
| |
| #include <list> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/base64url.h" |
| #include "base/compiler_specific.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/flat_set.h" |
| #include "base/files/file.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/raw_ptr_exclusion.h" |
| #include "base/path_service.h" |
| #include "base/rand_util.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/run_loop.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/gtest_util.h" |
| #include "base/test/scoped_command_line.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_mock_time_task_runner.h" |
| #include "base/time/tick_clock.h" |
| #include "base/time/time.h" |
| #include "base/timer/timer.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "components/cbor/reader.h" |
| #include "components/cbor/values.h" |
| #include "components/webauthn/content/browser/internal_authenticator_impl.h" |
| #include "content/browser/webauth/authenticator_common_impl.h" |
| #include "content/browser/webauth/authenticator_environment.h" |
| #include "content/browser/webauth/client_data_json.h" |
| #include "content/public/browser/authenticator_request_client_delegate.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/test_browser_context.h" |
| #include "content/test/test_render_frame_host.h" |
| #include "crypto/sha2.h" |
| #include "device/base/features.h" |
| #include "device/bluetooth/bluetooth_adapter_factory.h" |
| #include "device/bluetooth/test/mock_bluetooth_adapter.h" |
| #include "device/fido/attested_credential_data.h" |
| #include "device/fido/authenticator_data.h" |
| #include "device/fido/cable/fido_tunnel_device.h" |
| #include "device/fido/cable/v2_authenticator.h" |
| #include "device/fido/cable/v2_constants.h" |
| #include "device/fido/cable/v2_discovery.h" |
| #include "device/fido/cable/v2_handshake.h" |
| #include "device/fido/cable/v2_test_util.h" |
| #include "device/fido/discoverable_credential_metadata.h" |
| #include "device/fido/fake_fido_discovery.h" |
| #include "device/fido/features.h" |
| #include "device/fido/fido_authenticator.h" |
| #include "device/fido/fido_constants.h" |
| #include "device/fido/fido_parsing_utils.h" |
| #include "device/fido/fido_test_data.h" |
| #include "device/fido/fido_transport_protocol.h" |
| #include "device/fido/fido_types.h" |
| #include "device/fido/filter.h" |
| #include "device/fido/hid/fake_hid_impl_for_testing.h" |
| #include "device/fido/large_blob.h" |
| #include "device/fido/mock_fido_device.h" |
| #include "device/fido/multiple_virtual_fido_device_factory.h" |
| #include "device/fido/pin.h" |
| #include "device/fido/public_key.h" |
| #include "device/fido/public_key_credential_descriptor.h" |
| #include "device/fido/public_key_credential_user_entity.h" |
| #include "device/fido/test_callback_receiver.h" |
| #include "device/fido/virtual_ctap2_device.h" |
| #include "device/fido/virtual_fido_device.h" |
| #include "device/fido/virtual_fido_device_factory.h" |
| #include "mojo/public/cpp/base/big_buffer.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "mojo/public/cpp/system/functions.h" |
| #include "services/data_decoder/gzipper.h" |
| #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h" |
| #include "third_party/boringssl/src/include/openssl/bytestring.h" |
| #include "third_party/boringssl/src/include/openssl/ec_key.h" |
| #include "third_party/boringssl/src/include/openssl/evp.h" |
| #include "third_party/boringssl/src/include/openssl/hmac.h" |
| #include "third_party/boringssl/src/include/openssl/obj.h" |
| #include "third_party/zlib/google/compression_utils.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/base/resource/resource_scale_factor.h" |
| #include "ui/base/ui_base_paths.h" |
| #include "url/origin.h" |
| #include "url/url_util.h" |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "device/fido/mac/authenticator_config.h" |
| #include "device/fido/mac/credential_store.h" |
| #include "device/fido/mac/scoped_icloud_keychain_test_environment.h" |
| #include "device/fido/mac/scoped_touch_id_test_environment.h" |
| #endif |
| |
| #if BUILDFLAG(IS_WIN) |
| #include "device/fido/win/fake_webauthn_api.h" |
| #endif |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chromeos/dbus/tpm_manager/tpm_manager_client.h" |
| #include "chromeos/dbus/u2f/u2f_client.h" |
| #endif |
| |
| namespace content { |
| |
| using ::testing::_; |
| |
| using blink::mojom::AttestationConveyancePreference; |
| using blink::mojom::AuthenticatorSelectionCriteria; |
| using blink::mojom::AuthenticatorSelectionCriteriaPtr; |
| using blink::mojom::AuthenticatorStatus; |
| using blink::mojom::AuthenticatorTransport; |
| using blink::mojom::CableAuthentication; |
| using blink::mojom::CableAuthenticationPtr; |
| using blink::mojom::CommonCredentialInfo; |
| using blink::mojom::GetAssertionAuthenticatorResponse; |
| using blink::mojom::GetAssertionAuthenticatorResponsePtr; |
| using blink::mojom::MakeCredentialAuthenticatorResponse; |
| using blink::mojom::MakeCredentialAuthenticatorResponsePtr; |
| using blink::mojom::PublicKeyCredentialCreationOptions; |
| using blink::mojom::PublicKeyCredentialCreationOptionsPtr; |
| using blink::mojom::PublicKeyCredentialDescriptor; |
| using blink::mojom::PublicKeyCredentialDescriptorPtr; |
| using blink::mojom::PublicKeyCredentialParameters; |
| using blink::mojom::PublicKeyCredentialParametersPtr; |
| using blink::mojom::PublicKeyCredentialRequestOptions; |
| using blink::mojom::PublicKeyCredentialRequestOptionsPtr; |
| using blink::mojom::PublicKeyCredentialRpEntity; |
| using blink::mojom::PublicKeyCredentialRpEntityPtr; |
| using blink::mojom::PublicKeyCredentialType; |
| using blink::mojom::PublicKeyCredentialUserEntity; |
| using blink::mojom::PublicKeyCredentialUserEntityPtr; |
| using blink::mojom::RemoteDesktopClientOverride; |
| using blink::mojom::RemoteDesktopClientOverridePtr; |
| using blink::mojom::WebAuthnDOMExceptionDetails; |
| using blink::mojom::WebAuthnDOMExceptionDetailsPtr; |
| using cbor::Reader; |
| using cbor::Value; |
| using device::VirtualCtap2Device; |
| using device::VirtualFidoDevice; |
| using device::cablev2::Event; |
| |
| namespace { |
| |
| using InterestingFailureReason = |
| AuthenticatorRequestClientDelegate::InterestingFailureReason; |
| using FailureReasonCallbackReceiver = |
| ::device::test::TestCallbackReceiver<InterestingFailureReason>; |
| |
| constexpr base::TimeDelta kTestTimeout = base::Minutes(1); |
| |
| // The size of credential IDs returned by GetTestCredentials(). |
| constexpr size_t kTestCredentialIdLength = 32u; |
| |
| constexpr char kTestOrigin1[] = "https://a.google.com"; |
| constexpr char kTestOrigin2[] = "https://acme.org"; |
| constexpr char kTestRelyingPartyId[] = "google.com"; |
| constexpr char kExtensionScheme[] = "chrome-extension"; |
| static constexpr char kCorpCrdOrigin[] = |
| "https://remotedesktop.corp.google.com"; |
| |
| constexpr uint8_t kTestChallengeBytes[] = { |
| 0x68, 0x71, 0x34, 0x96, 0x82, 0x22, 0xEC, 0x17, 0x20, 0x2E, 0x42, |
| 0x50, 0x5F, 0x8E, 0xD2, 0xB1, 0x6A, 0xE2, 0x2F, 0x16, 0xBB, 0x05, |
| 0xB8, 0x8C, 0x25, 0xDB, 0x9E, 0x60, 0x26, 0x45, 0xF1, 0x41}; |
| |
| constexpr char kTestRegisterClientDataJsonString[] = |
| R"({"challenge":"aHE0loIi7BcgLkJQX47SsWriLxa7BbiMJdueYCZF8UE","origin":)" |
| R"("https://a.google.com", "type":"webauthn.create"})"; |
| |
| constexpr char kTestSignClientDataJsonString[] = |
| R"({"challenge":"aHE0loIi7BcgLkJQX47SsWriLxa7BbiMJdueYCZF8UE","origin":)" |
| R"("https://a.google.com", "type":"webauthn.get"})"; |
| |
| typedef struct { |
| const char* origin; |
| // Either a relying party ID or a U2F AppID. |
| const char* claimed_authority; |
| AuthenticatorStatus expected_status; |
| } OriginClaimedAuthorityPair; |
| |
| constexpr OriginClaimedAuthorityPair kValidRelyingPartyTestCases[] = { |
| {"http://localhost", "localhost", AuthenticatorStatus::SUCCESS}, |
| {"https://myawesomedomain", "myawesomedomain", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://foo.bar.google.com", "foo.bar.google.com", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://foo.bar.google.com", "bar.google.com", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://foo.bar.google.com", "google.com", AuthenticatorStatus::SUCCESS}, |
| {"https://earth.login.awesomecompany", "login.awesomecompany", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://google.com:1337", "google.com", AuthenticatorStatus::SUCCESS}, |
| |
| // Hosts with trailing dot valid for rpIds with or without trailing dot. |
| // Hosts without trailing dots only matches rpIDs without trailing dot. |
| // Two trailing dots only matches rpIDs with two trailing dots. |
| {"https://google.com.", "google.com", AuthenticatorStatus::SUCCESS}, |
| {"https://google.com.", "google.com.", AuthenticatorStatus::SUCCESS}, |
| {"https://google.com..", "google.com..", AuthenticatorStatus::SUCCESS}, |
| |
| // Leading dots are ignored in canonicalized hosts. |
| {"https://.google.com", "google.com", AuthenticatorStatus::SUCCESS}, |
| {"https://..google.com", "google.com", AuthenticatorStatus::SUCCESS}, |
| {"https://.google.com", ".google.com", AuthenticatorStatus::SUCCESS}, |
| {"https://..google.com", ".google.com", AuthenticatorStatus::SUCCESS}, |
| {"https://accounts.google.com", ".google.com", |
| AuthenticatorStatus::SUCCESS}, |
| }; |
| |
| constexpr OriginClaimedAuthorityPair kInvalidRelyingPartyTestCases[] = { |
| {"https://google.com", "com", AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"http://google.com", "google.com", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"http://myawesomedomain", "myawesomedomain", |
| AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://google.com", "foo.bar.google.com", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"http://myawesomedomain", "randomdomain", |
| AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://myawesomedomain", "randomdomain", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://notgoogle.com", "google.com)", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://not-google.com", "google.com)", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://evil.appspot.com", "appspot.com", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://evil.co.uk", "co.uk", AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| |
| {"https://google.com", "google.com.", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://google.com", "google.com..", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://google.com", ".google.com", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://google.com..", "google.com", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://.com", "com.", AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://.co.uk", "co.uk.", AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| |
| {"https://1.2.3", "1.2.3", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://1.2.3", "2.3", AuthenticatorStatus::INVALID_DOMAIN}, |
| |
| {"https://127.0.0.1", "127.0.0.1", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://127.0.0.1", "27.0.0.1", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://127.0.0.1", ".0.0.1", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://127.0.0.1", "0.0.1", AuthenticatorStatus::INVALID_DOMAIN}, |
| |
| {"https://[::127.0.0.1]", "127.0.0.1", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://[::127.0.0.1]", "[127.0.0.1]", |
| AuthenticatorStatus::INVALID_DOMAIN}, |
| |
| {"https://[::1]", "1", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://[::1]", "1]", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://[::1]", "::1", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://[::1]", "[::1]", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://[1::1]", "::1", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://[1::1]", "::1]", AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://[1::1]", "[::1]", AuthenticatorStatus::INVALID_DOMAIN}, |
| |
| {"http://google.com:443", "google.com", |
| AuthenticatorStatus::INVALID_DOMAIN}, |
| {"data:google.com", "google.com", AuthenticatorStatus::OPAQUE_DOMAIN}, |
| {"data:text/html,google.com", "google.com", |
| AuthenticatorStatus::OPAQUE_DOMAIN}, |
| {"ws://google.com", "google.com", AuthenticatorStatus::INVALID_PROTOCOL}, |
| {"gopher://google.com", "google.com", AuthenticatorStatus::OPAQUE_DOMAIN}, |
| {"ftp://google.com", "google.com", AuthenticatorStatus::INVALID_PROTOCOL}, |
| {"file:///google.com", "google.com", AuthenticatorStatus::INVALID_PROTOCOL}, |
| // Use of webauthn from a WSS origin may be technically valid, but we |
| // prohibit use on non-HTTPS origins. (At least for now.) |
| {"wss://google.com", "google.com", AuthenticatorStatus::INVALID_PROTOCOL}, |
| |
| {"data:,", "", AuthenticatorStatus::OPAQUE_DOMAIN}, |
| {"https://google.com", "", AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"ws:///google.com", "", AuthenticatorStatus::INVALID_PROTOCOL}, |
| {"wss:///google.com", "", AuthenticatorStatus::INVALID_PROTOCOL}, |
| {"gopher://google.com", "", AuthenticatorStatus::OPAQUE_DOMAIN}, |
| {"ftp://google.com", "", AuthenticatorStatus::INVALID_PROTOCOL}, |
| {"file:///google.com", "", AuthenticatorStatus::INVALID_PROTOCOL}, |
| |
| // This case is acceptable according to spec, but both renderer |
| // and browser handling currently do not permit it. |
| {"https://login.awesomecompany", "awesomecompany", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| |
| // These are AppID test cases, but should also be invalid relying party |
| // examples too. |
| {"https://example.com", "https://com/", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://example.com", "https://com/foo", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://example.com", "https://foo.com/", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://example.com", "http://example.com", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"http://example.com", "https://example.com", |
| AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://127.0.0.1", "https://127.0.0.1", |
| AuthenticatorStatus::INVALID_DOMAIN}, |
| {"https://www.notgoogle.com", |
| "https://www.gstatic.com/securitykey/origins.json", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://www.google.com", |
| "https://www.gstatic.com/securitykey/origins.json#x", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://www.google.com", |
| "https://www.gstatic.com/securitykey/origins.json2", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://www.google.com", "https://gstatic.com/securitykey/origins.json", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://ggoogle.com", "https://www.gstatic.com/securitykey/origi", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| {"https://com", "https://www.gstatic.com/securitykey/origins.json", |
| AuthenticatorStatus::BAD_RELYING_PARTY_ID}, |
| }; |
| |
| using TestIsUvpaaCallback = device::test::ValueCallbackReceiver<bool>; |
| using TestMakeCredentialCallback = |
| device::test::StatusAndValuesCallbackReceiver< |
| AuthenticatorStatus, |
| MakeCredentialAuthenticatorResponsePtr, |
| WebAuthnDOMExceptionDetailsPtr>; |
| using TestGetAssertionCallback = device::test::StatusAndValuesCallbackReceiver< |
| AuthenticatorStatus, |
| GetAssertionAuthenticatorResponsePtr, |
| WebAuthnDOMExceptionDetailsPtr>; |
| using TestRequestStartedCallback = device::test::TestCallbackReceiver<>; |
| |
| std::vector<uint8_t> GetTestChallengeBytes() { |
| return std::vector<uint8_t>(std::begin(kTestChallengeBytes), |
| std::end(kTestChallengeBytes)); |
| } |
| |
| device::PublicKeyCredentialRpEntity GetTestPublicKeyCredentialRPEntity() { |
| device::PublicKeyCredentialRpEntity entity; |
| entity.id = std::string(kTestRelyingPartyId); |
| entity.name = "TestRP@example.com"; |
| return entity; |
| } |
| |
| device::PublicKeyCredentialUserEntity GetTestPublicKeyCredentialUserEntity() { |
| device::PublicKeyCredentialUserEntity entity; |
| entity.display_name = "User A. Name"; |
| std::vector<uint8_t> id(32, 0x0A); |
| entity.id = id; |
| entity.name = "username@example.com"; |
| return entity; |
| } |
| |
| std::vector<device::PublicKeyCredentialParams::CredentialInfo> |
| GetTestPublicKeyCredentialParameters(int32_t algorithm_identifier) { |
| std::vector<device::PublicKeyCredentialParams::CredentialInfo> parameters; |
| device::PublicKeyCredentialParams::CredentialInfo fake_parameter; |
| fake_parameter.type = device::CredentialType::kPublicKey; |
| fake_parameter.algorithm = algorithm_identifier; |
| parameters.push_back(std::move(fake_parameter)); |
| return parameters; |
| } |
| |
| device::AuthenticatorSelectionCriteria GetTestAuthenticatorSelectionCriteria() { |
| return device::AuthenticatorSelectionCriteria( |
| device::AuthenticatorAttachment::kAny, |
| device::ResidentKeyRequirement::kDiscouraged, |
| device::UserVerificationRequirement::kPreferred); |
| } |
| |
| std::vector<device::PublicKeyCredentialDescriptor> GetTestCredentials( |
| size_t num_credentials = 1) { |
| std::vector<device::PublicKeyCredentialDescriptor> descriptors; |
| for (size_t i = 0; i < num_credentials; i++) { |
| DCHECK(i <= std::numeric_limits<uint8_t>::max()); |
| std::vector<uint8_t> id(kTestCredentialIdLength, static_cast<uint8_t>(i)); |
| base::flat_set<device::FidoTransportProtocol> transports{ |
| device::FidoTransportProtocol::kUsbHumanInterfaceDevice, |
| device::FidoTransportProtocol::kBluetoothLowEnergy}; |
| descriptors.emplace_back(device::CredentialType::kPublicKey, std::move(id), |
| std::move(transports)); |
| } |
| return descriptors; |
| } |
| |
| PublicKeyCredentialCreationOptionsPtr |
| GetTestPublicKeyCredentialCreationOptions() { |
| auto options = PublicKeyCredentialCreationOptions::New(); |
| options->relying_party = GetTestPublicKeyCredentialRPEntity(); |
| options->user = GetTestPublicKeyCredentialUserEntity(); |
| options->public_key_parameters = GetTestPublicKeyCredentialParameters( |
| static_cast<int32_t>(device::CoseAlgorithmIdentifier::kEs256)); |
| options->challenge.assign(32, 0x0A); |
| options->timeout = base::Minutes(1); |
| options->authenticator_selection = GetTestAuthenticatorSelectionCriteria(); |
| return options; |
| } |
| |
| PublicKeyCredentialRequestOptionsPtr |
| GetTestPublicKeyCredentialRequestOptions() { |
| auto options = PublicKeyCredentialRequestOptions::New(); |
| options->relying_party_id = std::string(kTestRelyingPartyId); |
| options->challenge.assign(32, 0x0A); |
| options->timeout = base::Minutes(1); |
| options->user_verification = device::UserVerificationRequirement::kPreferred; |
| options->allow_credentials = GetTestCredentials(); |
| return options; |
| } |
| |
| std::vector<device::CableDiscoveryData> GetTestCableExtension() { |
| device::CableDiscoveryData cable; |
| cable.version = device::CableDiscoveryData::Version::V1; |
| cable.v1.emplace(); |
| cable.v1->client_eid.fill(0x01); |
| cable.v1->authenticator_eid.fill(0x02); |
| cable.v1->session_pre_key.fill(0x03); |
| |
| std::vector<device::CableDiscoveryData> ret; |
| ret.emplace_back(std::move(cable)); |
| return ret; |
| } |
| |
| device::AuthenticatorData AuthDataFromMakeCredentialResponse( |
| const MakeCredentialAuthenticatorResponsePtr& response) { |
| absl::optional<Value> attestation_value = |
| Reader::Read(response->attestation_object); |
| CHECK(attestation_value); |
| const auto& attestation = attestation_value->GetMap(); |
| |
| const auto auth_data_it = attestation.find(Value(device::kAuthDataKey)); |
| CHECK(auth_data_it != attestation.end()); |
| const std::vector<uint8_t>& auth_data = auth_data_it->second.GetBytestring(); |
| absl::optional<device::AuthenticatorData> parsed_auth_data = |
| device::AuthenticatorData::DecodeAuthenticatorData(auth_data); |
| return std::move(parsed_auth_data.value()); |
| } |
| |
| bool HasUV(const MakeCredentialAuthenticatorResponsePtr& response) { |
| return AuthDataFromMakeCredentialResponse(response) |
| .obtained_user_verification(); |
| } |
| |
| bool HasUV(const GetAssertionAuthenticatorResponsePtr& response) { |
| absl::optional<device::AuthenticatorData> auth_data = |
| device::AuthenticatorData::DecodeAuthenticatorData( |
| response->info->authenticator_data); |
| return auth_data->obtained_user_verification(); |
| } |
| |
| url::Origin GetTestOrigin() { |
| const GURL test_relying_party_url(kTestOrigin1); |
| CHECK(test_relying_party_url.is_valid()); |
| return url::Origin::Create(test_relying_party_url); |
| } |
| |
| std::string GetTestClientDataJSON(ClientDataRequestType type) { |
| return BuildClientDataJson({std::move(type), GetTestOrigin(), |
| GetTestChallengeBytes(), |
| /*is_cross_origin_iframe=*/false}); |
| } |
| |
| device::LargeBlob CompressLargeBlob(base::span<const uint8_t> blob) { |
| data_decoder::Gzipper gzipper; |
| std::vector<uint8_t> compressed; |
| base::RunLoop run_loop; |
| gzipper.Deflate( |
| blob, base::BindLambdaForTesting( |
| [&](absl::optional<mojo_base::BigBuffer> result) { |
| compressed = device::fido_parsing_utils::Materialize(*result); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| return device::LargeBlob(std::move(compressed), blob.size()); |
| } |
| |
| std::vector<uint8_t> UncompressLargeBlob(device::LargeBlob blob) { |
| data_decoder::Gzipper gzipper; |
| std::vector<uint8_t> uncompressed; |
| base::RunLoop run_loop; |
| gzipper.Inflate( |
| blob.compressed_data, blob.original_size, |
| base::BindLambdaForTesting( |
| [&](absl::optional<mojo_base::BigBuffer> result) { |
| if (result) { |
| uncompressed = device::fido_parsing_utils::Materialize(*result); |
| } else { |
| // Magic value to indicate failure. |
| const char kErrorMsg[] = "decompress error"; |
| uncompressed.assign( |
| reinterpret_cast<const uint8_t*>(kErrorMsg), |
| reinterpret_cast<const uint8_t*>(std::end(kErrorMsg))); |
| } |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| return uncompressed; |
| } |
| |
| // Convert a blink::mojom::AttestationConveyancePreference to a |
| // device::AtttestationConveyancePreference. |
| device::AttestationConveyancePreference ConvertAttestationConveyancePreference( |
| AttestationConveyancePreference in) { |
| switch (in) { |
| case AttestationConveyancePreference::NONE: |
| return ::device::AttestationConveyancePreference::kNone; |
| case AttestationConveyancePreference::INDIRECT: |
| return ::device::AttestationConveyancePreference::kIndirect; |
| case AttestationConveyancePreference::DIRECT: |
| return ::device::AttestationConveyancePreference::kDirect; |
| case AttestationConveyancePreference::ENTERPRISE: |
| return ::device::AttestationConveyancePreference:: |
| kEnterpriseIfRPListedOnAuthenticator; |
| } |
| } |
| |
| std::array<uint8_t, crypto::kSHA256Length> EvaluateHMAC( |
| base::span<const uint8_t> key, |
| base::span<const uint8_t> salt) { |
| std::array<uint8_t, crypto::kSHA256Length> ret; |
| unsigned hmac_out_length; |
| HMAC(EVP_sha256(), key.data(), key.size(), salt.data(), salt.size(), |
| ret.data(), &hmac_out_length); |
| CHECK_EQ(hmac_out_length, ret.size()); |
| return ret; |
| } |
| |
| } // namespace |
| |
| class AuthenticatorTestBase : public RenderViewHostTestHarness { |
| protected: |
| AuthenticatorTestBase() |
| : RenderViewHostTestHarness( |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} |
| ~AuthenticatorTestBase() override = default; |
| |
| static void SetUpTestSuite() { |
| #if BUILDFLAG(IS_MAC) |
| // Load fido_strings, which can be required for exercising the Touch ID |
| // authenticator. |
| base::FilePath path; |
| ASSERT_TRUE(base::PathService::Get(base::DIR_ASSETS, &path)); |
| base::FilePath fido_test_strings = |
| path.Append(FILE_PATH_LITERAL("fido_test_strings.pak")); |
| ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath( |
| fido_test_strings, ui::kScaleFactorNone); |
| #endif |
| } |
| |
| void SetUp() override { |
| RenderViewHostTestHarness::SetUp(); |
| |
| mojo::SetDefaultProcessErrorHandler(base::BindRepeating( |
| &AuthenticatorTestBase::OnMojoError, base::Unretained(this))); |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| chromeos::TpmManagerClient::InitializeFake(); |
| chromeos::U2FClient::InitializeFake(); |
| #endif |
| |
| #if BUILDFLAG(IS_WIN) |
| // Disable the Windows WebAuthn API integration by default. Individual tests |
| // can modify this. |
| fake_win_webauthn_api_.set_available(false); |
| AuthenticatorEnvironment::GetInstance()->SetWinWebAuthnApiForTesting( |
| &fake_win_webauthn_api_); |
| #endif |
| |
| ResetVirtualDevice(); |
| } |
| |
| void TearDown() override { |
| RenderViewHostTestHarness::TearDown(); |
| |
| mojo::SetDefaultProcessErrorHandler(base::NullCallback()); |
| |
| AuthenticatorEnvironment::GetInstance()->Reset(); |
| #if BUILDFLAG(IS_CHROMEOS) |
| chromeos::U2FClient::Shutdown(); |
| chromeos::TpmManagerClient::Shutdown(); |
| #endif |
| } |
| |
| virtual void ResetVirtualDevice() { |
| auto virtual_device_factory = |
| std::make_unique<device::test::VirtualFidoDeviceFactory>(); |
| virtual_device_factory_ = virtual_device_factory.get(); |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::move(virtual_device_factory)); |
| #if BUILDFLAG(IS_WIN) |
| virtual_device_factory_->set_win_webauthn_api(&fake_win_webauthn_api_); |
| #endif |
| } |
| |
| void SetMojoErrorHandler( |
| base::RepeatingCallback<void(const std::string&)> callback) { |
| mojo_error_handler_ = callback; |
| } |
| |
| raw_ptr<device::test::VirtualFidoDeviceFactory, DanglingUntriaged> |
| virtual_device_factory_; |
| #if BUILDFLAG(IS_WIN) |
| device::FakeWinWebAuthnApi fake_win_webauthn_api_; |
| #endif |
| |
| private: |
| void OnMojoError(const std::string& error) { |
| if (mojo_error_handler_) { |
| mojo_error_handler_.Run(error); |
| return; |
| } |
| FAIL() << "Unhandled mojo error: " << error; |
| } |
| |
| base::RepeatingCallback<void(const std::string&)> mojo_error_handler_; |
| }; |
| |
| class AuthenticatorImplTest : public AuthenticatorTestBase { |
| protected: |
| AuthenticatorImplTest() { |
| url::AddStandardScheme("chrome-extension", url::SCHEME_WITH_HOST); |
| } |
| ~AuthenticatorImplTest() override = default; |
| |
| void SetUp() override { |
| AuthenticatorTestBase::SetUp(); |
| bluetooth_global_values_->SetLESupported(true); |
| device::BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter_); |
| } |
| |
| void NavigateAndCommit(const GURL& url) { |
| RenderViewHostTestHarness::NavigateAndCommit(url); |
| } |
| |
| mojo::Remote<blink::mojom::Authenticator> ConnectToAuthenticator() { |
| mojo::Remote<blink::mojom::Authenticator> authenticator; |
| static_cast<RenderFrameHostImpl*>(main_rfh()) |
| ->GetWebAuthenticationService( |
| authenticator.BindNewPipeAndPassReceiver()); |
| return authenticator; |
| } |
| |
| bool AuthenticatorIsUvpaa() { |
| TestIsUvpaaCallback cb; |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| authenticator->IsUserVerifyingPlatformAuthenticatorAvailable(cb.callback()); |
| cb.WaitForCallback(); |
| return cb.value(); |
| } |
| |
| bool AuthenticatorIsConditionalMediationAvailable() { |
| TestIsUvpaaCallback cb; |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| authenticator->IsConditionalMediationAvailable(cb.callback()); |
| cb.WaitForCallback(); |
| return cb.value(); |
| } |
| |
| struct MakeCredentialResult { |
| AuthenticatorStatus status; |
| MakeCredentialAuthenticatorResponsePtr response; |
| }; |
| |
| MakeCredentialResult AuthenticatorMakeCredential() { |
| return AuthenticatorMakeCredential( |
| GetTestPublicKeyCredentialCreationOptions()); |
| } |
| |
| MakeCredentialResult AuthenticatorMakeCredential( |
| PublicKeyCredentialCreationOptionsPtr options) { |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| TestMakeCredentialCallback callback_receiver; |
| authenticator->MakeCredential(std::move(options), |
| callback_receiver.callback()); |
| callback_receiver.WaitForCallback(); |
| auto [status, response, dom_exception] = callback_receiver.TakeResult(); |
| return {status, std::move(response)}; |
| } |
| |
| MakeCredentialResult AuthenticatorMakeCredentialAndWaitForTimeout( |
| PublicKeyCredentialCreationOptionsPtr options) { |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| TestMakeCredentialCallback callback_receiver; |
| authenticator->MakeCredential(std::move(options), |
| callback_receiver.callback()); |
| task_environment()->FastForwardBy(kTestTimeout); |
| callback_receiver.WaitForCallback(); |
| auto [status, response, dom_exception] = callback_receiver.TakeResult(); |
| return {status, std::move(response)}; |
| } |
| |
| struct GetAssertionResult { |
| AuthenticatorStatus status; |
| GetAssertionAuthenticatorResponsePtr response; |
| }; |
| |
| GetAssertionResult AuthenticatorGetAssertion() { |
| return AuthenticatorGetAssertion( |
| GetTestPublicKeyCredentialRequestOptions()); |
| } |
| |
| GetAssertionResult AuthenticatorGetAssertion( |
| PublicKeyCredentialRequestOptionsPtr options) { |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| TestGetAssertionCallback callback_receiver; |
| authenticator->GetAssertion(std::move(options), |
| callback_receiver.callback()); |
| callback_receiver.WaitForCallback(); |
| auto [status, response, dom_exception] = callback_receiver.TakeResult(); |
| return {status, std::move(response)}; |
| } |
| |
| GetAssertionResult AuthenticatorGetAssertionAndWaitForTimeout( |
| PublicKeyCredentialRequestOptionsPtr options) { |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| TestGetAssertionCallback callback_receiver; |
| authenticator->GetAssertion(std::move(options), |
| callback_receiver.callback()); |
| task_environment()->FastForwardBy(kTestTimeout); |
| auto [status, response, dom_exception] = callback_receiver.TakeResult(); |
| return {status, std::move(response)}; |
| } |
| |
| AuthenticatorStatus TryAuthenticationWithAppId(const std::string& origin, |
| const std::string& appid) { |
| const GURL origin_url(origin); |
| NavigateAndCommit(origin_url); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->relying_party_id = origin_url.host(); |
| options->appid = appid; |
| |
| return AuthenticatorGetAssertion(std::move(options)).status; |
| } |
| |
| AuthenticatorStatus TryRegistrationWithAppIdExclude( |
| const std::string& origin, |
| const std::string& appid_exclude) { |
| const GURL origin_url(origin); |
| NavigateAndCommit(origin_url); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->relying_party.id = origin_url.host(); |
| options->appid_exclude = appid_exclude; |
| |
| return AuthenticatorMakeCredential(std::move(options)).status; |
| } |
| |
| // HasDevicePublicKeyExtensionInAuthenticatorData returns true if `response` |
| // contains a DPK extension in the authenticator data of the response. |
| bool HasDevicePublicKeyExtensionInAuthenticatorData( |
| const MakeCredentialAuthenticatorResponsePtr& response) { |
| device::AuthenticatorData parsed_auth_data = |
| AuthDataFromMakeCredentialResponse(response); |
| |
| const auto& extensions = parsed_auth_data.extensions(); |
| if (!extensions) { |
| return false; |
| } |
| |
| const cbor::Value::MapValue& extensions_map = extensions->GetMap(); |
| return extensions_map.find(cbor::Value( |
| device::kExtensionDevicePublicKey)) != extensions_map.end(); |
| } |
| |
| scoped_refptr<::testing::NiceMock<device::MockBluetoothAdapter>> |
| mock_adapter_ = base::MakeRefCounted< |
| ::testing::NiceMock<device::MockBluetoothAdapter>>(); |
| |
| private: |
| std::unique_ptr<device::BluetoothAdapterFactory::GlobalValuesForTesting> |
| bluetooth_global_values_ = |
| device::BluetoothAdapterFactory::Get()->InitGlobalValuesForTesting(); |
| data_decoder::test::InProcessDataDecoder data_decoder_service_; |
| url::ScopedSchemeRegistryForTests scoped_registry_; |
| }; |
| |
| TEST_F(AuthenticatorImplTest, ClientDataJSONSerialization) { |
| // First test that the output is in the expected form. Some verifiers may be |
| // depending on the exact JSON serialisation. Since the serialisation may add |
| // extra elements, this can only test that the expected value is a prefix of |
| // the returned value. |
| std::vector<uint8_t> challenge_bytes = {1, 2, 3}; |
| EXPECT_EQ( |
| BuildClientDataJson({ClientDataRequestType::kWebAuthnCreate, |
| GetTestOrigin(), challenge_bytes, false}) |
| .find( |
| "{\"type\":\"webauthn.create\",\"challenge\":\"AQID\",\"origin\":" |
| "\"https://a.google.com\",\"crossOrigin\":false"), |
| 0u); |
| |
| // Second, check that a generic JSON parser correctly parses the result. |
| static const struct { |
| const ClientDataRequestType type; |
| url::Origin origin; |
| std::vector<uint8_t> challenge; |
| bool is_cross_origin; |
| } kTestCases[] = { |
| { |
| ClientDataRequestType::kWebAuthnGet, |
| GetTestOrigin(), |
| {1, 2, 3}, |
| false, |
| }, |
| { |
| ClientDataRequestType::kPaymentGet, |
| GetTestOrigin(), |
| {1, 2, 3}, |
| false, |
| }, |
| }; |
| |
| size_t num = 0; |
| for (const auto& test : kTestCases) { |
| SCOPED_TRACE(num++); |
| |
| const std::string json = BuildClientDataJson( |
| {test.type, test.origin, test.challenge, test.is_cross_origin}); |
| |
| const auto parsed = base::JSONReader::Read(json); |
| ASSERT_TRUE(parsed.has_value()); |
| std::string type_key; |
| std::string expected_type; |
| switch (test.type) { |
| case ClientDataRequestType::kWebAuthnCreate: |
| type_key = "type"; |
| expected_type = "webauthn.create"; |
| break; |
| case ClientDataRequestType::kWebAuthnGet: |
| type_key = "type"; |
| expected_type = "webauthn.get"; |
| break; |
| case ClientDataRequestType::kPaymentGet: |
| type_key = "type"; |
| expected_type = "payment.get"; |
| break; |
| } |
| ASSERT_TRUE(parsed->is_dict()); |
| EXPECT_EQ(*parsed->GetDict().FindString(type_key), expected_type); |
| EXPECT_EQ(*parsed->GetDict().FindString("origin"), test.origin.Serialize()); |
| std::string expected_challenge; |
| base::Base64UrlEncode( |
| base::StringPiece(reinterpret_cast<const char*>(test.challenge.data()), |
| test.challenge.size()), |
| base::Base64UrlEncodePolicy::OMIT_PADDING, &expected_challenge); |
| EXPECT_EQ(*parsed->GetDict().FindString("challenge"), expected_challenge); |
| EXPECT_EQ(*parsed->GetDict().FindBool("crossOrigin"), test.is_cross_origin); |
| } |
| } |
| |
| // Verify behavior for various combinations of origins and RP IDs. |
| TEST_F(AuthenticatorImplTest, MakeCredentialOriginAndRpIds) { |
| std::vector<OriginClaimedAuthorityPair> tests( |
| &kValidRelyingPartyTestCases[0], |
| &kValidRelyingPartyTestCases[std::size(kValidRelyingPartyTestCases)]); |
| tests.insert( |
| tests.end(), &kInvalidRelyingPartyTestCases[0], |
| &kInvalidRelyingPartyTestCases[std::size(kInvalidRelyingPartyTestCases)]); |
| |
| for (const auto& test_case : tests) { |
| SCOPED_TRACE(std::string(test_case.claimed_authority) + " " + |
| std::string(test_case.origin)); |
| |
| NavigateAndCommit(GURL(test_case.origin)); |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->relying_party.id = test_case.claimed_authority; |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| test_case.expected_status); |
| } |
| } |
| |
| // Test that MakeCredential request times out with NOT_ALLOWED_ERROR if user |
| // verification is required for U2F devices. |
| TEST_F(AuthenticatorImplTest, MakeCredentialUserVerification) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->authenticator_selection->user_verification_requirement = |
| device::UserVerificationRequirement::kRequired; |
| |
| EXPECT_EQ( |
| AuthenticatorMakeCredentialAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| TEST_F(AuthenticatorImplTest, MakeCredentialResidentKeyUnsupported) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->authenticator_selection->resident_key = |
| device::ResidentKeyRequirement::kRequired; |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::RESIDENT_CREDENTIALS_UNSUPPORTED); |
| } |
| |
| // Test that MakeCredential request times out with NOT_ALLOWED_ERROR if a |
| // platform authenticator is requested for U2F devices. |
| TEST_F(AuthenticatorImplTest, MakeCredentialPlatformAuthenticator) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->authenticator_selection->authenticator_attachment = |
| device::AuthenticatorAttachment::kPlatform; |
| |
| EXPECT_EQ( |
| AuthenticatorMakeCredentialAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| // Parses its arguments as JSON and expects that all the keys in the first are |
| // also in the second, and with the same value. |
| static void CheckJSONIsSubsetOfJSON(base::StringPiece subset_str, |
| base::StringPiece test_str) { |
| absl::optional<base::Value> subset = base::JSONReader::Read(subset_str); |
| ASSERT_TRUE(subset); |
| ASSERT_TRUE(subset->is_dict()); |
| const base::Value::Dict& subset_dict = subset->GetDict(); |
| absl::optional<base::Value> test = base::JSONReader::Read(test_str); |
| ASSERT_TRUE(test); |
| ASSERT_TRUE(test->is_dict()); |
| const base::Value::Dict& test_dict = test->GetDict(); |
| |
| for (auto item : subset_dict) { |
| const base::Value* test_value = test_dict.Find(item.first); |
| if (test_value == nullptr) { |
| ADD_FAILURE() << item.first << " does not exist in the test dictionary"; |
| continue; |
| } |
| |
| EXPECT_EQ(item.second, *test_value); |
| } |
| } |
| |
| // Test that client data serializes to JSON properly. |
| TEST(ClientDataSerializationTest, Register) { |
| CheckJSONIsSubsetOfJSON( |
| kTestRegisterClientDataJsonString, |
| GetTestClientDataJSON(ClientDataRequestType::kWebAuthnCreate)); |
| } |
| |
| TEST(ClientDataSerializationTest, Sign) { |
| CheckJSONIsSubsetOfJSON( |
| kTestSignClientDataJsonString, |
| GetTestClientDataJSON(ClientDataRequestType::kWebAuthnGet)); |
| } |
| |
| TEST_F(AuthenticatorImplTest, TestMakeCredentialTimeout) { |
| // Don't provide an authenticator tap so the request times out. |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting( |
| [&](device::VirtualFidoDevice* device) { return false; }); |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| |
| EXPECT_EQ( |
| AuthenticatorMakeCredentialAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| // Verify behavior for various combinations of origins and RP IDs. |
| TEST_F(AuthenticatorImplTest, GetAssertionOriginAndRpIds) { |
| // These instances should return security errors (for circumstances |
| // that would normally crash the renderer). |
| for (const OriginClaimedAuthorityPair& test_case : |
| kInvalidRelyingPartyTestCases) { |
| SCOPED_TRACE(std::string(test_case.claimed_authority) + " " + |
| std::string(test_case.origin)); |
| |
| NavigateAndCommit(GURL(test_case.origin)); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->relying_party_id = test_case.claimed_authority; |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| test_case.expected_status); |
| } |
| } |
| |
| constexpr OriginClaimedAuthorityPair kValidAppIdCases[] = { |
| {"https://example.com", "https://example.com", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://www.example.com", "https://example.com", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://example.com", "https://www.example.com", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://example.com", "https://foo.bar.example.com", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://example.com", "https://foo.bar.example.com/foo/bar", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://google.com", "https://www.gstatic.com/securitykey/origins.json", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://www.google.com", |
| "https://www.gstatic.com/securitykey/origins.json", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://www.google.com", |
| "https://www.gstatic.com/securitykey/a/google.com/origins.json", |
| AuthenticatorStatus::SUCCESS}, |
| {"https://accounts.google.com", |
| "https://www.gstatic.com/securitykey/origins.json", |
| AuthenticatorStatus::SUCCESS}, |
| }; |
| |
| // Verify behavior for various combinations of origins and RP IDs. |
| TEST_F(AuthenticatorImplTest, AppIdExtensionValues) { |
| for (const auto& test_case : kValidAppIdCases) { |
| SCOPED_TRACE(std::string(test_case.origin) + " " + |
| std::string(test_case.claimed_authority)); |
| |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, |
| TryAuthenticationWithAppId(test_case.origin, |
| test_case.claimed_authority)); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, |
| TryRegistrationWithAppIdExclude(test_case.origin, |
| test_case.claimed_authority)); |
| } |
| |
| // All the invalid relying party test cases should also be invalid as AppIDs. |
| for (const auto& test_case : kInvalidRelyingPartyTestCases) { |
| SCOPED_TRACE(std::string(test_case.origin) + " " + |
| std::string(test_case.claimed_authority)); |
| |
| if (strlen(test_case.claimed_authority) == 0) { |
| // In this case, no AppID is actually being tested. |
| continue; |
| } |
| |
| AuthenticatorStatus test_status = TryAuthenticationWithAppId( |
| test_case.origin, test_case.claimed_authority); |
| EXPECT_TRUE(test_status == AuthenticatorStatus::INVALID_DOMAIN || |
| test_status == test_case.expected_status); |
| |
| test_status = TryRegistrationWithAppIdExclude(test_case.origin, |
| test_case.claimed_authority); |
| EXPECT_TRUE(test_status == AuthenticatorStatus::INVALID_DOMAIN || |
| test_status == test_case.expected_status); |
| } |
| } |
| |
| // Verify that a credential registered with U2F can be used via webauthn. |
| TEST_F(AuthenticatorImplTest, AppIdExtension) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| { |
| // First, test that the appid extension isn't echoed at all when not |
| // requested. |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(result.response->echo_appid_extension, false); |
| } |
| |
| { |
| // Second, test that the appid extension is echoed, but is false, when appid |
| // is requested but not used. |
| ResetVirtualDevice(); |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| // This AppID won't be used because the RP ID will be tried (successfully) |
| // first. |
| options->appid = kTestOrigin1; |
| |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(result.response->echo_appid_extension, true); |
| EXPECT_EQ(result.response->appid_extension, false); |
| } |
| |
| { |
| // Lastly, when used, the appid extension result should be "true". |
| ResetVirtualDevice(); |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| // Inject a registration for the URL (which is a U2F AppID). |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestOrigin1)); |
| |
| options->appid = kTestOrigin1; |
| |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(result.response->echo_appid_extension, true); |
| EXPECT_EQ(result.response->appid_extension, true); |
| } |
| |
| { |
| // AppID should still work when the authenticator supports credProtect. |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| config.u2f_support = true; |
| config.pin_support = true; |
| config.resident_key_support = true; |
| config.cred_protect_support = true; |
| |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| // Inject a registration for the URL (which is a U2F AppID). |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestOrigin1)); |
| |
| options->appid = kTestOrigin1; |
| |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(result.response->echo_appid_extension, true); |
| EXPECT_EQ(result.response->appid_extension, true); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, AppIdExcludeExtension) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| // Attempt to register a credential using the appidExclude extension. It |
| // should fail when the registration already exists on the authenticator. |
| for (bool credential_already_exists : {false, true}) { |
| SCOPED_TRACE(credential_already_exists); |
| |
| for (bool is_ctap2 : {false, true}) { |
| SCOPED_TRACE(is_ctap2); |
| |
| ResetVirtualDevice(); |
| virtual_device_factory_->SetSupportedProtocol( |
| is_ctap2 ? device::ProtocolVersion::kCtap2 |
| : device::ProtocolVersion::kU2f); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->appid_exclude = kTestOrigin1; |
| options->exclude_credentials = GetTestCredentials(); |
| |
| if (credential_already_exists) { |
| ASSERT_TRUE( |
| virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->exclude_credentials[0].id, kTestOrigin1)); |
| } |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| |
| if (credential_already_exists) { |
| ASSERT_EQ(result.status, AuthenticatorStatus::CREDENTIAL_EXCLUDED); |
| } else { |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| } |
| } |
| } |
| |
| { |
| // Using appidExclude with an empty exclude list previously caused a crash. |
| // See https://bugs.chromium.org/p/chromium/issues/detail?id=1054499. |
| virtual_device_factory_->SetSupportedProtocol( |
| device::ProtocolVersion::kCtap2); |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->appid_exclude = kTestOrigin1; |
| options->exclude_credentials.clear(); |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| |
| { |
| // Also test the case where all credential IDs are eliminated because of |
| // their size. |
| device::VirtualCtap2Device::Config config; |
| config.max_credential_count_in_list = 1; |
| config.max_credential_id_length = 1; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->appid_exclude = kTestOrigin1; |
| options->exclude_credentials = GetTestCredentials(); |
| |
| for (const auto& cred : options->exclude_credentials) { |
| ASSERT_GT(cred.id.size(), config.max_credential_id_length); |
| } |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, TestGetAssertionTimeout) { |
| // The VirtualFidoAuthenticator simulates a tap immediately after it gets the |
| // request. Replace by the real discovery that will wait until timeout. |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::make_unique<device::FidoDiscoveryFactory>()); |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| |
| EXPECT_EQ( |
| AuthenticatorGetAssertionAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| TEST_F(AuthenticatorImplTest, OversizedCredentialId) { |
| // 255 is the maximum size of a U2F credential ID. We also test one greater |
| // (256) to ensure that nothing untoward happens. |
| const std::vector<size_t> kSizes = {255, 256}; |
| |
| for (const size_t size : kSizes) { |
| SCOPED_TRACE(size); |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| device::PublicKeyCredentialDescriptor credential; |
| credential.credential_type = device::CredentialType::kPublicKey; |
| credential.id.resize(size); |
| credential.transports.emplace( |
| device::FidoTransportProtocol::kUsbHumanInterfaceDevice); |
| |
| const bool should_be_valid = size < 256; |
| if (should_be_valid) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| credential.id, kTestRelyingPartyId)); |
| } |
| |
| options->allow_credentials.emplace_back(credential); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| should_be_valid ? AuthenticatorStatus::SUCCESS |
| : AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, NoSilentAuthenticationForCable) { |
| // https://crbug.com/954355 |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (bool is_cable_device : {false, true}) { |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| config.reject_silent_authentication_requests = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials = GetTestCredentials(/*num_credentials=*/2); |
| options->cable_authentication_data = GetTestCableExtension(); |
| |
| if (is_cable_device) { |
| virtual_device_factory_->SetTransport( |
| device::FidoTransportProtocol::kHybrid); |
| for (auto& cred : options->allow_credentials) { |
| cred.transports.clear(); |
| cred.transports.emplace(device::FidoTransportProtocol::kHybrid); |
| } |
| } |
| |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| // If a caBLE device is not simulated then silent requests should be used. |
| // The virtual device will return an error because |
| // |reject_silent_authentication_requests| is true and then it'll |
| // immediately resolve the touch request. |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| is_cable_device ? AuthenticatorStatus::SUCCESS |
| : AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, TestGetAssertionU2fDeviceBackwardsCompatibility) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| // Inject credential ID to the virtual device so that successful sign in is |
| // possible. |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| |
| TEST_F(AuthenticatorImplTest, GetAssertionWithEmptyAllowCredentials) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials.clear(); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::RESIDENT_CREDENTIALS_UNSUPPORTED); |
| } |
| |
| TEST_F(AuthenticatorImplTest, MakeCredentialAlreadyRegistered) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| |
| // Exclude the one already registered credential. |
| options->exclude_credentials = GetTestCredentials(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->exclude_credentials[0].id, kTestRelyingPartyId)); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::CREDENTIAL_EXCLUDED); |
| } |
| |
| TEST_F(AuthenticatorImplTest, MakeCredentialPendingRequest) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| |
| // Make first request. |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| TestMakeCredentialCallback callback_receiver; |
| authenticator->MakeCredential(std::move(options), |
| callback_receiver.callback()); |
| |
| // Make second request. |
| // TODO(crbug.com/785955): Rework to ensure there are potential race |
| // conditions once we have VirtualAuthenticatorEnvironment. |
| PublicKeyCredentialCreationOptionsPtr options2 = |
| GetTestPublicKeyCredentialCreationOptions(); |
| TestMakeCredentialCallback callback_receiver2; |
| authenticator->MakeCredential(std::move(options2), |
| callback_receiver2.callback()); |
| callback_receiver2.WaitForCallback(); |
| |
| EXPECT_EQ(AuthenticatorStatus::PENDING_REQUEST, callback_receiver2.status()); |
| |
| callback_receiver.WaitForCallback(); |
| } |
| |
| TEST_F(AuthenticatorImplTest, GetAssertionPendingRequest) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| |
| // Make first request. |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| TestGetAssertionCallback callback_receiver; |
| authenticator->GetAssertion(std::move(options), callback_receiver.callback()); |
| |
| // Make second request. |
| // TODO(crbug.com/785955): Rework to ensure there are potential race |
| // conditions once we have VirtualAuthenticatorEnvironment. |
| PublicKeyCredentialRequestOptionsPtr options2 = |
| GetTestPublicKeyCredentialRequestOptions(); |
| TestGetAssertionCallback callback_receiver2; |
| authenticator->GetAssertion(std::move(options2), |
| callback_receiver2.callback()); |
| callback_receiver2.WaitForCallback(); |
| |
| EXPECT_EQ(AuthenticatorStatus::PENDING_REQUEST, callback_receiver2.status()); |
| |
| callback_receiver.WaitForCallback(); |
| } |
| |
| TEST_F(AuthenticatorImplTest, NavigationDuringOperation) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| |
| base::RunLoop run_loop; |
| authenticator.set_disconnect_handler(run_loop.QuitClosure()); |
| |
| // Make first request. |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| TestGetAssertionCallback callback_receiver; |
| authenticator->GetAssertion(std::move(options), callback_receiver.callback()); |
| |
| // Simulate a navigation while waiting for the user to press the token. |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting([&](device::VirtualFidoDevice* device) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindLambdaForTesting( |
| [&]() { NavigateAndCommit(GURL(kTestOrigin2)); })); |
| return false; |
| }); |
| |
| run_loop.Run(); |
| } |
| |
| TEST_F(AuthenticatorImplTest, InvalidResponse) { |
| virtual_device_factory_->mutable_state()->simulate_invalid_response = true; |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| { |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| EXPECT_EQ( |
| AuthenticatorGetAssertionAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| { |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| EXPECT_EQ( |
| AuthenticatorMakeCredentialAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, Ctap2AssertionWithUnknownCredential) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (bool return_immediate_invalid_credential_error : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "return_immediate_invalid_credential_error=" |
| << return_immediate_invalid_credential_error); |
| |
| device::VirtualCtap2Device::Config config; |
| config.return_immediate_invalid_credential_error = |
| return_immediate_invalid_credential_error; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| bool pressed = false; |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindRepeating( |
| [](bool* flag, device::VirtualFidoDevice* device) { |
| *flag = true; |
| return true; |
| }, |
| &pressed); |
| |
| EXPECT_EQ( |
| AuthenticatorGetAssertion(GetTestPublicKeyCredentialRequestOptions()) |
| .status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| // The user must have pressed the authenticator for the operation to |
| // resolve. |
| EXPECT_TRUE(pressed); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, GetAssertionResponseWithAttestedCredentialData) { |
| device::VirtualCtap2Device::Config config; |
| config.return_attested_cred_data_in_get_assertion_response = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| EXPECT_EQ( |
| AuthenticatorGetAssertionAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| TEST_F(AuthenticatorImplTest, IsUVPAA) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| |
| for (const bool enable_win_webauthn_api : {false, true}) { |
| SCOPED_TRACE(enable_win_webauthn_api ? "enable_win_webauthn_api" |
| : "!enable_win_webauthn_api"); |
| for (const bool is_uvpaa : {false, true}) { |
| SCOPED_TRACE(is_uvpaa ? "is_uvpaa" : "!is_uvpaa"); |
| |
| fake_win_webauthn_api_.set_available(enable_win_webauthn_api); |
| fake_win_webauthn_api_.set_is_uvpaa(is_uvpaa); |
| |
| EXPECT_EQ(AuthenticatorIsUvpaa(), enable_win_webauthn_api && is_uvpaa); |
| } |
| } |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| TEST_F(AuthenticatorImplTest, IsUVPAA) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| EXPECT_FALSE(AuthenticatorIsUvpaa()); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| #if BUILDFLAG(IS_WIN) |
| class OffTheRecordAuthenticatorImplTest : public AuthenticatorImplTest { |
| protected: |
| std::unique_ptr<BrowserContext> CreateBrowserContext() override { |
| auto browser_context = std::make_unique<TestBrowserContext>(); |
| browser_context->set_is_off_the_record(true); |
| return browser_context; |
| } |
| }; |
| |
| // Tests that IsUVPAA returns true if the version of Windows supports an |
| // appropriate warning. |
| TEST_F(OffTheRecordAuthenticatorImplTest, WinIsUVPAAIncognito) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| fake_win_webauthn_api_.set_available(true); |
| fake_win_webauthn_api_.set_is_uvpaa(true); |
| |
| for (bool win_api_supports_incognito_warning : {false, true}) { |
| SCOPED_TRACE(win_api_supports_incognito_warning |
| ? "supports incognito" |
| : "does not support incognito"); |
| fake_win_webauthn_api_.set_version(win_api_supports_incognito_warning |
| ? WEBAUTHN_API_VERSION_4 |
| : WEBAUTHN_API_VERSION_3); |
| EXPECT_EQ(AuthenticatorIsUvpaa(), win_api_supports_incognito_warning); |
| } |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| |
| // TestWebAuthenticationRequestProxy is a test fake implementation of the |
| // WebAuthenticationRequestProxy embedder interface. |
| class TestWebAuthenticationRequestProxy : public WebAuthenticationRequestProxy { |
| public: |
| struct Config { |
| // If true, resolves all request event callbacks instantly. |
| bool resolve_callbacks = true; |
| |
| // The return value of IsActive(). |
| bool is_active = true; |
| |
| // The fake response to SignalIsUVPAARequest(). |
| bool is_uvpaa = true; |
| |
| // Whether the request to SignalCreateRequest() should succeed. |
| bool request_success = true; |
| |
| // If `request_success` is false, the name of the DOMError to be |
| // returned. |
| std::string request_error_name = "NotAllowedError"; |
| |
| // If `request_success` is true, the fake response to be returned for an |
| // onCreateRequest event. |
| blink::mojom::MakeCredentialAuthenticatorResponsePtr |
| make_credential_response = nullptr; |
| |
| // If `request_success` is true, the fake response to be returned for an |
| // onGetRequest event. |
| blink::mojom::GetAssertionAuthenticatorResponsePtr get_assertion_response = |
| nullptr; |
| }; |
| |
| struct Observations { |
| std::vector<PublicKeyCredentialCreationOptionsPtr> create_requests; |
| std::vector<PublicKeyCredentialRequestOptionsPtr> get_requests; |
| size_t num_isuvpaa; |
| size_t num_cancel; |
| }; |
| |
| ~TestWebAuthenticationRequestProxy() override { |
| DCHECK(!HasPendingRequest()); |
| } |
| |
| Config& config() { return config_; } |
| |
| Observations& observations() { return observations_; } |
| |
| bool IsActive(const url::Origin& caller_origin) override { |
| return config_.is_active; |
| } |
| |
| RequestId SignalCreateRequest( |
| const PublicKeyCredentialCreationOptionsPtr& options, |
| CreateCallback callback) override { |
| DCHECK(!HasPendingRequest()); |
| |
| current_request_id_++; |
| observations_.create_requests.push_back(options->Clone()); |
| pending_create_callback_ = std::move(callback); |
| if (config_.resolve_callbacks) { |
| RunPendingCreateCallback(); |
| return current_request_id_; |
| } |
| return current_request_id_; |
| } |
| |
| RequestId SignalGetRequest( |
| const PublicKeyCredentialRequestOptionsPtr& options, |
| GetCallback callback) override { |
| current_request_id_++; |
| observations_.get_requests.push_back(options->Clone()); |
| pending_get_callback_ = std::move(callback); |
| if (config_.resolve_callbacks) { |
| RunPendingGetCallback(); |
| return current_request_id_; |
| } |
| return current_request_id_; |
| } |
| |
| RequestId SignalIsUvpaaRequest(IsUvpaaCallback callback) override { |
| DCHECK(!HasPendingRequest()); |
| |
| current_request_id_++; |
| observations_.num_isuvpaa++; |
| if (config_.resolve_callbacks) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), config_.is_uvpaa)); |
| return current_request_id_; |
| } |
| DCHECK(!pending_is_uvpaa_callback_); |
| pending_is_uvpaa_callback_ = std::move(callback); |
| return current_request_id_; |
| } |
| |
| void CancelRequest(RequestId request_id) override { |
| DCHECK_EQ(request_id, current_request_id_); |
| observations_.num_cancel++; |
| if (pending_create_callback_) { |
| pending_create_callback_.Reset(); |
| } |
| if (pending_get_callback_) { |
| pending_get_callback_.Reset(); |
| } |
| } |
| |
| void RunPendingCreateCallback() { |
| DCHECK(pending_create_callback_); |
| auto callback = |
| config_.request_success |
| ? base::BindOnce(std::move(pending_create_callback_), |
| current_request_id_, nullptr, |
| config_.make_credential_response.Clone()) |
| : base::BindOnce(std::move(pending_create_callback_), |
| current_request_id_, |
| WebAuthnDOMExceptionDetails::New( |
| config_.request_error_name, "message"), |
| nullptr); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(callback)); |
| } |
| |
| void RunPendingGetCallback() { |
| DCHECK(pending_get_callback_); |
| auto callback = |
| config_.request_success |
| ? base::BindOnce(std::move(pending_get_callback_), |
| current_request_id_, nullptr, |
| config_.get_assertion_response.Clone()) |
| : base::BindOnce(std::move(pending_create_callback_), |
| current_request_id_, |
| WebAuthnDOMExceptionDetails::New( |
| config_.request_error_name, "message"), |
| nullptr); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(callback)); |
| } |
| |
| void RunPendingIsUvpaaCallback() { |
| DCHECK(pending_is_uvpaa_callback_); |
| std::move(pending_is_uvpaa_callback_).Run(config_.is_uvpaa); |
| } |
| |
| bool HasPendingRequest() { |
| return pending_create_callback_ || pending_get_callback_ || |
| pending_is_uvpaa_callback_; |
| } |
| |
| private: |
| Config config_; |
| Observations observations_; |
| |
| RequestId current_request_id_ = 0; |
| CreateCallback pending_create_callback_; |
| GetCallback pending_get_callback_; |
| IsUvpaaCallback pending_is_uvpaa_callback_; |
| }; |
| |
| // TestWebAuthenticationDelegate is a test fake implementation of the |
| // WebAuthenticationDelegate embedder interface. |
| class TestWebAuthenticationDelegate : public WebAuthenticationDelegate { |
| public: |
| absl::optional<bool> IsUserVerifyingPlatformAuthenticatorAvailableOverride( |
| RenderFrameHost*) override { |
| return is_uvpaa_override; |
| } |
| |
| bool OverrideCallerOriginAndRelyingPartyIdValidation( |
| content::BrowserContext* browser_context, |
| const url::Origin& origin, |
| const std::string& rp_id) override { |
| return permit_extensions && origin.scheme() == kExtensionScheme && |
| origin.host() == rp_id; |
| } |
| |
| absl::optional<std::string> MaybeGetRelyingPartyIdOverride( |
| const std::string& claimed_rp_id, |
| const url::Origin& caller_origin) override { |
| if (permit_extensions && caller_origin.scheme() == kExtensionScheme) { |
| return caller_origin.Serialize(); |
| } |
| return absl::nullopt; |
| } |
| |
| bool ShouldPermitIndividualAttestation( |
| content::BrowserContext* browser_context, |
| const url::Origin& caller_origin, |
| const std::string& relying_party_id) override { |
| return permit_individual_attestation || |
| (permit_individual_attestation_for_rp_id.has_value() && |
| relying_party_id == *permit_individual_attestation_for_rp_id); |
| } |
| |
| bool SupportsResidentKeys(RenderFrameHost*) override { |
| return supports_resident_keys; |
| } |
| |
| bool IsFocused(WebContents* web_contents) override { return is_focused; } |
| |
| #if BUILDFLAG(IS_MAC) |
| absl::optional<TouchIdAuthenticatorConfig> GetTouchIdAuthenticatorConfig( |
| BrowserContext* browser_context) override { |
| return touch_id_authenticator_config; |
| } |
| #endif |
| |
| WebAuthenticationRequestProxy* MaybeGetRequestProxy( |
| content::BrowserContext* browser_context, |
| const url::Origin& caller_origin) override { |
| return request_proxy && request_proxy->IsActive(caller_origin) |
| ? request_proxy.get() |
| : nullptr; |
| } |
| |
| bool OriginMayUseRemoteDesktopClientOverride( |
| content::BrowserContext* browser_context, |
| const url::Origin& caller_origin) override { |
| return caller_origin == remote_desktop_client_override_origin; |
| } |
| |
| bool IsSecurityLevelAcceptableForWebAuthn( |
| content::RenderFrameHost* rfh, |
| const url::Origin& origin) override { |
| return is_webauthn_security_level_acceptable; |
| } |
| |
| // If set, the return value of IsUVPAA() will be overridden with this value. |
| // Platform-specific implementations will not be invoked. |
| absl::optional<bool> is_uvpaa_override; |
| |
| // If set, the delegate will permit WebAuthn requests from chrome-extension |
| // origins. |
| bool permit_extensions = false; |
| |
| // Indicates whether individual attestation should be permitted by the |
| // delegate. |
| bool permit_individual_attestation = false; |
| |
| // A specific RP ID for which individual attestation will be permitted. |
| absl::optional<std::string> permit_individual_attestation_for_rp_id; |
| |
| // Indicates whether resident key operations should be permitted by the |
| // delegate. |
| bool supports_resident_keys = false; |
| |
| // The return value of the focus check issued at the end of a request. |
| bool is_focused = true; |
| |
| // The return value of IsSecurityLevelAcceptableForWebAuthn. |
| bool is_webauthn_security_level_acceptable = true; |
| |
| #if BUILDFLAG(IS_MAC) |
| // Configuration data for the macOS platform authenticator. |
| absl::optional<TouchIdAuthenticatorConfig> touch_id_authenticator_config; |
| #endif |
| |
| // The WebAuthenticationRequestProxy returned by |MaybeGetRequestProxy|. |
| std::unique_ptr<TestWebAuthenticationRequestProxy> request_proxy = nullptr; |
| |
| // The origin permitted to use the RemoteDesktopClientOverride extension. |
| absl::optional<url::Origin> remote_desktop_client_override_origin; |
| }; |
| |
| enum class EnterprisePolicy { |
| LISTED, |
| NOT_LISTED, |
| }; |
| |
| enum class AttestationConsent { |
| GRANTED, |
| DENIED, |
| GRANTED_FOR_ENTERPRISE_ATTESTATION, |
| DENIED_FOR_ENTERPRISE_ATTESTATION, |
| NOT_USED, |
| }; |
| |
| enum class AttestationType { |
| ANY, |
| NONE, |
| NONE_WITH_NONZERO_AAGUID, |
| U2F, |
| SELF, |
| SELF_WITH_NONZERO_AAGUID, |
| PACKED, |
| }; |
| |
| const char* AttestationConveyancePreferenceToString( |
| AttestationConveyancePreference v) { |
| switch (v) { |
| case AttestationConveyancePreference::NONE: |
| return "none"; |
| case AttestationConveyancePreference::INDIRECT: |
| return "indirect"; |
| case AttestationConveyancePreference::DIRECT: |
| return "direct"; |
| case AttestationConveyancePreference::ENTERPRISE: |
| return "enterprise"; |
| default: |
| NOTREACHED(); |
| return ""; |
| } |
| } |
| |
| const char* AttestationConveyancePreferenceToString( |
| device::AttestationConveyancePreference v) { |
| switch (v) { |
| case device::AttestationConveyancePreference::kNone: |
| return "none"; |
| case device::AttestationConveyancePreference::kIndirect: |
| return "indirect"; |
| case device::AttestationConveyancePreference::kDirect: |
| return "direct"; |
| case device::AttestationConveyancePreference:: |
| kEnterpriseIfRPListedOnAuthenticator: |
| return "enterprise(ep=1)"; |
| case device::AttestationConveyancePreference::kEnterpriseApprovedByBrowser: |
| return "enterprise(ep=2)"; |
| } |
| } |
| |
| const char* AttestationConsentToString(AttestationConsent ac) { |
| switch (ac) { |
| case AttestationConsent::GRANTED: |
| return "GRANTED"; |
| case AttestationConsent::DENIED: |
| return "DENIED"; |
| case AttestationConsent::GRANTED_FOR_ENTERPRISE_ATTESTATION: |
| return "GRANTED_FOR_ENTERPRISE_ATTESTATION"; |
| case AttestationConsent::DENIED_FOR_ENTERPRISE_ATTESTATION: |
| return "DENIED_FOR_ENTERPRISE_ATTESTATION"; |
| case AttestationConsent::NOT_USED: |
| return "NOT_USED"; |
| } |
| } |
| |
| // TestAuthenticatorRequestDelegate is a test fake implementation of the |
| // AuthenticatorRequestClientDelegate embedder interface. |
| class TestAuthenticatorRequestDelegate |
| : public AuthenticatorRequestClientDelegate { |
| public: |
| TestAuthenticatorRequestDelegate( |
| RenderFrameHost* render_frame_host, |
| base::OnceClosure action_callbacks_registered_callback, |
| AttestationConsent attestation_consent, |
| base::OnceClosure started_over_callback) |
| : action_callbacks_registered_callback_( |
| std::move(action_callbacks_registered_callback)), |
| attestation_consent_(attestation_consent), |
| started_over_callback_(std::move(started_over_callback)), |
| does_block_request_on_failure_(!started_over_callback_.is_null()) {} |
| |
| TestAuthenticatorRequestDelegate(const TestAuthenticatorRequestDelegate&) = |
| delete; |
| TestAuthenticatorRequestDelegate& operator=( |
| const TestAuthenticatorRequestDelegate&) = delete; |
| |
| ~TestAuthenticatorRequestDelegate() override { |
| CHECK(attestation_consent_queried_ || |
| attestation_consent_ == AttestationConsent::NOT_USED); |
| } |
| |
| void RegisterActionCallbacks( |
| base::OnceClosure cancel_callback, |
| base::RepeatingClosure start_over_callback, |
| AccountPreselectedCallback account_preselected_callback, |
| device::FidoRequestHandlerBase::RequestCallback request_callback, |
| base::RepeatingClosure bluetooth_adapter_power_on_callback) override { |
| ASSERT_TRUE(action_callbacks_registered_callback_) |
| << "RegisterActionCallbacks called twice."; |
| cancel_callback_ = std::move(cancel_callback); |
| std::move(action_callbacks_registered_callback_).Run(); |
| if (started_over_callback_) { |
| action_callbacks_registered_callback_ = std::move(started_over_callback_); |
| start_over_callback_ = start_over_callback; |
| } |
| } |
| |
| void ShouldReturnAttestation( |
| const std::string& relying_party_id, |
| const device::FidoAuthenticator* authenticator, |
| bool is_enterprise_attestation, |
| base::OnceCallback<void(bool)> callback) override { |
| bool result = false; |
| switch (attestation_consent_) { |
| case AttestationConsent::NOT_USED: |
| CHECK(false); |
| break; |
| case AttestationConsent::DENIED: |
| CHECK(!is_enterprise_attestation); |
| break; |
| case AttestationConsent::GRANTED: |
| CHECK(!is_enterprise_attestation); |
| result = true; |
| break; |
| case AttestationConsent::DENIED_FOR_ENTERPRISE_ATTESTATION: |
| CHECK(is_enterprise_attestation); |
| break; |
| case AttestationConsent::GRANTED_FOR_ENTERPRISE_ATTESTATION: |
| CHECK(is_enterprise_attestation); |
| result = true; |
| break; |
| } |
| |
| attestation_consent_queried_ = true; |
| std::move(callback).Run(result); |
| } |
| |
| void OnTransportAvailabilityEnumerated( |
| device::FidoRequestHandlerBase::TransportAvailabilityInfo transport_info) |
| override { |
| // Simulate the behaviour of Chrome's |AuthenticatorRequestDialogModel| |
| // which shows a specific error when no transports are available and lets |
| // the user cancel the request. |
| if (transport_info.available_transports.empty()) { |
| std::move(cancel_callback_).Run(); |
| } |
| } |
| |
| bool DoesBlockRequestOnFailure(InterestingFailureReason reason) override { |
| if (!does_block_request_on_failure_) { |
| return false; |
| } |
| |
| std::move(start_over_callback_).Run(); |
| does_block_request_on_failure_ = false; |
| return true; |
| } |
| |
| base::OnceClosure action_callbacks_registered_callback_; |
| base::OnceClosure cancel_callback_; |
| const AttestationConsent attestation_consent_; |
| base::OnceClosure started_over_callback_; |
| base::OnceClosure start_over_callback_; |
| bool does_block_request_on_failure_ = false; |
| bool attestation_consent_queried_ = false; |
| }; |
| |
| // TestAuthenticatorContentBrowserClient is a test fake implementation of the |
| // ContentBrowserClient interface that injects |TestWebAuthenticationDelegate| |
| // and |TestAuthenticatorRequestDelegate| instances into |AuthenticatorImpl|. |
| class TestAuthenticatorContentBrowserClient : public ContentBrowserClient { |
| public: |
| TestWebAuthenticationDelegate* GetTestWebAuthenticationDelegate() { |
| return &web_authentication_delegate; |
| } |
| |
| // ContentBrowserClient: |
| WebAuthenticationDelegate* GetWebAuthenticationDelegate() override { |
| return &web_authentication_delegate; |
| } |
| |
| std::unique_ptr<AuthenticatorRequestClientDelegate> |
| GetWebAuthenticationRequestDelegate( |
| RenderFrameHost* render_frame_host) override { |
| if (return_null_delegate) { |
| return nullptr; |
| } |
| return std::make_unique<TestAuthenticatorRequestDelegate>( |
| render_frame_host, |
| action_callbacks_registered_callback |
| ? std::move(action_callbacks_registered_callback) |
| : base::DoNothing(), |
| attestation_consent, std::move(started_over_callback_)); |
| } |
| |
| TestWebAuthenticationDelegate web_authentication_delegate; |
| |
| // If set, this closure will be called when the subsequently constructed |
| // delegate is informed that the request has started. |
| base::OnceClosure action_callbacks_registered_callback; |
| |
| AttestationConsent attestation_consent = AttestationConsent::NOT_USED; |
| |
| // This emulates scenarios where a nullptr RequestClientDelegate is returned |
| // because a request is already in progress. |
| bool return_null_delegate = false; |
| |
| // If started_over_callback_ is set to a non-null callback, the request will |
| // be restarted after action callbacks are registered, and |
| // |started_over_callback| will replace |
| // |action_callbacks_registered_callback|. This should then be called the |
| // second time action callbacks are registered. It also causes |
| // DoesBlockRequestOnFailure to return true, once. |
| base::OnceClosure started_over_callback_; |
| }; |
| |
| // A test class that installs and removes an |
| // |TestAuthenticatorContentBrowserClient| automatically and can run tests |
| // against simulated attestation results. |
| class AuthenticatorContentBrowserClientTest : public AuthenticatorImplTest { |
| public: |
| AuthenticatorContentBrowserClientTest() = default; |
| |
| AuthenticatorContentBrowserClientTest( |
| const AuthenticatorContentBrowserClientTest&) = delete; |
| AuthenticatorContentBrowserClientTest& operator=( |
| const AuthenticatorContentBrowserClientTest&) = delete; |
| |
| struct TestCase { |
| AttestationConveyancePreference attestation_requested; |
| EnterprisePolicy enterprise_policy; |
| AttestationConsent attestation_consent; |
| AuthenticatorStatus expected_status; |
| AttestationType expected_attestation; |
| const char* expected_certificate_substring; |
| }; |
| |
| void SetUp() override { |
| AuthenticatorImplTest::SetUp(); |
| old_client_ = SetBrowserClientForTesting(&test_client_); |
| } |
| |
| void TearDown() override { |
| SetBrowserClientForTesting(old_client_); |
| AuthenticatorImplTest::TearDown(); |
| } |
| |
| void RunTestCases(const std::vector<TestCase>& tests) { |
| for (size_t i = 0; i < tests.size(); i++) { |
| const auto& test = tests[i]; |
| if (test.attestation_consent != AttestationConsent::NOT_USED) { |
| SCOPED_TRACE(test.attestation_consent == AttestationConsent::GRANTED |
| ? "consent granted" |
| : "consent denied"); |
| } |
| SCOPED_TRACE(test.enterprise_policy == EnterprisePolicy::LISTED |
| ? "individual attestation" |
| : "no individual attestation"); |
| SCOPED_TRACE( |
| AttestationConveyancePreferenceToString(test.attestation_requested)); |
| SCOPED_TRACE(i); |
| |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->permit_individual_attestation = |
| test.enterprise_policy == EnterprisePolicy::LISTED; |
| test_client_.attestation_consent = test.attestation_consent; |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->relying_party.id = "example.com"; |
| options->timeout = base::Seconds(1); |
| options->attestation = |
| ConvertAttestationConveyancePreference(test.attestation_requested); |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| EXPECT_EQ(result.status, test.expected_status); |
| |
| if (test.expected_status != AuthenticatorStatus::SUCCESS) { |
| ASSERT_EQ(AttestationType::ANY, test.expected_attestation); |
| continue; |
| } |
| |
| const device::AuthenticatorData auth_data = |
| AuthDataFromMakeCredentialResponse(result.response); |
| |
| absl::optional<Value> attestation_value = |
| Reader::Read(result.response->attestation_object); |
| ASSERT_TRUE(attestation_value); |
| ASSERT_TRUE(attestation_value->is_map()); |
| const auto& attestation = attestation_value->GetMap(); |
| |
| switch (test.expected_attestation) { |
| case AttestationType::ANY: |
| ASSERT_STREQ("", test.expected_certificate_substring); |
| break; |
| |
| case AttestationType::NONE: |
| ASSERT_STREQ("", test.expected_certificate_substring); |
| ExpectMapHasKeyWithStringValue(attestation, "fmt", "none"); |
| EXPECT_TRUE(auth_data.attested_data()->IsAaguidZero()); |
| break; |
| |
| case AttestationType::NONE_WITH_NONZERO_AAGUID: |
| ASSERT_STREQ("", test.expected_certificate_substring); |
| ExpectMapHasKeyWithStringValue(attestation, "fmt", "none"); |
| EXPECT_FALSE(auth_data.attested_data()->IsAaguidZero()); |
| break; |
| |
| case AttestationType::U2F: |
| ExpectMapHasKeyWithStringValue(attestation, "fmt", "fido-u2f"); |
| if (strlen(test.expected_certificate_substring) > 0) { |
| ExpectCertificateContainingSubstring( |
| attestation, test.expected_certificate_substring); |
| } |
| break; |
| |
| case AttestationType::PACKED: |
| ExpectMapHasKeyWithStringValue(attestation, "fmt", "packed"); |
| if (strlen(test.expected_certificate_substring) > 0) { |
| ExpectCertificateContainingSubstring( |
| attestation, test.expected_certificate_substring); |
| } |
| break; |
| |
| case AttestationType::SELF: { |
| ASSERT_STREQ("", test.expected_certificate_substring); |
| ExpectMapHasKeyWithStringValue(attestation, "fmt", "packed"); |
| |
| // A self-attestation should not include an X.509 chain nor ECDAA key. |
| const auto attestation_statement_it = |
| attestation.find(Value("attStmt")); |
| ASSERT_TRUE(attestation_statement_it != attestation.end()); |
| ASSERT_TRUE(attestation_statement_it->second.is_map()); |
| const auto& attestation_statement = |
| attestation_statement_it->second.GetMap(); |
| |
| ASSERT_TRUE(attestation_statement.find(Value("x5c")) == |
| attestation_statement.end()); |
| ASSERT_TRUE(attestation_statement.find(Value("ecdaaKeyId")) == |
| attestation_statement.end()); |
| EXPECT_TRUE(auth_data.attested_data()->IsAaguidZero()); |
| break; |
| } |
| case AttestationType::SELF_WITH_NONZERO_AAGUID: { |
| ASSERT_STREQ("", test.expected_certificate_substring); |
| ExpectMapHasKeyWithStringValue(attestation, "fmt", "packed"); |
| |
| // A self-attestation should not include an X.509 chain nor ECDAA key. |
| const auto attestation_statement_it = |
| attestation.find(Value("attStmt")); |
| ASSERT_TRUE(attestation_statement_it != attestation.end()); |
| ASSERT_TRUE(attestation_statement_it->second.is_map()); |
| const auto& attestation_statement = |
| attestation_statement_it->second.GetMap(); |
| |
| ASSERT_TRUE(attestation_statement.find(Value("x5c")) == |
| attestation_statement.end()); |
| ASSERT_TRUE(attestation_statement.find(Value("ecdaaKeyId")) == |
| attestation_statement.end()); |
| EXPECT_FALSE(auth_data.attested_data()->IsAaguidZero()); |
| break; |
| } |
| } |
| } |
| } |
| |
| protected: |
| TestAuthenticatorContentBrowserClient test_client_; |
| |
| // Expects that |map| contains the given key with a string-value equal to |
| // |expected|. |
| static void ExpectMapHasKeyWithStringValue(const Value::MapValue& map, |
| const char* key, |
| const char* expected) { |
| const auto it = map.find(Value(key)); |
| ASSERT_TRUE(it != map.end()) << "No such key '" << key << "'"; |
| const auto& value = it->second; |
| EXPECT_TRUE(value.is_string()) |
| << "Value of '" << key << "' has type " |
| << static_cast<int>(value.type()) << ", but expected to find a string"; |
| EXPECT_EQ(std::string(expected), value.GetString()) |
| << "Value of '" << key << "' is '" << value.GetString() |
| << "', but expected to find '" << expected << "'"; |
| } |
| |
| // Asserts that the webauthn attestation CBOR map in |attestation| contains a |
| // single X.509 certificate containing |substring|. |
| static void ExpectCertificateContainingSubstring( |
| const Value::MapValue& attestation, |
| const std::string& substring) { |
| const auto& attestation_statement_it = attestation.find(Value("attStmt")); |
| ASSERT_TRUE(attestation_statement_it != attestation.end()); |
| ASSERT_TRUE(attestation_statement_it->second.is_map()); |
| const auto& attestation_statement = |
| attestation_statement_it->second.GetMap(); |
| const auto& x5c_it = attestation_statement.find(Value("x5c")); |
| ASSERT_TRUE(x5c_it != attestation_statement.end()); |
| ASSERT_TRUE(x5c_it->second.is_array()); |
| const auto& x5c = x5c_it->second.GetArray(); |
| ASSERT_EQ(1u, x5c.size()); |
| ASSERT_TRUE(x5c[0].is_bytestring()); |
| base::StringPiece cert = x5c[0].GetBytestringAsString(); |
| EXPECT_TRUE(cert.find(substring) != cert.npos); |
| } |
| |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| }; |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, MakeCredentialTLSError) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->is_webauthn_security_level_acceptable = false; |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::CERTIFICATE_ERROR); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, GetAssertionTLSError) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->is_webauthn_security_level_acceptable = false; |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::CERTIFICATE_ERROR); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, |
| MakeCredentialSkipTLSCheckWithVirtualEnvironment) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| content::AuthenticatorEnvironment::GetInstance() |
| ->EnableVirtualAuthenticatorFor( |
| static_cast<content::RenderFrameHostImpl*>(main_rfh()) |
| ->frame_tree_node(), |
| /*enable_ui=*/false); |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->is_webauthn_security_level_acceptable = false; |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, |
| GetAssertionSkipTLSCheckWithVirtualEnvironment) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| content::AuthenticatorEnvironment::GetInstance() |
| ->EnableVirtualAuthenticatorFor( |
| static_cast<content::RenderFrameHostImpl*>(main_rfh()) |
| ->frame_tree_node(), |
| /*enable_ui=*/false); |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->is_webauthn_security_level_acceptable = false; |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| |
| // Test that credentials can be created and used from an extension origin when |
| // permitted by the delegate. |
| TEST_F(AuthenticatorContentBrowserClientTest, ChromeExtensions) { |
| constexpr char kExtensionId[] = "abcdefg"; |
| static const std::string kExtensionOrigin = |
| std::string(kExtensionScheme) + "://" + kExtensionId; |
| |
| NavigateAndCommit(GURL(kExtensionOrigin + "/test.html")); |
| |
| for (bool permit_webauthn_in_extensions : {false, true}) { |
| SCOPED_TRACE(testing::Message() |
| << "permit=" << permit_webauthn_in_extensions); |
| test_client_.GetTestWebAuthenticationDelegate()->permit_extensions = |
| permit_webauthn_in_extensions; |
| |
| std::vector<uint8_t> credential_id; |
| { |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->relying_party.id = kExtensionId; |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| if (permit_webauthn_in_extensions) { |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| credential_id = result.response->info->raw_id; |
| } else { |
| EXPECT_EQ(result.status, AuthenticatorStatus::INVALID_PROTOCOL); |
| } |
| } |
| |
| { |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->relying_party_id = kExtensionId; |
| options->allow_credentials[0] = device::PublicKeyCredentialDescriptor( |
| device::CredentialType::kPublicKey, std::move(credential_id)); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| permit_webauthn_in_extensions |
| ? AuthenticatorStatus::SUCCESS |
| : AuthenticatorStatus::INVALID_PROTOCOL); |
| } |
| } |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, ChromeExtensionBadRpIds) { |
| // Permit WebAuthn in extensions. |
| static const std::string kExtensionOrigin = |
| base::StrCat({kExtensionScheme, "://abcdefg"}); |
| test_client_.GetTestWebAuthenticationDelegate()->permit_extensions = true; |
| |
| // Extensions are not permitted to assert RP IDs different from their |
| // extension ID. |
| for (auto* rp_id : {"", "xyz", "localhost", "xyz.com", |
| "chrome-extension://abcdefg", "https://abcdefg"}) { |
| NavigateAndCommit(GURL(kExtensionOrigin + "/test.html")); |
| { |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->relying_party.id = rp_id; |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::INVALID_PROTOCOL); |
| } |
| |
| { |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->relying_party_id = rp_id; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::INVALID_PROTOCOL); |
| } |
| } |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, AttestationBehaviour) { |
| const char kStandardCommonName[] = "U2F Attestation"; |
| const char kIndividualCommonName[] = "Individual Cert"; |
| |
| const std::vector<TestCase> kTests = { |
| { |
| AttestationConveyancePreference::NONE, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| { |
| AttestationConveyancePreference::NONE, |
| EnterprisePolicy::LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| { |
| AttestationConveyancePreference::INDIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::DENIED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| { |
| AttestationConveyancePreference::INDIRECT, |
| EnterprisePolicy::LISTED, |
| AttestationConsent::DENIED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| { |
| AttestationConveyancePreference::INDIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::GRANTED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::U2F, |
| kStandardCommonName, |
| }, |
| { |
| AttestationConveyancePreference::INDIRECT, |
| EnterprisePolicy::LISTED, |
| AttestationConsent::GRANTED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::U2F, |
| kStandardCommonName, |
| }, |
| { |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::DENIED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| { |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::LISTED, |
| AttestationConsent::DENIED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| { |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::GRANTED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::U2F, |
| kStandardCommonName, |
| }, |
| { |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::LISTED, |
| AttestationConsent::GRANTED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::U2F, |
| kStandardCommonName, |
| }, |
| { |
| // Requesting enterprise attestation and not being approved results in |
| // no attestation. |
| AttestationConveyancePreference::ENTERPRISE, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| { |
| AttestationConveyancePreference::ENTERPRISE, |
| EnterprisePolicy::LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::U2F, |
| kIndividualCommonName, |
| }, |
| }; |
| |
| virtual_device_factory_->mutable_state()->attestation_cert_common_name = |
| kStandardCommonName; |
| virtual_device_factory_->mutable_state() |
| ->individual_attestation_cert_common_name = kIndividualCommonName; |
| NavigateAndCommit(GURL("https://example.com")); |
| |
| RunTestCases(kTests); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, Ctap2EnterpriseAttestation) { |
| const char kStandardCommonName[] = "U2F Attestation"; |
| const char kIndividualCommonName[] = "Individual Cert"; |
| virtual_device_factory_->mutable_state()->attestation_cert_common_name = |
| kStandardCommonName; |
| virtual_device_factory_->mutable_state() |
| ->individual_attestation_cert_common_name = kIndividualCommonName; |
| NavigateAndCommit(GURL("https://example.com")); |
| |
| { |
| SCOPED_TRACE("Without RP listed"); |
| |
| device::VirtualCtap2Device::Config config; |
| config.support_enterprise_attestation = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| const std::vector<TestCase> kTests = { |
| { |
| AttestationConveyancePreference::ENTERPRISE, |
| EnterprisePolicy::LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::PACKED, |
| kIndividualCommonName, |
| }, |
| { |
| // Requesting enterprise attestation and not being approved results |
| // in no attestation. |
| AttestationConveyancePreference::ENTERPRISE, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| }; |
| |
| RunTestCases(kTests); |
| } |
| |
| { |
| SCOPED_TRACE("With RP listed"); |
| |
| device::VirtualCtap2Device::Config config; |
| config.support_enterprise_attestation = true; |
| config.enterprise_attestation_rps = {"example.com"}; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| const std::vector<TestCase> kTests = { |
| { |
| // Despite not being listed in enterprise policy, since the |
| // authenticator recognises the RP ID, attestation should still be |
| // returned. |
| AttestationConveyancePreference::ENTERPRISE, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::GRANTED_FOR_ENTERPRISE_ATTESTATION, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::PACKED, |
| kIndividualCommonName, |
| }, |
| { |
| // Consent is required in the case that an enterprise attestation is |
| // approved by an authenticator, however. |
| AttestationConveyancePreference::ENTERPRISE, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::DENIED_FOR_ENTERPRISE_ATTESTATION, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| }; |
| |
| RunTestCases(kTests); |
| } |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, |
| Ctap2EnterpriseAttestationUnsolicited) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.support_enterprise_attestation = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| { |
| EXPECT_EQ( |
| AuthenticatorMakeCredential(GetTestPublicKeyCredentialCreationOptions()) |
| .status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| |
| config.always_return_enterprise_attestation = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| { |
| EXPECT_EQ( |
| AuthenticatorMakeCredential(GetTestPublicKeyCredentialCreationOptions()) |
| .status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, |
| InappropriatelyIdentifyingAttestation) { |
| // This common name is used by several devices that have inappropriately |
| // identifying attestation certificates. |
| const char kCommonName[] = "FT FIDO 0100"; |
| |
| const std::vector<TestCase> kTests = { |
| { |
| AttestationConveyancePreference::ENTERPRISE, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| { |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::GRANTED, |
| AuthenticatorStatus::SUCCESS, |
| // If individual attestation was not requested then the attestation |
| // certificate will be removed, even if consent is given, because the |
| // consent isn't to be tracked. |
| AttestationType::NONE, |
| "", |
| }, |
| { |
| AttestationConveyancePreference::ENTERPRISE, |
| EnterprisePolicy::LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::U2F, |
| kCommonName, |
| }, |
| }; |
| |
| virtual_device_factory_->mutable_state()->attestation_cert_common_name = |
| kCommonName; |
| virtual_device_factory_->mutable_state() |
| ->individual_attestation_cert_common_name = kCommonName; |
| NavigateAndCommit(GURL("https://example.com")); |
| |
| RunTestCases(kTests); |
| } |
| |
| // Test attestation erasure for an authenticator that uses self-attestation |
| // (which requires a zero AAGUID), but has a non-zero AAGUID. This mirrors the |
| // behavior of the Touch ID platform authenticator. |
| TEST_F(AuthenticatorContentBrowserClientTest, |
| PlatformAuthenticatorAttestation) { |
| test_client_.GetTestWebAuthenticationDelegate()->is_uvpaa_override = true; |
| virtual_device_factory_->SetSupportedProtocol( |
| device::ProtocolVersion::kCtap2); |
| virtual_device_factory_->SetTransport( |
| device::FidoTransportProtocol::kInternal); |
| virtual_device_factory_->mutable_state()->self_attestation = true; |
| virtual_device_factory_->mutable_state() |
| ->non_zero_aaguid_with_self_attestation = true; |
| NavigateAndCommit(GURL("https://example.com")); |
| |
| const std::vector<TestCase> kTests = { |
| { |
| // Self-attestation is defined as having a zero AAGUID, but |
| // |non_zero_aaguid_with_self_attestation| is set above. Thus, if no |
| // attestation is requested, the self-attestation will be removed but, |
| // because the transport is kInternal, the AAGUID will be preserved. |
| AttestationConveyancePreference::NONE, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE_WITH_NONZERO_AAGUID, |
| "", |
| }, |
| { |
| // Attestation is always returned if requested because it is privacy |
| // preserving. The AttestationConsent value is irrelevant. |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::SELF_WITH_NONZERO_AAGUID, |
| "", |
| }, |
| }; |
| |
| RunTestCases(kTests); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, Ctap2SelfAttestation) { |
| virtual_device_factory_->SetSupportedProtocol( |
| device::ProtocolVersion::kCtap2); |
| virtual_device_factory_->mutable_state()->self_attestation = true; |
| NavigateAndCommit(GURL("https://example.com")); |
| |
| const std::vector<TestCase> kTests = { |
| { |
| // If no attestation is requested, we'll return the self attestation |
| // rather than erasing it. |
| AttestationConveyancePreference::NONE, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::SELF, |
| "", |
| }, |
| { |
| // If attestation is requested, but denied, we'll return none |
| // attestation. |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::DENIED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| { |
| // If attestation is requested and granted, the self attestation will |
| // be returned. |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::GRANTED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::SELF, |
| "", |
| }, |
| }; |
| |
| RunTestCases(kTests); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, |
| Ctap2SelfAttestationNonZeroAaguid) { |
| virtual_device_factory_->SetSupportedProtocol( |
| device::ProtocolVersion::kCtap2); |
| virtual_device_factory_->mutable_state()->self_attestation = true; |
| virtual_device_factory_->mutable_state() |
| ->non_zero_aaguid_with_self_attestation = true; |
| NavigateAndCommit(GURL("https://example.com")); |
| |
| const std::vector<TestCase> kTests = { |
| { |
| // Since the virtual device is configured to set a non-zero AAGUID the |
| // self-attestation should still be replaced with a "none" |
| // attestation. |
| AttestationConveyancePreference::NONE, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| AttestationType::NONE, |
| "", |
| }, |
| }; |
| |
| RunTestCases(kTests); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, BlockedAttestation) { |
| NavigateAndCommit(GURL("https://foo.example.com")); |
| |
| static constexpr struct { |
| const char* filter_json; |
| AttestationConveyancePreference attestation; |
| EnterprisePolicy enterprise_policy; |
| AttestationType result; |
| } kTests[] = { |
| // Empty or nonsense filter doesn't block anything. |
| { |
| "", |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationType::U2F, |
| }, |
| { |
| R"({"filters": []})", |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationType::U2F, |
| }, |
| // Direct listing of domain blocks... |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "example.com", |
| "action": "no-attestation" |
| }]})", |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationType::NONE, |
| }, |
| // ... unless attestation is permitted by policy. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "example.com", |
| "action": "no-attestation" |
| }]})", |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::LISTED, |
| AttestationType::U2F, |
| }, |
| // The whole domain can be blocked. (Note, blocking a domain would |
| // normally want to list both the base domain and a pattern for |
| // subdomains because the below also matches fooexample.com.) |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "*example.com", |
| "action": "no-attestation" |
| }]})", |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationType::NONE, |
| }, |
| // Policy again overrides |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "*example.com", |
| "action": "no-attestation" |
| }]})", |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::LISTED, |
| AttestationType::U2F, |
| }, |
| // An explicit wildcard will match everything, be careful. (Omitting |
| // both RP ID and device is a parse error, however.) |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "*", |
| "action": "no-attestation" |
| }]})", |
| AttestationConveyancePreference::DIRECT, |
| EnterprisePolicy::NOT_LISTED, |
| AttestationType::NONE, |
| }, |
| }; |
| |
| int test_num = 0; |
| for (const auto& test : kTests) { |
| SCOPED_TRACE(test_num++); |
| SCOPED_TRACE(test.filter_json); |
| |
| device::fido_filter::ScopedFilterForTesting filter(test.filter_json); |
| |
| const std::vector<TestCase> kTestCase = { |
| { |
| test.attestation, |
| test.enterprise_policy, |
| test.result == AttestationType::U2F ? AttestationConsent::GRANTED |
| : AttestationConsent::NOT_USED, |
| AuthenticatorStatus::SUCCESS, |
| test.result, |
| "", |
| }, |
| }; |
| |
| RunTestCases(kTestCase); |
| } |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, FilteringMakeCredential) { |
| static const struct { |
| const char* filter_json; |
| bool expect_make_credential_success; |
| } kTests[] = { |
| { |
| R"()", |
| true, |
| }, |
| // Block by device. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "device": "VirtualFidoDevice-*", |
| "action": "block", |
| }]})", |
| false, |
| }, |
| // Shouldn't block when the device is unrelated. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "device": "OtherDevice-*", |
| "action": "block", |
| }]})", |
| true, |
| }, |
| // Block by RP ID. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "google.com", |
| "action": "block", |
| }]})", |
| false, |
| }, |
| // Unrelated RP ID. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "other.com", |
| "action": "block", |
| }]})", |
| true, |
| }, |
| // Block specific user ID. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "*", |
| "id_type": "user", |
| "id": "0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A", |
| "action": "block", |
| }]})", |
| false, |
| }, |
| // Different user ID. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "*", |
| "id_type": "user", |
| "id": "FF0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A", |
| "action": "block", |
| }]})", |
| true, |
| }, |
| // Block by user ID length. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "*", |
| "id_type": "user", |
| "id_min_size": 32, |
| "id_max_size": 32, |
| "action": "block", |
| }]})", |
| false, |
| }, |
| // Block user IDs that are longer than specified by |
| // |GetTestPublicKeyCredentialUserEntity|. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "*", |
| "id_type": "user", |
| "id_min_size": 33, |
| "action": "block", |
| }]})", |
| true, |
| }, |
| // Block excluded credential ID. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "*", |
| "id_type": "cred", |
| "id": "0000000000000000000000000000000000000000000000000000000000000000", |
| "action": "block", |
| }]})", |
| false, |
| }, |
| // Block different credential ID. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "*", |
| "id_type": "cred", |
| "id": "FF00000000000000000000000000000000000000000000000000000000000000", |
| "action": "block", |
| }]})", |
| true, |
| }, |
| // Block by excluded credential ID length. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "*", |
| "id_type": "cred", |
| "id_min_size": 32, |
| "id_max_size": 32, |
| "action": "block", |
| }]})", |
| false, |
| }, |
| // Block longer credentials IDs than are used. |
| { |
| R"({"filters": [{ |
| "operation": "mc", |
| "rp_id": "*", |
| "id_type": "cred", |
| "id_min_size": 33, |
| "action": "block", |
| }]})", |
| true, |
| }, |
| }; |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| int test_num = 0; |
| for (const auto& test : kTests) { |
| SCOPED_TRACE(test_num++); |
| SCOPED_TRACE(test.filter_json); |
| device::fido_filter::ScopedFilterForTesting filter(test.filter_json); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->exclude_credentials = GetTestCredentials(); |
| EXPECT_EQ(AuthenticatorMakeCredentialAndWaitForTimeout(std::move(options)) |
| .status == AuthenticatorStatus::SUCCESS, |
| test.expect_make_credential_success); |
| } |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, FilteringGetAssertion) { |
| static const struct { |
| const char* filter_json; |
| bool expect_get_assertion_success; |
| } kTests[] = { |
| { |
| R"()", |
| true, |
| }, |
| // Block by device. |
| { |
| R"({"filters": [{ |
| "operation": "ga", |
| "device": "VirtualFidoDevice-*", |
| "action": "block", |
| }]})", |
| false, |
| }, |
| // Shouldn't block when the device is unrelated. |
| { |
| R"({"filters": [{ |
| "operation": "ga", |
| "device": "OtherDevice-*", |
| "action": "block", |
| }]})", |
| true, |
| }, |
| // Block by RP ID. |
| { |
| R"({"filters": [{ |
| "operation": "ga", |
| "rp_id": "google.com", |
| "action": "block", |
| }]})", |
| false, |
| }, |
| // Unrelated RP ID. |
| { |
| R"({"filters": [{ |
| "operation": "ga", |
| "rp_id": "other.com", |
| "action": "block", |
| }]})", |
| true, |
| }, |
| // Block allowList credential ID. |
| { |
| R"({"filters": [{ |
| "operation": "ga", |
| "rp_id": "*", |
| "id_type": "cred", |
| "id": "0000000000000000000000000000000000000000000000000000000000000000", |
| "action": "block", |
| }]})", |
| false, |
| }, |
| // Block different credential ID. |
| { |
| R"({"filters": [{ |
| "operation": "ga", |
| "rp_id": "*", |
| "id_type": "cred", |
| "id": "FF00000000000000000000000000000000000000000000000000000000000000", |
| "action": "block", |
| }]})", |
| true, |
| }, |
| // Block by allowList credential ID length for credentials returned by |
| // |GetTestCredentials|. |
| { |
| R"({"filters": [{ |
| "operation": "ga", |
| "rp_id": "*", |
| "id_type": "cred", |
| "id_min_size": 32, |
| "id_max_size": 32, |
| "action": "block", |
| }]})", |
| false, |
| }, |
| // Block longer credentials IDs than are used. |
| { |
| R"({"filters": [{ |
| "operation": "ga", |
| "rp_id": "*", |
| "id_type": "cred", |
| "id_min_size": 33, |
| "action": "block", |
| }]})", |
| true, |
| }, |
| }; |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| int test_num = 0; |
| bool credential_added = false; |
| for (const auto& test : kTests) { |
| SCOPED_TRACE(test_num++); |
| SCOPED_TRACE(test.filter_json); |
| device::fido_filter::ScopedFilterForTesting filter(test.filter_json); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| if (!credential_added) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| credential_added = true; |
| } |
| |
| EXPECT_EQ( |
| AuthenticatorGetAssertionAndWaitForTimeout(std::move(options)).status == |
| AuthenticatorStatus::SUCCESS, |
| test.expect_get_assertion_success); |
| } |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, FilteringFailsOpen) { |
| // Setting the filter to invalid JSON should not filter anything. |
| device::fido_filter::ScopedFilterForTesting filter( |
| "nonsense", |
| device::fido_filter::ScopedFilterForTesting::PermitInvalidJSON::kYes); |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->exclude_credentials = GetTestCredentials(); |
| EXPECT_EQ( |
| AuthenticatorMakeCredentialAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, |
| MakeCredentialRequestStartedCallback) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| |
| TestRequestStartedCallback request_started; |
| test_client_.action_callbacks_registered_callback = |
| request_started.callback(); |
| authenticator->MakeCredential(std::move(options), base::DoNothing()); |
| request_started.WaitForCallback(); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, |
| GetAssertionRequestStartedCallback) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| |
| TestRequestStartedCallback request_started; |
| test_client_.action_callbacks_registered_callback = |
| request_started.callback(); |
| authenticator->GetAssertion(std::move(options), base::DoNothing()); |
| request_started.WaitForCallback(); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, MakeCredentialStartOver) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| // Make the request fail so that it's started over. |
| options->authenticator_selection->user_verification_requirement = |
| device::UserVerificationRequirement::kRequired; |
| |
| TestRequestStartedCallback request_started; |
| test_client_.action_callbacks_registered_callback = |
| request_started.callback(); |
| TestRequestStartedCallback request_restarted; |
| test_client_.started_over_callback_ = request_restarted.callback(); |
| |
| authenticator->MakeCredential(std::move(options), base::DoNothing()); |
| request_started.WaitForCallback(); |
| request_restarted.WaitForCallback(); |
| |
| const auto& discoveries_trace = virtual_device_factory_->trace()->discoveries; |
| ASSERT_EQ(discoveries_trace.size(), 2u); |
| EXPECT_TRUE(discoveries_trace[0].is_stopped); |
| EXPECT_TRUE(discoveries_trace[0].is_destroyed); |
| EXPECT_FALSE(discoveries_trace[1].is_stopped); |
| EXPECT_FALSE(discoveries_trace[1].is_destroyed); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, GetAssertionStartOver) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| |
| TestRequestStartedCallback request_started; |
| test_client_.action_callbacks_registered_callback = |
| request_started.callback(); |
| TestRequestStartedCallback request_restarted; |
| test_client_.started_over_callback_ = request_restarted.callback(); |
| |
| authenticator->GetAssertion(std::move(options), base::DoNothing()); |
| request_started.WaitForCallback(); |
| request_restarted.WaitForCallback(); |
| |
| const auto& discoveries_trace = virtual_device_factory_->trace()->discoveries; |
| ASSERT_EQ(discoveries_trace.size(), 2u); |
| EXPECT_TRUE(discoveries_trace[0].is_stopped); |
| EXPECT_TRUE(discoveries_trace[0].is_destroyed); |
| EXPECT_FALSE(discoveries_trace[1].is_stopped); |
| EXPECT_FALSE(discoveries_trace[1].is_destroyed); |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, Unfocused) { |
| // When the |ContentBrowserClient| considers the tab to be unfocused, |
| // registration requests should fail with a |NOT_FOCUSED| error, but getting |
| // assertions should still work. |
| test_client_.GetTestWebAuthenticationDelegate()->is_focused = false; |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| { |
| TestRequestStartedCallback request_started; |
| test_client_.action_callbacks_registered_callback = |
| request_started.callback(); |
| |
| EXPECT_EQ( |
| AuthenticatorMakeCredential(GetTestPublicKeyCredentialCreationOptions()) |
| .status, |
| AuthenticatorStatus::NOT_FOCUSED); |
| EXPECT_FALSE(request_started.was_called()); |
| } |
| |
| { |
| device::PublicKeyCredentialDescriptor credential; |
| credential.credential_type = device::CredentialType::kPublicKey; |
| credential.id.resize(16); |
| credential.transports = { |
| device::FidoTransportProtocol::kUsbHumanInterfaceDevice}; |
| |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| credential.id, kTestRelyingPartyId)); |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials.emplace_back(credential); |
| |
| TestRequestStartedCallback request_started; |
| test_client_.action_callbacks_registered_callback = |
| request_started.callback(); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_TRUE(request_started.was_called()); |
| } |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, |
| NullDelegate_RejectsWithPendingRequest) { |
| test_client_.return_null_delegate = true; |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| { |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::PENDING_REQUEST); |
| } |
| |
| { |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::PENDING_REQUEST); |
| } |
| } |
| |
| TEST_F(AuthenticatorContentBrowserClientTest, IsUVPAAOverride) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (const bool is_uvpaa : {false, true}) { |
| SCOPED_TRACE(::testing::Message() << "is_uvpaa=" << is_uvpaa); |
| test_client_.GetTestWebAuthenticationDelegate()->is_uvpaa_override = |
| is_uvpaa; |
| |
| EXPECT_EQ(AuthenticatorIsUvpaa(), is_uvpaa); |
| } |
| } |
| |
| // AuthenticatorImplRemoteDesktopClientOverrideTest exercises the |
| // RemoteDesktopClientOverride extension, which is used by remote desktop |
| // applications exercising requests on behalf of other origins. |
| class AuthenticatorImplRemoteDesktopClientOverrideTest |
| : public AuthenticatorContentBrowserClientTest { |
| protected: |
| static constexpr char kOtherRdpOrigin[] = "https://myrdp.test"; |
| static constexpr char kExampleOrigin[] = "https://example.test"; |
| static constexpr char kExampleRpId[] = "example.test"; |
| static constexpr char kExampleAppid[] = "https://example.test/appid.json"; |
| static constexpr char kOtherRpId[] = "other.test"; |
| static constexpr char kOtherAppid[] = "https://other.test/appid.json"; |
| |
| void SetUp() override { |
| AuthenticatorContentBrowserClientTest::SetUp(); |
| // Authorize `kCorpCrdOrigin` to exercise the extension. In //chrome, this |
| // is controlled by the `WebAuthenticationRemoteProxiedRequestsAllowed` |
| // enterprise policy. |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->remote_desktop_client_override_origin = |
| url::Origin::Create(GURL(kCorpCrdOrigin)); |
| // Controls the Blink feature gating the extension. |
| scoped_command_line_.GetProcessCommandLine()->AppendSwitch( |
| switches::kWebAuthRemoteDesktopSupport); |
| } |
| |
| base::test::ScopedCommandLine scoped_command_line_; |
| base::test::ScopedFeatureList scoped_feature_list_{ |
| device::kWebAuthnGoogleCorpRemoteDesktopClientPrivilege}; |
| }; |
| |
| TEST_F(AuthenticatorImplRemoteDesktopClientOverrideTest, MakeCredential) { |
| // Verify that an authorized origin may use the extension. Regular RP ID |
| // processing applies, i.e. the origin override must be authorized to claim |
| // the specified RP ID. |
| const struct TestCase { |
| std::string local_origin; |
| std::string remote_origin; |
| std::string rp_id; |
| bool success; |
| } test_cases[] = { |
| {kCorpCrdOrigin, kExampleOrigin, kExampleRpId, true}, |
| {kOtherRdpOrigin, kExampleOrigin, kExampleRpId, false}, |
| {kOtherRdpOrigin, kExampleOrigin, kOtherRpId, false}, |
| {kExampleOrigin, kExampleOrigin, kExampleRpId, false}, |
| }; |
| |
| for (const auto& test : test_cases) { |
| SCOPED_TRACE(testing::Message() |
| << "local=" << test.local_origin |
| << " remote=" << test.remote_origin << " rp=" << test.rp_id); |
| NavigateAndCommit(GURL(test.local_origin)); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->relying_party.id = test.rp_id; |
| options->remote_desktop_client_override = RemoteDesktopClientOverride::New( |
| url::Origin::Create(GURL(test.remote_origin)), true); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| test.success ? AuthenticatorStatus::SUCCESS |
| : AuthenticatorStatus:: |
| REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplRemoteDesktopClientOverrideTest, GetAssertion) { |
| // Verify that an authorized origin may use the extension. Regular RP ID |
| // processing applies, i.e. the origin override must be authorized to claim |
| // the specified RP ID. |
| const struct TestCase { |
| std::string local_origin; |
| std::string remote_origin; |
| std::string rp_id; |
| bool success; |
| } test_cases[] = { |
| {kCorpCrdOrigin, kExampleOrigin, kExampleRpId, true}, |
| {kOtherRdpOrigin, kExampleOrigin, kExampleRpId, false}, |
| {kOtherRdpOrigin, kExampleOrigin, kOtherRpId, false}, |
| {kExampleOrigin, kExampleOrigin, kExampleRpId, false}, |
| }; |
| |
| for (const auto& test : test_cases) { |
| SCOPED_TRACE(testing::Message() |
| << "local=" << test.local_origin |
| << " remote=" << test.remote_origin << " rp=" << test.rp_id); |
| ResetVirtualDevice(); |
| NavigateAndCommit(GURL(test.local_origin)); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->relying_party_id = test.rp_id; |
| options->remote_desktop_client_override = RemoteDesktopClientOverride::New( |
| url::Origin::Create(GURL(test.remote_origin)), true); |
| |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, test.rp_id)); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| test.success ? AuthenticatorStatus::SUCCESS |
| : AuthenticatorStatus:: |
| REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplRemoteDesktopClientOverrideTest, MakeCredentialAppid) { |
| // Verify that origin overriding extends to the appidExclude extension. If the |
| // caller origin is authorized to use the extension, App ID processing is |
| // applied to the overridden origin. |
| const struct TestCase { |
| std::string local_origin; |
| std::string remote_origin; |
| std::string rp_id; |
| std::string app_id; |
| AuthenticatorStatus expected; |
| } test_cases[] = { |
| {kCorpCrdOrigin, kExampleOrigin, kExampleRpId, kExampleAppid, |
| AuthenticatorStatus::SUCCESS}, |
| {kCorpCrdOrigin, kExampleOrigin, kExampleRpId, kOtherAppid, |
| AuthenticatorStatus::INVALID_DOMAIN}, |
| {kOtherRdpOrigin, kExampleOrigin, kExampleRpId, kExampleAppid, |
| AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED}, |
| {kOtherRdpOrigin, kExampleOrigin, kExampleRpId, kOtherAppid, |
| AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED}, |
| {kExampleOrigin, kExampleOrigin, kExampleRpId, kExampleAppid, |
| AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED}, |
| {kExampleOrigin, kExampleOrigin, kExampleRpId, kOtherAppid, |
| AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED}, |
| }; |
| |
| for (const auto& test : test_cases) { |
| SCOPED_TRACE(testing::Message() |
| << "local=" << test.local_origin |
| << " remote=" << test.remote_origin << " rp=" << test.rp_id |
| << " appid=" << test.app_id); |
| NavigateAndCommit(GURL(test.local_origin)); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->relying_party.id = test.rp_id; |
| options->appid_exclude = test.app_id; |
| options->remote_desktop_client_override = RemoteDesktopClientOverride::New( |
| url::Origin::Create(GURL(test.remote_origin)), true); |
| |
| auto result = AuthenticatorMakeCredential(std::move(options)); |
| EXPECT_EQ(result.status, test.expected); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplRemoteDesktopClientOverrideTest, GetAssertionAppid) { |
| // Verify that origin overriding extends to the appid extension. If the |
| // caller origin is authorized to use the extension, App ID processing is |
| // applied to the overridden origin. |
| const struct TestCase { |
| std::string local_origin; |
| std::string remote_origin; |
| std::string rp_id; |
| std::string app_id; |
| AuthenticatorStatus expected; |
| } test_cases[] = { |
| {kCorpCrdOrigin, kExampleOrigin, kExampleRpId, kExampleAppid, |
| AuthenticatorStatus::SUCCESS}, |
| {kCorpCrdOrigin, kExampleOrigin, kExampleRpId, kOtherAppid, |
| AuthenticatorStatus::INVALID_DOMAIN}, |
| {kOtherRdpOrigin, kExampleOrigin, kExampleRpId, kExampleAppid, |
| AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED}, |
| {kOtherRdpOrigin, kExampleOrigin, kExampleRpId, kOtherAppid, |
| AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED}, |
| {kExampleOrigin, kExampleOrigin, kExampleRpId, kExampleAppid, |
| AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED}, |
| {kExampleOrigin, kExampleOrigin, kExampleRpId, kOtherAppid, |
| AuthenticatorStatus::REMOTE_DESKTOP_CLIENT_OVERRIDE_NOT_AUTHORIZED}, |
| }; |
| |
| for (const auto& test : test_cases) { |
| SCOPED_TRACE(testing::Message() |
| << "local=" << test.local_origin |
| << " remote=" << test.remote_origin << " rp=" << test.rp_id |
| << " appid=" << test.app_id); |
| ResetVirtualDevice(); |
| NavigateAndCommit(GURL(test.local_origin)); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->relying_party_id = test.rp_id; |
| options->appid = test.app_id; |
| options->remote_desktop_client_override = RemoteDesktopClientOverride::New( |
| url::Origin::Create(GURL(test.remote_origin)), true); |
| |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, test.rp_id)); |
| |
| auto result = AuthenticatorGetAssertion(std::move(options)); |
| EXPECT_EQ(result.status, test.expected); |
| } |
| } |
| |
| class MockAuthenticatorRequestDelegateObserver |
| : public TestAuthenticatorRequestDelegate { |
| public: |
| using InterestingFailureReasonCallback = |
| base::OnceCallback<void(InterestingFailureReason)>; |
| |
| explicit MockAuthenticatorRequestDelegateObserver( |
| InterestingFailureReasonCallback failure_reasons_callback = |
| base::DoNothing()) |
| : TestAuthenticatorRequestDelegate( |
| nullptr /* render_frame_host */, |
| base::DoNothing() /* did_start_request_callback */, |
| AttestationConsent::NOT_USED, |
| /*started_over_callback=*/base::OnceClosure()), |
| failure_reasons_callback_(std::move(failure_reasons_callback)) {} |
| |
| MockAuthenticatorRequestDelegateObserver( |
| const MockAuthenticatorRequestDelegateObserver&) = delete; |
| MockAuthenticatorRequestDelegateObserver& operator=( |
| const MockAuthenticatorRequestDelegateObserver&) = delete; |
| |
| ~MockAuthenticatorRequestDelegateObserver() override = default; |
| |
| bool DoesBlockRequestOnFailure(InterestingFailureReason reason) override { |
| CHECK(failure_reasons_callback_); |
| std::move(failure_reasons_callback_).Run(reason); |
| return false; |
| } |
| |
| MOCK_METHOD1( |
| OnTransportAvailabilityEnumerated, |
| void(device::FidoRequestHandlerBase::TransportAvailabilityInfo data)); |
| MOCK_METHOD1(EmbedderControlsAuthenticatorDispatch, |
| bool(const device::FidoAuthenticator&)); |
| MOCK_METHOD1(FidoAuthenticatorAdded, void(const device::FidoAuthenticator&)); |
| MOCK_METHOD1(FidoAuthenticatorRemoved, void(base::StringPiece)); |
| |
| private: |
| InterestingFailureReasonCallback failure_reasons_callback_; |
| }; |
| |
| // Fake test construct that shares all other behavior with |
| // AuthenticatorCommonImpl except that: |
| // - FakeAuthenticatorCommonImpl does not trigger UI activity. |
| // - MockAuthenticatorRequestDelegateObserver is injected to |
| // |request_delegate_| |
| // instead of ChromeAuthenticatorRequestDelegate. |
| class FakeAuthenticatorCommonImpl : public AuthenticatorCommonImpl { |
| public: |
| explicit FakeAuthenticatorCommonImpl( |
| RenderFrameHost* render_frame_host, |
| std::unique_ptr<MockAuthenticatorRequestDelegateObserver> mock_delegate) |
| : AuthenticatorCommonImpl(render_frame_host), |
| mock_delegate_(std::move(mock_delegate)) {} |
| ~FakeAuthenticatorCommonImpl() override = default; |
| |
| std::unique_ptr<AuthenticatorRequestClientDelegate> |
| MaybeCreateRequestDelegate() override { |
| DCHECK(mock_delegate_); |
| return std::move(mock_delegate_); |
| } |
| |
| private: |
| friend class AuthenticatorImplRequestDelegateTest; |
| |
| std::unique_ptr<MockAuthenticatorRequestDelegateObserver> mock_delegate_; |
| }; |
| |
| class AuthenticatorImplRequestDelegateTest : public AuthenticatorImplTest { |
| public: |
| AuthenticatorImplRequestDelegateTest() = default; |
| ~AuthenticatorImplRequestDelegateTest() override = default; |
| |
| mojo::Remote<blink::mojom::Authenticator> ConnectToFakeAuthenticator( |
| std::unique_ptr<MockAuthenticatorRequestDelegateObserver> delegate) { |
| mojo::Remote<blink::mojom::Authenticator> authenticator; |
| // AuthenticatorImpl owns itself. It self-destructs when the RenderFrameHost |
| // navigates or is deleted. |
| AuthenticatorImpl::CreateForTesting( |
| *main_rfh(), authenticator.BindNewPipeAndPassReceiver(), |
| std::make_unique<FakeAuthenticatorCommonImpl>(main_rfh(), |
| std::move(delegate))); |
| return authenticator; |
| } |
| }; |
| |
| TEST_F(AuthenticatorImplRequestDelegateTest, |
| TestRequestDelegateObservesFidoRequestHandler) { |
| EXPECT_CALL(*mock_adapter_, IsPresent()) |
| .WillRepeatedly(::testing::Return(true)); |
| |
| auto discovery_factory = |
| std::make_unique<device::test::FakeFidoDiscoveryFactory>(); |
| auto* fake_hid_discovery = discovery_factory->ForgeNextHidDiscovery(); |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting(std::move(discovery_factory)); |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| TestGetAssertionCallback callback_receiver; |
| |
| auto mock_delegate = |
| std::make_unique<MockAuthenticatorRequestDelegateObserver>(); |
| auto* const mock_delegate_ptr = mock_delegate.get(); |
| auto authenticator = ConnectToFakeAuthenticator(std::move(mock_delegate)); |
| |
| auto mock_usb_device = device::MockFidoDevice::MakeCtap(); |
| mock_usb_device->StubGetId(); |
| mock_usb_device->SetDeviceTransport( |
| device::FidoTransportProtocol::kUsbHumanInterfaceDevice); |
| const auto device_id = mock_usb_device->GetId(); |
| |
| EXPECT_CALL(*mock_delegate_ptr, OnTransportAvailabilityEnumerated(_)); |
| EXPECT_CALL(*mock_delegate_ptr, EmbedderControlsAuthenticatorDispatch(_)) |
| .WillOnce(testing::Return(true)); |
| |
| base::RunLoop usb_device_found_done; |
| EXPECT_CALL(*mock_delegate_ptr, FidoAuthenticatorAdded(_)) |
| .WillOnce(testing::InvokeWithoutArgs( |
| [&usb_device_found_done]() { usb_device_found_done.Quit(); })); |
| |
| base::RunLoop usb_device_lost_done; |
| EXPECT_CALL(*mock_delegate_ptr, FidoAuthenticatorRemoved(_)) |
| .WillOnce(testing::InvokeWithoutArgs( |
| [&usb_device_lost_done]() { usb_device_lost_done.Quit(); })); |
| |
| authenticator->GetAssertion(std::move(options), callback_receiver.callback()); |
| fake_hid_discovery->WaitForCallToStartAndSimulateSuccess(); |
| fake_hid_discovery->AddDevice(std::move(mock_usb_device)); |
| usb_device_found_done.Run(); |
| |
| fake_hid_discovery->RemoveDevice(device_id); |
| usb_device_lost_done.Run(); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(AuthenticatorImplRequestDelegateTest, FailureReasonForTimeout) { |
| // The VirtualFidoAuthenticator simulates a tap immediately after it gets the |
| // request. Replace by the real discovery that will wait until timeout. |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::make_unique<device::FidoDiscoveryFactory>()); |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| FailureReasonCallbackReceiver failure_reason_receiver; |
| auto mock_delegate = std::make_unique< |
| ::testing::NiceMock<MockAuthenticatorRequestDelegateObserver>>( |
| failure_reason_receiver.callback()); |
| auto authenticator = ConnectToFakeAuthenticator(std::move(mock_delegate)); |
| |
| TestGetAssertionCallback callback_receiver; |
| authenticator->GetAssertion(GetTestPublicKeyCredentialRequestOptions(), |
| callback_receiver.callback()); |
| |
| task_environment()->FastForwardBy(kTestTimeout); |
| |
| callback_receiver.WaitForCallback(); |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, callback_receiver.status()); |
| |
| ASSERT_TRUE(failure_reason_receiver.was_called()); |
| EXPECT_EQ( |
| AuthenticatorRequestClientDelegate::InterestingFailureReason::kTimeout, |
| std::get<0>(*failure_reason_receiver.result())); |
| } |
| |
| TEST_F(AuthenticatorImplRequestDelegateTest, |
| FailureReasonForDuplicateRegistration) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| FailureReasonCallbackReceiver failure_reason_receiver; |
| auto mock_delegate = std::make_unique< |
| ::testing::NiceMock<MockAuthenticatorRequestDelegateObserver>>( |
| failure_reason_receiver.callback()); |
| auto authenticator = ConnectToFakeAuthenticator(std::move(mock_delegate)); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->exclude_credentials = GetTestCredentials(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->exclude_credentials[0].id, kTestRelyingPartyId)); |
| |
| TestMakeCredentialCallback callback_receiver; |
| authenticator->MakeCredential(std::move(options), |
| callback_receiver.callback()); |
| |
| callback_receiver.WaitForCallback(); |
| EXPECT_EQ(AuthenticatorStatus::CREDENTIAL_EXCLUDED, |
| callback_receiver.status()); |
| |
| ASSERT_TRUE(failure_reason_receiver.was_called()); |
| EXPECT_EQ(AuthenticatorRequestClientDelegate::InterestingFailureReason:: |
| kKeyAlreadyRegistered, |
| std::get<0>(*failure_reason_receiver.result())); |
| } |
| |
| TEST_F(AuthenticatorImplRequestDelegateTest, |
| FailureReasonForMissingRegistration) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| FailureReasonCallbackReceiver failure_reason_receiver; |
| auto mock_delegate = std::make_unique< |
| ::testing::NiceMock<MockAuthenticatorRequestDelegateObserver>>( |
| failure_reason_receiver.callback()); |
| auto authenticator = ConnectToFakeAuthenticator(std::move(mock_delegate)); |
| |
| TestGetAssertionCallback callback_receiver; |
| authenticator->GetAssertion(GetTestPublicKeyCredentialRequestOptions(), |
| callback_receiver.callback()); |
| |
| callback_receiver.WaitForCallback(); |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, callback_receiver.status()); |
| |
| ASSERT_TRUE(failure_reason_receiver.was_called()); |
| EXPECT_EQ(AuthenticatorRequestClientDelegate::InterestingFailureReason:: |
| kKeyNotRegistered, |
| std::get<0>(*failure_reason_receiver.result())); |
| } |
| |
| TEST_F(AuthenticatorImplTest, NoNonAuthoritativeTransports) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| virtual_device_factory_->SetSupportedProtocol( |
| device::ProtocolVersion::kCtap2); |
| device::VirtualCtap2Device::Config config; |
| // If there are no transports in the attestation certificate, and none from |
| // getInfo, then none should be reported because there isn't enough |
| // information to say. |
| config.include_transports_in_attestation_certificate = false; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| MakeCredentialResult result = AuthenticatorMakeCredential(); |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| |
| EXPECT_TRUE(result.response->transports.empty()); |
| } |
| |
| TEST_F(AuthenticatorImplTest, TransportsFromGetInfo) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| virtual_device_factory_->SetSupportedProtocol( |
| device::ProtocolVersion::kCtap2); |
| device::VirtualCtap2Device::Config config; |
| config.include_transports_in_attestation_certificate = false; |
| config.transports_in_get_info = { |
| device::FidoTransportProtocol::kBluetoothLowEnergy}; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| MakeCredentialResult result = AuthenticatorMakeCredential(); |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| |
| base::flat_set<device::FidoTransportProtocol> reported( |
| result.response->transports.begin(), result.response->transports.end()); |
| EXPECT_EQ(reported.size(), 2u); |
| // The transports from the getInfo are authoritative and so they should be |
| // reported. In addition to 'ble' from getInfo, 'usb' should be included |
| // because that's what was used to communicate with the virtual authenticator. |
| EXPECT_TRUE( |
| reported.contains(device::FidoTransportProtocol::kBluetoothLowEnergy)); |
| EXPECT_TRUE(reported.contains( |
| device::FidoTransportProtocol::kUsbHumanInterfaceDevice)); |
| } |
| |
| TEST_F(AuthenticatorImplTest, TransportsInAttestationCertificate) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (auto protocol : |
| {device::ProtocolVersion::kU2f, device::ProtocolVersion::kCtap2}) { |
| SCOPED_TRACE(static_cast<int>(protocol)); |
| virtual_device_factory_->SetSupportedProtocol(protocol); |
| |
| for (const auto transport : std::map<device::FidoTransportProtocol, |
| blink::mojom::AuthenticatorTransport>( |
| {{device::FidoTransportProtocol::kUsbHumanInterfaceDevice, |
| blink::mojom::AuthenticatorTransport::USB}, |
| {device::FidoTransportProtocol::kBluetoothLowEnergy, |
| blink::mojom::AuthenticatorTransport::BLE}, |
| {device::FidoTransportProtocol::kNearFieldCommunication, |
| blink::mojom::AuthenticatorTransport::NFC}})) { |
| virtual_device_factory_->SetTransport(transport.first); |
| |
| MakeCredentialResult result = AuthenticatorMakeCredential(); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| |
| const std::vector<device::FidoTransportProtocol>& transports( |
| result.response->transports); |
| ASSERT_EQ(1u, transports.size()); |
| EXPECT_EQ(transport.first, transports[0]); |
| } |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, ExtensionHMACSecret) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (const bool include_extension : {false, true}) { |
| for (const bool authenticator_support : {false, true}) { |
| for (const bool pin_support : {false, true}) { |
| SCOPED_TRACE(include_extension); |
| SCOPED_TRACE(authenticator_support); |
| SCOPED_TRACE(pin_support); |
| |
| device::VirtualCtap2Device::Config config; |
| config.hmac_secret_support = authenticator_support; |
| config.pin_support = pin_support; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->hmac_create_secret = include_extension; |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| |
| device::AuthenticatorData parsed_auth_data = |
| AuthDataFromMakeCredentialResponse(result.response); |
| |
| // The virtual CTAP2 device always echos the hmac-secret extension on |
| // registrations. Therefore, if |hmac_secret| was set above it should be |
| // serialised in the CBOR and correctly passed all the way back around |
| // to the reply's authenticator data. |
| bool has_hmac_secret = false; |
| const auto& extensions = parsed_auth_data.extensions(); |
| if (extensions) { |
| CHECK(extensions->is_map()); |
| const cbor::Value::MapValue& extensions_map = extensions->GetMap(); |
| const auto hmac_secret_it = |
| extensions_map.find(cbor::Value(device::kExtensionHmacSecret)); |
| if (hmac_secret_it != extensions_map.end()) { |
| ASSERT_TRUE(hmac_secret_it->second.is_bool()); |
| EXPECT_TRUE(hmac_secret_it->second.GetBool()); |
| has_hmac_secret = true; |
| } |
| } |
| |
| EXPECT_EQ(include_extension && authenticator_support && pin_support, |
| has_hmac_secret); |
| } |
| } |
| } |
| } |
| |
| // Tests that for an authenticator that does not support batching, credential |
| // lists get probed silently to work around authenticators rejecting exclude |
| // lists exceeding a certain size. |
| TEST_F(AuthenticatorImplTest, MakeCredentialWithLargeExcludeList) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (bool has_excluded_credential : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "has_excluded_credential=" << has_excluded_credential); |
| |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| config.reject_large_allow_and_exclude_lists = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->exclude_credentials = GetTestCredentials(/*num_credentials=*/10); |
| if (has_excluded_credential) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->exclude_credentials.back().id, kTestRelyingPartyId)); |
| } |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| has_excluded_credential ? AuthenticatorStatus::CREDENTIAL_EXCLUDED |
| : AuthenticatorStatus::SUCCESS); |
| } |
| } |
| |
| // Tests that for an authenticator that does not support batching, credential |
| // lists get probed silently to work around authenticators rejecting allow lists |
| // exceeding a certain size. |
| TEST_F(AuthenticatorImplTest, GetAssertionWithLargeAllowList) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (bool has_allowed_credential : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "has_allowed_credential=" << has_allowed_credential); |
| |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| config.reject_large_allow_and_exclude_lists = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials = GetTestCredentials(/*num_credentials=*/10); |
| if (has_allowed_credential) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials.back().id, kTestRelyingPartyId)); |
| } |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| has_allowed_credential ? AuthenticatorStatus::SUCCESS |
| : AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| } |
| |
| // Tests that, regardless of batching support, GetAssertion requests with a |
| // single allowed credential ID don't result in a silent probing request. |
| TEST_F(AuthenticatorImplTest, GetAssertionSingleElementAllowListDoesNotProbe) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (bool supports_batching : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "supports_batching=" << supports_batching); |
| |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| if (supports_batching) { |
| config.max_credential_id_length = kTestCredentialIdLength; |
| config.max_credential_count_in_list = 10; |
| } |
| config.reject_silent_authentication_requests = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| auto test_credentials = GetTestCredentials(/*num_credentials=*/1); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| test_credentials.front().id, kTestRelyingPartyId)); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials = std::move(test_credentials); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| } |
| |
| // Tests that an allow list that fits into a single batch does not result in a |
| // silent probing request. |
| TEST_F(AuthenticatorImplTest, GetAssertionSingleBatchListDoesNotProbe) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (bool allow_list_fits_single_batch : {false, true}) { |
| SCOPED_TRACE(::testing::Message() << "allow_list_fits_single_batch=" |
| << allow_list_fits_single_batch); |
| |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| config.max_credential_id_length = kTestCredentialIdLength; |
| constexpr size_t kBatchSize = 10; |
| config.max_credential_count_in_list = kBatchSize; |
| config.reject_silent_authentication_requests = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| auto test_credentials = GetTestCredentials( |
| /*num_credentials=*/kBatchSize + |
| (allow_list_fits_single_batch ? 0 : 1)); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| test_credentials.back().id, kTestRelyingPartyId)); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials = std::move(test_credentials); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| allow_list_fits_single_batch |
| ? AuthenticatorStatus::SUCCESS |
| : AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, OptionalCredentialInAssertionResponse) { |
| // This test exercises the unfortunate optionality in the CTAP2 spec r.e. |
| // whether an authenticator returns credential information when the allowlist |
| // only has a single entry. |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (const auto behavior : |
| {device::VirtualCtap2Device::Config::IncludeCredential::ONLY_IF_NEEDED, |
| device::VirtualCtap2Device::Config::IncludeCredential::ALWAYS, |
| device::VirtualCtap2Device::Config::IncludeCredential::NEVER}) { |
| SCOPED_TRACE(static_cast<int>(behavior)); |
| |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| config.include_credential_in_assertion_response = behavior; |
| config.max_credential_count_in_list = 10; |
| config.max_credential_id_length = 256; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| size_t num_credentials; |
| bool should_timeout = false; |
| switch (behavior) { |
| case device::VirtualCtap2Device::Config::IncludeCredential:: |
| ONLY_IF_NEEDED: |
| // The behaviour to test for |ONLY_IF_NEEDED| is that an omitted |
| // credential in the response is handled correctly. |
| num_credentials = 1; |
| break; |
| case device::VirtualCtap2Device::Config::IncludeCredential::ALWAYS: |
| // Also test that a technically-superfluous credential in the response |
| // is handled. |
| num_credentials = 1; |
| break; |
| case device::VirtualCtap2Device::Config::IncludeCredential::NEVER: |
| // Test that omitting a credential in an ambiguous context causes a |
| // failure. |
| num_credentials = 2; |
| should_timeout = true; |
| break; |
| } |
| |
| auto test_credentials = GetTestCredentials(num_credentials); |
| for (const auto& cred : test_credentials) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| cred.id, kTestRelyingPartyId)); |
| } |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials = std::move(test_credentials); |
| |
| if (should_timeout) { |
| EXPECT_EQ( |
| AuthenticatorGetAssertionAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } else { |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| } |
| } |
| |
| // Tests that an allowList with only credential IDs of a length exceeding the |
| // maxCredentialIdLength parameter is not mistakenly interpreted as an empty |
| // allow list. |
| TEST_F(AuthenticatorImplTest, AllowListWithOnlyOversizedCredentialIds) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.u2f_support = true; |
| config.max_credential_id_length = kTestCredentialIdLength; |
| config.max_credential_count_in_list = 10; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| const std::vector<uint8_t> cred_id(kTestCredentialIdLength + 1, 0); |
| // Inject registration so that the test will fail (because of a successful |
| // response) if the oversized credential ID is sent. |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| cred_id, kTestRelyingPartyId)); |
| |
| for (const bool has_app_id : {false, true}) { |
| SCOPED_TRACE(has_app_id); |
| virtual_device_factory_->mutable_state()->allow_list_history.clear(); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| if (has_app_id) { |
| options->appid = kTestOrigin1; |
| } |
| options->allow_credentials = {device::PublicKeyCredentialDescriptor( |
| device::CredentialType::kPublicKey, cred_id)}; |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| const auto& allow_list_history = |
| virtual_device_factory_->mutable_state()->allow_list_history; |
| // No empty allow-list requests should have been made. |
| EXPECT_TRUE(base::ranges::none_of( |
| allow_list_history, |
| [](const std::vector<device::PublicKeyCredentialDescriptor>& |
| allow_list) { return allow_list.empty(); })); |
| } |
| } |
| |
| // Tests that duplicate credential IDs are filtered from an assertion allow_list |
| // parameter. |
| TEST_F(AuthenticatorImplTest, AllowListWithDuplicateCredentialIds) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.u2f_support = true; |
| config.max_credential_id_length = kTestCredentialIdLength; |
| config.max_credential_count_in_list = 10; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| device::PublicKeyCredentialDescriptor cred_a( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 1), {}); |
| device::PublicKeyCredentialDescriptor cred_b( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 2), |
| {device::FidoTransportProtocol::kUsbHumanInterfaceDevice}); |
| // Same ID as `cred_a` and `cred_b` but with different transports. Transport |
| // hints from descriptors with equal IDs should be merged. |
| device::PublicKeyCredentialDescriptor cred_c( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 1), |
| {device::FidoTransportProtocol::kBluetoothLowEnergy}); |
| device::PublicKeyCredentialDescriptor cred_d( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 2), |
| {device::FidoTransportProtocol::kBluetoothLowEnergy}); |
| |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| cred_b.id, kTestRelyingPartyId)); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials.clear(); |
| options->allow_credentials.insert(options->allow_credentials.end(), 5, |
| cred_a); |
| options->allow_credentials.push_back(cred_b); |
| options->allow_credentials.insert(options->allow_credentials.end(), 3, |
| cred_c); |
| options->allow_credentials.insert(options->allow_credentials.end(), 2, |
| cred_d); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(virtual_device_factory_->mutable_state()->allow_list_history.size(), |
| 1u); |
| device::PublicKeyCredentialDescriptor cred_a_and_c( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 1)); |
| device::PublicKeyCredentialDescriptor cred_b_and_d( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 2)); |
| EXPECT_THAT( |
| virtual_device_factory_->mutable_state()->allow_list_history.at(0), |
| testing::UnorderedElementsAre(cred_a_and_c, cred_b_and_d)); |
| } |
| |
| // Tests that duplicate credential IDs are filtered from a registration |
| // exclude_list parameter. |
| TEST_F(AuthenticatorImplTest, ExcludeListWithDuplicateCredentialIds) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.u2f_support = true; |
| config.max_credential_id_length = kTestCredentialIdLength; |
| config.max_credential_count_in_list = 100; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| device::PublicKeyCredentialDescriptor cred_a( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 1), {}); |
| device::PublicKeyCredentialDescriptor cred_b( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 2), |
| {device::FidoTransportProtocol::kUsbHumanInterfaceDevice}); |
| // Same ID as `cred_a` and `cred_b` but with different transports. Transport |
| // hints from descriptors with equal IDs should be merged. |
| device::PublicKeyCredentialDescriptor cred_c( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 1), |
| {device::FidoTransportProtocol::kBluetoothLowEnergy}); |
| device::PublicKeyCredentialDescriptor cred_d( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 2), |
| {device::FidoTransportProtocol::kBluetoothLowEnergy}); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->exclude_credentials.clear(); |
| options->exclude_credentials.insert(options->exclude_credentials.end(), 5, |
| cred_a); |
| options->exclude_credentials.push_back(cred_b); |
| options->exclude_credentials.insert(options->exclude_credentials.end(), 3, |
| cred_c); |
| options->exclude_credentials.insert(options->exclude_credentials.end(), 2, |
| cred_d); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ( |
| virtual_device_factory_->mutable_state()->exclude_list_history.size(), |
| 1u); |
| device::PublicKeyCredentialDescriptor cred_a_and_c( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 1)); |
| device::PublicKeyCredentialDescriptor cred_b_and_d( |
| device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength, 2)); |
| EXPECT_THAT( |
| virtual_device_factory_->mutable_state()->exclude_list_history.at(0), |
| testing::UnorderedElementsAre(cred_a_and_c, cred_b_and_d)); |
| } |
| |
| // Test that allow lists over 64 entries are verboten. |
| TEST_F(AuthenticatorImplTest, OversizedAllowList) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.u2f_support = true; |
| config.max_credential_id_length = kTestCredentialIdLength; |
| config.max_credential_count_in_list = 100; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| auto test_credentials = GetTestCredentials( |
| /*num_credentials=*/blink::mojom:: |
| kPublicKeyCredentialDescriptorListMaxSize + |
| 1); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| test_credentials.at(0).id, kTestRelyingPartyId)); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials = test_credentials; |
| |
| bool has_mojo_error = false; |
| SetMojoErrorHandler(base::BindLambdaForTesting( |
| [&](const std::string& error) { has_mojo_error = true; })); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_TRUE(has_mojo_error); |
| } |
| |
| // Test that exclude lists over 64 entries are verboten. |
| TEST_F(AuthenticatorImplTest, OversizedExcludeList) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.u2f_support = true; |
| config.max_credential_id_length = kTestCredentialIdLength; |
| config.max_credential_count_in_list = 100; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| auto test_credentials = GetTestCredentials( |
| /*num_credentials=*/blink::mojom:: |
| kPublicKeyCredentialDescriptorListMaxSize + |
| 1); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->exclude_credentials = test_credentials; |
| |
| bool has_mojo_error = false; |
| SetMojoErrorHandler(base::BindLambdaForTesting( |
| [&](const std::string& error) { has_mojo_error = true; })); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_TRUE(has_mojo_error); |
| } |
| |
| TEST_F(AuthenticatorImplTest, NoUnexpectedAuthenticatorExtensions) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.add_extra_extension = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| // Check that extra authenticator extensions are rejected when creating a |
| // credential. |
| EXPECT_EQ(AuthenticatorMakeCredential().status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| |
| // Extensions should also be rejected when getting an assertion. |
| PublicKeyCredentialRequestOptionsPtr assertion_options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| assertion_options->allow_credentials.back().id, kTestRelyingPartyId)); |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(assertion_options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| TEST_F(AuthenticatorImplTest, NoUnexpectedClientExtensions) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.reject_all_extensions = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| // Check that no unexpected client extensions are sent to the authenticator. |
| EXPECT_EQ(AuthenticatorMakeCredential().status, AuthenticatorStatus::SUCCESS); |
| |
| // No extensions should be sent when getting an assertion either. |
| PublicKeyCredentialRequestOptionsPtr assertion_options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| assertion_options->allow_credentials.back().id, kTestRelyingPartyId)); |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(assertion_options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| |
| // Tests that on an authenticator that supports batching, exclude lists that fit |
| // into a single batch are sent without probing. |
| TEST_F(AuthenticatorImplTest, ExcludeListBatching) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (bool authenticator_has_excluded_credential : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "authenticator_has_excluded_credential=" |
| << authenticator_has_excluded_credential); |
| |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| config.max_credential_id_length = kTestCredentialIdLength; |
| constexpr size_t kBatchSize = 10; |
| config.max_credential_count_in_list = kBatchSize; |
| // Reject silent authentication requests to ensure we are not probing |
| // credentials silently, since the exclude list should fit into a single |
| // batch. |
| config.reject_silent_authentication_requests = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| auto test_credentials = GetTestCredentials(kBatchSize); |
| test_credentials.insert( |
| test_credentials.end() - 1, |
| {device::CredentialType::kPublicKey, |
| std::vector<uint8_t>(kTestCredentialIdLength + 1, 1)}); |
| if (authenticator_has_excluded_credential) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| test_credentials.back().id, kTestRelyingPartyId)); |
| } |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->exclude_credentials = std::move(test_credentials); |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| authenticator_has_excluded_credential |
| ? AuthenticatorStatus::CREDENTIAL_EXCLUDED |
| : AuthenticatorStatus::SUCCESS); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, GetPublicKey) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| static constexpr struct { |
| device::CoseAlgorithmIdentifier algo; |
| absl::optional<int> evp_id; |
| } kTests[] = { |
| {device::CoseAlgorithmIdentifier::kEs256, EVP_PKEY_EC}, |
| {device::CoseAlgorithmIdentifier::kRs256, EVP_PKEY_RSA}, |
| {device::CoseAlgorithmIdentifier::kEdDSA, EVP_PKEY_ED25519}, |
| {device::CoseAlgorithmIdentifier::kInvalidForTesting, absl::nullopt}, |
| }; |
| |
| std::vector<device::CoseAlgorithmIdentifier> advertised_algorithms; |
| for (const auto& test : kTests) { |
| advertised_algorithms.push_back(test.algo); |
| } |
| |
| device::VirtualCtap2Device::Config config; |
| config.advertised_algorithms = std::move(advertised_algorithms); |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| for (const auto& test : kTests) { |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->public_key_parameters = |
| GetTestPublicKeyCredentialParameters(static_cast<int32_t>(test.algo)); |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| const auto& response = result.response; |
| EXPECT_EQ(response->public_key_algo, static_cast<int32_t>(test.algo)); |
| |
| // The value of the parsed authenticator data should match what's in |
| // the attestation object. |
| absl::optional<Value> attestation_value = |
| Reader::Read(response->attestation_object); |
| CHECK(attestation_value); |
| const auto& attestation = attestation_value->GetMap(); |
| const auto auth_data_it = attestation.find(Value(device::kAuthDataKey)); |
| CHECK(auth_data_it != attestation.end()); |
| const std::vector<uint8_t>& auth_data = |
| auth_data_it->second.GetBytestring(); |
| EXPECT_EQ(auth_data, response->info->authenticator_data); |
| |
| ASSERT_EQ(test.evp_id.has_value(), response->public_key_der.has_value()); |
| if (!test.evp_id) { |
| continue; |
| } |
| |
| const std::vector<uint8_t>& public_key_der = |
| response->public_key_der.value(); |
| |
| CBS cbs; |
| CBS_init(&cbs, public_key_der.data(), public_key_der.size()); |
| bssl::UniquePtr<EVP_PKEY> pkey(EVP_parse_public_key(&cbs)); |
| EXPECT_EQ(0u, CBS_len(&cbs)); |
| ASSERT_TRUE(pkey.get()); |
| |
| EXPECT_EQ(test.evp_id.value(), EVP_PKEY_id(pkey.get())); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, AlgorithmsOmitted) { |
| // Some CTAP 2.0 security keys shipped support for algorithms other than |
| // ECDSA P-256 but the algorithms field didn't exist then. makeCredential |
| // requests should get routed to them anyway. |
| |
| device::VirtualCtap2Device::Config config; |
| // Remove the algorithms field from the getInfo. |
| config.advertised_algorithms.clear(); |
| virtual_device_factory_->SetCtap2Config(config); |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| // Test that an Ed25519 credential can still be created. (The virtual |
| // authenticator supports that algorithm.) |
| { |
| const int32_t algo = |
| static_cast<int32_t>(device::CoseAlgorithmIdentifier::kEdDSA); |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->public_key_parameters = GetTestPublicKeyCredentialParameters(algo); |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| const auto& response = result.response; |
| EXPECT_EQ(response->public_key_algo, algo); |
| } |
| |
| // Test that requesting an unsupported algorithm still collects a touch. |
| { |
| bool touched = false; |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting([&](device::VirtualFidoDevice* device) { |
| touched = true; |
| return true; |
| }); |
| |
| const int32_t algo = static_cast<int32_t>( |
| device::CoseAlgorithmIdentifier::kInvalidForTesting); |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->public_key_parameters = GetTestPublicKeyCredentialParameters(algo); |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_TRUE(touched); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, VirtualAuthenticatorPublicKeyAlgos) { |
| // Exercise all the public key types in the virtual authenticator for create() |
| // and get(). |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| static const struct { |
| device::CoseAlgorithmIdentifier algo; |
| // This field is not a raw_ptr<> because it was filtered by the rewriter |
| // for: #global-scope |
| RAW_PTR_EXCLUSION const EVP_MD* digest; |
| } kTests[] = { |
| {device::CoseAlgorithmIdentifier::kEs256, EVP_sha256()}, |
| {device::CoseAlgorithmIdentifier::kRs256, EVP_sha256()}, |
| {device::CoseAlgorithmIdentifier::kEdDSA, nullptr}, |
| }; |
| |
| std::vector<device::CoseAlgorithmIdentifier> advertised_algorithms; |
| for (const auto& test : kTests) { |
| advertised_algorithms.push_back(test.algo); |
| } |
| |
| device::VirtualCtap2Device::Config config; |
| config.advertised_algorithms = std::move(advertised_algorithms); |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| for (const auto& test : kTests) { |
| SCOPED_TRACE(static_cast<int>(test.algo)); |
| |
| PublicKeyCredentialCreationOptionsPtr create_options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| create_options->public_key_parameters = |
| GetTestPublicKeyCredentialParameters(static_cast<int32_t>(test.algo)); |
| |
| MakeCredentialResult create_result = |
| AuthenticatorMakeCredential(std::move(create_options)); |
| ASSERT_EQ(create_result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(create_result.response->public_key_algo, |
| static_cast<int32_t>(test.algo)); |
| |
| const std::vector<uint8_t>& public_key_der = |
| create_result.response->public_key_der.value(); |
| CBS cbs; |
| CBS_init(&cbs, public_key_der.data(), public_key_der.size()); |
| bssl::UniquePtr<EVP_PKEY> pkey(EVP_parse_public_key(&cbs)); |
| EXPECT_EQ(0u, CBS_len(&cbs)); |
| ASSERT_TRUE(pkey.get()); |
| |
| PublicKeyCredentialRequestOptionsPtr get_options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| device::PublicKeyCredentialDescriptor public_key( |
| device::CredentialType::kPublicKey, |
| create_result.response->info->raw_id, |
| {device::FidoTransportProtocol::kUsbHumanInterfaceDevice}); |
| get_options->allow_credentials = {std::move(public_key)}; |
| GetAssertionResult get_result = |
| AuthenticatorGetAssertion(std::move(get_options)); |
| ASSERT_EQ(get_result.status, AuthenticatorStatus::SUCCESS); |
| base::span<const uint8_t> signature(get_result.response->signature); |
| std::vector<uint8_t> signed_data( |
| get_result.response->info->authenticator_data); |
| const std::array<uint8_t, crypto::kSHA256Length> client_data_json_hash( |
| crypto::SHA256Hash(get_result.response->info->client_data_json)); |
| signed_data.insert(signed_data.end(), client_data_json_hash.begin(), |
| client_data_json_hash.end()); |
| |
| bssl::ScopedEVP_MD_CTX md_ctx; |
| ASSERT_EQ(EVP_DigestVerifyInit(md_ctx.get(), /*pctx=*/nullptr, test.digest, |
| /*e=*/nullptr, pkey.get()), |
| 1); |
| EXPECT_EQ(EVP_DigestVerify(md_ctx.get(), signature.data(), signature.size(), |
| signed_data.data(), signed_data.size()), |
| 1); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, TestAuthenticationTransport) { |
| // TODO(crbug.com/1249057): handle case where the transport is unknown. |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| // Verify transport used during authentication is correctly being returned |
| // to the renderer layer. |
| for (const device::FidoTransportProtocol transport : |
| {device::FidoTransportProtocol::kUsbHumanInterfaceDevice, |
| device::FidoTransportProtocol::kBluetoothLowEnergy, |
| device::FidoTransportProtocol::kNearFieldCommunication, |
| device::FidoTransportProtocol::kInternal}) { |
| device::AuthenticatorAttachment attachment = |
| (transport == device::FidoTransportProtocol::kInternal |
| ? device::AuthenticatorAttachment::kPlatform |
| : device::AuthenticatorAttachment::kCrossPlatform); |
| ResetVirtualDevice(); |
| virtual_device_factory_->SetSupportedProtocol( |
| device::ProtocolVersion::kCtap2); |
| virtual_device_factory_->SetTransport(transport); |
| virtual_device_factory_->mutable_state()->transport = transport; |
| |
| PublicKeyCredentialCreationOptionsPtr create_options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| MakeCredentialResult create_result = |
| AuthenticatorMakeCredential(std::move(create_options)); |
| ASSERT_EQ(create_result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(create_result.response->authenticator_attachment, attachment); |
| |
| PublicKeyCredentialRequestOptionsPtr get_options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| device::PublicKeyCredentialDescriptor public_key( |
| device::CredentialType::kPublicKey, |
| create_result.response->info->raw_id, {transport}); |
| get_options->allow_credentials = {std::move(public_key)}; |
| GetAssertionResult get_result = |
| AuthenticatorGetAssertion(std::move(get_options)); |
| ASSERT_EQ(get_result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(get_result.response->authenticator_attachment, attachment); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, ResetDiscoveryFactoryOverride) { |
| // This is a regression test for crbug.com/1087158. |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| // Make the entire discovery factory disappear mid-request. |
| bool was_called = false; |
| virtual_device_factory_->SetSupportedProtocol( |
| device::ProtocolVersion::kCtap2); |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting([&](device::VirtualFidoDevice* device) { |
| was_called = true; |
| ResetVirtualDevice(); |
| return false; |
| }); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| EXPECT_EQ( |
| AuthenticatorMakeCredentialAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| TEST_F(AuthenticatorImplTest, InvalidU2FPublicKey) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| virtual_device_factory_->SetSupportedProtocol(device::ProtocolVersion::kU2f); |
| virtual_device_factory_->mutable_state()->u2f_invalid_public_key = true; |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| TEST_F(AuthenticatorImplTest, InvalidU2FSignature) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| virtual_device_factory_->SetSupportedProtocol(device::ProtocolVersion::kU2f); |
| virtual_device_factory_->mutable_state()->u2f_invalid_signature = true; |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestOrigin1)); |
| options->appid = kTestOrigin1; |
| |
| EXPECT_EQ( |
| AuthenticatorGetAssertionAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| TEST_F(AuthenticatorImplTest, CredBlob) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.cred_blob_support = true; |
| // credProtect is required for credBlob per CTAP 2.1. |
| config.cred_protect_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| const std::vector<uint8_t> cred_blob = {1, 2, 3, 4}; |
| |
| std::vector<uint8_t> credential_id; |
| // Create a credential with a credBlob set. |
| { |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->cred_blob = cred_blob; |
| auto result = AuthenticatorMakeCredential(std::move(options)); |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| credential_id = std::move(result.response->info->raw_id); |
| EXPECT_TRUE(result.response->echo_cred_blob); |
| EXPECT_TRUE(result.response->cred_blob); |
| } |
| |
| // Expect to be able to fetch the credBlob with an assertion. |
| { |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials[0] = device::PublicKeyCredentialDescriptor( |
| device::CredentialType::kPublicKey, std::move(credential_id)); |
| options->get_cred_blob = true; |
| |
| auto result = AuthenticatorGetAssertion(std::move(options)); |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(result.response->get_cred_blob, cred_blob); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, MinPINLength) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (const bool min_pin_length_supported : {false, true}) { |
| device::VirtualCtap2Device::Config config; |
| config.min_pin_length_extension_support = min_pin_length_supported; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| for (const bool min_pin_length_requested : {false, true}) { |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->min_pin_length_requested = min_pin_length_requested; |
| auto result = AuthenticatorMakeCredential(std::move(options)); |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| |
| const device::AuthenticatorData auth_data = |
| AuthDataFromMakeCredentialResponse(result.response); |
| bool has_min_pin_length = false; |
| if (auth_data.extensions().has_value()) { |
| const cbor::Value::MapValue& extensions = |
| auth_data.extensions()->GetMap(); |
| const auto it = |
| extensions.find(cbor::Value(device::kExtensionMinPINLength)); |
| has_min_pin_length = it != extensions.end() && it->second.is_unsigned(); |
| } |
| ASSERT_EQ(has_min_pin_length, |
| min_pin_length_supported && min_pin_length_requested); |
| } |
| } |
| } |
| |
| // Regression test for crbug.com/1257281. |
| // Tests that a request is not cancelled when an authenticator returns |
| // CTAP2_ERR_KEEPALIVE_CANCEL after selecting another authenticator for a |
| // request. |
| TEST_F(AuthenticatorImplTest, CancellingAuthenticatorDoesNotTerminateRequest) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| for (auto request_type : {device::FidoRequestType::kMakeCredential, |
| device::FidoRequestType::kGetAssertion}) { |
| SCOPED_TRACE(::testing::Message() |
| << "request_type=" |
| << (request_type == device::FidoRequestType::kMakeCredential |
| ? "make_credential" |
| : "get_assertion")); |
| // Make a device that supports getting a PUAT with UV. |
| auto discovery = |
| std::make_unique<device::test::MultipleVirtualFidoDeviceFactory>(); |
| device::test::MultipleVirtualFidoDeviceFactory::DeviceDetails device_1; |
| device_1.config.internal_uv_support = true; |
| device_1.config.pin_uv_auth_token_support = true; |
| device_1.config.user_verification_succeeds = true; |
| device_1.config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| device_1.state->fingerprints_enrolled = true; |
| PublicKeyCredentialRequestOptionsPtr dummy_options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| ASSERT_TRUE(device_1.state->InjectRegistration( |
| dummy_options->allow_credentials[0].id, kTestRelyingPartyId)); |
| discovery->AddDevice(std::move(device_1)); |
| |
| // Make a device that does not support PUATs but can still handle the |
| // request. This device will not respond to the request. |
| device::test::MultipleVirtualFidoDeviceFactory::DeviceDetails device_2; |
| device_2.config.internal_uv_support = false; |
| device_2.config.pin_uv_auth_token_support = false; |
| device_2.config.ctap2_versions = {device::Ctap2Version::kCtap2_0}; |
| device_2.state->simulate_press_callback = |
| base::BindRepeating([](VirtualFidoDevice* ignore) { return false; }); |
| discovery->AddDevice(std::move(device_2)); |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting(std::move(discovery)); |
| |
| if (request_type == device::FidoRequestType::kMakeCredential) { |
| MakeCredentialResult result = AuthenticatorMakeCredential(); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| } else { |
| GetAssertionResult result = AuthenticatorGetAssertion(); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| } |
| } |
| } |
| |
| TEST_F(AuthenticatorImplTest, PRFWithoutSupport) { |
| // This tests that the PRF extension doesn't trigger any DCHECKs or crashes |
| // when used with an authenticator doesn't doesn't support hmac-secret. |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| auto prf_value = blink::mojom::PRFValues::New(); |
| const std::vector<uint8_t> salt1(32, 1); |
| prf_value->first = salt1; |
| std::vector<blink::mojom::PRFValuesPtr> prf_inputs; |
| prf_inputs.emplace_back(std::move(prf_value)); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->prf = true; |
| options->prf_inputs = std::move(prf_inputs); |
| |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| |
| EXPECT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| class AuthenticatorDevicePublicKeyTest : public AuthenticatorImplTest { |
| void SetUp() override { |
| AuthenticatorImplTest::SetUp(); |
| old_client_ = SetBrowserClientForTesting(&test_client_); |
| } |
| |
| void TearDown() override { |
| SetBrowserClientForTesting(old_client_); |
| AuthenticatorImplTest::TearDown(); |
| } |
| |
| protected: |
| TestAuthenticatorContentBrowserClient test_client_; |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| }; |
| |
| TEST_F(AuthenticatorDevicePublicKeyTest, DevicePublicKeyMakeCredential) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (const bool dpk_support : {false, true}) { |
| device::VirtualCtap2Device::Config config; |
| config.device_public_key_support = dpk_support; |
| config.backup_eligible = true; |
| // None attestation is needed because, otherwise, zeroing the AAGUID |
| // invalidates the DPK signature. |
| config.none_attestation = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->device_public_key = blink::mojom::DevicePublicKeyRequest::New(); |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| ASSERT_EQ(static_cast<bool>(result.response->device_public_key), |
| dpk_support); |
| ASSERT_EQ(HasDevicePublicKeyExtensionInAuthenticatorData(result.response), |
| dpk_support); |
| if (dpk_support) { |
| ASSERT_FALSE( |
| result.response->device_public_key->authenticator_output.empty()); |
| ASSERT_FALSE(result.response->device_public_key->signature.empty()); |
| } |
| } |
| } |
| |
| TEST_F(AuthenticatorDevicePublicKeyTest, |
| DevicePublicKeyWithPrimaryAttestation) { |
| // If the authenticator returns regular attestation and a devicePubKey |
| // response, and the browser needs to strip that attesation, then the DPK |
| // extension should be removed. |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.device_public_key_support = true; |
| config.backup_eligible = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->device_public_key = blink::mojom::DevicePublicKeyRequest::New(); |
| MakeCredentialResult result = AuthenticatorMakeCredential(std::move(options)); |
| |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| ASSERT_FALSE(static_cast<bool>(result.response->device_public_key)); |
| ASSERT_FALSE(HasDevicePublicKeyExtensionInAuthenticatorData(result.response)); |
| } |
| |
| TEST_F(AuthenticatorDevicePublicKeyTest, |
| DevicePublicKeyMakeCredentialRequiresBackupEligible) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (const bool backup_eligible : {false, true}) { |
| device::VirtualCtap2Device::Config config; |
| config.device_public_key_support = true; |
| config.none_attestation = true; |
| config.backup_eligible = backup_eligible; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->device_public_key = blink::mojom::DevicePublicKeyRequest::New(); |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| |
| if (backup_eligible) { |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| ASSERT_TRUE(static_cast<bool>(result.response->device_public_key)); |
| ASSERT_TRUE( |
| HasDevicePublicKeyExtensionInAuthenticatorData(result.response)); |
| } else { |
| ASSERT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| } |
| } |
| |
| TEST_F(AuthenticatorDevicePublicKeyTest, DevicePublicKeyGetAssertion) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| bool credential_injected = false; |
| for (const bool dpk_support : {false, true}) { |
| device::VirtualCtap2Device::Config config; |
| config.device_public_key_support = dpk_support; |
| config.backup_eligible = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| if (!credential_injected) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| credential_injected = true; |
| } |
| options->device_public_key = blink::mojom::DevicePublicKeyRequest::New(); |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| ASSERT_EQ(static_cast<bool>(result.response->device_public_key), |
| dpk_support); |
| if (dpk_support) { |
| ASSERT_FALSE( |
| result.response->device_public_key->authenticator_output.empty()); |
| ASSERT_FALSE(result.response->device_public_key->signature.empty()); |
| } |
| } |
| } |
| |
| TEST_F(AuthenticatorDevicePublicKeyTest, |
| DevicePublicKeyGetAssertionRequiresBackupEligible) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| bool credential_injected = false; |
| for (const bool backup_eligible : {false, true}) { |
| device::VirtualCtap2Device::Config config; |
| config.device_public_key_support = true; |
| config.backup_eligible = backup_eligible; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| if (!credential_injected) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| credential_injected = true; |
| } |
| options->device_public_key = blink::mojom::DevicePublicKeyRequest::New(); |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| |
| if (backup_eligible) { |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| ASSERT_TRUE(static_cast<bool>(result.response->device_public_key)); |
| } else { |
| ASSERT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| } |
| } |
| |
| TEST_F(AuthenticatorDevicePublicKeyTest, DevicePublicKeyBadResponse) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| bool credential_injected = false; |
| for (int breakage = 0; breakage < 3; breakage++) { |
| SCOPED_TRACE(::testing::Message() << "breakage=" << breakage); |
| |
| device::VirtualCtap2Device::Config config; |
| config.device_public_key_support = true; |
| config.backup_eligible = true; |
| // None attestation is needed because, otherwise, zeroing the AAGUID |
| // invalidates the DPK signature. |
| config.none_attestation = true; |
| |
| switch (breakage) { |
| case 0: |
| break; |
| |
| case 1: |
| config.device_public_key_drop_extension_response = true; |
| break; |
| |
| case 2: |
| config.device_public_key_drop_signature = true; |
| break; |
| |
| default: |
| CHECK(false); |
| } |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| AuthenticatorStatus status; |
| for (const bool is_make_credential : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "is_make_credential=" << is_make_credential); |
| |
| if (is_make_credential) { |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->device_public_key = |
| blink::mojom::DevicePublicKeyRequest::New(); |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| status = result.status; |
| } else { |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| if (!credential_injected) { |
| ASSERT_TRUE( |
| virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| credential_injected = true; |
| } |
| options->device_public_key = |
| blink::mojom::DevicePublicKeyRequest::New(); |
| GetAssertionResult result = |
| AuthenticatorGetAssertion(std::move(options)); |
| status = result.status; |
| } |
| |
| if (breakage) { |
| EXPECT_NE(status, AuthenticatorStatus::SUCCESS); |
| } else { |
| EXPECT_EQ(status, AuthenticatorStatus::SUCCESS); |
| } |
| } |
| } |
| } |
| |
| TEST_F(AuthenticatorDevicePublicKeyTest, |
| DevicePublicKeyMakeCredentialAttestation) { |
| constexpr device::AttestationConveyancePreference req_none = |
| device::AttestationConveyancePreference::kNone; |
| constexpr device::AttestationConveyancePreference req_indirect = |
| device::AttestationConveyancePreference::kIndirect; |
| constexpr device::AttestationConveyancePreference req_direct = |
| device::AttestationConveyancePreference::kDirect; |
| constexpr device::AttestationConveyancePreference req_enterprise = device:: |
| AttestationConveyancePreference::kEnterpriseIfRPListedOnAuthenticator; |
| constexpr bool no_allowlist = false; |
| constexpr bool allowlisted = true; |
| constexpr bool none = false; |
| constexpr bool direct = true; |
| constexpr bool ep = true; |
| constexpr bool no_ep = false; |
| constexpr AttestationConsent no_prompt = AttestationConsent::NOT_USED; |
| constexpr AttestationConsent prompt_no = AttestationConsent::DENIED; |
| constexpr AttestationConsent prompt_yes = AttestationConsent::GRANTED; |
| constexpr AttestationConsent prompt_ep_no = |
| AttestationConsent::DENIED_FOR_ENTERPRISE_ATTESTATION; |
| constexpr AttestationConsent prompt_ep_yes = |
| AttestationConsent::GRANTED_FOR_ENTERPRISE_ATTESTATION; |
| |
| constexpr struct { |
| device::AttestationConveyancePreference requested_attestation; |
| bool is_permitted_by_policy; |
| bool has_attestation; |
| bool enterprise_attestation_returned; |
| AttestationConsent prompt; |
| bool ok; |
| } kTests[] = { |
| // clang-format off |
| // |Requested | In policy? | Att | Ent? | Prompt? | OK? | |
| // ---------------------------------------------------------------- |
| |
| // No attestation requested and none provided is the simple case. |
| {req_none, no_allowlist, none, no_ep, no_prompt, true}, |
| |
| // Any non-enterprise DPK attestation request is mapped to "none", so the |
| // authenticator cannot return a DPK attestation in these cases. |
| {req_none, no_allowlist, direct, no_ep, no_prompt, false}, |
| {req_indirect, no_allowlist, direct, no_ep, no_prompt, false}, |
| {req_direct, no_allowlist, direct, no_ep, no_prompt, false}, |
| // ... and certainly can't return an enterprise attestation. |
| {req_none, no_allowlist, direct, ep, no_prompt, false}, |
| {req_indirect, no_allowlist, direct, ep, no_prompt, false}, |
| {req_direct, no_allowlist, direct, ep, no_prompt, false}, |
| |
| // Requesting a DPK attestation results in a prompt, same as requesting |
| // a normal attestation, even if no attestation results. |
| {req_indirect, no_allowlist, none, no_ep, prompt_yes, true}, |
| {req_direct, no_allowlist, none, no_ep, prompt_yes, true}, |
| |
| // A failed ep=1 attestation results in a standard prompt. |
| {req_enterprise, no_allowlist, none, no_ep, prompt_yes, true}, |
| // ... rejecting that prompt is ok if there's no attestation. |
| {req_enterprise, no_allowlist, none, no_ep, prompt_no, true}, |
| // A successful ep=1 attestation results in a special prompt. |
| {req_enterprise, no_allowlist, direct, ep, prompt_ep_yes, true}, |
| // ... rejecting that prompt results in the DPK being stripped. |
| {req_enterprise, no_allowlist, direct, ep, prompt_ep_no, true}, |
| |
| // An ep=1 request should either return an enterprise attestation or |
| // nothing. So a "direct" response is invalid and should fail. |
| {req_enterprise, no_allowlist, direct, no_ep, no_prompt, false}, |
| |
| // RP IDs listed in policy will cause prompts, but they'll be immediately |
| // resolved because of the policy allowlisting. |
| {req_indirect, allowlisted, none, no_ep, prompt_yes, true}, |
| {req_direct, allowlisted, none, no_ep, prompt_yes, true}, |
| {req_enterprise, allowlisted, none, no_ep, prompt_yes, true}, |
| {req_enterprise, allowlisted, direct, no_ep, prompt_yes, true}, |
| {req_enterprise, allowlisted, direct, ep, prompt_ep_yes, true}, |
| // clang-format on |
| }; |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| unsigned test_num = 0; |
| for (const auto& test : kTests) { |
| SCOPED_TRACE(::testing::Message() << "ok=" << test.ok); |
| SCOPED_TRACE(::testing::Message() |
| << "prompt=" << AttestationConsentToString(test.prompt)); |
| SCOPED_TRACE(::testing::Message() << "enterprise_attestation_returned=" |
| << test.enterprise_attestation_returned); |
| SCOPED_TRACE(::testing::Message() |
| << "has_attestation=" << test.has_attestation); |
| SCOPED_TRACE(::testing::Message() |
| << "is_permitted_by_policy=" << test.is_permitted_by_policy); |
| SCOPED_TRACE( |
| ::testing::Message() |
| << "requested_attestation=" |
| << AttestationConveyancePreferenceToString(test.requested_attestation)); |
| SCOPED_TRACE(::testing::Message() << "kTests[" << test_num++ << "]"); |
| |
| CHECK(!test.enterprise_attestation_returned || test.has_attestation); |
| |
| device::VirtualCtap2Device::Config config; |
| config.device_public_key_support = true; |
| config.backup_eligible = true; |
| config.device_public_key_always_return_attestation = test.has_attestation; |
| config.device_public_key_always_return_enterprise_attestation = |
| test.enterprise_attestation_returned; |
| // None attestation is needed because, otherwise, zeroing the AAGUID |
| // invalidates the DPK signature. |
| config.none_attestation = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->permit_individual_attestation = test.is_permitted_by_policy; |
| test_client_.attestation_consent = test.prompt; |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->device_public_key = blink::mojom::DevicePublicKeyRequest::New(); |
| options->device_public_key->attestation = test.requested_attestation; |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| |
| EXPECT_EQ(result.status == AuthenticatorStatus::SUCCESS, test.ok); |
| } |
| } |
| |
| TEST_F(AuthenticatorDevicePublicKeyTest, |
| DevicePublicKeyGetAssertionAttestation) { |
| constexpr device::AttestationConveyancePreference req_none = |
| device::AttestationConveyancePreference::kNone; |
| constexpr device::AttestationConveyancePreference req_indirect = |
| device::AttestationConveyancePreference::kIndirect; |
| constexpr device::AttestationConveyancePreference req_direct = |
| device::AttestationConveyancePreference::kDirect; |
| constexpr device::AttestationConveyancePreference req_enterprise = device:: |
| AttestationConveyancePreference::kEnterpriseIfRPListedOnAuthenticator; |
| constexpr bool no_allowlist = false; |
| constexpr bool allowlisted = true; |
| constexpr bool none = false; |
| constexpr bool direct = true; |
| constexpr bool ep = true; |
| constexpr bool no_ep = false; |
| |
| constexpr struct { |
| device::AttestationConveyancePreference requested_attestation; |
| bool is_permitted_by_policy; |
| bool has_attestation; |
| bool enterprise_attestation_returned; |
| bool ok; |
| } kTests[] = { |
| // clang-format off |
| // |Requested | In policy? | Att | Ent? | OK? | |
| // ---------------------------------------------------------------- |
| |
| // No attestation requested and none provided is the simple case. |
| {req_none, no_allowlist, none, no_ep, true}, |
| |
| // If the authenticator doesn't provide any attestation then anything |
| // works. |
| {req_indirect, no_allowlist, none, no_ep, true}, |
| {req_direct, no_allowlist, none, no_ep, true}, |
| {req_enterprise, no_allowlist, none, no_ep, true}, |
| {req_enterprise, allowlisted, none, no_ep, true}, |
| |
| // If the authenticator provides attestation in unsupported cases then |
| // the request should fail. |
| {req_none, no_allowlist, direct, no_ep, false}, |
| {req_indirect, no_allowlist, direct, no_ep, false}, |
| {req_direct, no_allowlist, direct, no_ep, false}, |
| {req_enterprise, no_allowlist, direct, no_ep, false}, |
| {req_enterprise, no_allowlist, direct, ep, false}, |
| |
| // The only supported case for DPK attestation during getAssertion is |
| // via policy, when all requests should work. |
| {req_indirect, allowlisted, direct, no_ep, true}, |
| {req_direct, allowlisted, direct, no_ep, true}, |
| {req_enterprise, allowlisted, direct, no_ep, true}, |
| {req_enterprise, allowlisted, direct, ep, true}, |
| |
| // But no ep responses to non-ep requests are allowed. |
| {req_indirect, allowlisted, direct, ep, false}, |
| {req_direct, allowlisted, direct, ep, false}, |
| |
| // clang-format on |
| }; |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| bool credential_injected = false; |
| unsigned test_num = 0; |
| for (const auto& test : kTests) { |
| SCOPED_TRACE(::testing::Message() << "ok=" << test.ok); |
| SCOPED_TRACE(::testing::Message() << "enterprise_attestation_returned=" |
| << test.enterprise_attestation_returned); |
| SCOPED_TRACE(::testing::Message() |
| << "has_attestation=" << test.has_attestation); |
| SCOPED_TRACE(::testing::Message() |
| << "is_permitted_by_policy=" << test.is_permitted_by_policy); |
| SCOPED_TRACE( |
| ::testing::Message() |
| << "requested_attestation=" |
| << AttestationConveyancePreferenceToString(test.requested_attestation)); |
| SCOPED_TRACE(::testing::Message() << "kTests[" << test_num++ << "]"); |
| |
| CHECK(!test.enterprise_attestation_returned || test.has_attestation); |
| |
| device::VirtualCtap2Device::Config config; |
| config.device_public_key_support = true; |
| config.backup_eligible = true; |
| config.device_public_key_always_return_attestation = test.has_attestation; |
| config.device_public_key_always_return_enterprise_attestation = |
| test.enterprise_attestation_returned; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->permit_individual_attestation = test.is_permitted_by_policy; |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| if (!credential_injected) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| credential_injected = true; |
| } |
| options->device_public_key = blink::mojom::DevicePublicKeyRequest::New(); |
| options->device_public_key->attestation = test.requested_attestation; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| |
| EXPECT_EQ(result.status == AuthenticatorStatus::SUCCESS, test.ok); |
| } |
| } |
| |
| static constexpr char kTestPIN[] = "1234"; |
| static constexpr char16_t kTestPIN16[] = u"1234"; |
| |
| class UVTestAuthenticatorClientDelegate |
| : public AuthenticatorRequestClientDelegate { |
| public: |
| explicit UVTestAuthenticatorClientDelegate(bool* collected_pin, |
| uint32_t* min_pin_length, |
| bool* did_bio_enrollment, |
| bool cancel_bio_enrollment) |
| : collected_pin_(collected_pin), |
| min_pin_length_(min_pin_length), |
| did_bio_enrollment_(did_bio_enrollment), |
| cancel_bio_enrollment_(cancel_bio_enrollment) { |
| *collected_pin_ = false; |
| *did_bio_enrollment_ = false; |
| } |
| |
| bool SupportsPIN() const override { return true; } |
| |
| void CollectPIN( |
| CollectPINOptions options, |
| base::OnceCallback<void(std::u16string)> provide_pin_cb) override { |
| *collected_pin_ = true; |
| *min_pin_length_ = options.min_pin_length; |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(provide_pin_cb), kTestPIN16)); |
| } |
| |
| void StartBioEnrollment(base::OnceClosure next_callback) override { |
| *did_bio_enrollment_ = true; |
| if (cancel_bio_enrollment_) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(next_callback)); |
| return; |
| } |
| bio_callback_ = std::move(next_callback); |
| } |
| |
| void OnSampleCollected(int remaining_samples) override { |
| if (remaining_samples <= 0) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(bio_callback_)); |
| } |
| } |
| |
| void FinishCollectToken() override {} |
| |
| private: |
| raw_ptr<bool> collected_pin_; |
| raw_ptr<uint32_t> min_pin_length_; |
| base::OnceClosure bio_callback_; |
| raw_ptr<bool> did_bio_enrollment_; |
| bool cancel_bio_enrollment_; |
| }; |
| |
| class UVTestAuthenticatorContentBrowserClient : public ContentBrowserClient { |
| public: |
| // ContentBrowserClient: |
| WebAuthenticationDelegate* GetWebAuthenticationDelegate() override { |
| return &web_authentication_delegate; |
| } |
| |
| std::unique_ptr<AuthenticatorRequestClientDelegate> |
| GetWebAuthenticationRequestDelegate( |
| RenderFrameHost* render_frame_host) override { |
| return std::make_unique<UVTestAuthenticatorClientDelegate>( |
| &collected_pin, &min_pin_length, &did_bio_enrollment, |
| cancel_bio_enrollment); |
| } |
| |
| TestWebAuthenticationDelegate web_authentication_delegate; |
| |
| bool collected_pin; |
| uint32_t min_pin_length = 0; |
| bool did_bio_enrollment; |
| bool cancel_bio_enrollment = false; |
| }; |
| |
| class UVAuthenticatorImplTest : public AuthenticatorImplTest { |
| public: |
| UVAuthenticatorImplTest() = default; |
| |
| UVAuthenticatorImplTest(const UVAuthenticatorImplTest&) = delete; |
| UVAuthenticatorImplTest& operator=(const UVAuthenticatorImplTest&) = delete; |
| |
| void SetUp() override { |
| AuthenticatorImplTest::SetUp(); |
| old_client_ = SetBrowserClientForTesting(&test_client_); |
| } |
| |
| void TearDown() override { |
| SetBrowserClientForTesting(old_client_); |
| AuthenticatorImplTest::TearDown(); |
| } |
| |
| protected: |
| static PublicKeyCredentialCreationOptionsPtr make_credential_options( |
| device::UserVerificationRequirement uv = |
| device::UserVerificationRequirement::kRequired, |
| bool exclude_credentials = false, |
| bool appid_exclude = false) { |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| if (exclude_credentials) { |
| options->exclude_credentials = GetTestCredentials(/*num_credentials=*/1); |
| } |
| if (appid_exclude) { |
| CHECK(exclude_credentials); |
| options->appid_exclude = kTestOrigin1; |
| } |
| options->authenticator_selection->user_verification_requirement = uv; |
| return options; |
| } |
| |
| static PublicKeyCredentialRequestOptionsPtr get_credential_options( |
| device::UserVerificationRequirement uv = |
| device::UserVerificationRequirement::kRequired) { |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->user_verification = uv; |
| return options; |
| } |
| |
| static const char* UVToString(device::UserVerificationRequirement uv) { |
| switch (uv) { |
| case device::UserVerificationRequirement::kDiscouraged: |
| return "discouraged"; |
| case device::UserVerificationRequirement::kPreferred: |
| return "preferred"; |
| case device::UserVerificationRequirement::kRequired: |
| return "required"; |
| } |
| } |
| |
| UVTestAuthenticatorContentBrowserClient test_client_; |
| |
| private: |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| }; |
| |
| using PINReason = device::pin::PINEntryReason; |
| using PINError = device::pin::PINEntryError; |
| |
| // PINExpectation represent expected |mode|, |attempts|, |min_pin_length| and |
| // the PIN to answer with. |
| struct PINExpectation { |
| PINReason reason; |
| std::u16string pin; |
| int attempts; |
| uint32_t min_pin_length = device::kMinPinLength; |
| PINError error = PINError::kNoError; |
| }; |
| |
| class PINTestAuthenticatorRequestDelegate |
| : public AuthenticatorRequestClientDelegate { |
| public: |
| PINTestAuthenticatorRequestDelegate( |
| bool supports_pin, |
| const std::list<PINExpectation>& pins, |
| absl::optional<InterestingFailureReason>* failure_reason) |
| : supports_pin_(supports_pin), |
| expected_(pins), |
| failure_reason_(failure_reason) {} |
| |
| PINTestAuthenticatorRequestDelegate( |
| const PINTestAuthenticatorRequestDelegate&) = delete; |
| PINTestAuthenticatorRequestDelegate& operator=( |
| const PINTestAuthenticatorRequestDelegate&) = delete; |
| |
| ~PINTestAuthenticatorRequestDelegate() override { |
| DCHECK(expected_.empty()) |
| << expected_.size() << " unsatisifed PIN expectations"; |
| } |
| |
| bool SupportsPIN() const override { return supports_pin_; } |
| |
| void CollectPIN( |
| CollectPINOptions options, |
| base::OnceCallback<void(std::u16string)> provide_pin_cb) override { |
| DCHECK(supports_pin_); |
| DCHECK(!expected_.empty()) << "unexpected PIN request"; |
| if (expected_.front().reason == PINReason::kChallenge) { |
| DCHECK(options.attempts == expected_.front().attempts) |
| << "got: " << options.attempts |
| << " expected: " << expected_.front().attempts; |
| } |
| DCHECK_EQ(expected_.front().min_pin_length, options.min_pin_length); |
| DCHECK_EQ(expected_.front().reason, options.reason); |
| DCHECK_EQ(expected_.front().error, options.error); |
| std::u16string pin = std::move(expected_.front().pin); |
| expected_.pop_front(); |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(provide_pin_cb), std::move(pin))); |
| } |
| |
| void FinishCollectToken() override {} |
| |
| bool DoesBlockRequestOnFailure(InterestingFailureReason reason) override { |
| *failure_reason_ = reason; |
| return AuthenticatorRequestClientDelegate::DoesBlockRequestOnFailure( |
| reason); |
| } |
| |
| private: |
| const bool supports_pin_; |
| std::list<PINExpectation> expected_; |
| const raw_ptr<absl::optional<InterestingFailureReason>> failure_reason_; |
| }; |
| |
| class PINTestAuthenticatorContentBrowserClient : public ContentBrowserClient { |
| public: |
| // ContentBrowserClient: |
| WebAuthenticationDelegate* GetWebAuthenticationDelegate() override { |
| return &web_authentication_delegate; |
| } |
| |
| std::unique_ptr<AuthenticatorRequestClientDelegate> |
| GetWebAuthenticationRequestDelegate( |
| RenderFrameHost* render_frame_host) override { |
| return std::make_unique<PINTestAuthenticatorRequestDelegate>( |
| supports_pin, expected, &failure_reason); |
| } |
| |
| TestWebAuthenticationDelegate web_authentication_delegate; |
| |
| bool supports_pin = true; |
| std::list<PINExpectation> expected; |
| absl::optional<InterestingFailureReason> failure_reason; |
| }; |
| |
| class PINAuthenticatorImplTest : public UVAuthenticatorImplTest { |
| public: |
| PINAuthenticatorImplTest() = default; |
| |
| PINAuthenticatorImplTest(const PINAuthenticatorImplTest&) = delete; |
| PINAuthenticatorImplTest& operator=(const PINAuthenticatorImplTest&) = delete; |
| |
| void SetUp() override { |
| UVAuthenticatorImplTest::SetUp(); |
| old_client_ = SetBrowserClientForTesting(&test_client_); |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| } |
| |
| void TearDown() override { |
| SetBrowserClientForTesting(old_client_); |
| UVAuthenticatorImplTest::TearDown(); |
| } |
| |
| protected: |
| PINTestAuthenticatorContentBrowserClient test_client_; |
| |
| // An enumerate of outcomes for PIN tests. |
| enum { |
| kFailure, |
| kNoPIN, |
| kSetPIN, |
| kUsePIN, |
| }; |
| |
| void ConfigureVirtualDevice(device::PINUVAuthProtocol pin_protocol, |
| bool pin_uv_auth_token, |
| int support_level) { |
| device::VirtualCtap2Device::Config config; |
| config.pin_protocol = pin_protocol; |
| config.pin_uv_auth_token_support = pin_uv_auth_token; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_0, |
| device::Ctap2Version::kCtap2_1}; |
| switch (support_level) { |
| case 0: |
| // No support. |
| config.pin_support = false; |
| virtual_device_factory_->mutable_state()->pin = ""; |
| virtual_device_factory_->mutable_state()->pin_retries = 0; |
| break; |
| |
| case 1: |
| // PIN supported, but no PIN set. |
| config.pin_support = true; |
| virtual_device_factory_->mutable_state()->pin = ""; |
| virtual_device_factory_->mutable_state()->pin_retries = 0; |
| break; |
| |
| case 2: |
| // PIN set. |
| config.pin_support = true; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| |
| virtual_device_factory_->SetCtap2Config(config); |
| } |
| |
| private: |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| }; |
| |
| static constexpr device::UserVerificationRequirement kUVLevel[3] = { |
| device::UserVerificationRequirement::kDiscouraged, |
| device::UserVerificationRequirement::kPreferred, |
| device::UserVerificationRequirement::kRequired, |
| }; |
| |
| static const char* kUVDescription[3] = {"discouraged", "preferred", "required"}; |
| |
| static const char* kPINSupportDescription[3] = {"no PIN support", "PIN not set", |
| "PIN set"}; |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredential) { |
| typedef int Expectations[3][3]; |
| // kExpectedWithUISupport enumerates the expected behaviour when the embedder |
| // supports prompting the user for a PIN. |
| // clang-format off |
| const Expectations kExpectedWithUISupport = { |
| // discouraged | preferred | required |
| /* No support */ { kNoPIN, kNoPIN, kFailure }, |
| /* PIN not set */ { kNoPIN, kNoPIN, kSetPIN }, |
| /* PIN set */ { kUsePIN, kUsePIN, kUsePIN }, |
| // ^ |
| // | |
| // VirtualCtap2Device cannot fall back to U2F. |
| }; |
| // clang-format on |
| |
| // kExpectedWithoutUISupport enumerates the expected behaviour when the |
| // embedder cannot prompt the user for a PIN. |
| // clang-format off |
| const Expectations kExpectedWithoutUISupport = { |
| // discouraged | preferred | required |
| /* No support */ { kNoPIN, kNoPIN, kFailure }, |
| /* PIN not set */ { kNoPIN, kNoPIN, kFailure }, |
| /* PIN set */ { kFailure, kFailure, kFailure }, |
| // ^ ^ |
| // | | |
| // VirtualCtap2Device cannot fall back to U2F and |
| // a PIN is required to create credentials once set |
| // in CTAP 2.0. |
| }; |
| // clang-format on |
| |
| for (bool pin_uv_auth_token : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "pin_uv_auth_token=" << pin_uv_auth_token); |
| for (bool ui_support : {false, true}) { |
| SCOPED_TRACE(::testing::Message() << "ui_support=" << ui_support); |
| const Expectations& expected = |
| ui_support ? kExpectedWithUISupport : kExpectedWithoutUISupport; |
| test_client_.supports_pin = ui_support; |
| |
| for (int support_level = 0; support_level <= 2; support_level++) { |
| for (const auto pin_protocol : |
| {device::PINUVAuthProtocol::kV1, device::PINUVAuthProtocol::kV2}) { |
| SCOPED_TRACE(testing::Message() |
| << "support_level=" |
| << kPINSupportDescription[support_level] |
| << ", pin_protocol=" << static_cast<int>(pin_protocol)); |
| for (const bool excluded_credentials : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "excluded_credentials=" << excluded_credentials); |
| for (const bool appid_exclude : {false, true}) { |
| if (appid_exclude && !excluded_credentials) { |
| continue; |
| } |
| SCOPED_TRACE(::testing::Message() |
| << "appid_exclude=" << appid_exclude); |
| |
| for (const bool always_uv : {false, true}) { |
| if (always_uv && |
| (!ui_support || |
| virtual_device_factory_->mutable_state()->pin.empty())) { |
| continue; |
| } |
| SCOPED_TRACE(::testing::Message() << "always_uv=" << always_uv); |
| |
| ConfigureVirtualDevice(pin_protocol, pin_uv_auth_token, |
| support_level); |
| |
| for (int uv_level = 0; uv_level <= 2; uv_level++) { |
| SCOPED_TRACE(kUVDescription[uv_level]); |
| |
| switch (expected[support_level][uv_level]) { |
| case kNoPIN: |
| case kFailure: |
| // There shouldn't be any PIN prompts. |
| test_client_.expected.clear(); |
| break; |
| |
| case kSetPIN: |
| // A single PIN prompt to set a PIN is expected. |
| test_client_.expected = {{PINReason::kSet, kTestPIN16}}; |
| break; |
| |
| case kUsePIN: |
| // A single PIN prompt to get the PIN is expected. |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, 8}}; |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(make_credential_options( |
| kUVLevel[uv_level], excluded_credentials, |
| appid_exclude)); |
| |
| switch (expected[support_level][uv_level]) { |
| case kFailure: |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, |
| result.status); |
| break; |
| |
| case kNoPIN: |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_EQ("", |
| virtual_device_factory_->mutable_state()->pin); |
| EXPECT_FALSE(HasUV(result.response)); |
| break; |
| |
| case kSetPIN: |
| case kUsePIN: |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_EQ(kTestPIN, |
| virtual_device_factory_->mutable_state()->pin); |
| EXPECT_TRUE(HasUV(result.response)); |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredentialSoftLock) { |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| |
| test_client_.expected = {{PINReason::kChallenge, u"wrong", 8}, |
| {PINReason::kChallenge, u"wrong", 7, |
| device::kMinPinLength, PINError::kWrongPIN}, |
| {PINReason::kChallenge, u"wrong", 6, |
| device::kMinPinLength, PINError::kWrongPIN}}; |
| EXPECT_EQ(AuthenticatorMakeCredential(make_credential_options()).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(5, virtual_device_factory_->mutable_state()->pin_retries); |
| EXPECT_TRUE(virtual_device_factory_->mutable_state()->soft_locked); |
| ASSERT_TRUE(test_client_.failure_reason.has_value()); |
| EXPECT_EQ(InterestingFailureReason::kSoftPINBlock, |
| *test_client_.failure_reason); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredentialHardLock) { |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = 1; |
| |
| test_client_.expected = {{PINReason::kChallenge, u"wrong", 1}}; |
| EXPECT_EQ(AuthenticatorMakeCredential().status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(0, virtual_device_factory_->mutable_state()->pin_retries); |
| ASSERT_TRUE(test_client_.failure_reason.has_value()); |
| EXPECT_EQ(InterestingFailureReason::kHardPINBlock, |
| *test_client_.failure_reason); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredentialWrongPINFirst) { |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| |
| // Test that we can successfully get a PIN token after a failure. |
| test_client_.expected = {{PINReason::kChallenge, u"wrong", 8}, |
| {PINReason::kChallenge, kTestPIN16, 7, |
| device::kMinPinLength, PINError::kWrongPIN}}; |
| EXPECT_EQ(AuthenticatorMakeCredential().status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(static_cast<int>(device::kMaxPinRetries), |
| virtual_device_factory_->mutable_state()->pin_retries); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredentialSkipPINTouch) { |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| int taps = 0; |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting([&](device::VirtualFidoDevice* device) { |
| ++taps; |
| return true; |
| }); |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, device::kMaxPinRetries}}; |
| EXPECT_EQ(AuthenticatorMakeCredential().status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(taps, 1); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredentialDontSkipPINTouch) { |
| // Create two devices. Both are candidates but only the second one will |
| // respond to touches. |
| auto discovery = |
| std::make_unique<device::test::MultipleVirtualFidoDeviceFactory>(); |
| device::test::MultipleVirtualFidoDeviceFactory::DeviceDetails device_1; |
| device_1.config.pin_support = true; |
| device_1.state->simulate_press_callback = |
| base::BindRepeating([](VirtualFidoDevice* ignore) { return false; }); |
| discovery->AddDevice(std::move(device_1)); |
| |
| int taps = 0; |
| device::test::MultipleVirtualFidoDeviceFactory::DeviceDetails device_2; |
| device_2.state->pin = kTestPIN; |
| device_2.config.pin_support = true; |
| device_2.state->simulate_press_callback = |
| base::BindLambdaForTesting([&](VirtualFidoDevice* ignore) { |
| ++taps; |
| return true; |
| }); |
| discovery->AddDevice(std::move(device_2)); |
| |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting(std::move(discovery)); |
| |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, device::kMaxPinRetries}}; |
| EXPECT_EQ(AuthenticatorMakeCredential().status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(taps, 2); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredentialAlwaysUv) { |
| // Test that if an authenticator is reporting alwaysUv = 1, UV is attempted |
| // even if the user verification requirement is discouraged. |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.always_uv = true; |
| |
| // Enable u2f support. Normally, this would allow chrome to create a |
| // credential without internal user verification, but we should not attempt |
| // that with the alwaysUv flag on. |
| config.u2f_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, device::kMaxPinRetries}}; |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(make_credential_options( |
| device::UserVerificationRequirement::kDiscouraged)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_TRUE(HasUV(result.response)); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredentialMinPINLengthNewPIN) { |
| // Test that an authenticator advertising a min PIN length other than the |
| // default makes it all the way to CollectPIN when setting a new PIN. |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.min_pin_length_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->min_pin_length = 6; |
| test_client_.expected = {{PINReason::kSet, u"123456", 0, 6}}; |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(make_credential_options()); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredentialMinPINLengthExistingPIN) { |
| // Test that an authenticator advertising a min PIN length other than the |
| // default makes it all the way to CollectPIN when using an existing PIN and |
| // the forcePINChange flag is false. |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.min_pin_length_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->min_pin_length = 6; |
| virtual_device_factory_->mutable_state()->pin = "123456"; |
| test_client_.expected = { |
| {PINReason::kChallenge, u"123456", device::kMaxPinRetries, 6}}; |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(make_credential_options()); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredentialForcePINChange) { |
| // Test that an authenticator with the forcePINChange flag set to true updates |
| // the PIN before attempting to make a credential. When querying for an |
| // existing PIN, the default min PIN length should be asked since there is no |
| // way to know the current PIN length. |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.min_pin_length_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->force_pin_change = true; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| virtual_device_factory_->mutable_state()->min_pin_length = 6; |
| test_client_.expected = {{PINReason::kChallenge, kTestPIN16, |
| device::kMaxPinRetries, device::kMinPinLength}, |
| {PINReason::kChange, u"567890", 0, 6}}; |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(make_credential_options()); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ("567890", virtual_device_factory_->mutable_state()->pin); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredUvNotRqd) { |
| // Test that on an authenticator with the makeCredUvNotRqd option enabled, |
| // non-discoverable credentials can be created without requiring a PIN. |
| for (bool discoverable : {false, true}) { |
| for (bool request_uv : {false, true}) { |
| SCOPED_TRACE(testing::Message() << "discoverable=" << discoverable |
| << " request_uv=" << request_uv); |
| |
| test_client_.web_authentication_delegate.supports_resident_keys = true; |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| config.u2f_support = true; |
| config.pin_support = true; |
| config.resident_key_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.allow_non_resident_credential_creation_without_uv = true; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| // PIN is still required for discoverable credentials, or if the caller |
| // requests it. |
| if (discoverable || request_uv) { |
| test_client_.expected = {{PINReason::kChallenge, kTestPIN16, |
| device::kMaxPinRetries, |
| device::kMinPinLength}}; |
| } else { |
| test_client_.expected = {}; |
| } |
| |
| PublicKeyCredentialCreationOptionsPtr request = make_credential_options(); |
| request->authenticator_selection->user_verification_requirement = |
| request_uv ? device::UserVerificationRequirement::kPreferred |
| : device::UserVerificationRequirement::kDiscouraged; |
| request->authenticator_selection->resident_key = |
| discoverable ? device::ResidentKeyRequirement::kPreferred |
| : device::ResidentKeyRequirement::kDiscouraged; |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(request)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| // Requests shouldn't fall back to creating U2F credentials. |
| EXPECT_FALSE(virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second.is_u2f); |
| } |
| } |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredUvNotRqdAndAlwaysUv) { |
| // makeCredUvNotRqd and alwaysUv can be combined even though they contradict |
| // each other. In that case, makeCredUvNotRqd should be ignored and PIN/UV |
| // should be collected before creating non-discoverable credentials. If PIN/UV |
| // isn't configured, that should be taken care of first. |
| for (bool pin_set : {false, true}) { |
| SCOPED_TRACE(testing::Message() << "pin_set=" << pin_set); |
| |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.always_uv = true; |
| config.allow_non_resident_credential_creation_without_uv = true; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| virtual_device_factory_->SetCtap2Config(config); |
| if (pin_set) { |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| test_client_.expected = {{PINReason::kChallenge, kTestPIN16, |
| device::kMaxPinRetries, device::kMinPinLength}}; |
| } else { |
| test_client_.expected = {{PINReason::kSet, kTestPIN16, |
| device::kMaxPinRetries, device::kMinPinLength}}; |
| } |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(make_credential_options()); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| } |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredentialHMACSecret) { |
| // uv=preferred is more preferred when hmac-secret is in use so that the |
| // PRF is consistent. (Security keys have two PRFs per credential: one for |
| // UV and one for non-UV assertions.) |
| struct TestCase { |
| device::UserVerificationRequirement uv; |
| bool hmac_secret; |
| bool should_configure_uv; |
| }; |
| |
| constexpr TestCase kTests[] = { |
| {device::UserVerificationRequirement::kDiscouraged, false, false}, |
| {device::UserVerificationRequirement::kPreferred, false, false}, |
| {device::UserVerificationRequirement::kRequired, false, true}, |
| {device::UserVerificationRequirement::kDiscouraged, true, true}, |
| {device::UserVerificationRequirement::kPreferred, true, true}, |
| {device::UserVerificationRequirement::kRequired, true, true}, |
| }; |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| unsigned index = 0; |
| for (const TestCase& test : kTests) { |
| SCOPED_TRACE(index++); |
| |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| config.hmac_secret_support = true; |
| config.pin_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.allow_non_resident_credential_creation_without_uv = true; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| if (test.should_configure_uv) { |
| test_client_.expected = {{PINReason::kSet, kTestPIN16, |
| device::kMaxPinRetries, device::kMinPinLength}}; |
| } else { |
| test_client_.expected.clear(); |
| } |
| |
| auto options = make_credential_options(test.uv); |
| options->hmac_create_secret = test.hmac_secret; |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| } |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, GetAssertion) { |
| typedef int Expectations[3][3]; |
| // kExpectedWithUISupport enumerates the expected behaviour when the embedder |
| // supports prompting the user for a PIN. |
| // clang-format off |
| const Expectations kExpectedWithUISupport = { |
| // discouraged | preferred | required |
| /* No support */ { kNoPIN, kNoPIN, kFailure }, |
| /* PIN not set */ { kNoPIN, kNoPIN, kFailure }, |
| /* PIN set */ { kNoPIN, kUsePIN, kUsePIN }, |
| }; |
| // clang-format on |
| |
| // kExpectedWithoutUISupport enumerates the expected behaviour when the |
| // embedder cannot prompt the user for a PIN. |
| // clang-format off |
| const Expectations kExpectedWithoutUISupport = { |
| // discouraged | preferred | required |
| /* No support */ { kNoPIN, kNoPIN, kFailure }, |
| /* PIN not set */ { kNoPIN, kNoPIN, kFailure }, |
| /* PIN set */ { kNoPIN, kNoPIN, kFailure }, |
| }; |
| // clang-format on |
| |
| PublicKeyCredentialRequestOptionsPtr dummy_options = get_credential_options(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| dummy_options->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| for (bool pin_uv_auth_token : {false, true}) { |
| for (bool ui_support : {false, true}) { |
| SCOPED_TRACE(::testing::Message() << "ui_support=" << ui_support); |
| const Expectations& expected = |
| ui_support ? kExpectedWithUISupport : kExpectedWithoutUISupport; |
| test_client_.supports_pin = ui_support; |
| |
| for (int support_level = 0; support_level <= 2; support_level++) { |
| SCOPED_TRACE(kPINSupportDescription[support_level]); |
| for (const auto pin_protocol : |
| {device::PINUVAuthProtocol::kV1, device::PINUVAuthProtocol::kV2}) { |
| SCOPED_TRACE(testing::Message() |
| << "support_level=" |
| << kPINSupportDescription[support_level] |
| << ", pin_protocol=" << static_cast<int>(pin_protocol)); |
| ConfigureVirtualDevice(pin_protocol, pin_uv_auth_token, |
| support_level); |
| |
| for (int uv_level = 0; uv_level <= 2; uv_level++) { |
| SCOPED_TRACE(kUVDescription[uv_level]); |
| |
| switch (expected[support_level][uv_level]) { |
| case kNoPIN: |
| case kFailure: |
| // No PIN prompts are expected. |
| test_client_.expected.clear(); |
| break; |
| |
| case kUsePIN: |
| // A single prompt to get the PIN is expected. |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, 8}}; |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| |
| GetAssertionResult result = AuthenticatorGetAssertion( |
| get_credential_options(kUVLevel[uv_level])); |
| |
| switch (expected[support_level][uv_level]) { |
| case kFailure: |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, |
| result.status); |
| break; |
| |
| case kNoPIN: |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_FALSE(HasUV(result.response)); |
| break; |
| |
| case kUsePIN: |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_EQ(kTestPIN, |
| virtual_device_factory_->mutable_state()->pin); |
| EXPECT_TRUE(HasUV(result.response)); |
| break; |
| |
| default: |
| NOTREACHED(); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, GetAssertionSoftLock) { |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| test_client_.expected = {{PINReason::kChallenge, u"wrong", 8}, |
| {PINReason::kChallenge, u"wrong", 7, |
| device::kMinPinLength, PINError::kWrongPIN}, |
| {PINReason::kChallenge, u"wrong", 6, |
| device::kMinPinLength, PINError::kWrongPIN}}; |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(5, virtual_device_factory_->mutable_state()->pin_retries); |
| EXPECT_TRUE(virtual_device_factory_->mutable_state()->soft_locked); |
| ASSERT_TRUE(test_client_.failure_reason.has_value()); |
| EXPECT_EQ(InterestingFailureReason::kSoftPINBlock, |
| *test_client_.failure_reason); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, GetAssertionHardLock) { |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = 1; |
| |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| test_client_.expected = {{PINReason::kChallenge, u"wrong", 1}}; |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(0, virtual_device_factory_->mutable_state()->pin_retries); |
| ASSERT_TRUE(test_client_.failure_reason.has_value()); |
| EXPECT_EQ(InterestingFailureReason::kHardPINBlock, |
| *test_client_.failure_reason); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, GetAssertionSkipPINTouch) { |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| int taps = 0; |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting([&](device::VirtualFidoDevice* device) { |
| ++taps; |
| return true; |
| }); |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, device::kMaxPinRetries}}; |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(taps, 1); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, GetAssertionDontSkipPINTouch) { |
| // Create two devices. Both are candidates but only the second one will |
| // respond to touches. |
| auto discovery = |
| std::make_unique<device::test::MultipleVirtualFidoDeviceFactory>(); |
| device::test::MultipleVirtualFidoDeviceFactory::DeviceDetails device_1; |
| device_1.config.pin_support = true; |
| device_1.state->simulate_press_callback = |
| base::BindRepeating([](VirtualFidoDevice* ignore) { return false; }); |
| discovery->AddDevice(std::move(device_1)); |
| |
| int taps = 0; |
| device::test::MultipleVirtualFidoDeviceFactory::DeviceDetails device_2; |
| device_2.state->pin = kTestPIN; |
| device_2.config.pin_support = true; |
| device_2.state->simulate_press_callback = |
| base::BindLambdaForTesting([&](VirtualFidoDevice* ignore) { |
| ++taps; |
| return true; |
| }); |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| ASSERT_TRUE(device_2.state->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| discovery->AddDevice(std::move(device_2)); |
| |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting(std::move(discovery)); |
| |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, device::kMaxPinRetries}}; |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(taps, 2); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, GetAssertionAlwaysUv) { |
| // Test that if an authenticator is reporting alwaysUv = 1, UV is attempted |
| // even if the user verification requirement is discouraged. |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.always_uv = true; |
| config.u2f_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| PublicKeyCredentialRequestOptionsPtr options = |
| get_credential_options(device::UserVerificationRequirement::kDiscouraged); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, kTestRelyingPartyId)); |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, device::kMaxPinRetries}}; |
| |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_TRUE(HasUV(result.response)); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, MakeCredentialNoSupportedAlgorithm) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (int i = 0; i < 3; i++) { |
| SCOPED_TRACE(i); |
| |
| test_client_.expected.clear(); |
| bool expected_to_succeed = false; |
| if (i == 0) { |
| device::VirtualCtap2Device::Config config; |
| // The first config is a CTAP2 device that doesn't support the |
| // kInvalidForTesting algorithm. A dummy touch should be requested in this |
| // case. |
| virtual_device_factory_->SetCtap2Config(config); |
| } else if (i == 1) { |
| device::VirtualCtap2Device::Config config; |
| // The second config is a device with a PIN set that _does_ support the |
| // algorithm. Since the PIN is set, we might convert the makeCredential |
| // request to U2F, but shouldn't because the algorithm cannot be |
| // represented in U2F. |
| config.u2f_support = true; |
| config.pin_support = true; |
| config.advertised_algorithms = { |
| device::CoseAlgorithmIdentifier::kInvalidForTesting}; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| virtual_device_factory_->SetCtap2Config(config); |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, device::kMaxPinRetries}}; |
| // Since converting to U2F isn't possible, this will trigger a PIN prompt |
| // and succeed because the device does actually support the algorithm. |
| expected_to_succeed = true; |
| } else if (i == 2) { |
| // The third case is a plain U2F authenticator, which implicitly only |
| // supports ES256. |
| virtual_device_factory_->SetSupportedProtocol( |
| device::ProtocolVersion::kU2f); |
| } |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| // Set uv=discouraged so that U2F fallback is possible. |
| options->authenticator_selection->user_verification_requirement = |
| device::UserVerificationRequirement::kDiscouraged; |
| options->public_key_parameters = |
| GetTestPublicKeyCredentialParameters(static_cast<int32_t>( |
| device::CoseAlgorithmIdentifier::kInvalidForTesting)); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| expected_to_succeed ? AuthenticatorStatus::SUCCESS |
| : AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, PRFCreatedOnCTAP2) { |
| // Check that credential creation requests that include the PRF extension use |
| // CTAP2 if possible. |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (int i = 0; i < 3; i++) { |
| SCOPED_TRACE(i); |
| |
| device::VirtualCtap2Device::Config config; |
| config.u2f_support = true; |
| config.pin_support = true; |
| config.hmac_secret_support = true; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| // Set uv=discouraged so that U2F fallback is possible. |
| options->authenticator_selection->user_verification_requirement = |
| device::UserVerificationRequirement::kDiscouraged; |
| |
| if (i == 0) { |
| // Sanity check: request should fallback to U2F. (If it doesn't fallback |
| // to U2F then the PIN test infrastructure will CHECK because |
| // |test_client_.expected| is empty.) |
| test_client_.expected.clear(); |
| } else if (i == 1) { |
| // If PRF is requested, the fallback to U2F should not happen because the |
| // PRF request is higher priority than avoiding a PIN prompt. (The PIN |
| // test infrastructure will CHECK if |expected| is set and not used.) |
| options->prf_enable = true; |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, device::kMaxPinRetries}}; |
| } else { |
| // If PRF is requested, but the authenticator doesn't support it, then we |
| // should still use U2F. |
| options->prf_enable = true; |
| config.hmac_secret_support = false; |
| test_client_.expected.clear(); |
| } |
| |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| } |
| |
| // Test that pinUvAuthToken gets sent with every single batch of an exclude |
| // list. If it wasn't, any batch after the first would be unable to match |
| // credProtect=uvRequired credentials. |
| TEST_F(PINAuthenticatorImplTest, ExcludeListBatchesIncludePinToken) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| // Set up a CTAP 2.1 authenticator with pinUvAuthToken and exclude list |
| // batching. |
| device::VirtualCtap2Device::Config config; |
| config.max_credential_id_length = kTestCredentialIdLength; |
| constexpr size_t kBatchSize = 10; |
| config.max_credential_count_in_list = kBatchSize; |
| config.pin_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_0, |
| device::Ctap2Version::kCtap2_1}; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, device::kMaxPinRetries}}; |
| |
| // Craft an exclude list that is large enough to trigger batched probing and |
| // includes one match for a credProtect=uvRequired credential. |
| auto test_credentials = GetTestCredentials(kBatchSize + 1); |
| |
| device::VirtualFidoDevice::RegistrationData cred_protect_credential( |
| kTestRelyingPartyId); |
| cred_protect_credential.protection = device::CredProtect::kUVRequired; |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| test_credentials.back().id, std::move(cred_protect_credential))); |
| |
| // The request should fail because the exclude list matches. |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->exclude_credentials = std::move(test_credentials); |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::CREDENTIAL_EXCLUDED); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, RemoveSecondAuthenticator) { |
| // Create two PIN-capable devices. Touch one of them to trigger a prompt for |
| // a PIN. Remove the other. Don't crash. |
| base::RepeatingCallback<void(bool)> disconnect_1, disconnect_2; |
| |
| device::test::MultipleVirtualFidoDeviceFactory::DeviceDetails device_1; |
| device_1.state->pin = kTestPIN; |
| device_1.config.pin_support = true; |
| std::tie(disconnect_1, device_1.disconnect_events) = |
| device::FidoDeviceDiscovery::EventStream<bool>::New(); |
| |
| device::test::MultipleVirtualFidoDeviceFactory::DeviceDetails device_2; |
| device_2.state->pin = kTestPIN; |
| device_2.config.pin_support = true; |
| std::tie(disconnect_2, device_2.disconnect_events) = |
| device::FidoDeviceDiscovery::EventStream<bool>::New(); |
| |
| int callbacks = 0; |
| auto touch_callback = [&](int device_num) -> bool { |
| callbacks++; |
| if (callbacks == 1) { |
| // Wait for the other authenticator to be triggered. |
| return false; |
| } else if (callbacks == 2) { |
| // Touch authenticator to collect a PIN. |
| return true; |
| } else { |
| CHECK_EQ(callbacks, 3); |
| |
| // Disconnect other authenticator then complete with a touch. |
| if (device_num == 1) { |
| disconnect_2.Run(false); |
| } else { |
| disconnect_1.Run(false); |
| } |
| return true; |
| } |
| }; |
| |
| device_1.state->simulate_press_callback = base::BindLambdaForTesting( |
| [&](VirtualFidoDevice* ignore) -> bool { return touch_callback(1); }); |
| device_2.state->simulate_press_callback = base::BindLambdaForTesting( |
| [&](VirtualFidoDevice* ignore) -> bool { return touch_callback(2); }); |
| |
| auto discovery = |
| std::make_unique<device::test::MultipleVirtualFidoDeviceFactory>(); |
| discovery->AddDevice(std::move(device_1)); |
| discovery->AddDevice(std::move(device_2)); |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting(std::move(discovery)); |
| |
| test_client_.expected = { |
| {PINReason::kChallenge, kTestPIN16, device::kMaxPinRetries}}; |
| EXPECT_EQ(AuthenticatorMakeCredential().status, AuthenticatorStatus::SUCCESS); |
| } |
| |
| TEST_F(PINAuthenticatorImplTest, AppIdExcludeExtensionWithPinRequiredError) { |
| // Some alwaysUv authenticators apply the alwaysUv logic even when up=false. |
| // That causes them to return `kCtap2ErrPinRequired` to appIdExclude probes |
| // which broke makeCredential at one point. See crbug.com/1443039. |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.always_uv = true; |
| config.always_uv_for_up_false = true; |
| config.pin_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| test_client_.expected = {{PINReason::kSet, kTestPIN16}}; |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->authenticator_selection->user_verification_requirement = |
| device::UserVerificationRequirement::kRequired; |
| options->appid_exclude = kTestOrigin1; |
| options->exclude_credentials = GetTestCredentials(); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| |
| class InternalUVAuthenticatorImplTest : public UVAuthenticatorImplTest { |
| public: |
| struct TestCase { |
| const bool fingerprints_enrolled; |
| const bool supports_pin; |
| const device::UserVerificationRequirement uv; |
| }; |
| |
| InternalUVAuthenticatorImplTest() = default; |
| |
| InternalUVAuthenticatorImplTest(const InternalUVAuthenticatorImplTest&) = |
| delete; |
| InternalUVAuthenticatorImplTest& operator=( |
| const InternalUVAuthenticatorImplTest&) = delete; |
| |
| void SetUp() override { |
| UVAuthenticatorImplTest::SetUp(); |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| } |
| |
| std::vector<TestCase> GetTestCases() { |
| std::vector<TestCase> test_cases; |
| for (const bool fingerprints_enrolled : {true, false}) { |
| for (const bool supports_pin : {true, false}) { |
| // Avoid just testing for PIN. |
| if (!fingerprints_enrolled && supports_pin) { |
| continue; |
| } |
| for (const auto uv : {device::UserVerificationRequirement::kDiscouraged, |
| device::UserVerificationRequirement::kPreferred, |
| device::UserVerificationRequirement::kRequired}) { |
| test_cases.push_back({fingerprints_enrolled, supports_pin, uv}); |
| } |
| } |
| } |
| return test_cases; |
| } |
| |
| void ConfigureDevice(const TestCase& test_case) { |
| device::VirtualCtap2Device::Config config; |
| config.internal_uv_support = true; |
| config.u2f_support = true; |
| config.pin_support = test_case.supports_pin; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = |
| test_case.fingerprints_enrolled; |
| virtual_device_factory_->SetCtap2Config(config); |
| SCOPED_TRACE(::testing::Message() << "fingerprints_enrolled=" |
| << test_case.fingerprints_enrolled); |
| SCOPED_TRACE(::testing::Message() |
| << "supports_pin=" << test_case.supports_pin); |
| SCOPED_TRACE(UVToString(test_case.uv)); |
| } |
| }; |
| |
| TEST_F(InternalUVAuthenticatorImplTest, MakeCredential) { |
| for (const auto test_case : GetTestCases()) { |
| ConfigureDevice(test_case); |
| |
| auto options = make_credential_options(test_case.uv); |
| // UV cannot be satisfied without fingerprints. |
| const bool should_timeout = |
| !test_case.fingerprints_enrolled && |
| test_case.uv == device::UserVerificationRequirement::kRequired; |
| if (should_timeout) { |
| options->timeout = base::Milliseconds(100); |
| } |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| |
| if (should_timeout) { |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, result.status); |
| } else { |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_EQ(test_case.fingerprints_enrolled, HasUV(result.response)); |
| } |
| } |
| } |
| |
| // Test falling back to PIN for devices that support internal user verification |
| // but not uv token. |
| TEST_F(InternalUVAuthenticatorImplTest, MakeCredentialFallBackToPin) { |
| device::VirtualCtap2Device::Config config; |
| config.internal_uv_support = true; |
| config.pin_support = true; |
| config.user_verification_succeeds = false; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| auto options = |
| make_credential_options(device::UserVerificationRequirement::kRequired); |
| |
| MakeCredentialResult result = AuthenticatorMakeCredential(std::move(options)); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| EXPECT_TRUE(test_client_.collected_pin); |
| EXPECT_EQ(device::kMinPinLength, test_client_.min_pin_length); |
| } |
| |
| // Test making a credential on an authenticator that supports biometric |
| // enrollment but has no fingerprints enrolled. |
| TEST_F(InternalUVAuthenticatorImplTest, MakeCredentialInlineBioEnrollment) { |
| device::VirtualCtap2Device::Config config; |
| config.internal_uv_support = true; |
| config.pin_support = true; |
| config.user_verification_succeeds = true; |
| config.bio_enrollment_support = true; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = false; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| MakeCredentialResult result = AuthenticatorMakeCredential( |
| make_credential_options(device::UserVerificationRequirement::kRequired)); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| EXPECT_TRUE(test_client_.collected_pin); |
| EXPECT_EQ(device::kMinPinLength, test_client_.min_pin_length); |
| EXPECT_TRUE(test_client_.did_bio_enrollment); |
| EXPECT_TRUE(virtual_device_factory_->mutable_state()->fingerprints_enrolled); |
| } |
| |
| // Test making a credential skipping biometric enrollment during credential |
| // creation. |
| TEST_F(InternalUVAuthenticatorImplTest, MakeCredentialSkipInlineBioEnrollment) { |
| test_client_.cancel_bio_enrollment = true; |
| |
| device::VirtualCtap2Device::Config config; |
| config.internal_uv_support = true; |
| config.pin_support = true; |
| config.user_verification_succeeds = true; |
| config.bio_enrollment_support = true; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = false; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| MakeCredentialResult result = AuthenticatorMakeCredential( |
| make_credential_options(device::UserVerificationRequirement::kRequired)); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| EXPECT_TRUE(test_client_.collected_pin); |
| EXPECT_EQ(device::kMinPinLength, test_client_.min_pin_length); |
| EXPECT_TRUE(test_client_.did_bio_enrollment); |
| EXPECT_FALSE(virtual_device_factory_->mutable_state()->fingerprints_enrolled); |
| } |
| |
| TEST_F(InternalUVAuthenticatorImplTest, MakeCredUvNotRqd) { |
| // Test that on an authenticator with the makeCredUvNotRqd option enabled, |
| // non-discoverable credentials can be created without requiring UV or a PIN. |
| for (bool discoverable : {false, true}) { |
| for (bool request_uv : {false, true}) { |
| SCOPED_TRACE(testing::Message() << "discoverable=" << discoverable |
| << " request_uv=" << request_uv); |
| |
| test_client_.web_authentication_delegate.supports_resident_keys = true; |
| ResetVirtualDevice(); |
| device::VirtualCtap2Device::Config config; |
| config.u2f_support = true; |
| config.internal_uv_support = true; |
| config.user_verification_succeeds = true; |
| config.pin_support = true; |
| config.resident_key_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.allow_non_resident_credential_creation_without_uv = true; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| |
| PublicKeyCredentialCreationOptionsPtr request = make_credential_options(); |
| request->authenticator_selection->user_verification_requirement = |
| request_uv ? device::UserVerificationRequirement::kPreferred |
| : device::UserVerificationRequirement::kDiscouraged; |
| request->authenticator_selection->resident_key = |
| discoverable ? device::ResidentKeyRequirement::kPreferred |
| : device::ResidentKeyRequirement::kDiscouraged; |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(request)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(HasUV(result.response), discoverable || request_uv); |
| EXPECT_FALSE(test_client_.collected_pin); |
| // Requests shouldn't fall back to creating U2F credentials. |
| EXPECT_FALSE(virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second.is_u2f); |
| } |
| } |
| } |
| |
| TEST_F(InternalUVAuthenticatorImplTest, GetAssertion) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| get_credential_options()->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| for (const auto test_case : GetTestCases()) { |
| ConfigureDevice(test_case); |
| // Without a fingerprint enrolled we assume that a UV=required request |
| // cannot be satisfied by an authenticator that cannot do UV. It is |
| // possible for a credential to be created without UV and then later |
| // asserted with UV=required, but that would be bizarre behaviour from |
| // an RP and we currently don't worry about it. |
| const bool should_be_unrecognized = |
| !test_case.fingerprints_enrolled && |
| test_case.uv == device::UserVerificationRequirement::kRequired; |
| |
| GetAssertionResult result = |
| AuthenticatorGetAssertion(get_credential_options(test_case.uv)); |
| |
| if (should_be_unrecognized) { |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, result.status); |
| } else { |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_EQ( |
| test_case.fingerprints_enrolled && |
| test_case.uv != device::UserVerificationRequirement::kDiscouraged, |
| HasUV(result.response)); |
| } |
| } |
| } |
| |
| // Test falling back to PIN for devices that support internal user verification |
| // but not uv token. |
| TEST_F(InternalUVAuthenticatorImplTest, GetAssertionFallbackToPIN) { |
| device::VirtualCtap2Device::Config config; |
| config.internal_uv_support = true; |
| config.pin_support = true; |
| config.user_verification_succeeds = false; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| get_credential_options()->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| GetAssertionResult result = AuthenticatorGetAssertion( |
| get_credential_options(device::UserVerificationRequirement::kRequired)); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| EXPECT_TRUE(test_client_.collected_pin); |
| EXPECT_EQ(device::kMinPinLength, test_client_.min_pin_length); |
| } |
| |
| class UVTokenAuthenticatorImplTest : public UVAuthenticatorImplTest { |
| public: |
| UVTokenAuthenticatorImplTest() = default; |
| UVTokenAuthenticatorImplTest(const UVTokenAuthenticatorImplTest&) = delete; |
| |
| void SetUp() override { |
| UVAuthenticatorImplTest::SetUp(); |
| device::VirtualCtap2Device::Config config; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| config.internal_uv_support = true; |
| config.pin_uv_auth_token_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| } |
| }; |
| |
| TEST_F(UVTokenAuthenticatorImplTest, GetAssertionUVToken) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| get_credential_options()->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| for (const auto fingerprints_enrolled : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "fingerprints_enrolled=" << fingerprints_enrolled); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = |
| fingerprints_enrolled; |
| |
| for (auto uv : {device::UserVerificationRequirement::kDiscouraged, |
| device::UserVerificationRequirement::kPreferred, |
| device::UserVerificationRequirement::kRequired}) { |
| SCOPED_TRACE(UVToString(uv)); |
| |
| // Without a fingerprint enrolled we assume that a UV=required request |
| // cannot be satisfied by an authenticator that cannot do UV. It is |
| // possible for a credential to be created without UV and then later |
| // asserted with UV=required, but that would be bizarre behaviour from |
| // an RP and we currently don't worry about it. |
| const bool should_be_unrecognized = |
| !fingerprints_enrolled && |
| uv == device::UserVerificationRequirement::kRequired; |
| |
| GetAssertionResult result = |
| AuthenticatorGetAssertion(get_credential_options(uv)); |
| |
| if (should_be_unrecognized) { |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, result.status); |
| } else { |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_EQ(fingerprints_enrolled && |
| uv != device::UserVerificationRequirement::kDiscouraged, |
| HasUV(result.response)); |
| } |
| } |
| } |
| } |
| |
| // Test exhausting all internal user verification attempts on an authenticator |
| // that does not support PINs. |
| TEST_F(UVTokenAuthenticatorImplTest, GetAssertionUvFails) { |
| device::VirtualCtap2Device::Config config; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| config.internal_uv_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.user_verification_succeeds = false; |
| config.pin_support = false; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| get_credential_options()->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| int expected_retries = 5; |
| virtual_device_factory_->mutable_state()->uv_retries = expected_retries; |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting([&](device::VirtualFidoDevice* device) { |
| EXPECT_EQ(--expected_retries, |
| virtual_device_factory_->mutable_state()->uv_retries); |
| return true; |
| }); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(get_credential_options()).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(0, expected_retries); |
| } |
| |
| // Test exhausting all internal user verification attempts on an authenticator |
| // that supports PINs. |
| TEST_F(UVTokenAuthenticatorImplTest, GetAssertionFallBackToPin) { |
| device::VirtualCtap2Device::Config config; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| config.internal_uv_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.user_verification_succeeds = false; |
| config.pin_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| get_credential_options()->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| int taps = 0; |
| virtual_device_factory_->mutable_state()->uv_retries = 5; |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting([&](device::VirtualFidoDevice* device) { |
| ++taps; |
| return true; |
| }); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(get_credential_options()).status, |
| AuthenticatorStatus::SUCCESS); |
| // 5 retries + 1 tap for the actual get assertion request. |
| EXPECT_EQ(taps, 6); |
| EXPECT_TRUE(test_client_.collected_pin); |
| EXPECT_EQ(device::kMinPinLength, test_client_.min_pin_length); |
| EXPECT_EQ(5, virtual_device_factory_->mutable_state()->uv_retries); |
| } |
| |
| // Tests that a device supporting UV token with UV blocked at the start of a get |
| // assertion request gets a touch and then falls back to PIN. |
| TEST_F(UVTokenAuthenticatorImplTest, GetAssertionUvBlockedFallBackToPin) { |
| device::VirtualCtap2Device::Config config; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| config.internal_uv_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.user_verification_succeeds = false; |
| config.pin_support = true; |
| |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->uv_retries = 0; |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| get_credential_options()->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(get_credential_options()).status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_TRUE(test_client_.collected_pin); |
| EXPECT_EQ(device::kMinPinLength, test_client_.min_pin_length); |
| EXPECT_EQ(5, virtual_device_factory_->mutable_state()->uv_retries); |
| } |
| |
| TEST_F(UVTokenAuthenticatorImplTest, MakeCredentialUVToken) { |
| for (const auto fingerprints_enrolled : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "fingerprints_enrolled=" << fingerprints_enrolled); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = |
| fingerprints_enrolled; |
| |
| for (const auto uv : {device::UserVerificationRequirement::kDiscouraged, |
| device::UserVerificationRequirement::kPreferred, |
| device::UserVerificationRequirement::kRequired}) { |
| SCOPED_TRACE(UVToString(uv)); |
| |
| // UV cannot be satisfied without fingerprints. |
| const bool should_timeout = |
| !fingerprints_enrolled && |
| uv == device::UserVerificationRequirement::kRequired; |
| |
| if (should_timeout) { |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, |
| AuthenticatorMakeCredentialAndWaitForTimeout( |
| make_credential_options(uv)) |
| .status); |
| } else { |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(make_credential_options(uv)); |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_EQ(fingerprints_enrolled, HasUV(result.response)); |
| } |
| } |
| } |
| } |
| |
| // Test exhausting all internal user verification attempts on an authenticator |
| // that does not support PINs. |
| TEST_F(UVTokenAuthenticatorImplTest, MakeCredentialUvFails) { |
| device::VirtualCtap2Device::Config config; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| config.internal_uv_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.user_verification_succeeds = false; |
| config.pin_support = false; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| get_credential_options()->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| int expected_retries = 5; |
| virtual_device_factory_->mutable_state()->uv_retries = expected_retries; |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting([&](device::VirtualFidoDevice* device) { |
| EXPECT_EQ(--expected_retries, |
| virtual_device_factory_->mutable_state()->uv_retries); |
| return true; |
| }); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(make_credential_options()).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(0, expected_retries); |
| } |
| |
| // Test exhausting all internal user verification attempts on an authenticator |
| // that supports PINs. |
| TEST_F(UVTokenAuthenticatorImplTest, MakeCredentialFallBackToPin) { |
| device::VirtualCtap2Device::Config config; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| config.internal_uv_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.user_verification_succeeds = false; |
| config.pin_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| get_credential_options()->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| int taps = 0; |
| virtual_device_factory_->mutable_state()->uv_retries = 5; |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting([&](device::VirtualFidoDevice* device) { |
| ++taps; |
| return true; |
| }); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(make_credential_options()).status, |
| AuthenticatorStatus::SUCCESS); |
| // 5 retries + 1 tap for the actual get assertion request. |
| EXPECT_EQ(taps, 6); |
| EXPECT_TRUE(test_client_.collected_pin); |
| EXPECT_EQ(device::kMinPinLength, test_client_.min_pin_length); |
| EXPECT_EQ(5, virtual_device_factory_->mutable_state()->uv_retries); |
| } |
| |
| // Tests that a device supporting UV token with UV blocked at the start of a get |
| // assertion request gets a touch and then falls back to PIN. |
| TEST_F(UVTokenAuthenticatorImplTest, MakeCredentialUvBlockedFallBackToPin) { |
| device::VirtualCtap2Device::Config config; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| config.internal_uv_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.user_verification_succeeds = false; |
| config.pin_support = true; |
| |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->uv_retries = 0; |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| get_credential_options()->allow_credentials[0].id, kTestRelyingPartyId)); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(make_credential_options()).status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_TRUE(test_client_.collected_pin); |
| EXPECT_EQ(device::kMinPinLength, test_client_.min_pin_length); |
| EXPECT_EQ(5, virtual_device_factory_->mutable_state()->uv_retries); |
| } |
| |
| class BlockingAuthenticatorRequestDelegate |
| : public AuthenticatorRequestClientDelegate { |
| public: |
| BlockingAuthenticatorRequestDelegate() = default; |
| |
| void RegisterActionCallbacks( |
| base::OnceClosure cancel_callback, |
| base::RepeatingClosure start_over_callback, |
| AccountPreselectedCallback account_preselected_callback, |
| device::FidoRequestHandlerBase::RequestCallback request_callback, |
| base::RepeatingClosure bluetooth_adapter_power_on_callback) override { |
| cancel_callback_ = std::move(cancel_callback); |
| } |
| |
| bool DoesBlockRequestOnFailure(InterestingFailureReason reason) override { |
| // Post a task to cancel the request to give the second authenticator a |
| // chance to return a status from the cancelled request. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(cancel_callback_)); |
| return true; |
| } |
| |
| private: |
| base::OnceClosure cancel_callback_; |
| }; |
| |
| class BlockingDelegateContentBrowserClient : public ContentBrowserClient { |
| public: |
| BlockingDelegateContentBrowserClient() = default; |
| |
| WebAuthenticationDelegate* GetWebAuthenticationDelegate() override { |
| return &web_authentication_delegate_; |
| } |
| |
| std::unique_ptr<AuthenticatorRequestClientDelegate> |
| GetWebAuthenticationRequestDelegate( |
| RenderFrameHost* render_frame_host) override { |
| auto ret = std::make_unique<BlockingAuthenticatorRequestDelegate>(); |
| delegate_ = ret.get(); |
| return ret; |
| } |
| |
| private: |
| TestWebAuthenticationDelegate web_authentication_delegate_; |
| raw_ptr<BlockingAuthenticatorRequestDelegate, DanglingUntriaged> delegate_ = |
| nullptr; |
| }; |
| |
| class BlockingDelegateAuthenticatorImplTest : public AuthenticatorImplTest { |
| public: |
| BlockingDelegateAuthenticatorImplTest() = default; |
| |
| BlockingDelegateAuthenticatorImplTest( |
| const BlockingDelegateAuthenticatorImplTest&) = delete; |
| BlockingDelegateAuthenticatorImplTest& operator=( |
| const BlockingDelegateAuthenticatorImplTest&) = delete; |
| |
| void SetUp() override { |
| AuthenticatorImplTest::SetUp(); |
| old_client_ = SetBrowserClientForTesting(&test_client_); |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| } |
| |
| void TearDown() override { |
| SetBrowserClientForTesting(old_client_); |
| AuthenticatorImplTest::TearDown(); |
| } |
| |
| protected: |
| BlockingDelegateContentBrowserClient test_client_; |
| |
| private: |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| }; |
| |
| TEST_F(BlockingDelegateAuthenticatorImplTest, PostCancelMessage) { |
| // Create a fingerprint-reading device and a UP-only device. Advance the |
| // first till it's waiting for a fingerprint then simulate a touch on the |
| // UP device that claims that it failed due to an excluded credential. |
| // This will cancel the request on the fingerprint device, which will resolve |
| // the UV with an error. Don't crash (crbug.com/1225899). |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->exclude_credentials = GetTestCredentials(); |
| |
| device::test::MultipleVirtualFidoDeviceFactory::DeviceDetails device_1; |
| scoped_refptr<VirtualFidoDevice::State> state_1 = device_1.state; |
| device_1.state->simulate_press_callback = |
| base::BindLambdaForTesting([&](VirtualFidoDevice* ignore) -> bool { |
| // Drop all makeCredential requests. The reply will be sent when |
| // the second authenticator is asked for a fingerprint. |
| return false; |
| }); |
| |
| device::test::MultipleVirtualFidoDeviceFactory::DeviceDetails device_2; |
| scoped_refptr<VirtualFidoDevice::State> state_2 = device_2.state; |
| device_2.config.internal_uv_support = true; |
| device_2.config.pin_support = true; |
| device_2.config.pin_uv_auth_token_support = true; |
| device_2.config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| device_2.state->pin = kTestPIN; |
| device_2.state->fingerprints_enrolled = true; |
| device_2.state->uv_retries = 8; |
| device_2.state->cancel_response_code = |
| device::CtapDeviceResponseCode::kCtap2ErrOperationDenied; |
| device_2.state->simulate_press_callback = |
| base::BindLambdaForTesting([&](VirtualFidoDevice* ignore) -> bool { |
| // If asked for a fingerprint, fail the makeCredential request by |
| // simulating a matched excluded credential by the other authenticator. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(state_1->transact_callback), |
| std::vector<uint8_t>{static_cast<uint8_t>( |
| device::CtapDeviceResponseCode:: |
| kCtap2ErrCredentialExcluded)})); |
| return false; |
| }); |
| |
| auto discovery = |
| std::make_unique<device::test::MultipleVirtualFidoDeviceFactory>(); |
| discovery->AddDevice(std::move(device_1)); |
| discovery->AddDevice(std::move(device_2)); |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting(std::move(discovery)); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::CREDENTIAL_EXCLUDED); |
| } |
| |
| // ResidentKeyTestAuthenticatorRequestDelegate is a delegate that: |
| // a) always returns |kTestPIN| when asked for a PIN. |
| // b) sorts potential resident-key accounts by user ID, maps them to a string |
| // form ("<hex user ID>:<user name>:<display name>"), joins the strings |
| // with "/", and compares the result against |expected_accounts|. |
| // c) auto-selects the account with the user ID matching |selected_user_id|. |
| class ResidentKeyTestAuthenticatorRequestDelegate |
| : public AuthenticatorRequestClientDelegate { |
| public: |
| struct Config { |
| // A string representation of the accounts expected to be passed to |
| // `SelectAccount()`. |
| std::string expected_accounts; |
| |
| // The user ID of the account that should be selected by `SelectAccount()`. |
| std::vector<uint8_t> selected_user_id; |
| |
| // Indicates whether `SetConditional(true)` is expected to be called. |
| bool expect_conditional = false; |
| |
| // If set, indicates that `DoesBlockRequestOnFailure()` is expected to be |
| // called with this value. |
| absl::optional<AuthenticatorRequestClientDelegate::InterestingFailureReason> |
| expected_failure_reason; |
| |
| // If set, indicates that the `AccountPreselectCallback` should be invoked |
| // with this credential ID at the beginning of the request. |
| // `preselected_authenticator_id` contains the authenticator ID to which the |
| // request should be dispatched in this case. |
| absl::optional<std::vector<uint8_t>> preselected_credential_id; |
| absl::optional<std::string> preselected_authenticator_id; |
| }; |
| |
| explicit ResidentKeyTestAuthenticatorRequestDelegate(Config config) |
| : config_(std::move(config)) {} |
| |
| ~ResidentKeyTestAuthenticatorRequestDelegate() override { |
| DCHECK(!config_.expect_conditional || expect_conditional_satisfied_) |
| << "SetConditionalRequest() expected but not called"; |
| DCHECK(!config_.expected_failure_reason || |
| expected_failure_reason_satisfied_) |
| << "DoesRequestBlockOnFailure() expected but not called"; |
| } |
| |
| ResidentKeyTestAuthenticatorRequestDelegate( |
| const ResidentKeyTestAuthenticatorRequestDelegate&) = delete; |
| ResidentKeyTestAuthenticatorRequestDelegate& operator=( |
| const ResidentKeyTestAuthenticatorRequestDelegate&) = delete; |
| |
| bool SupportsPIN() const override { return true; } |
| |
| void CollectPIN( |
| CollectPINOptions options, |
| base::OnceCallback<void(std::u16string)> provide_pin_cb) override { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(provide_pin_cb), kTestPIN16)); |
| } |
| |
| void FinishCollectToken() override {} |
| |
| void RegisterActionCallbacks( |
| base::OnceClosure cancel_callback, |
| base::RepeatingClosure start_over_callback, |
| AccountPreselectedCallback account_preselected_callback, |
| device::FidoRequestHandlerBase::RequestCallback request_callback, |
| base::RepeatingClosure bluetooth_adapter_power_on_callback) override { |
| account_preselected_callback_ = account_preselected_callback; |
| request_callback_ = request_callback; |
| } |
| |
| void SelectAccount( |
| std::vector<device::AuthenticatorGetAssertionResponse> responses, |
| base::OnceCallback<void(device::AuthenticatorGetAssertionResponse)> |
| callback) override { |
| std::sort(responses.begin(), responses.end(), |
| [](const device::AuthenticatorGetAssertionResponse& a, |
| const device::AuthenticatorGetAssertionResponse& b) { |
| return a.user_entity->id < b.user_entity->id; |
| }); |
| |
| std::vector<std::string> string_reps; |
| base::ranges::transform( |
| responses, std::back_inserter(string_reps), |
| [](const device::AuthenticatorGetAssertionResponse& response) { |
| const device::PublicKeyCredentialUserEntity& user = |
| response.user_entity.value(); |
| return base::HexEncode(user.id.data(), user.id.size()) + ":" + |
| user.name.value_or("") + ":" + user.display_name.value_or(""); |
| }); |
| |
| EXPECT_EQ(config_.expected_accounts, base::JoinString(string_reps, "/")); |
| |
| const auto selected = base::ranges::find( |
| responses, config_.selected_user_id, |
| [](const device::AuthenticatorGetAssertionResponse& response) { |
| return response.user_entity->id; |
| }); |
| ASSERT_TRUE(selected != responses.end()); |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), std::move(*selected))); |
| } |
| |
| bool DoesBlockRequestOnFailure(InterestingFailureReason reason) override { |
| if (config_.expected_failure_reason) { |
| EXPECT_EQ(*config_.expected_failure_reason, reason); |
| expected_failure_reason_satisfied_ = true; |
| } |
| return AuthenticatorRequestClientDelegate::DoesBlockRequestOnFailure( |
| reason); |
| } |
| |
| void SetConditionalRequest(bool is_conditional) override { |
| EXPECT_EQ(config_.expect_conditional, is_conditional); |
| EXPECT_TRUE(!expect_conditional_satisfied_); |
| expect_conditional_satisfied_ = true; |
| } |
| |
| bool EmbedderControlsAuthenticatorDispatch( |
| const device::FidoAuthenticator& authenticator) override { |
| // Don't instantly dispatch platform authenticator requests if the test is |
| // exercising platform credential preselection. |
| // `OnTransportAvailabilityEnumerated()` will run the `request_callback_` in |
| // this case to mimic behavior of the real UI. |
| return authenticator.AuthenticatorTransport() == |
| device::FidoTransportProtocol::kInternal && |
| config_.preselected_credential_id; |
| } |
| |
| void OnTransportAvailabilityEnumerated( |
| device::FidoRequestHandlerBase::TransportAvailabilityInfo info) override { |
| if (config_.preselected_credential_id) { |
| DCHECK(config_.preselected_authenticator_id); |
| EXPECT_EQ(info.has_platform_authenticator_credential, |
| device::FidoRequestHandlerBase::RecognizedCredential:: |
| kHasRecognizedCredential); |
| EXPECT_TRUE( |
| base::Contains(info.recognized_platform_authenticator_credentials, |
| *config_.preselected_credential_id, |
| &device::DiscoverableCredentialMetadata::cred_id)); |
| std::move(account_preselected_callback_) |
| .Run(*config_.preselected_credential_id); |
| request_callback_.Run(*config_.preselected_authenticator_id); |
| } |
| } |
| |
| private: |
| const Config config_; |
| bool expect_conditional_satisfied_ = false; |
| bool expected_failure_reason_satisfied_ = false; |
| device::FidoRequestHandlerBase::RequestCallback request_callback_; |
| AccountPreselectedCallback account_preselected_callback_; |
| }; |
| |
| class ResidentKeyTestAuthenticatorContentBrowserClient |
| : public ContentBrowserClient { |
| public: |
| ResidentKeyTestAuthenticatorContentBrowserClient() { |
| web_authentication_delegate.supports_resident_keys = true; |
| } |
| |
| WebAuthenticationDelegate* GetWebAuthenticationDelegate() override { |
| return &web_authentication_delegate; |
| } |
| |
| std::unique_ptr<AuthenticatorRequestClientDelegate> |
| GetWebAuthenticationRequestDelegate( |
| RenderFrameHost* render_frame_host) override { |
| return std::make_unique<ResidentKeyTestAuthenticatorRequestDelegate>( |
| delegate_config); |
| } |
| |
| TestWebAuthenticationDelegate web_authentication_delegate; |
| |
| ResidentKeyTestAuthenticatorRequestDelegate::Config delegate_config; |
| }; |
| |
| class ResidentKeyAuthenticatorImplTest : public UVAuthenticatorImplTest { |
| public: |
| ResidentKeyAuthenticatorImplTest(const ResidentKeyAuthenticatorImplTest&) = |
| delete; |
| ResidentKeyAuthenticatorImplTest& operator=( |
| const ResidentKeyAuthenticatorImplTest&) = delete; |
| |
| protected: |
| ResidentKeyAuthenticatorImplTest() = default; |
| |
| void SetUp() override { |
| UVAuthenticatorImplTest::SetUp(); |
| old_client_ = SetBrowserClientForTesting(&test_client_); |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.resident_key_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| } |
| |
| void TearDown() override { |
| SetBrowserClientForTesting(old_client_); |
| UVAuthenticatorImplTest::TearDown(); |
| } |
| |
| static PublicKeyCredentialCreationOptionsPtr make_credential_options( |
| device::ResidentKeyRequirement resident_key = |
| device::ResidentKeyRequirement::kRequired) { |
| PublicKeyCredentialCreationOptionsPtr options = |
| UVAuthenticatorImplTest::make_credential_options(); |
| options->authenticator_selection->resident_key = resident_key; |
| options->user.id = {1, 2, 3, 4}; |
| return options; |
| } |
| |
| static PublicKeyCredentialRequestOptionsPtr get_credential_options() { |
| PublicKeyCredentialRequestOptionsPtr options = |
| UVAuthenticatorImplTest::get_credential_options(); |
| options->allow_credentials.clear(); |
| return options; |
| } |
| |
| ResidentKeyTestAuthenticatorContentBrowserClient test_client_; |
| |
| private: |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| }; |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, MakeCredentialRkRequired) { |
| for (const bool internal_uv : {false, true}) { |
| SCOPED_TRACE(::testing::Message() << "internal_uv=" << internal_uv); |
| |
| if (internal_uv) { |
| device::VirtualCtap2Device::Config config; |
| config.resident_key_support = true; |
| config.internal_uv_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| } |
| |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(make_credential_options()); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| ASSERT_EQ(1u, |
| virtual_device_factory_->mutable_state()->registrations.size()); |
| const device::VirtualFidoDevice::RegistrationData& registration = |
| virtual_device_factory_->mutable_state()->registrations.begin()->second; |
| EXPECT_TRUE(registration.is_resident); |
| ASSERT_TRUE(registration.user.has_value()); |
| const auto options = make_credential_options(); |
| EXPECT_EQ(options->user.name, registration.user->name); |
| EXPECT_EQ(options->user.display_name, registration.user->display_name); |
| EXPECT_EQ(options->user.id, registration.user->id); |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, MakeCredentialRkPreferred) { |
| for (const bool supports_rk : {false, true}) { |
| SCOPED_TRACE(::testing::Message() << "supports_rk=" << supports_rk); |
| ResetVirtualDevice(); |
| |
| device::VirtualCtap2Device::Config config; |
| config.internal_uv_support = true; |
| config.resident_key_support = supports_rk; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| |
| MakeCredentialResult result = AuthenticatorMakeCredential( |
| make_credential_options(device::ResidentKeyRequirement::kPreferred)); |
| |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| ASSERT_EQ(1u, |
| virtual_device_factory_->mutable_state()->registrations.size()); |
| const device::VirtualFidoDevice::RegistrationData& registration = |
| virtual_device_factory_->mutable_state()->registrations.begin()->second; |
| EXPECT_EQ(registration.is_resident, supports_rk); |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, MakeCredentialRkPreferredStorageFull) { |
| // Making a credential on an authenticator with full storage falls back to |
| // making a non-resident key. |
| for (bool is_ctap_2_1 : {false, true}) { |
| ResetVirtualDevice(); |
| |
| size_t num_taps = 0; |
| virtual_device_factory_->mutable_state()->simulate_press_callback = |
| base::BindLambdaForTesting( |
| [&num_taps](device::VirtualFidoDevice* device) { |
| num_taps++; |
| return true; |
| }); |
| |
| device::VirtualCtap2Device::Config config; |
| if (is_ctap_2_1) { |
| config.ctap2_versions = {std::begin(device::kCtap2Versions2_1), |
| std::end(device::kCtap2Versions2_1)}; |
| } |
| |
| config.internal_uv_support = true; |
| config.resident_key_support = true; |
| config.resident_credential_storage = 0; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| |
| MakeCredentialResult result = AuthenticatorMakeCredential( |
| make_credential_options(device::ResidentKeyRequirement::kPreferred)); |
| |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| ASSERT_EQ(1u, |
| virtual_device_factory_->mutable_state()->registrations.size()); |
| const device::VirtualFidoDevice::RegistrationData& registration = |
| virtual_device_factory_->mutable_state()->registrations.begin()->second; |
| EXPECT_EQ(registration.is_resident, false); |
| // In CTAP 2.0, the first request with rk=false fails due to exhausted |
| // storage and then needs to be retried with rk=false, requiring a second |
| // tap. In 2.1 remaining storage capacity can be checked up front such that |
| // the request is sent with rk=false right away. |
| EXPECT_EQ(num_taps, is_ctap_2_1 ? 1u : 2u); |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, MakeCredentialRkPreferredSetsPIN) { |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.internal_uv_support = false; |
| config.resident_key_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->pin = ""; |
| |
| MakeCredentialResult result = AuthenticatorMakeCredential( |
| make_credential_options(device::ResidentKeyRequirement::kPreferred)); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| ASSERT_EQ(1u, virtual_device_factory_->mutable_state()->registrations.size()); |
| const device::VirtualFidoDevice::RegistrationData& registration = |
| virtual_device_factory_->mutable_state()->registrations.begin()->second; |
| EXPECT_EQ(registration.is_resident, true); |
| EXPECT_EQ(virtual_device_factory_->mutable_state()->pin, kTestPIN); |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, StorageFull) { |
| device::VirtualCtap2Device::Config config; |
| config.resident_key_support = true; |
| config.internal_uv_support = true; |
| config.resident_credential_storage = 1; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| |
| // Add a resident key to fill the authenticator. |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId, |
| /*user_id=*/{{1, 1, 1, 1}}, "test@example.com", "Test User")); |
| |
| test_client_.delegate_config.expected_failure_reason = |
| AuthenticatorRequestClientDelegate::InterestingFailureReason:: |
| kStorageFull; |
| EXPECT_EQ(AuthenticatorMakeCredential(make_credential_options()).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, MakeCredentialEmptyFields) { |
| PublicKeyCredentialCreationOptionsPtr options = make_credential_options(); |
| // This value is perfectly legal, but our VirtualCtap2Device simulates |
| // some security keys in rejecting empty values. CBOR serialisation should |
| // omit these values rather than send empty ones. |
| options->user.display_name = ""; |
| |
| MakeCredentialResult result = AuthenticatorMakeCredential(std::move(options)); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, GetAssertionSingleNoPII) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, absl::nullopt)); |
| |
| // |SelectAccount| should not be called when there's only a single response |
| // with no identifying user info because the UI is bad in that case: we can |
| // only display the single choice of "Unknown user". |
| test_client_.delegate_config.expected_accounts = "<invalid>"; |
| GetAssertionResult result = |
| AuthenticatorGetAssertion(get_credential_options()); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, GetAssertionUserSelected) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, "Test", "User")); |
| |
| for (const bool internal_account_chooser : {false, true}) { |
| SCOPED_TRACE(internal_account_chooser); |
| |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.resident_key_support = true; |
| config.internal_account_chooser = internal_account_chooser; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| // |SelectAccount| should not be called when userSelected is set. |
| if (internal_account_chooser) { |
| test_client_.delegate_config.expected_accounts = "<invalid>"; |
| } else { |
| test_client_.delegate_config.expected_accounts = "01020304:Test:User"; |
| test_client_.delegate_config.selected_user_id = {1, 2, 3, 4}; |
| } |
| GetAssertionResult result = |
| AuthenticatorGetAssertion(get_credential_options()); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, GetAssertionSingleWithPII) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, "Test User")); |
| |
| // |SelectAccount| should be called when PII is available. |
| test_client_.delegate_config.expected_accounts = "01020304::Test User"; |
| test_client_.delegate_config.selected_user_id = {1, 2, 3, 4}; |
| GetAssertionResult result = |
| AuthenticatorGetAssertion(get_credential_options()); |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, GetAssertionMulti) { |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, "test@example.com", "Test User")); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| /*credential_id=*/{{4, 3, 2, 2}}, kTestRelyingPartyId, |
| /*user_id=*/{{5, 6, 7, 8}}, "test2@example.com", "Test User 2")); |
| |
| test_client_.delegate_config.expected_accounts = |
| "01020304:test@example.com:Test User/" |
| "05060708:test2@example.com:Test User 2"; |
| test_client_.delegate_config.selected_user_id = {1, 2, 3, 4}; |
| |
| GetAssertionResult result = |
| AuthenticatorGetAssertion(get_credential_options()); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, GetAssertionUVDiscouraged) { |
| device::VirtualCtap2Device::Config config; |
| config.resident_key_support = true; |
| config.internal_uv_support = true; |
| config.u2f_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, absl::nullopt)); |
| |
| // |SelectAccount| should not be called when there's only a single response |
| // without identifying information. |
| test_client_.delegate_config.expected_accounts = "<invalid>"; |
| PublicKeyCredentialRequestOptionsPtr options(get_credential_options()); |
| options->user_verification = |
| device::UserVerificationRequirement::kDiscouraged; |
| |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| // The UV=discouraged should have been ignored for a resident-credential |
| // request. |
| EXPECT_TRUE(HasUV(result.response)); |
| } |
| |
| static const char* BlobSupportDescription(device::LargeBlobSupport support) { |
| switch (support) { |
| case device::LargeBlobSupport::kNotRequested: |
| return "Blob not requested"; |
| case device::LargeBlobSupport::kPreferred: |
| return "Blob preferred"; |
| case device::LargeBlobSupport::kRequired: |
| return "Blob required"; |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, MakeCredentialLargeBlob) { |
| constexpr auto BlobRequired = device::LargeBlobSupport::kRequired; |
| constexpr auto BlobPreferred = device::LargeBlobSupport::kPreferred; |
| constexpr auto BlobNotRequested = device::LargeBlobSupport::kNotRequested; |
| constexpr auto nullopt = absl::nullopt; |
| |
| constexpr struct { |
| bool large_blob_extension; |
| absl::optional<bool> large_blob_support; |
| bool rk_required; |
| device::LargeBlobSupport large_blob_enable; |
| bool request_success; |
| bool did_create_large_blob; |
| } kLargeBlobTestCases[] = { |
| // clang-format off |
| // ext, support, rk, enabled, success, did create |
| { false, true, true, BlobRequired, true, true}, |
| { false, true, true, BlobPreferred, true, true}, |
| { false, true, true, BlobNotRequested, true, false}, |
| { false, true, false, BlobRequired, false, false}, |
| { false, true, false, BlobPreferred, true, false}, |
| { false, true, true, BlobNotRequested, true, false}, |
| { false, false, true, BlobRequired, false, false}, |
| { false, false, true, BlobPreferred, true, false}, |
| { false, true, true, BlobNotRequested, true, false}, |
| |
| { true, true, true, BlobRequired, true, true}, |
| { true, true, true, BlobPreferred, true, true}, |
| { true, true, true, BlobNotRequested, true, false}, |
| { true, true, false, BlobRequired, false, false}, |
| { true, true, false, BlobPreferred, true, false}, |
| { true, true, true, BlobNotRequested, true, false}, |
| { true, nullopt, true, BlobRequired, false, false}, |
| { true, nullopt, true, BlobPreferred, true, false}, |
| { true, true, true, BlobNotRequested, true, false}, |
| { true, false, true, BlobPreferred, true, false}, |
| { true, false, true, BlobRequired, false, false}, |
| // clang-format on |
| }; |
| for (auto& test : kLargeBlobTestCases) { |
| if (test.large_blob_support) { |
| SCOPED_TRACE(::testing::Message() |
| << "support=" << *test.large_blob_support); |
| } else { |
| SCOPED_TRACE(::testing::Message() << "support={}"); |
| } |
| SCOPED_TRACE(::testing::Message() << "rk_required=" << test.rk_required); |
| SCOPED_TRACE(::testing::Message() |
| << "enabled=" |
| << BlobSupportDescription(test.large_blob_enable)); |
| SCOPED_TRACE(::testing::Message() << "success=" << test.request_success); |
| SCOPED_TRACE(::testing::Message() |
| << "did create=" << test.did_create_large_blob); |
| SCOPED_TRACE(::testing::Message() |
| << "large_blob_extension=" << test.large_blob_extension); |
| |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.resident_key_support = true; |
| config.ctap2_versions = {std::begin(device::kCtap2Versions2_1), |
| std::end(device::kCtap2Versions2_1)}; |
| if (test.large_blob_extension) { |
| config.large_blob_extension_support = test.large_blob_support; |
| } else { |
| config.large_blob_support = *test.large_blob_support; |
| } |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialCreationOptionsPtr options = make_credential_options( |
| test.rk_required ? device::ResidentKeyRequirement::kRequired |
| : device::ResidentKeyRequirement::kDiscouraged); |
| options->large_blob_enable = test.large_blob_enable; |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| |
| if (test.request_success) { |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| ASSERT_EQ(1u, |
| virtual_device_factory_->mutable_state()->registrations.size()); |
| const device::VirtualFidoDevice::RegistrationData& registration = |
| virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second; |
| EXPECT_EQ(test.did_create_large_blob && !test.large_blob_extension, |
| registration.large_blob_key.has_value()); |
| EXPECT_EQ(test.large_blob_enable != BlobNotRequested, |
| result.response->echo_large_blob); |
| EXPECT_EQ(test.did_create_large_blob, |
| result.response->supports_large_blob); |
| } else { |
| ASSERT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, result.status); |
| ASSERT_EQ(0u, |
| virtual_device_factory_->mutable_state()->registrations.size()); |
| } |
| virtual_device_factory_->mutable_state()->registrations.clear(); |
| virtual_device_factory_->mutable_state()->ClearLargeBlobs(); |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, GetAssertionLargeBlobRead) { |
| constexpr struct { |
| bool large_blob_support; |
| bool large_blob_set; |
| bool large_blob_key_set; |
| bool did_read_large_blob; |
| } kLargeBlobTestCases[] = { |
| // clang-format off |
| // support, set, key_set, did_read |
| { true, true, true, true }, |
| { true, false, false, false }, |
| { true, false, true, false }, |
| { false, false, false, false }, |
| // clang-format on |
| }; |
| for (auto& test : kLargeBlobTestCases) { |
| SCOPED_TRACE(::testing::Message() << "support=" << test.large_blob_support); |
| SCOPED_TRACE(::testing::Message() << "set=" << test.large_blob_set); |
| SCOPED_TRACE(::testing::Message() << "key_set=" << test.large_blob_key_set); |
| SCOPED_TRACE(::testing::Message() |
| << "did_read=" << test.did_read_large_blob); |
| |
| const std::vector<uint8_t> large_blob = {'b', 'l', 'o', 'b'}; |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.resident_key_support = true; |
| config.ctap2_versions = {std::begin(device::kCtap2Versions2_1), |
| std::end(device::kCtap2Versions2_1)}; |
| config.large_blob_support = test.large_blob_support; |
| virtual_device_factory_->SetCtap2Config(config); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, absl::nullopt)); |
| |
| if (test.large_blob_set) { |
| virtual_device_factory_->mutable_state()->InjectLargeBlob( |
| &virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second, |
| CompressLargeBlob(large_blob)); |
| } else if (test.large_blob_key_set) { |
| virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second.large_blob_key = {{0}}; |
| } |
| |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| options->large_blob_read = true; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(result.response->echo_large_blob); |
| EXPECT_FALSE(result.response->echo_large_blob_written); |
| if (test.did_read_large_blob) { |
| EXPECT_EQ(large_blob, *result.response->large_blob); |
| } else { |
| EXPECT_FALSE(result.response->large_blob.has_value()); |
| } |
| virtual_device_factory_->mutable_state()->registrations.clear(); |
| virtual_device_factory_->mutable_state()->ClearLargeBlobs(); |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, GetAssertionLargeBlobWrite) { |
| constexpr struct { |
| bool large_blob_support; |
| bool large_blob_set; |
| bool large_blob_key_set; |
| bool did_write_large_blob; |
| } kLargeBlobTestCases[] = { |
| // clang-format off |
| // support, set, key_set, did_write |
| { true, true, true, true }, |
| { true, false, false, false }, |
| { true, false, true, true }, |
| { false, false, false, false }, |
| // clang-format on |
| }; |
| for (auto& test : kLargeBlobTestCases) { |
| SCOPED_TRACE(::testing::Message() << "support=" << test.large_blob_support); |
| SCOPED_TRACE(::testing::Message() << "set=" << test.large_blob_set); |
| SCOPED_TRACE(::testing::Message() << "key_set=" << test.large_blob_key_set); |
| SCOPED_TRACE(::testing::Message() |
| << "did_write=" << test.did_write_large_blob); |
| |
| const std::vector<uint8_t> large_blob = {'b', 'l', 'o', 'b'}; |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.resident_key_support = true; |
| config.ctap2_versions = {std::begin(device::kCtap2Versions2_1), |
| std::end(device::kCtap2Versions2_1)}; |
| config.large_blob_support = test.large_blob_support; |
| virtual_device_factory_->SetCtap2Config(config); |
| const std::vector<uint8_t> cred_id = {4, 3, 2, 1}; |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| cred_id, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, absl::nullopt)); |
| |
| if (test.large_blob_set) { |
| virtual_device_factory_->mutable_state()->InjectLargeBlob( |
| &virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second, |
| CompressLargeBlob(large_blob)); |
| } else if (test.large_blob_key_set) { |
| virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second.large_blob_key = {{0}}; |
| } |
| |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| options->allow_credentials = {device::PublicKeyCredentialDescriptor( |
| device::CredentialType::kPublicKey, cred_id)}; |
| options->large_blob_write = large_blob; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(result.response->echo_large_blob); |
| EXPECT_FALSE(result.response->large_blob.has_value()); |
| EXPECT_TRUE(result.response->echo_large_blob_written); |
| EXPECT_EQ(test.did_write_large_blob, result.response->large_blob_written); |
| if (test.did_write_large_blob) { |
| absl::optional<device::LargeBlob> compressed_blob = |
| virtual_device_factory_->mutable_state()->GetLargeBlob( |
| virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second); |
| EXPECT_EQ(large_blob, UncompressLargeBlob(*compressed_blob)); |
| } |
| virtual_device_factory_->mutable_state()->registrations.clear(); |
| virtual_device_factory_->mutable_state()->ClearLargeBlobs(); |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, |
| GetAssertionLargeBlobExtensionNoSupport) { |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.resident_key_support = true; |
| config.ctap2_versions = {std::begin(device::kCtap2Versions2_1), |
| std::end(device::kCtap2Versions2_1)}; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| const std::vector<uint8_t> cred_id = {4, 3, 2, 1}; |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| cred_id, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, absl::nullopt)); |
| |
| // Try to read a large blob that doesn't exist and couldn't exist because the |
| // authenticator doesn't support large blobs. |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| options->allow_credentials = {device::PublicKeyCredentialDescriptor( |
| device::CredentialType::kPublicKey, cred_id)}; |
| options->large_blob_read = true; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(result.response->echo_large_blob); |
| EXPECT_FALSE(result.response->echo_large_blob_written); |
| ASSERT_FALSE(result.response->large_blob); |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, GetAssertionLargeBlobExtension) { |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.resident_key_support = true; |
| config.large_blob_extension_support = true; |
| config.ctap2_versions = {std::begin(device::kCtap2Versions2_1), |
| std::end(device::kCtap2Versions2_1)}; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| const std::vector<uint8_t> large_blob = {'b', 'l', 'o', 'b'}; |
| const std::vector<uint8_t> cred_id = {4, 3, 2, 1}; |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| cred_id, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, absl::nullopt)); |
| |
| { |
| // Try to read a large blob that doesn't exist. |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| options->allow_credentials = {device::PublicKeyCredentialDescriptor( |
| device::CredentialType::kPublicKey, cred_id)}; |
| options->large_blob_read = true; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(result.response->echo_large_blob); |
| EXPECT_FALSE(result.response->echo_large_blob_written); |
| ASSERT_FALSE(result.response->large_blob); |
| } |
| |
| { |
| // Write a large blob. |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| options->allow_credentials = {device::PublicKeyCredentialDescriptor( |
| device::CredentialType::kPublicKey, cred_id)}; |
| options->large_blob_write = large_blob; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(result.response->echo_large_blob); |
| EXPECT_TRUE(result.response->echo_large_blob_written); |
| EXPECT_FALSE(result.response->large_blob); |
| } |
| |
| { |
| // Read it back. |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| options->allow_credentials = {device::PublicKeyCredentialDescriptor( |
| device::CredentialType::kPublicKey, cred_id)}; |
| options->large_blob_read = true; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(result.response->echo_large_blob); |
| EXPECT_FALSE(result.response->echo_large_blob_written); |
| ASSERT_TRUE(result.response->large_blob); |
| EXPECT_EQ(large_blob, *result.response->large_blob); |
| } |
| |
| // Corrupt the large blob data and attempt to read it back. The invalid |
| // large blob should be ignored. |
| virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second.large_blob->compressed_data = {1, 2, 3, 4}; |
| |
| { |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| options->allow_credentials = {device::PublicKeyCredentialDescriptor( |
| device::CredentialType::kPublicKey, cred_id)}; |
| options->large_blob_read = true; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| ASSERT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(result.response->echo_large_blob); |
| EXPECT_FALSE(result.response->echo_large_blob_written); |
| ASSERT_FALSE(result.response->large_blob); |
| } |
| } |
| |
| static const char* ProtectionPolicyDescription( |
| blink::mojom::ProtectionPolicy p) { |
| switch (p) { |
| case blink::mojom::ProtectionPolicy::UNSPECIFIED: |
| return "UNSPECIFIED"; |
| case blink::mojom::ProtectionPolicy::NONE: |
| return "NONE"; |
| case blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED: |
| return "UV_OR_CRED_ID_REQUIRED"; |
| case blink::mojom::ProtectionPolicy::UV_REQUIRED: |
| return "UV_REQUIRED"; |
| } |
| } |
| |
| static const char* CredProtectDescription(device::CredProtect cred_protect) { |
| switch (cred_protect) { |
| case device::CredProtect::kUVOptional: |
| return "UV optional"; |
| case device::CredProtect::kUVOrCredIDRequired: |
| return "UV or cred ID required"; |
| case device::CredProtect::kUVRequired: |
| return "UV required"; |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, CredProtectRegistration) { |
| const auto UNSPECIFIED = blink::mojom::ProtectionPolicy::UNSPECIFIED; |
| const auto NONE = blink::mojom::ProtectionPolicy::NONE; |
| const auto UV_OR_CRED = |
| blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED; |
| const auto UV_REQ = blink::mojom::ProtectionPolicy::UV_REQUIRED; |
| const int kOk = 0; |
| const int kNonsense = 1; |
| const int kNotAllow = 2; |
| const device::UserVerificationRequirement kUV = |
| device::UserVerificationRequirement::kRequired; |
| const device::UserVerificationRequirement kUP = |
| device::UserVerificationRequirement::kDiscouraged; |
| const device::UserVerificationRequirement kUVPref = |
| device::UserVerificationRequirement::kPreferred; |
| |
| const struct { |
| bool supported_by_authenticator; |
| bool is_resident; |
| blink::mojom::ProtectionPolicy protection; |
| bool enforce; |
| device::UserVerificationRequirement uv; |
| int expected_outcome; |
| blink::mojom::ProtectionPolicy resulting_policy; |
| } kExpectations[] = { |
| // clang-format off |
| // Support | Resdnt | Level | Enf | UV || Result | Prot level |
| { false, false, UNSPECIFIED, false, kUP, kOk, NONE}, |
| { false, false, UNSPECIFIED, true, kUP, kNonsense, UNSPECIFIED}, |
| { false, false, UNSPECIFIED, false, kUVPref, kOk, NONE}, |
| { false, false, NONE, false, kUP, kNonsense, UNSPECIFIED}, |
| { false, false, NONE, true, kUP, kNonsense, UNSPECIFIED}, |
| { false, false, UV_OR_CRED, false, kUP, kOk, NONE}, |
| { false, false, UV_OR_CRED, true, kUP, kNotAllow, UNSPECIFIED}, |
| { false, false, UV_OR_CRED, false, kUV, kOk, NONE}, |
| { false, false, UV_OR_CRED, true, kUV, kNotAllow, UNSPECIFIED}, |
| { false, false, UV_REQ, false, kUP, kNonsense, UNSPECIFIED}, |
| { false, false, UV_REQ, false, kUV, kOk, NONE}, |
| { false, false, UV_REQ, true, kUP, kNonsense, UNSPECIFIED}, |
| { false, false, UV_REQ, true, kUV, kNotAllow, UNSPECIFIED}, |
| { false, true, UNSPECIFIED, false, kUP, kOk, NONE}, |
| { false, true, UNSPECIFIED, true, kUP, kNonsense, UNSPECIFIED}, |
| { false, true, NONE, false, kUP, kOk, NONE}, |
| { false, true, NONE, true, kUP, kNonsense, UNSPECIFIED}, |
| { false, true, UV_OR_CRED, false, kUP, kOk, NONE}, |
| { false, true, UV_OR_CRED, true, kUP, kNotAllow, UNSPECIFIED}, |
| { false, true, UV_REQ, false, kUP, kNonsense, UNSPECIFIED}, |
| { false, true, UV_REQ, false, kUV, kOk, NONE}, |
| { false, true, UV_REQ, true, kUP, kNonsense, UNSPECIFIED}, |
| { false, true, UV_REQ, true, kUV, kNotAllow, UNSPECIFIED}, |
| |
| // For the case where the authenticator supports credProtect we do not |
| // repeat the cases above that are |kNonsense| on the assumption that |
| // authenticator support is irrelevant. Therefore these are just the non- |
| // kNonsense cases from the prior block. |
| { true, false, UNSPECIFIED, false, kUP, kOk, NONE}, |
| { true, false, UV_OR_CRED, false, kUP, kOk, UV_OR_CRED}, |
| { true, false, UV_OR_CRED, true, kUP, kOk, UV_OR_CRED}, |
| { true, false, UV_OR_CRED, false, kUV, kOk, UV_OR_CRED}, |
| { true, false, UV_OR_CRED, true, kUV, kOk, UV_OR_CRED}, |
| { true, false, UV_REQ, false, kUV, kOk, UV_REQ}, |
| { true, false, UV_REQ, true, kUV, kOk, UV_REQ}, |
| { true, true, UNSPECIFIED, false, kUP, kOk, UV_OR_CRED}, |
| { true, true, UNSPECIFIED, false, kUVPref, kOk, UV_REQ}, |
| { true, true, NONE, false, kUP, kOk, NONE}, |
| { true, true, NONE, false, kUVPref, kOk, NONE}, |
| { true, true, UV_OR_CRED, false, kUP, kOk, UV_OR_CRED}, |
| { true, true, UV_OR_CRED, true, kUP, kOk, UV_OR_CRED}, |
| { true, true, UV_OR_CRED, false, kUVPref, kOk, UV_OR_CRED}, |
| { true, true, UV_REQ, false, kUV, kOk, UV_REQ}, |
| { true, true, UV_REQ, true, kUV, kOk, UV_REQ}, |
| // clang-format on |
| }; |
| |
| for (const auto& test : kExpectations) { |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.resident_key_support = true; |
| config.cred_protect_support = test.supported_by_authenticator; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->registrations.clear(); |
| |
| SCOPED_TRACE(::testing::Message() << "uv=" << UVToString(test.uv)); |
| SCOPED_TRACE(::testing::Message() << "enforce=" << test.enforce); |
| SCOPED_TRACE(::testing::Message() |
| << "level=" << ProtectionPolicyDescription(test.protection)); |
| SCOPED_TRACE(::testing::Message() << "resident=" << test.is_resident); |
| SCOPED_TRACE(::testing::Message() |
| << "support=" << test.supported_by_authenticator); |
| |
| PublicKeyCredentialCreationOptionsPtr options = make_credential_options(); |
| options->authenticator_selection->resident_key = |
| test.is_resident ? device::ResidentKeyRequirement::kRequired |
| : device::ResidentKeyRequirement::kDiscouraged; |
| options->protection_policy = test.protection; |
| options->enforce_protection_policy = test.enforce; |
| options->authenticator_selection->user_verification_requirement = test.uv; |
| |
| AuthenticatorStatus status = |
| AuthenticatorMakeCredential(std::move(options)).status; |
| |
| switch (test.expected_outcome) { |
| case kOk: { |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, status); |
| ASSERT_EQ( |
| 1u, virtual_device_factory_->mutable_state()->registrations.size()); |
| const device::CredProtect result = |
| virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second.protection; |
| |
| switch (test.resulting_policy) { |
| case UNSPECIFIED: |
| NOTREACHED(); |
| break; |
| case NONE: |
| EXPECT_EQ(device::CredProtect::kUVOptional, result); |
| break; |
| case UV_OR_CRED: |
| EXPECT_EQ(device::CredProtect::kUVOrCredIDRequired, result); |
| break; |
| case UV_REQ: |
| EXPECT_EQ(device::CredProtect::kUVRequired, result); |
| break; |
| } |
| break; |
| } |
| case kNonsense: |
| EXPECT_EQ(AuthenticatorStatus::PROTECTION_POLICY_INCONSISTENT, status); |
| break; |
| case kNotAllow: |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, status); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, AuthenticatorSetsCredProtect) { |
| // Some authenticators are expected to set the credProtect extension ad |
| // libitum. Therefore we should only require that the returned extension is at |
| // least as restrictive as requested, but perhaps not exactly equal. |
| constexpr blink::mojom::ProtectionPolicy kMojoLevels[] = { |
| blink::mojom::ProtectionPolicy::NONE, |
| blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED, |
| blink::mojom::ProtectionPolicy::UV_REQUIRED, |
| }; |
| constexpr device::CredProtect kDeviceLevels[] = { |
| device::CredProtect::kUVOptional, |
| device::CredProtect::kUVOrCredIDRequired, |
| device::CredProtect::kUVRequired, |
| }; |
| |
| for (int requested_level = 0; requested_level < 3; requested_level++) { |
| for (int forced_level = 1; forced_level < 3; forced_level++) { |
| SCOPED_TRACE(::testing::Message() << "requested=" << requested_level); |
| SCOPED_TRACE(::testing::Message() << "forced=" << forced_level); |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.resident_key_support = true; |
| config.cred_protect_support = true; |
| config.force_cred_protect = kDeviceLevels[forced_level]; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->registrations.clear(); |
| |
| PublicKeyCredentialCreationOptionsPtr options = make_credential_options(); |
| options->authenticator_selection->resident_key = |
| device::ResidentKeyRequirement::kRequired; |
| options->protection_policy = kMojoLevels[requested_level]; |
| options->authenticator_selection->user_verification_requirement = |
| device::UserVerificationRequirement::kRequired; |
| |
| AuthenticatorStatus status = |
| AuthenticatorMakeCredential(std::move(options)).status; |
| |
| if (requested_level <= forced_level) { |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, status); |
| ASSERT_EQ( |
| 1u, virtual_device_factory_->mutable_state()->registrations.size()); |
| const absl::optional<device::CredProtect> result = |
| virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second.protection; |
| EXPECT_EQ(*result, config.force_cred_protect); |
| } else { |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, status); |
| } |
| } |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, AuthenticatorDefaultCredProtect) { |
| // Some authenticators may have a default credProtect level that isn't |
| // kUVOptional. This has complex interactions that are tested here. |
| constexpr struct { |
| blink::mojom::ProtectionPolicy requested_level; |
| device::CredProtect authenticator_default; |
| device::CredProtect result; |
| } kExpectations[] = { |
| // Standard case: normal authenticator and nothing specified. Chrome sets |
| // a default of kUVOrCredIDRequired for discoverable credentials. |
| { |
| blink::mojom::ProtectionPolicy::UNSPECIFIED, |
| device::CredProtect::kUVOptional, |
| device::CredProtect::kUVOrCredIDRequired, |
| }, |
| // Chrome's default of |kUVOrCredIDRequired| should not prevent a site |
| // from requesting |kUVRequired| from a normal authenticator. |
| { |
| blink::mojom::ProtectionPolicy::UV_REQUIRED, |
| device::CredProtect::kUVOptional, |
| device::CredProtect::kUVRequired, |
| }, |
| // Authenticator has a non-standard default, which should work fine. |
| { |
| blink::mojom::ProtectionPolicy::UNSPECIFIED, |
| device::CredProtect::kUVOrCredIDRequired, |
| device::CredProtect::kUVOrCredIDRequired, |
| }, |
| // Authenticators can have a default of kUVRequired, but Chrome has a |
| // default of kUVOrCredIDRequired for discoverable credentials. We should |
| // not get a lesser protection level because of that. |
| { |
| blink::mojom::ProtectionPolicy::UNSPECIFIED, |
| device::CredProtect::kUVRequired, |
| device::CredProtect::kUVRequired, |
| }, |
| // Site should be able to explicitly set credProtect kUVOptional despite |
| // an authenticator default. |
| { |
| blink::mojom::ProtectionPolicy::NONE, |
| device::CredProtect::kUVOrCredIDRequired, |
| device::CredProtect::kUVOptional, |
| }, |
| }; |
| |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.resident_key_support = true; |
| config.cred_protect_support = true; |
| |
| for (const auto& test : kExpectations) { |
| config.default_cred_protect = test.authenticator_default; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->registrations.clear(); |
| |
| SCOPED_TRACE(::testing::Message() |
| << "result=" << CredProtectDescription(test.result)); |
| SCOPED_TRACE(::testing::Message() |
| << "default=" |
| << CredProtectDescription(test.authenticator_default)); |
| SCOPED_TRACE(::testing::Message() |
| << "request=" |
| << ProtectionPolicyDescription(test.requested_level)); |
| |
| PublicKeyCredentialCreationOptionsPtr options = make_credential_options(); |
| options->authenticator_selection->resident_key = |
| device::ResidentKeyRequirement::kRequired; |
| options->protection_policy = test.requested_level; |
| options->authenticator_selection->user_verification_requirement = |
| device::UserVerificationRequirement::kRequired; |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| ASSERT_EQ(1u, |
| virtual_device_factory_->mutable_state()->registrations.size()); |
| const device::CredProtect result = virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second.protection; |
| |
| EXPECT_EQ(result, test.result) << CredProtectDescription(result); |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, ProtectedNonResidentCreds) { |
| // Until we have UVToken, there's a danger that we'll preflight UV-required |
| // credential IDs such that the authenticator denies knowledge of all of them |
| // for silent requests and then we fail the whole request. |
| device::VirtualCtap2Device::Config config; |
| config.pin_support = true; |
| config.resident_key_support = true; |
| config.cred_protect_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId)); |
| ASSERT_EQ(1u, virtual_device_factory_->mutable_state()->registrations.size()); |
| virtual_device_factory_->mutable_state() |
| ->registrations.begin() |
| ->second.protection = device::CredProtect::kUVRequired; |
| |
| // |SelectAccount| should not be called when there's only a single response. |
| test_client_.delegate_config.expected_accounts = "<invalid>"; |
| |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| options->allow_credentials = GetTestCredentials(5); |
| options->allow_credentials[0].id = {4, 3, 2, 1}; |
| |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, WithAppIDExtension) { |
| // Setting an AppID value for a resident-key request should be ignored. |
| device::VirtualCtap2Device::Config config; |
| config.u2f_support = true; |
| config.pin_support = true; |
| config.resident_key_support = true; |
| config.cred_protect_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, absl::nullopt)); |
| |
| // |SelectAccount| should not be called when there's only a single response |
| // without identifying information. |
| test_client_.delegate_config.expected_accounts = "<invalid>"; |
| |
| PublicKeyCredentialRequestOptionsPtr options = get_credential_options(); |
| options->appid = kTestOrigin1; |
| |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| EXPECT_TRUE(HasUV(result.response)); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| // Requests with a credProtect extension that have |enforce_protection_policy| |
| // set should be rejected if the Windows WebAuthn API doesn't support |
| // credProtect. |
| TEST_F(ResidentKeyAuthenticatorImplTest, WinCredProtectApiVersion) { |
| // The canned response returned by the Windows API fake is for acme.com. |
| fake_win_webauthn_api_.set_available(true); |
| NavigateAndCommit(GURL("https://acme.com")); |
| for (const bool supports_cred_protect : {false, true}) { |
| SCOPED_TRACE(testing::Message() |
| << "supports_cred_protect: " << supports_cred_protect); |
| |
| fake_win_webauthn_api_.set_version(supports_cred_protect |
| ? WEBAUTHN_API_VERSION_2 |
| : WEBAUTHN_API_VERSION_1); |
| |
| PublicKeyCredentialCreationOptionsPtr options = make_credential_options(); |
| options->relying_party = device::PublicKeyCredentialRpEntity(); |
| options->relying_party.id = device::test_data::kRelyingPartyId; |
| options->relying_party.name = ""; |
| options->authenticator_selection->user_verification_requirement = |
| device::UserVerificationRequirement::kRequired; |
| options->authenticator_selection->resident_key = |
| device::ResidentKeyRequirement::kRequired; |
| options->protection_policy = |
| blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED; |
| options->enforce_protection_policy = true; |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| supports_cred_protect ? AuthenticatorStatus::SUCCESS |
| : AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| } |
| } |
| |
| // Tests that the incognito flag is plumbed through conditional UI requests. |
| TEST_F(ResidentKeyAuthenticatorImplTest, ConditionalUI_Incognito) { |
| fake_win_webauthn_api_.set_available(true); |
| fake_win_webauthn_api_.set_version(WEBAUTHN_API_VERSION_4); |
| fake_win_webauthn_api_.set_supports_silent_discovery(true); |
| device::PublicKeyCredentialRpEntity rp(kTestRelyingPartyId); |
| device::PublicKeyCredentialUserEntity user({1, 2, 3, 4}); |
| fake_win_webauthn_api_.InjectDiscoverableCredential( |
| /*credential_id=*/{{4, 3, 2, 1}}, std::move(rp), std::move(user)); |
| |
| // |SelectAccount| should not be called for conditional UI requests. |
| test_client_.delegate_config.expected_accounts = "<invalid>"; |
| test_client_.delegate_config.expect_conditional = true; |
| |
| for (bool is_off_the_record : {true, false}) { |
| SCOPED_TRACE(is_off_the_record ? "off the record" : "on the record"); |
| static_cast<TestBrowserContext*>(GetBrowserContext()) |
| ->set_is_off_the_record(is_off_the_record); |
| PublicKeyCredentialRequestOptionsPtr options(get_credential_options()); |
| options->is_conditional = true; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| ASSERT_TRUE(fake_win_webauthn_api_.last_get_credentials_options()); |
| EXPECT_EQ(fake_win_webauthn_api_.last_get_credentials_options() |
| ->bBrowserInPrivateMode, |
| is_off_the_record); |
| } |
| } |
| |
| // Tests that attempting to make a credential with large blob = required and |
| // attachment = platform on Windows fails and the request is not sent to the |
| // WebAuthn API. |
| // This is because largeBlob = required is ignored by the Windows platform |
| // authenticator at the time of writing (Feb 2023). |
| TEST_F(ResidentKeyAuthenticatorImplTest, MakeCredentialLargeBlobWinPlatform) { |
| fake_win_webauthn_api_.set_available(true); |
| fake_win_webauthn_api_.set_version(WEBAUTHN_API_VERSION_3); |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->large_blob_enable = device::LargeBlobSupport::kRequired; |
| options->authenticator_selection->resident_key = |
| device::ResidentKeyRequirement::kRequired; |
| options->authenticator_selection->authenticator_attachment = |
| device::AuthenticatorAttachment::kPlatform; |
| MakeCredentialResult result = AuthenticatorMakeCredential(std::move(options)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_FALSE(fake_win_webauthn_api_.last_make_credential_options()); |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| |
| // Tests that chrome does not attempt setting the PRF extension during a |
| // PinUvAuthToken GetAssertion request if it is not supported by the |
| // authenticator. |
| // Regression test for crbug.com/1408786. |
| TEST_F(ResidentKeyAuthenticatorImplTest, PRFNotSupportedWithPinUvAuthToken) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| device::VirtualCtap2Device::Config config; |
| config.resident_key_support = true; |
| config.u2f_support = true; |
| config.pin_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.hmac_secret_support = false; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| virtual_device_factory_->mutable_state()->pin = kTestPIN; |
| virtual_device_factory_->mutable_state()->pin_retries = |
| device::kMaxPinRetries; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->user_verification = device::UserVerificationRequirement::kRequired; |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| options->allow_credentials[0].id, options->relying_party_id, |
| std::vector<uint8_t>{1, 2, 3, 4}, absl::nullopt, absl::nullopt)); |
| |
| auto prf_value = blink::mojom::PRFValues::New(); |
| prf_value->first = std::vector<uint8_t>(32, 1); |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value)); |
| options->prf = true; |
| options->prf_inputs = std::move(inputs); |
| options->allow_credentials.clear(); |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, PRFExtension) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| for (bool use_prf_extension_instead : {false, true}) { |
| SCOPED_TRACE(use_prf_extension_instead); |
| absl::optional<device::PublicKeyCredentialDescriptor> credential; |
| for (bool authenticator_support : {false, true}) { |
| // Setting the PRF extension on an authenticator that doesn't support it |
| // should cause the extension to be echoed, but with enabled=false. |
| // Otherwise, enabled should be true. |
| device::VirtualCtap2Device::Config config; |
| if (authenticator_support) { |
| config.prf_support = use_prf_extension_instead; |
| config.hmac_secret_support = !use_prf_extension_instead; |
| } |
| config.internal_account_chooser = config.prf_support; |
| config.always_uv = config.prf_support; |
| config.max_credential_count_in_list = 3; |
| config.max_credential_id_length = 256; |
| config.pin_support = true; |
| config.resident_key_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->prf_enable = true; |
| options->authenticator_selection->resident_key = |
| authenticator_support ? device::ResidentKeyRequirement::kRequired |
| : device::ResidentKeyRequirement::kDiscouraged; |
| options->user.id = {1, 2, 3, 4}; |
| options->user.name = "name"; |
| options->user.display_name = "displayName"; |
| MakeCredentialResult result = |
| AuthenticatorMakeCredential(std::move(options)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| |
| ASSERT_TRUE(result.response->echo_prf); |
| ASSERT_EQ(result.response->prf, authenticator_support); |
| |
| if (authenticator_support) { |
| device::AuthenticatorData auth_data = |
| AuthDataFromMakeCredentialResponse(result.response); |
| credential.emplace(device::CredentialType::kPublicKey, |
| auth_data.GetCredentialId()); |
| } |
| } |
| |
| auto assertion = [&](std::vector<blink::mojom::PRFValuesPtr> inputs, |
| unsigned allow_list_size = 1, |
| device::UserVerificationRequirement uv = |
| device::UserVerificationRequirement::kPreferred) |
| -> blink::mojom::PRFValuesPtr { |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->prf = true; |
| options->prf_inputs = std::move(inputs); |
| options->allow_credentials.clear(); |
| options->user_verification = uv; |
| if (allow_list_size >= 1) { |
| for (unsigned i = 0; i < allow_list_size - 1; i++) { |
| std::vector<uint8_t> random_credential_id(32, |
| static_cast<uint8_t>(i)); |
| options->allow_credentials.emplace_back( |
| device::CredentialType::kPublicKey, |
| std::move(random_credential_id)); |
| } |
| options->allow_credentials.push_back(*credential); |
| } |
| |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| CHECK(result.response->prf_results); |
| CHECK(!result.response->prf_results->id); |
| return std::move(result.response->prf_results); |
| }; |
| |
| const std::vector<uint8_t> salt1(32, 1); |
| const std::vector<uint8_t> salt2(32, 2); |
| std::vector<uint8_t> salt1_eval; |
| std::vector<uint8_t> salt2_eval; |
| |
| { |
| auto prf_value = blink::mojom::PRFValues::New(); |
| prf_value->first = salt1; |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value)); |
| auto result = assertion(std::move(inputs)); |
| salt1_eval = std::move(result->first); |
| } |
| |
| // The result should be consistent |
| { |
| auto prf_value = blink::mojom::PRFValues::New(); |
| prf_value->first = salt1; |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value)); |
| auto result = assertion(std::move(inputs)); |
| ASSERT_EQ(result->first, salt1_eval); |
| } |
| |
| // Security keys will use a different PRF if UV isn't done. But the PRF |
| // extension should always get the UV PRF so uv=discouraged shouldn't |
| // change the output. |
| { |
| auto prf_value = blink::mojom::PRFValues::New(); |
| prf_value->first = salt1; |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value)); |
| auto result = |
| assertion(std::move(inputs), 1, |
| device::UserVerificationRequirement::kDiscouraged); |
| ASSERT_EQ(result->first, salt1_eval); |
| } |
| |
| // Should be able to evaluate two points at once. |
| { |
| auto prf_value = blink::mojom::PRFValues::New(); |
| prf_value->first = salt1; |
| prf_value->second = salt2; |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value)); |
| auto result = assertion(std::move(inputs)); |
| ASSERT_EQ(result->first, salt1_eval); |
| ASSERT_TRUE(result->second); |
| salt2_eval = std::move(*result->second); |
| ASSERT_NE(salt1_eval, salt2_eval); |
| } |
| |
| // Should be consistent if swapped. |
| { |
| auto prf_value = blink::mojom::PRFValues::New(); |
| prf_value->first = salt2; |
| prf_value->second = salt1; |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value)); |
| auto result = assertion(std::move(inputs)); |
| ASSERT_EQ(result->first, salt2_eval); |
| ASSERT_TRUE(result->second); |
| ASSERT_EQ(*result->second, salt1_eval); |
| } |
| |
| // Should still trigger if the credential ID is specified |
| { |
| auto prf_value = blink::mojom::PRFValues::New(); |
| prf_value->id.emplace(credential->id); |
| prf_value->first = salt1; |
| prf_value->second = salt2; |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value)); |
| auto result = assertion(std::move(inputs)); |
| ASSERT_EQ(result->first, salt1_eval); |
| ASSERT_TRUE(result->second); |
| ASSERT_EQ(*result->second, salt2_eval); |
| } |
| |
| // And the specified credential ID should override any default inputs. |
| { |
| auto prf_value1 = blink::mojom::PRFValues::New(); |
| prf_value1->first = std::vector<uint8_t>(32, 3); |
| auto prf_value2 = blink::mojom::PRFValues::New(); |
| prf_value2->id.emplace(credential->id); |
| prf_value2->first = salt1; |
| prf_value2->second = salt2; |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value1)); |
| inputs.emplace_back(std::move(prf_value2)); |
| auto result = assertion(std::move(inputs)); |
| ASSERT_EQ(result->first, salt1_eval); |
| ASSERT_TRUE(result->second); |
| ASSERT_EQ(*result->second, salt2_eval); |
| } |
| |
| // ... and that should still be true if there there are lots of dummy |
| // entries in the allowlist. Note that the virtual authenticator was |
| // configured such that this will cause multiple batches. |
| { |
| auto prf_value = blink::mojom::PRFValues::New(); |
| prf_value->id.emplace(credential->id); |
| prf_value->first = salt1; |
| prf_value->second = salt2; |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value)); |
| auto result = assertion(std::move(inputs), /*allowlist_size=*/20); |
| ASSERT_EQ(result->first, salt1_eval); |
| ASSERT_TRUE(result->second); |
| ASSERT_EQ(*result->second, salt2_eval); |
| } |
| |
| // Default PRF values should be passed down when the allowlist is empty. |
| { |
| auto prf_value = blink::mojom::PRFValues::New(); |
| prf_value->first = salt1; |
| prf_value->second = salt2; |
| test_client_.delegate_config.expected_accounts = |
| "01020304:name:displayName"; |
| test_client_.delegate_config.selected_user_id = {1, 2, 3, 4}; |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value)); |
| auto result = assertion(std::move(inputs), /*allowlist_size=*/0); |
| ASSERT_EQ(result->first, salt1_eval); |
| ASSERT_TRUE(result->second); |
| ASSERT_EQ(*result->second, salt2_eval); |
| } |
| |
| // And the default PRF values should be used if none of the specific values |
| // match. |
| { |
| auto prf_value1 = blink::mojom::PRFValues::New(); |
| prf_value1->first = salt1; |
| auto prf_value2 = blink::mojom::PRFValues::New(); |
| prf_value2->first = std::vector<uint8_t>(32, 3); |
| prf_value2->id = std::vector<uint8_t>(32, 4); |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value1)); |
| inputs.emplace_back(std::move(prf_value2)); |
| auto result = assertion(std::move(inputs), /*allowlist_size=*/20); |
| ASSERT_EQ(result->first, salt1_eval); |
| ASSERT_FALSE(result->second); |
| } |
| } |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, |
| PRFExtensionOnUnconfiguredAuthenticator) { |
| // If a credential is on a UV-capable, but not UV-configured authenticator and |
| // then an assertion with `prf` is requested there shouldn't be a result |
| // because it would be from the wrong PRF. (This state should only happen when |
| // the credential was created without the `prf` extension, which is an RP |
| // issue.) |
| device::VirtualCtap2Device::Config config; |
| config.hmac_secret_support = true; |
| config.internal_uv_support = true; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| config.resident_key_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| options->allow_credentials[0].id, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, absl::nullopt)); |
| device::VirtualFidoDevice::RegistrationData& registration = |
| virtual_device_factory_->mutable_state()->registrations.begin()->second; |
| const std::array<uint8_t, 32> key1 = {1}; |
| const std::array<uint8_t, 32> key2 = {2}; |
| registration.hmac_key.emplace(key1, key2); |
| |
| auto prf_value = blink::mojom::PRFValues::New(); |
| const std::vector<uint8_t> salt1(32, 1); |
| prf_value->first = salt1; |
| std::vector<blink::mojom::PRFValuesPtr> inputs; |
| inputs.emplace_back(std::move(prf_value)); |
| |
| options->prf = true; |
| options->prf_inputs = std::move(inputs); |
| options->user_verification = |
| device::UserVerificationRequirement::kDiscouraged; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_FALSE(result.response->prf_results); |
| } |
| |
| TEST_F(ResidentKeyAuthenticatorImplTest, ConditionalUI) { |
| device::VirtualCtap2Device::Config config; |
| config.resident_key_support = true; |
| config.internal_uv_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId, |
| /*user_id=*/{{1, 2, 3, 4}}, absl::nullopt, absl::nullopt)); |
| |
| // |SelectAccount| should not be called for conditional UI requests. |
| test_client_.delegate_config.expected_accounts = "<invalid>"; |
| test_client_.delegate_config.expect_conditional = true; |
| PublicKeyCredentialRequestOptionsPtr options(get_credential_options()); |
| options->is_conditional = true; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| EXPECT_EQ(AuthenticatorStatus::SUCCESS, result.status); |
| } |
| |
| // Tests that the AuthenticatorRequestDelegate can choose a known platform |
| // authentictor credential as "preselected", which causes the request to be |
| // specialized to the chosen credential ID and post-request account selection UI |
| // to be skipped. |
| TEST_F(ResidentKeyAuthenticatorImplTest, PreselectDiscoverableCredential) { |
| device::VirtualCtap2Device::Config config; |
| config.resident_key_support = true; |
| config.internal_uv_support = true; |
| virtual_device_factory_->SetCtap2Config(config); |
| virtual_device_factory_->SetTransport( |
| device::FidoTransportProtocol::kInternal); |
| virtual_device_factory_->mutable_state()->fingerprints_enrolled = true; |
| constexpr char kAuthenticatorId[] = "internal-authenticator"; |
| virtual_device_factory_->mutable_state()->device_id_override = |
| kAuthenticatorId; |
| std::vector<uint8_t> kFirstCredentialId{{1, 2, 3, 4}}; |
| std::vector<uint8_t> kSecondCredentialId{{10, 20, 30, 40}}; |
| std::vector<uint8_t> kFirstUserId{{2, 3, 4, 5}}; |
| std::vector<uint8_t> kSecondUserId{{20, 30, 40, 50}}; |
| |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| kFirstCredentialId, kTestRelyingPartyId, kFirstUserId, absl::nullopt, |
| absl::nullopt)); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey( |
| kSecondCredentialId, kTestRelyingPartyId, kSecondUserId, absl::nullopt, |
| absl::nullopt)); |
| |
| // |SelectAccount| should not be called if an account was chosen from |
| // pre-select UI. |
| test_client_.delegate_config.expected_accounts = "<invalid>"; |
| |
| for (const auto& id : {kFirstCredentialId, kSecondCredentialId}) { |
| test_client_.delegate_config.preselected_credential_id = id; |
| test_client_.delegate_config.preselected_authenticator_id = |
| kAuthenticatorId; |
| PublicKeyCredentialRequestOptionsPtr options(get_credential_options()); |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(options)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(result.response->info->raw_id, id); |
| } |
| } |
| |
| class InternalAuthenticatorImplTest : public AuthenticatorTestBase { |
| protected: |
| InternalAuthenticatorImplTest() = default; |
| |
| void SetUp() override { |
| AuthenticatorTestBase::SetUp(); |
| old_client_ = SetBrowserClientForTesting(&test_client_); |
| } |
| |
| void TearDown() override { |
| // The |RenderFrameHost| must outlive |AuthenticatorImpl|. |
| internal_authenticator_impl_.reset(); |
| SetBrowserClientForTesting(old_client_); |
| AuthenticatorTestBase::TearDown(); |
| } |
| |
| void NavigateAndCommit(const GURL& url) { |
| // The |RenderFrameHost| must outlive |AuthenticatorImpl|. |
| internal_authenticator_impl_.reset(); |
| RenderViewHostTestHarness::NavigateAndCommit(url); |
| } |
| |
| InternalAuthenticatorImpl* GetAuthenticator( |
| const url::Origin& effective_origin_url) { |
| internal_authenticator_impl_ = |
| std::make_unique<InternalAuthenticatorImpl>(main_rfh()); |
| internal_authenticator_impl_->SetEffectiveOrigin(effective_origin_url); |
| return internal_authenticator_impl_.get(); |
| } |
| |
| protected: |
| std::unique_ptr<InternalAuthenticatorImpl> internal_authenticator_impl_; |
| TestAuthenticatorContentBrowserClient test_client_; |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| }; |
| |
| // Regression test for crbug.com/1433416. |
| TEST_F(InternalAuthenticatorImplTest, MakeCredentialSkipTLSCheck) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| InternalAuthenticatorImpl* authenticator = |
| GetAuthenticator(url::Origin::Create(GURL(kTestOrigin1))); |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->is_webauthn_security_level_acceptable = false; |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| TestMakeCredentialCallback callback; |
| authenticator->MakeCredential(std::move(options), callback.callback()); |
| callback.WaitForCallback(); |
| EXPECT_EQ(callback.status(), blink::mojom::AuthenticatorStatus::SUCCESS); |
| } |
| |
| // Regression test for crbug.com/1433416. |
| TEST_F(InternalAuthenticatorImplTest, GetAssertionSkipTLSCheck) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| InternalAuthenticatorImpl* authenticator = |
| GetAuthenticator(url::Origin::Create(GURL(kTestOrigin1))); |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->is_webauthn_security_level_acceptable = false; |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, options->relying_party_id)); |
| TestGetAssertionCallback callback; |
| authenticator->GetAssertion(std::move(options), callback.callback()); |
| callback.WaitForCallback(); |
| EXPECT_EQ(callback.status(), blink::mojom::AuthenticatorStatus::SUCCESS); |
| } |
| |
| // Verify behavior for various combinations of origins and RP IDs. |
| TEST_F(InternalAuthenticatorImplTest, MakeCredentialOriginAndRpIds) { |
| // These instances should return security errors (for circumstances |
| // that would normally crash the renderer). |
| for (auto test_case : kInvalidRelyingPartyTestCases) { |
| SCOPED_TRACE(std::string(test_case.claimed_authority) + " " + |
| std::string(test_case.origin)); |
| |
| GURL origin = GURL(test_case.origin); |
| if (url::Origin::Create(origin).opaque()) { |
| // Opaque origins will cause DCHECK to fail. |
| continue; |
| } |
| |
| NavigateAndCommit(origin); |
| InternalAuthenticatorImpl* authenticator = |
| GetAuthenticator(url::Origin::Create(origin)); |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->relying_party.id = test_case.claimed_authority; |
| TestMakeCredentialCallback callback_receiver; |
| authenticator->MakeCredential(std::move(options), |
| callback_receiver.callback()); |
| callback_receiver.WaitForCallback(); |
| EXPECT_EQ(test_case.expected_status, callback_receiver.status()); |
| } |
| |
| // These instances should bypass security errors, by setting the effective |
| // origin to a valid one. |
| for (auto test_case : kValidRelyingPartyTestCases) { |
| SCOPED_TRACE(std::string(test_case.claimed_authority) + " " + |
| std::string(test_case.origin)); |
| |
| NavigateAndCommit(GURL("https://this.isthewrong.origin")); |
| auto* authenticator = |
| GetAuthenticator(url::Origin::Create(GURL(test_case.origin))); |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->relying_party.id = test_case.claimed_authority; |
| |
| ResetVirtualDevice(); |
| TestMakeCredentialCallback callback_receiver; |
| authenticator->MakeCredential(std::move(options), |
| callback_receiver.callback()); |
| callback_receiver.WaitForCallback(); |
| EXPECT_EQ(test_case.expected_status, callback_receiver.status()); |
| } |
| } |
| |
| // Verify behavior for various combinations of origins and RP IDs. |
| TEST_F(InternalAuthenticatorImplTest, GetAssertionOriginAndRpIds) { |
| // These instances should return security errors (for circumstances |
| // that would normally crash the renderer). |
| for (const OriginClaimedAuthorityPair& test_case : |
| kInvalidRelyingPartyTestCases) { |
| SCOPED_TRACE(std::string(test_case.claimed_authority) + " " + |
| std::string(test_case.origin)); |
| |
| GURL origin = GURL(test_case.origin); |
| if (url::Origin::Create(origin).opaque()) { |
| // Opaque origins will cause DCHECK to fail. |
| continue; |
| } |
| |
| NavigateAndCommit(origin); |
| InternalAuthenticatorImpl* authenticator = |
| GetAuthenticator(url::Origin::Create(origin)); |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->relying_party_id = test_case.claimed_authority; |
| |
| TestGetAssertionCallback callback_receiver; |
| authenticator->GetAssertion(std::move(options), |
| callback_receiver.callback()); |
| callback_receiver.WaitForCallback(); |
| EXPECT_EQ(test_case.expected_status, callback_receiver.status()); |
| } |
| |
| // These instances should bypass security errors, by setting the effective |
| // origin to a valid one. |
| for (const OriginClaimedAuthorityPair& test_case : |
| kValidRelyingPartyTestCases) { |
| SCOPED_TRACE(std::string(test_case.claimed_authority) + " " + |
| std::string(test_case.origin)); |
| |
| NavigateAndCommit(GURL("https://this.isthewrong.origin")); |
| InternalAuthenticatorImpl* authenticator = |
| GetAuthenticator(url::Origin::Create(GURL(test_case.origin))); |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->relying_party_id = test_case.claimed_authority; |
| |
| ResetVirtualDevice(); |
| ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, test_case.claimed_authority)); |
| TestGetAssertionCallback callback_receiver; |
| authenticator->GetAssertion(std::move(options), |
| callback_receiver.callback()); |
| callback_receiver.WaitForCallback(); |
| EXPECT_EQ(test_case.expected_status, callback_receiver.status()); |
| } |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| class TouchIdAuthenticatorImplTest : public AuthenticatorImplTest { |
| protected: |
| using Credential = device::fido::mac::Credential; |
| using CredentialMetadata = device::fido::mac::CredentialMetadata; |
| |
| void SetUp() override { |
| AuthenticatorImplTest::SetUp(); |
| test_client_.web_authentication_delegate.touch_id_authenticator_config = |
| config_; |
| test_client_.web_authentication_delegate.supports_resident_keys = true; |
| old_client_ = SetBrowserClientForTesting(&test_client_); |
| } |
| |
| void TearDown() override { |
| SetBrowserClientForTesting(old_client_); |
| AuthenticatorImplTest::TearDown(); |
| } |
| |
| void ResetVirtualDevice() override {} |
| |
| std::vector<Credential> GetCredentials(const std::string& rp_id) { |
| return device::fido::mac::TouchIdCredentialStore::FindCredentialsForTesting( |
| config_, rp_id); |
| } |
| |
| TestAuthenticatorContentBrowserClient test_client_; |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| device::fido::mac::AuthenticatorConfig config_{ |
| .keychain_access_group = "test-keychain-access-group", |
| .metadata_secret = "TestMetadataSecret"}; |
| device::fido::mac::ScopedTouchIdTestEnvironment touch_id_test_environment_{ |
| config_}; |
| }; |
| |
| TEST_F(TouchIdAuthenticatorImplTest, IsUVPAA) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| for (const bool touch_id_available : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "touch_id_available=" << touch_id_available); |
| touch_id_test_environment_.SetTouchIdAvailable(touch_id_available); |
| EXPECT_EQ(AuthenticatorIsUvpaa(), touch_id_available); |
| } |
| } |
| |
| TEST_F(TouchIdAuthenticatorImplTest, MakeCredential) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| auto options = GetTestPublicKeyCredentialCreationOptions(); |
| options->authenticator_selection->authenticator_attachment = |
| device::AuthenticatorAttachment::kPlatform; |
| touch_id_test_environment_.SimulateTouchIdPromptSuccess(); |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| auto credentials = GetCredentials(kTestRelyingPartyId); |
| EXPECT_EQ(credentials.size(), 1u); |
| const CredentialMetadata& metadata = credentials.at(0).metadata; |
| // New credentials are always created discoverable. |
| EXPECT_TRUE(metadata.is_resident); |
| auto expected_user = GetTestPublicKeyCredentialUserEntity(); |
| EXPECT_EQ(metadata.ToPublicKeyCredentialUserEntity(), expected_user); |
| } |
| |
| TEST_F(TouchIdAuthenticatorImplTest, OptionalUv) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| for (const auto uv : {device::UserVerificationRequirement::kDiscouraged, |
| device::UserVerificationRequirement::kPreferred, |
| device::UserVerificationRequirement::kRequired}) { |
| auto options = GetTestPublicKeyCredentialCreationOptions(); |
| options->authenticator_selection->authenticator_attachment = |
| device::AuthenticatorAttachment::kPlatform; |
| options->authenticator_selection->user_verification_requirement = uv; |
| bool requires_uv = uv == device::UserVerificationRequirement::kRequired; |
| if (requires_uv) { |
| touch_id_test_environment_.SimulateTouchIdPromptSuccess(); |
| } else { |
| touch_id_test_environment_.DoNotResolveNextPrompt(); |
| } |
| auto result = AuthenticatorMakeCredential(std::move(options)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(HasUV(result.response), requires_uv); |
| auto credentials = GetCredentials(kTestRelyingPartyId); |
| EXPECT_EQ(credentials.size(), 1u); |
| |
| auto assertion_options = GetTestPublicKeyCredentialRequestOptions(); |
| assertion_options->user_verification = uv; |
| assertion_options->allow_credentials = |
| std::vector<device::PublicKeyCredentialDescriptor>( |
| {{device::CredentialType::kPublicKey, |
| credentials[0].credential_id}}); |
| if (requires_uv) { |
| touch_id_test_environment_.SimulateTouchIdPromptSuccess(); |
| } else { |
| touch_id_test_environment_.DoNotResolveNextPrompt(); |
| } |
| auto assertion = AuthenticatorGetAssertion(std::move(assertion_options)); |
| EXPECT_EQ(assertion.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(HasUV(assertion.response), requires_uv); |
| } |
| } |
| |
| TEST_F(TouchIdAuthenticatorImplTest, MakeCredential_Resident) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| auto options = GetTestPublicKeyCredentialCreationOptions(); |
| options->authenticator_selection->authenticator_attachment = |
| device::AuthenticatorAttachment::kPlatform; |
| options->authenticator_selection->resident_key = |
| device::ResidentKeyRequirement::kRequired; |
| touch_id_test_environment_.SimulateTouchIdPromptSuccess(); |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| auto credentials = GetCredentials(kTestRelyingPartyId); |
| EXPECT_EQ(credentials.size(), 1u); |
| EXPECT_TRUE(credentials.at(0).metadata.is_resident); |
| } |
| |
| TEST_F(TouchIdAuthenticatorImplTest, MakeCredential_Eviction) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| mojo::Remote<blink::mojom::Authenticator> authenticator = |
| ConnectToAuthenticator(); |
| |
| // A resident credential will overwrite the non-resident one. |
| auto options = GetTestPublicKeyCredentialCreationOptions(); |
| options->authenticator_selection->authenticator_attachment = |
| device::AuthenticatorAttachment::kPlatform; |
| options->authenticator_selection->resident_key = |
| device::ResidentKeyRequirement::kRequired; |
| touch_id_test_environment_.SimulateTouchIdPromptSuccess(); |
| EXPECT_EQ(AuthenticatorMakeCredential(options->Clone()).status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(GetCredentials(kTestRelyingPartyId).size(), 1u); |
| |
| // Another resident credential for the same user will evict the previous one. |
| touch_id_test_environment_.SimulateTouchIdPromptSuccess(); |
| EXPECT_EQ(AuthenticatorMakeCredential(options->Clone()).status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(GetCredentials(kTestRelyingPartyId).size(), 1u); |
| |
| // But a resident credential for a different user shouldn't. |
| touch_id_test_environment_.SimulateTouchIdPromptSuccess(); |
| options->user.id = std::vector<uint8_t>({99}); |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(GetCredentials(kTestRelyingPartyId).size(), 2u); |
| |
| // Neither should a credential for a different RP. |
| touch_id_test_environment_.SimulateTouchIdPromptSuccess(); |
| options = GetTestPublicKeyCredentialCreationOptions(); |
| options->authenticator_selection->authenticator_attachment = |
| device::AuthenticatorAttachment::kPlatform; |
| options->relying_party.id = "a.google.com"; |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(GetCredentials(kTestRelyingPartyId).size(), 2u); |
| } |
| |
| class ICloudKeychainAuthenticatorImplTest : public AuthenticatorImplTest { |
| protected: |
| class InspectTAIAuthenticatorRequestDelegate |
| : public AuthenticatorRequestClientDelegate { |
| public: |
| using Callback = base::RepeatingCallback<void( |
| const device::FidoRequestHandlerBase::TransportAvailabilityInfo&)>; |
| explicit InspectTAIAuthenticatorRequestDelegate(Callback callback) |
| : callback_(std::move(callback)) {} |
| |
| void OnTransportAvailabilityEnumerated( |
| device::FidoRequestHandlerBase::TransportAvailabilityInfo tai) |
| override { |
| callback_.Run(tai); |
| } |
| |
| private: |
| Callback callback_; |
| }; |
| |
| class InspectTAIContentBrowserClient : public ContentBrowserClient { |
| public: |
| explicit InspectTAIContentBrowserClient( |
| InspectTAIAuthenticatorRequestDelegate::Callback callback) |
| : callback_(std::move(callback)) {} |
| |
| std::unique_ptr<AuthenticatorRequestClientDelegate> |
| GetWebAuthenticationRequestDelegate( |
| RenderFrameHost* render_frame_host) override { |
| return std::make_unique<InspectTAIAuthenticatorRequestDelegate>( |
| callback_); |
| } |
| |
| private: |
| InspectTAIAuthenticatorRequestDelegate::Callback callback_; |
| }; |
| |
| void SetUp() override { |
| AuthenticatorImplTest::SetUp(); |
| old_client_ = SetBrowserClientForTesting(&test_client_); |
| // This test uses the real discoveries and sets the transports on an |
| // allowlist entry to limit it to kInternal. |
| AuthenticatorEnvironment::GetInstance()->Reset(); |
| } |
| |
| void TearDown() override { |
| SetBrowserClientForTesting(old_client_); |
| AuthenticatorImplTest::TearDown(); |
| } |
| |
| void OnTransportAvailabilityEnumerated( |
| const device::FidoRequestHandlerBase::TransportAvailabilityInfo& tai) { |
| if (tai_callback_) { |
| std::move(tai_callback_).Run(tai); |
| } |
| } |
| |
| static std::vector<device::DiscoverableCredentialMetadata> GetCredentials() { |
| device::DiscoverableCredentialMetadata metadata( |
| device::AuthenticatorType::kICloudKeychain, kTestRelyingPartyId, |
| {1, 2, 3, 4}, {{5, 6, 7, 8}, "name", "displayName"}); |
| return {std::move(metadata)}; |
| } |
| |
| InspectTAIContentBrowserClient test_client_{base::BindRepeating( |
| &ICloudKeychainAuthenticatorImplTest::OnTransportAvailabilityEnumerated, |
| base::Unretained(this))}; |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| InspectTAIAuthenticatorRequestDelegate::Callback tai_callback_; |
| }; |
| |
| TEST_F(ICloudKeychainAuthenticatorImplTest, Discovery) { |
| if (__builtin_available(macOS 13.3, *)) { |
| for (const bool feature_enabled : {false, true}) { |
| SCOPED_TRACE(feature_enabled); |
| |
| absl::optional<base::test::ScopedFeatureList> scoped_feature_list; |
| if (feature_enabled) { |
| scoped_feature_list.emplace(device::kWebAuthnICloudKeychain); |
| } |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| device::fido::icloud_keychain::ScopedTestEnvironment test_environment( |
| GetCredentials()); |
| bool tai_seen = false; |
| tai_callback_ = base::BindLambdaForTesting( |
| [&tai_seen, feature_enabled]( |
| const device::FidoRequestHandlerBase::TransportAvailabilityInfo& |
| tai) { |
| tai_seen = true; |
| CHECK_EQ(tai.has_icloud_keychain, feature_enabled); |
| CHECK_EQ(tai.recognized_platform_authenticator_credentials.size(), |
| feature_enabled ? 1u : 0u); |
| CHECK_EQ(tai.has_icloud_keychain_credential, |
| feature_enabled |
| ? device::FidoRequestHandlerBase:: |
| RecognizedCredential::kHasRecognizedCredential |
| : device::FidoRequestHandlerBase:: |
| RecognizedCredential::kNoRecognizedCredential); |
| |
| if (feature_enabled) { |
| CHECK_EQ(tai.recognized_platform_authenticator_credentials[0] |
| .user.name.value(), |
| "name"); |
| } |
| }); |
| |
| auto options = GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials.clear(); |
| options->allow_credentials.push_back( |
| device::PublicKeyCredentialDescriptor( |
| device::CredentialType::kPublicKey, {1, 2, 3, 4}, |
| {device::FidoTransportProtocol::kInternal})); |
| const auto result = AuthenticatorGetAssertion(std::move(options)); |
| EXPECT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_TRUE(tai_seen); |
| } |
| } else { |
| GTEST_SKIP() << "Need macOS 13.3 for this test"; |
| } |
| } |
| |
| #endif // BUILDFLAG(IS_MAC) |
| |
| // AuthenticatorCableV2Test tests features of the caBLEv2 transport and |
| // protocol. |
| class AuthenticatorCableV2Test : public AuthenticatorImplRequestDelegateTest { |
| public: |
| void SetUp() override { |
| AuthenticatorImplTest::SetUp(); |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| old_client_ = SetBrowserClientForTesting(&browser_client_); |
| |
| bssl::UniquePtr<EC_GROUP> p256( |
| EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); |
| bssl::UniquePtr<EC_KEY> peer_identity(EC_KEY_derive_from_secret( |
| p256.get(), zero_seed_.data(), zero_seed_.size())); |
| CHECK_EQ(sizeof(peer_identity_x962_), |
| EC_POINT_point2oct( |
| p256.get(), EC_KEY_get0_public_key(peer_identity.get()), |
| POINT_CONVERSION_UNCOMPRESSED, peer_identity_x962_, |
| sizeof(peer_identity_x962_), /*ctx=*/nullptr)); |
| |
| std::tie(ble_advert_callback_, ble_advert_events_) = |
| device::cablev2::Discovery::AdvertEventStream::New(); |
| } |
| |
| void TearDown() override { |
| // Ensure that all pending caBLE connections have timed out and closed. |
| task_environment()->FastForwardBy(base::Minutes(10)); |
| |
| SetBrowserClientForTesting(old_client_); |
| AuthenticatorImplTest::TearDown(); |
| |
| // All `EstablishedConnection` instances should have been destroyed. |
| CHECK_EQ(device::cablev2::FidoTunnelDevice:: |
| GetNumEstablishedConnectionInstancesForTesting(), |
| 0); |
| } |
| |
| base::RepeatingCallback<void(std::unique_ptr<device::cablev2::Pairing>)> |
| GetPairingCallback() { |
| return base::BindRepeating(&AuthenticatorCableV2Test::OnNewPairing, |
| base::Unretained(this)); |
| } |
| |
| base::RepeatingCallback<void(size_t)> GetInvalidatedPairingCallback() { |
| return base::BindRepeating(&AuthenticatorCableV2Test::OnInvalidatedPairing, |
| base::Unretained(this)); |
| } |
| |
| base::RepeatingCallback<void(Event)> GetEventCallback() { |
| return base::BindRepeating(&AuthenticatorCableV2Test::OnCableEvent, |
| base::Unretained(this)); |
| } |
| |
| void EnableConnectionSignalAtTunnelServer() { |
| // Recreate the tunnel server so that it supports the connection signal. |
| network_context_ = device::cablev2::NewMockTunnelServer( |
| base::BindRepeating(&AuthenticatorCableV2Test::OnContact, |
| base::Unretained(this)), |
| /*supports_connect_signal=*/true); |
| } |
| |
| protected: |
| class DiscoveryFactory : public device::FidoDiscoveryFactory { |
| public: |
| explicit DiscoveryFactory( |
| std::unique_ptr<device::cablev2::Discovery> discovery) |
| : discovery_(std::move(discovery)) {} |
| |
| std::vector<std::unique_ptr<device::FidoDiscoveryBase>> Create( |
| device::FidoTransportProtocol transport) override { |
| if (transport != device::FidoTransportProtocol::kHybrid || !discovery_) { |
| return {}; |
| } |
| |
| return SingleDiscovery(std::move(discovery_)); |
| } |
| |
| private: |
| std::unique_ptr<device::cablev2::Discovery> discovery_; |
| }; |
| |
| class TestAuthenticationDelegate : public WebAuthenticationDelegate { |
| public: |
| bool SupportsResidentKeys(RenderFrameHost*) override { return true; } |
| |
| bool IsFocused(WebContents* web_contents) override { return true; } |
| }; |
| |
| class ContactWhenReadyAuthenticatorRequestDelegate |
| : public AuthenticatorRequestClientDelegate { |
| public: |
| explicit ContactWhenReadyAuthenticatorRequestDelegate( |
| base::RepeatingClosure callback) |
| : callback_(callback) {} |
| ~ContactWhenReadyAuthenticatorRequestDelegate() override = default; |
| |
| void OnTransportAvailabilityEnumerated( |
| device::FidoRequestHandlerBase::TransportAvailabilityInfo) override { |
| callback_.Run(); |
| } |
| |
| private: |
| base::RepeatingClosure callback_; |
| }; |
| |
| class ContactWhenReadyContentBrowserClient : public ContentBrowserClient { |
| public: |
| explicit ContactWhenReadyContentBrowserClient( |
| base::RepeatingClosure callback) |
| : callback_(callback) {} |
| |
| std::unique_ptr<AuthenticatorRequestClientDelegate> |
| GetWebAuthenticationRequestDelegate( |
| RenderFrameHost* render_frame_host) override { |
| return std::make_unique<ContactWhenReadyAuthenticatorRequestDelegate>( |
| callback_); |
| } |
| |
| WebAuthenticationDelegate* GetWebAuthenticationDelegate() override { |
| return &authentication_delegate_; |
| } |
| |
| private: |
| base::RepeatingClosure callback_; |
| TestAuthenticationDelegate authentication_delegate_; |
| }; |
| |
| // MaybeContactPhones is called when OnTransportAvailabilityEnumerated is |
| // called by the request handler. |
| void MaybeContactPhones() { |
| if (maybe_contact_phones_callback_) { |
| std::move(maybe_contact_phones_callback_).Run(); |
| } |
| } |
| |
| void OnContact( |
| base::span<const uint8_t, device::cablev2::kTunnelIdSize> tunnel_id, |
| base::span<const uint8_t, device::cablev2::kPairingIDSize> pairing_id, |
| base::span<const uint8_t, device::cablev2::kClientNonceSize> client_nonce, |
| const std::string& request_type_hint) { |
| std::move(contact_callback_) |
| .Run(tunnel_id, pairing_id, client_nonce, request_type_hint); |
| } |
| |
| void OnNewPairing(std::unique_ptr<device::cablev2::Pairing> pairing) { |
| pairings_.emplace_back(std::move(pairing)); |
| } |
| |
| void OnInvalidatedPairing(size_t disabled_public_key_index) { |
| // When testing failed contacts, only a single pairing is supported |
| // otherwise a more complex way of handling the indexes will be needed. |
| CHECK(disabled_public_key_index == 0 && pairings_.size() == 1); |
| pairings_.clear(); |
| } |
| |
| void OnCableEvent(Event event) { events_.push_back(event); } |
| |
| void DoPairingConnection() { |
| // First do unpaired exchange to get pairing data. |
| auto discovery = std::make_unique<device::cablev2::Discovery>( |
| device::FidoRequestType::kGetAssertion, network_context_.get(), |
| qr_generator_key_, std::move(ble_advert_events_), |
| /*pairings=*/std::vector<std::unique_ptr<device::cablev2::Pairing>>(), |
| /*contact_device_stream=*/nullptr, |
| /*extension_contents=*/std::vector<device::CableDiscoveryData>(), |
| GetPairingCallback(), GetInvalidatedPairingCallback(), |
| GetEventCallback()); |
| |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::make_unique<DiscoveryFactory>(std::move(discovery))); |
| |
| const std::vector<uint8_t> contact_id(/*count=*/200, /*value=*/1); |
| std::unique_ptr<device::cablev2::authenticator::Transaction> transaction = |
| device::cablev2::authenticator::TransactFromQRCode( |
| device::cablev2::authenticator::NewMockPlatform( |
| std::move(ble_advert_callback_), &virtual_device_, |
| /*observer=*/nullptr), |
| network_context_.get(), root_secret_, "Test Authenticator", |
| zero_qr_secret_, peer_identity_x962_, contact_id); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential().status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(pairings_.size(), 1u); |
| |
| // Now do a pairing-based exchange. Generate a random request type hint to |
| // ensure that all values work. |
| device::FidoRequestType request_type = |
| device::FidoRequestType::kMakeCredential; |
| std::string expected_request_type_string = "mc"; |
| if (base::RandDouble() < 0.5) { |
| request_type = device::FidoRequestType::kGetAssertion; |
| expected_request_type_string = "ga"; |
| } |
| |
| std::tie(ble_advert_callback_, ble_advert_events_) = |
| device::cablev2::Discovery::EventStream< |
| base::span<const uint8_t, device::cablev2::kAdvertSize>>::New(); |
| auto callback_and_event_stream = |
| device::cablev2::Discovery::EventStream<size_t>::New(); |
| discovery = std::make_unique<device::cablev2::Discovery>( |
| request_type, network_context_.get(), qr_generator_key_, |
| std::move(ble_advert_events_), std::move(pairings_), |
| std::move(callback_and_event_stream.second), |
| /*extension_contents=*/std::vector<device::CableDiscoveryData>(), |
| GetPairingCallback(), GetInvalidatedPairingCallback(), |
| GetEventCallback()); |
| |
| maybe_contact_phones_callback_ = |
| base::BindLambdaForTesting([&callback_and_event_stream]() { |
| callback_and_event_stream.first.Run(0); |
| }); |
| |
| const std::array<uint8_t, device::cablev2::kRoutingIdSize> routing_id = {0}; |
| bool contact_callback_was_called = false; |
| // When the |cablev2::Discovery| starts it'll make a connection to the |
| // tunnel service with the contact ID from the pairing data. This will be |
| // handled by the |TestNetworkContext| and turned into a call to |
| // |contact_callback_|. This simulates the tunnel server sending a cloud |
| // message to a phone. Given the information from the connection, a |
| // transaction can be created. |
| contact_callback_ = base::BindLambdaForTesting( |
| [this, &transaction, routing_id, contact_id, |
| &contact_callback_was_called, &expected_request_type_string]( |
| base::span<const uint8_t, device::cablev2::kTunnelIdSize> tunnel_id, |
| base::span<const uint8_t, device::cablev2::kPairingIDSize> |
| pairing_id, |
| base::span<const uint8_t, device::cablev2::kClientNonceSize> |
| client_nonce, |
| const std::string& request_type_hint) -> void { |
| contact_callback_was_called = true; |
| CHECK_EQ(request_type_hint, expected_request_type_string); |
| transaction = device::cablev2::authenticator::TransactFromFCM( |
| device::cablev2::authenticator::NewMockPlatform( |
| std::move(ble_advert_callback_), &virtual_device_, |
| /*observer=*/nullptr), |
| network_context_.get(), root_secret_, routing_id, tunnel_id, |
| pairing_id, client_nonce, contact_id); |
| }); |
| |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::make_unique<DiscoveryFactory>(std::move(discovery))); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential().status, |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_TRUE(contact_callback_was_called); |
| } |
| |
| const std::array<uint8_t, device::cablev2::kRootSecretSize> root_secret_ = { |
| 0}; |
| const std::array<uint8_t, device::cablev2::kQRKeySize> qr_generator_key_ = { |
| 0}; |
| const std::array<uint8_t, device::cablev2::kQRSecretSize> zero_qr_secret_ = { |
| 0}; |
| const std::array<uint8_t, device::cablev2::kQRSeedSize> zero_seed_ = {0}; |
| |
| std::unique_ptr<network::mojom::NetworkContext> network_context_ = |
| device::cablev2::NewMockTunnelServer( |
| base::BindRepeating(&AuthenticatorCableV2Test::OnContact, |
| base::Unretained(this))); |
| uint8_t peer_identity_x962_[device::kP256X962Length] = {0}; |
| device::VirtualCtap2Device virtual_device_{DeviceState(), DeviceConfig()}; |
| std::vector<std::unique_ptr<device::cablev2::Pairing>> pairings_; |
| base::OnceCallback<void( |
| base::span<const uint8_t, device::cablev2::kTunnelIdSize> tunnel_id, |
| base::span<const uint8_t, device::cablev2::kPairingIDSize> pairing_id, |
| base::span<const uint8_t, device::cablev2::kClientNonceSize> client_nonce, |
| const std::string& request_type_hint)> |
| contact_callback_; |
| std::unique_ptr<device::cablev2::Discovery::AdvertEventStream> |
| ble_advert_events_; |
| device::cablev2::Discovery::AdvertEventStream::Callback ble_advert_callback_; |
| ContactWhenReadyContentBrowserClient browser_client_{ |
| base::BindRepeating(&AuthenticatorCableV2Test::MaybeContactPhones, |
| base::Unretained(this))}; |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| base::OnceClosure maybe_contact_phones_callback_; |
| std::vector<Event> events_; |
| |
| private: |
| static VirtualCtap2Device::State* DeviceState() { |
| VirtualCtap2Device::State* state = new VirtualCtap2Device::State; |
| state->fingerprints_enrolled = true; |
| return state; |
| } |
| |
| static VirtualCtap2Device::Config DeviceConfig() { |
| // `MockPlatform` uses a virtual device to answer requests, but it can't |
| // handle the credential ID being omitted in responses. |
| VirtualCtap2Device::Config ret; |
| ret.include_credential_in_assertion_response = |
| VirtualCtap2Device::Config::IncludeCredential::ALWAYS; |
| ret.device_public_key_support = true; |
| ret.prf_support = true; |
| ret.internal_account_chooser = true; |
| ret.internal_uv_support = true; |
| ret.always_uv = true; |
| ret.backup_eligible = true; |
| // None attestation is needed because, otherwise, zeroing the AAGUID |
| // invalidates the DPK signature. |
| ret.none_attestation = true; |
| return ret; |
| } |
| }; |
| |
| TEST_F(AuthenticatorCableV2Test, QRBasedWithNoPairing) { |
| auto discovery = std::make_unique<device::cablev2::Discovery>( |
| device::FidoRequestType::kGetAssertion, network_context_.get(), |
| qr_generator_key_, std::move(ble_advert_events_), |
| /*pairings=*/std::vector<std::unique_ptr<device::cablev2::Pairing>>(), |
| /*contact_device_stream=*/nullptr, |
| /*extension_contents=*/std::vector<device::CableDiscoveryData>(), |
| GetPairingCallback(), GetInvalidatedPairingCallback(), |
| GetEventCallback()); |
| |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::make_unique<DiscoveryFactory>(std::move(discovery))); |
| |
| std::unique_ptr<device::cablev2::authenticator::Transaction> transaction = |
| device::cablev2::authenticator::TransactFromQRCode( |
| device::cablev2::authenticator::NewMockPlatform( |
| std::move(ble_advert_callback_), &virtual_device_, |
| /*observer=*/nullptr), |
| network_context_.get(), root_secret_, "Test Authenticator", |
| zero_qr_secret_, peer_identity_x962_, |
| /*contact_id=*/absl::nullopt); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential().status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(pairings_.size(), 0u); |
| } |
| |
| TEST_F(AuthenticatorCableV2Test, HandshakeError) { |
| base::test::ScopedFeatureList scoped_feature_list{ |
| device::kWebAuthnNewHybridUI}; |
| // A handshake error should be fatal to the request with |
| // `kHybridTransportError`. |
| auto discovery = std::make_unique<device::cablev2::Discovery>( |
| device::FidoRequestType::kGetAssertion, network_context_.get(), |
| qr_generator_key_, std::move(ble_advert_events_), |
| /*pairings=*/std::vector<std::unique_ptr<device::cablev2::Pairing>>(), |
| /*contact_device_stream=*/nullptr, |
| /*extension_contents=*/std::vector<device::CableDiscoveryData>(), |
| GetPairingCallback(), GetInvalidatedPairingCallback(), |
| GetEventCallback()); |
| |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::make_unique<DiscoveryFactory>(std::move(discovery))); |
| |
| std::unique_ptr<device::cablev2::authenticator::Transaction> transaction = |
| device::cablev2::authenticator::NewHandshakeErrorDevice( |
| device::cablev2::authenticator::NewMockPlatform( |
| std::move(ble_advert_callback_), &virtual_device_, |
| /*observer=*/nullptr), |
| network_context_.get(), zero_qr_secret_); |
| |
| FailureReasonCallbackReceiver failure_reason_receiver; |
| auto mock_delegate = std::make_unique< |
| ::testing::NiceMock<MockAuthenticatorRequestDelegateObserver>>( |
| failure_reason_receiver.callback()); |
| auto authenticator = ConnectToFakeAuthenticator(std::move(mock_delegate)); |
| |
| TestMakeCredentialCallback callback_receiver; |
| authenticator->MakeCredential(GetTestPublicKeyCredentialCreationOptions(), |
| callback_receiver.callback()); |
| |
| callback_receiver.WaitForCallback(); |
| EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, callback_receiver.status()); |
| |
| ASSERT_TRUE(failure_reason_receiver.was_called()); |
| EXPECT_EQ(AuthenticatorRequestClientDelegate::InterestingFailureReason:: |
| kHybridTransportError, |
| std::get<0>(*failure_reason_receiver.result())); |
| } |
| |
| TEST_F(AuthenticatorCableV2Test, PairingBased) { |
| DoPairingConnection(); |
| |
| const std::vector<Event> kExpectedEvents = { |
| // From the QR connection |
| Event::kBLEAdvertReceived, |
| Event::kReady, |
| // From the paired connection |
| Event::kBLEAdvertReceived, |
| Event::kReady, |
| }; |
| EXPECT_EQ(events_, kExpectedEvents); |
| } |
| |
| TEST_F(AuthenticatorCableV2Test, PairingBasedWithConnectionSignal) { |
| EnableConnectionSignalAtTunnelServer(); |
| DoPairingConnection(); |
| |
| const std::vector<Event> kExpectedEvents = { |
| // From the QR connection |
| Event::kBLEAdvertReceived, |
| Event::kReady, |
| // From the paired connection |
| Event::kPhoneConnected, |
| Event::kBLEAdvertReceived, |
| Event::kReady, |
| }; |
| EXPECT_EQ(events_, kExpectedEvents); |
| } |
| |
| static std::unique_ptr<device::cablev2::Pairing> DummyPairing() { |
| auto ret = std::make_unique<device::cablev2::Pairing>(); |
| ret->tunnel_server_domain = "example.com"; |
| ret->contact_id = {1, 2, 3, 4, 5}; |
| ret->id = {6, 7, 8, 9}; |
| ret->secret = {10, 11, 12, 13}; |
| std::fill(ret->peer_public_key_x962.begin(), ret->peer_public_key_x962.end(), |
| 22); |
| ret->name = __func__; |
| |
| return ret; |
| } |
| |
| TEST_F(AuthenticatorCableV2Test, ContactIDDisabled) { |
| std::vector<std::unique_ptr<device::cablev2::Pairing>> pairings; |
| pairings.emplace_back(DummyPairing()); |
| // Passing |nullopt| as the callback here causes all contact IDs to be |
| // rejected. |
| auto network_context = device::cablev2::NewMockTunnelServer(absl::nullopt); |
| auto callback_and_event_stream = |
| device::cablev2::Discovery::EventStream<size_t>::New(); |
| auto discovery = std::make_unique<device::cablev2::Discovery>( |
| device::FidoRequestType::kGetAssertion, network_context.get(), |
| qr_generator_key_, std::move(ble_advert_events_), std::move(pairings), |
| std::move(callback_and_event_stream.second), |
| /*extension_contents=*/std::vector<device::CableDiscoveryData>(), |
| GetPairingCallback(), GetInvalidatedPairingCallback(), |
| GetEventCallback()); |
| |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::make_unique<DiscoveryFactory>(std::move(discovery))); |
| |
| maybe_contact_phones_callback_ = |
| base::BindLambdaForTesting([&callback_and_event_stream]() { |
| callback_and_event_stream.first.Run(0); |
| }); |
| |
| pairings_.emplace_back(DummyPairing()); |
| ASSERT_EQ(pairings_.size(), 1u); |
| EXPECT_EQ(AuthenticatorMakeCredentialAndWaitForTimeout( |
| GetTestPublicKeyCredentialCreationOptions()) |
| .status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| // The pairing should be been erased because of the signal from the tunnel |
| // server. |
| ASSERT_EQ(pairings_.size(), 0u); |
| } |
| |
| // ServerLinkValues contains keys that mimic those created by a site doing |
| // caBLEv2 server-link. |
| struct ServerLinkValues { |
| // This value would be provided by the site to the desktop, in a caBLE |
| // extension in the get() call. |
| device::CableDiscoveryData desktop_side; |
| |
| // These values would be provided to the phone via a custom mechanism. |
| std::array<uint8_t, device::cablev2::kQRSecretSize> secret; |
| std::array<uint8_t, device::kP256X962Length> peer_identity; |
| }; |
| |
| // CreateServerLink simulates a site doing caBLEv2 server-link and calculates |
| // server-link values that could be sent to the desktop and phone sides of a |
| // transaction. |
| static ServerLinkValues CreateServerLink() { |
| std::vector<uint8_t> seed(device::cablev2::kQRSeedSize); |
| base::RandBytes(seed.data(), seed.size()); |
| |
| bssl::UniquePtr<EC_GROUP> p256( |
| EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); |
| bssl::UniquePtr<EC_KEY> ec_key( |
| EC_KEY_derive_from_secret(p256.get(), seed.data(), seed.size())); |
| |
| ServerLinkValues ret; |
| base::RandBytes(ret.secret.data(), ret.secret.size()); |
| CHECK_EQ(ret.peer_identity.size(), |
| EC_POINT_point2oct(p256.get(), EC_KEY_get0_public_key(ec_key.get()), |
| POINT_CONVERSION_UNCOMPRESSED, |
| ret.peer_identity.data(), |
| ret.peer_identity.size(), /*ctx=*/nullptr)); |
| |
| ret.desktop_side.version = device::CableDiscoveryData::Version::V2; |
| ret.desktop_side.v2.emplace(seed, std::vector<uint8_t>()); |
| ret.desktop_side.v2->server_link_data.insert( |
| ret.desktop_side.v2->server_link_data.end(), ret.secret.begin(), |
| ret.secret.end()); |
| |
| return ret; |
| } |
| |
| TEST_F(AuthenticatorCableV2Test, ServerLink) { |
| const ServerLinkValues server_link_1 = CreateServerLink(); |
| const ServerLinkValues server_link_2 = CreateServerLink(); |
| const std::vector<device::CableDiscoveryData> extension_values = { |
| server_link_1.desktop_side, server_link_2.desktop_side}; |
| |
| auto discovery = std::make_unique<device::cablev2::Discovery>( |
| device::FidoRequestType::kGetAssertion, network_context_.get(), |
| qr_generator_key_, std::move(ble_advert_events_), |
| /*pairings=*/std::vector<std::unique_ptr<device::cablev2::Pairing>>(), |
| /*contact_device_stream=*/nullptr, extension_values, GetPairingCallback(), |
| GetInvalidatedPairingCallback(), GetEventCallback()); |
| |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::make_unique<DiscoveryFactory>(std::move(discovery))); |
| |
| // Both extension values should work, but we can only do a single transaction |
| // per test because a lot of state is setup for a test. Therefore pick one of |
| // the two to check, at random. |
| const auto& server_link = |
| (base::RandUint64() & 1) ? server_link_1 : server_link_2; |
| |
| std::unique_ptr<device::cablev2::authenticator::Transaction> transaction = |
| device::cablev2::authenticator::TransactFromQRCode( |
| device::cablev2::authenticator::NewMockPlatform( |
| std::move(ble_advert_callback_), &virtual_device_, |
| /*observer=*/nullptr), |
| network_context_.get(), root_secret_, "Test Authenticator", |
| server_link.secret, server_link.peer_identity, |
| /*contact_id=*/absl::nullopt); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential().status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(pairings_.size(), 0u); |
| } |
| |
| TEST_F(AuthenticatorCableV2Test, LateLinking) { |
| auto discovery = std::make_unique<device::cablev2::Discovery>( |
| device::FidoRequestType::kGetAssertion, network_context_.get(), |
| qr_generator_key_, std::move(ble_advert_events_), |
| /*pairings=*/std::vector<std::unique_ptr<device::cablev2::Pairing>>(), |
| /*contact_device_stream=*/nullptr, |
| /*extension_contents=*/std::vector<device::CableDiscoveryData>(), |
| GetPairingCallback(), GetInvalidatedPairingCallback(), |
| GetEventCallback()); |
| |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::make_unique<DiscoveryFactory>(std::move(discovery))); |
| |
| const std::vector<uint8_t> contact_id(/*count=*/200, /*value=*/1); |
| std::unique_ptr<device::cablev2::authenticator::Transaction> transaction = |
| device::cablev2::authenticator::NewLateLinkingDevice( |
| device::CtapDeviceResponseCode::kCtap2ErrOperationDenied, |
| device::cablev2::authenticator::NewMockPlatform( |
| std::move(ble_advert_callback_), &virtual_device_, |
| /*observer=*/nullptr), |
| network_context_.get(), zero_qr_secret_, peer_identity_x962_); |
| |
| EXPECT_EQ(AuthenticatorMakeCredential().status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| |
| // There should not be any pairing at this point because the device shouldn't |
| // have sent the information yet. |
| EXPECT_EQ(pairings_.size(), 0u); |
| |
| // After 30 seconds, a pairing should have been recorded even though the |
| // WebAuthn request has completed. |
| task_environment()->FastForwardBy(base::Seconds(30)); |
| EXPECT_EQ(pairings_.size(), 1u); |
| } |
| |
| // AuthenticatorCableV2AuthenticatorTest tests aspects of the authenticator |
| // implementation, rather than of the underlying caBLEv2 transport. |
| class AuthenticatorCableV2AuthenticatorTest |
| : public AuthenticatorCableV2Test, |
| public device::cablev2::authenticator::Observer { |
| public: |
| void SetUp() override { |
| AuthenticatorCableV2Test::SetUp(); |
| |
| auto discovery = std::make_unique<device::cablev2::Discovery>( |
| device::FidoRequestType::kGetAssertion, network_context_.get(), |
| qr_generator_key_, std::move(ble_advert_events_), |
| /*pairings=*/std::vector<std::unique_ptr<device::cablev2::Pairing>>(), |
| /*contact_device_stream=*/nullptr, |
| /*extension_contents=*/std::vector<device::CableDiscoveryData>(), |
| GetPairingCallback(), GetInvalidatedPairingCallback(), |
| GetEventCallback()); |
| |
| AuthenticatorEnvironment::GetInstance() |
| ->ReplaceDefaultDiscoveryFactoryForTesting( |
| std::make_unique<DiscoveryFactory>(std::move(discovery))); |
| |
| transaction_ = device::cablev2::authenticator::TransactFromQRCode( |
| device::cablev2::authenticator::NewMockPlatform( |
| std::move(ble_advert_callback_), &virtual_device_, this), |
| network_context_.get(), root_secret_, "Test Authenticator", |
| zero_qr_secret_, peer_identity_x962_, |
| /*contact_id=*/absl::nullopt); |
| } |
| |
| protected: |
| // device::cablev2::authenticator::Observer |
| void OnStatus(device::cablev2::authenticator::Platform::Status) override {} |
| void OnCompleted( |
| absl::optional<device::cablev2::authenticator::Platform::Error> error) |
| override { |
| CHECK(!did_complete_); |
| did_complete_ = true; |
| error_ = error; |
| } |
| |
| std::unique_ptr<device::cablev2::authenticator::Transaction> transaction_; |
| bool did_complete_ = false; |
| absl::optional<device::cablev2::authenticator::Platform::Error> error_; |
| }; |
| |
| TEST_F(AuthenticatorCableV2AuthenticatorTest, GetAssertion) { |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials[0].transports.insert( |
| device::FidoTransportProtocol::kHybrid); |
| ASSERT_TRUE(virtual_device_.mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, options->relying_party_id)); |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| AuthenticatorStatus::SUCCESS); |
| } |
| |
| TEST_F(AuthenticatorCableV2AuthenticatorTest, MakeDiscoverableCredential) { |
| auto options = GetTestPublicKeyCredentialCreationOptions(); |
| options->authenticator_selection->resident_key = |
| device::ResidentKeyRequirement::kRequired; |
| EXPECT_EQ( |
| AuthenticatorMakeCredentialAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| |
| ASSERT_TRUE(did_complete_); |
| ASSERT_TRUE(error_.has_value()); |
| EXPECT_EQ(*error_, device::cablev2::authenticator::Platform::Error:: |
| DISCOVERABLE_CREDENTIALS_REQUEST); |
| } |
| |
| TEST_F(AuthenticatorCableV2AuthenticatorTest, EmptyAllowList) { |
| auto options = GetTestPublicKeyCredentialRequestOptions(); |
| options->allow_credentials.clear(); |
| EXPECT_EQ( |
| AuthenticatorGetAssertionAndWaitForTimeout(std::move(options)).status, |
| AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| |
| ASSERT_TRUE(did_complete_); |
| ASSERT_TRUE(error_.has_value()); |
| EXPECT_EQ(*error_, device::cablev2::authenticator::Platform::Error:: |
| DISCOVERABLE_CREDENTIALS_REQUEST); |
| } |
| |
| TEST_F(AuthenticatorCableV2AuthenticatorTest, DevicePublicKeyMakeCredential) { |
| auto options = GetTestPublicKeyCredentialCreationOptions(); |
| options->device_public_key = blink::mojom::DevicePublicKeyRequest::New(); |
| |
| const auto result = AuthenticatorMakeCredential(std::move(options)); |
| |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_TRUE(result.response->device_public_key); |
| EXPECT_TRUE(HasDevicePublicKeyExtensionInAuthenticatorData(result.response)); |
| } |
| |
| TEST_F(AuthenticatorCableV2AuthenticatorTest, DevicePublicKeyGetAssertion) { |
| auto options = GetTestPublicKeyCredentialRequestOptions(); |
| options->device_public_key = blink::mojom::DevicePublicKeyRequest::New(); |
| options->allow_credentials[0].transports.insert( |
| device::FidoTransportProtocol::kHybrid); |
| ASSERT_TRUE(virtual_device_.mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, options->relying_party_id)); |
| |
| const auto result = AuthenticatorGetAssertion(std::move(options)); |
| |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_TRUE(result.response->device_public_key); |
| } |
| |
| TEST_F(AuthenticatorCableV2AuthenticatorTest, PRFMakeCredential) { |
| auto options = GetTestPublicKeyCredentialCreationOptions(); |
| options->prf_enable = true; |
| |
| const auto result = AuthenticatorMakeCredential(std::move(options)); |
| |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_TRUE(result.response->echo_prf); |
| EXPECT_TRUE(result.response->prf); |
| } |
| |
| static std::tuple<PublicKeyCredentialRequestOptionsPtr, |
| std::vector<uint8_t>, |
| std::vector<uint8_t>> |
| BuildPRFGetAssertion(device::VirtualCtap2Device& virtual_device, |
| bool use_eval_by_credential) { |
| const std::vector<uint8_t> salt1(32, 1); |
| const std::vector<uint8_t> salt2(32, 2); |
| const std::array<uint8_t, 32> key1 = {1}; |
| const std::array<uint8_t, 32> key2 = {2}; |
| const std::array<uint8_t, 32> output1 = EvaluateHMAC(key2, salt1); |
| const std::array<uint8_t, 32> output2 = EvaluateHMAC(key2, salt2); |
| auto options = GetTestPublicKeyCredentialRequestOptions(); |
| |
| CHECK(virtual_device.mutable_state()->InjectRegistration( |
| options->allow_credentials[0].id, options->relying_party_id)); |
| virtual_device.mutable_state() |
| ->registrations.begin() |
| ->second.hmac_key.emplace(key1, key2); |
| |
| std::vector<blink::mojom::PRFValuesPtr> prf_inputs; |
| auto prf_value = blink::mojom::PRFValues::New(); |
| prf_value->first = salt1; |
| prf_value->second = salt2; |
| if (use_eval_by_credential) { |
| prf_value->id = options->allow_credentials[0].id; |
| } |
| prf_inputs.emplace_back(std::move(prf_value)); |
| |
| options->allow_credentials[0].transports.insert( |
| device::FidoTransportProtocol::kHybrid); |
| options->prf = true; |
| options->prf_inputs = std::move(prf_inputs); |
| options->user_verification = device::UserVerificationRequirement::kRequired; |
| |
| return std::make_tuple(std::move(options), |
| device::fido_parsing_utils::Materialize(output1), |
| device::fido_parsing_utils::Materialize(output2)); |
| } |
| |
| TEST_F(AuthenticatorCableV2AuthenticatorTest, PRFGetAssertion) { |
| PublicKeyCredentialRequestOptionsPtr options; |
| std::vector<uint8_t> output1, output2; |
| std::tie(options, output1, output2) = BuildPRFGetAssertion( |
| virtual_device_, /* use_eval_by_credential= */ false); |
| |
| const auto result = AuthenticatorGetAssertion(std::move(options)); |
| |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_TRUE(result.response->echo_prf); |
| EXPECT_TRUE(result.response->prf_results); |
| EXPECT_EQ(result.response->prf_results->first, output1); |
| ASSERT_TRUE(result.response->prf_results->second.has_value()); |
| EXPECT_EQ(*result.response->prf_results->second, output2); |
| } |
| |
| TEST_F(AuthenticatorCableV2AuthenticatorTest, PRFGetAssertionByCredential) { |
| PublicKeyCredentialRequestOptionsPtr options; |
| std::vector<uint8_t> output1, output2; |
| std::tie(options, output1, output2) = |
| BuildPRFGetAssertion(virtual_device_, /* use_eval_by_credential= */ true); |
| |
| const auto result = AuthenticatorGetAssertion(std::move(options)); |
| |
| ASSERT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_TRUE(result.response->echo_prf); |
| EXPECT_TRUE(result.response->prf_results); |
| EXPECT_EQ(result.response->prf_results->first, output1); |
| ASSERT_TRUE(result.response->prf_results->second.has_value()); |
| EXPECT_EQ(*result.response->prf_results->second, output2); |
| } |
| |
| // AuthenticatorImplWithRequestProxyTest tests behavior with an installed |
| // TestWebAuthenticationRequestProxy that takes over WebAuthn request handling. |
| class AuthenticatorImplWithRequestProxyTest : public AuthenticatorImplTest { |
| protected: |
| void SetUp() override { |
| AuthenticatorImplTest::SetUp(); |
| old_client_ = SetBrowserClientForTesting(&test_client_); |
| test_client_.GetTestWebAuthenticationDelegate()->request_proxy = |
| std::make_unique<TestWebAuthenticationRequestProxy>(); |
| } |
| |
| void TearDown() override { |
| SetBrowserClientForTesting(old_client_); |
| AuthenticatorImplTest::TearDown(); |
| } |
| |
| TestWebAuthenticationRequestProxy& request_proxy() { |
| return static_cast<TestWebAuthenticationRequestProxy&>( |
| *test_client_.GetTestWebAuthenticationDelegate()->request_proxy); |
| } |
| |
| raw_ptr<ContentBrowserClient> old_client_ = nullptr; |
| TestAuthenticatorContentBrowserClient test_client_; |
| base::test::ScopedFeatureList scoped_feature_list_{ |
| device::kWebAuthnGoogleCorpRemoteDesktopClientPrivilege}; |
| }; |
| |
| TEST_F(AuthenticatorImplWithRequestProxyTest, Inactive) { |
| request_proxy().config().is_active = false; |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| AuthenticatorIsUvpaa(); |
| EXPECT_EQ(request_proxy().observations().num_isuvpaa, 0u); |
| } |
| |
| TEST_F(AuthenticatorImplWithRequestProxyTest, IsUVPAA) { |
| size_t i = 0; |
| for (const bool is_uvpaa : {false, true}) { |
| SCOPED_TRACE(testing::Message() << "is_uvpaa=" << is_uvpaa); |
| request_proxy().config().is_uvpaa = is_uvpaa; |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| EXPECT_EQ(AuthenticatorIsUvpaa(), is_uvpaa); |
| EXPECT_EQ(request_proxy().observations().num_isuvpaa, ++i); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplWithRequestProxyTest, IsConditionalMediationAvailable) { |
| // We can't autofill credentials over the request proxy. Hence, conditional |
| // mediation is unavailable, even if IsUVPAA returns true. |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| |
| // Ensure there is no test override set and we're testing the real |
| // implementation. |
| ASSERT_EQ(test_client_.GetTestWebAuthenticationDelegate()->is_uvpaa_override, |
| absl::nullopt); |
| |
| // Proxy says `IsUVPAA()` is true. |
| request_proxy().config().is_uvpaa = true; |
| EXPECT_TRUE(AuthenticatorIsUvpaa()); |
| EXPECT_EQ(request_proxy().observations().num_isuvpaa, 1u); |
| |
| // But `IsConditionalMediationAvailable()` still returns false, bypassing the |
| // proxy. |
| EXPECT_FALSE(AuthenticatorIsConditionalMediationAvailable()); |
| EXPECT_EQ(request_proxy().observations().num_isuvpaa, 1u); |
| } |
| |
| TEST_F(AuthenticatorImplWithRequestProxyTest, MakeCredential) { |
| request_proxy().config().request_success = true; |
| request_proxy().config().make_credential_response = |
| MakeCredentialAuthenticatorResponse::New(); |
| request_proxy().config().make_credential_response->info = |
| CommonCredentialInfo::New(); |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| auto request = GetTestPublicKeyCredentialCreationOptions(); |
| MakeCredentialResult result = AuthenticatorMakeCredential(request->Clone()); |
| |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(request_proxy().observations().num_cancel, 0u); |
| EXPECT_EQ(request_proxy().observations().create_requests.size(), 1u); |
| |
| auto expected = request->Clone(); |
| expected->remote_desktop_client_override = RemoteDesktopClientOverride::New(); |
| expected->remote_desktop_client_override->origin = |
| url::Origin::Create(GURL(kTestOrigin1)); |
| expected->remote_desktop_client_override->same_origin_with_ancestors = true; |
| EXPECT_EQ(request_proxy().observations().create_requests.at(0), expected); |
| } |
| |
| // Verify requests with an attached proxy run RP ID checks. |
| TEST_F(AuthenticatorImplWithRequestProxyTest, MakeCredentialOriginAndRpIds) { |
| request_proxy().config().request_success = true; |
| request_proxy().config().make_credential_response = |
| MakeCredentialAuthenticatorResponse::New(); |
| request_proxy().config().make_credential_response->info = |
| CommonCredentialInfo::New(); |
| |
| for (const OriginClaimedAuthorityPair& test_case : |
| kInvalidRelyingPartyTestCases) { |
| SCOPED_TRACE(std::string(test_case.claimed_authority) + " " + |
| std::string(test_case.origin)); |
| |
| NavigateAndCommit(GURL(test_case.origin)); |
| BrowserContext* context = main_rfh()->GetBrowserContext(); |
| ASSERT_TRUE( |
| test_client_.GetWebAuthenticationDelegate()->MaybeGetRequestProxy( |
| context, url::Origin::Create(GURL(test_case.origin)))); |
| |
| PublicKeyCredentialCreationOptionsPtr options = |
| GetTestPublicKeyCredentialCreationOptions(); |
| options->relying_party.id = test_case.claimed_authority; |
| |
| EXPECT_EQ(AuthenticatorMakeCredential(std::move(options)).status, |
| test_case.expected_status); |
| EXPECT_EQ(request_proxy().observations().create_requests.size(), 0u); |
| } |
| } |
| |
| // Tests that attempting to make a credential when a request is already proxied |
| // fails with NotAllowedError. |
| TEST_F(AuthenticatorImplWithRequestProxyTest, MakeCredentialAlreadyProxied) { |
| GURL origin(kCorpCrdOrigin); |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->remote_desktop_client_override_origin = url::Origin::Create(origin); |
| NavigateAndCommit(origin); |
| auto request = GetTestPublicKeyCredentialCreationOptions(); |
| request->remote_desktop_client_override = |
| RemoteDesktopClientOverride::New(url::Origin::Create(origin), true); |
| MakeCredentialResult result = AuthenticatorMakeCredential(std::move(request)); |
| |
| EXPECT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(request_proxy().observations().create_requests.size(), 0u); |
| } |
| |
| TEST_F(AuthenticatorImplWithRequestProxyTest, AppId) { |
| request_proxy().config().request_success = true; |
| request_proxy().config().make_credential_response = |
| MakeCredentialAuthenticatorResponse::New(); |
| request_proxy().config().make_credential_response->info = |
| CommonCredentialInfo::New(); |
| |
| for (const auto& test_case : kValidAppIdCases) { |
| SCOPED_TRACE(std::string(test_case.origin) + " " + |
| std::string(test_case.claimed_authority)); |
| |
| BrowserContext* context = main_rfh()->GetBrowserContext(); |
| ASSERT_TRUE( |
| test_client_.GetWebAuthenticationDelegate()->MaybeGetRequestProxy( |
| context, url::Origin::Create(GURL(test_case.origin)))); |
| |
| EXPECT_EQ(TryAuthenticationWithAppId(test_case.origin, |
| test_case.claimed_authority), |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(request_proxy().observations().get_requests.size(), 1u); |
| request_proxy().observations().get_requests.clear(); |
| |
| EXPECT_EQ(TryRegistrationWithAppIdExclude(test_case.origin, |
| test_case.claimed_authority), |
| AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(request_proxy().observations().create_requests.size(), 1u); |
| request_proxy().observations().create_requests.clear(); |
| } |
| |
| // Test invalid cases that should be rejected. `kInvalidRelyingPartyTestCases` |
| // contains a mix of RP ID an App ID cases, but they should all be rejected. |
| for (const OriginClaimedAuthorityPair& test_case : |
| kInvalidRelyingPartyTestCases) { |
| SCOPED_TRACE(std::string(test_case.claimed_authority) + " " + |
| std::string(test_case.origin)); |
| |
| if (strlen(test_case.claimed_authority) == 0) { |
| // In this case, no AppID is actually being tested. |
| continue; |
| } |
| |
| BrowserContext* context = main_rfh()->GetBrowserContext(); |
| ASSERT_TRUE( |
| test_client_.GetWebAuthenticationDelegate()->MaybeGetRequestProxy( |
| context, url::Origin::Create(GURL(test_case.origin)))); |
| |
| AuthenticatorStatus test_status = TryAuthenticationWithAppId( |
| test_case.origin, test_case.claimed_authority); |
| EXPECT_TRUE(test_status == AuthenticatorStatus::INVALID_DOMAIN || |
| test_status == test_case.expected_status); |
| EXPECT_EQ(request_proxy().observations().get_requests.size(), 0u); |
| |
| test_status = TryRegistrationWithAppIdExclude(test_case.origin, |
| test_case.claimed_authority); |
| EXPECT_TRUE(test_status == AuthenticatorStatus::INVALID_DOMAIN || |
| test_status == test_case.expected_status); |
| EXPECT_EQ(request_proxy().observations().create_requests.size(), 0u); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplWithRequestProxyTest, MakeCredential_Timeout) { |
| request_proxy().config().resolve_callbacks = false; |
| request_proxy().config().request_success = true; |
| request_proxy().config().make_credential_response = |
| MakeCredentialAuthenticatorResponse::New(); |
| request_proxy().config().make_credential_response->info = |
| CommonCredentialInfo::New(); |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| MakeCredentialResult result = AuthenticatorMakeCredentialAndWaitForTimeout( |
| GetTestPublicKeyCredentialCreationOptions()); |
| |
| EXPECT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(request_proxy().observations().create_requests.size(), 1u); |
| EXPECT_EQ(request_proxy().observations().num_cancel, 1u); |
| |
| // Proxy should not hold a pending request after cancellation. |
| EXPECT_FALSE(request_proxy().HasPendingRequest()); |
| } |
| |
| TEST_F(AuthenticatorImplWithRequestProxyTest, GetAssertion) { |
| request_proxy().config().request_success = true; |
| request_proxy().config().get_assertion_response = |
| GetAssertionAuthenticatorResponse::New(); |
| request_proxy().config().get_assertion_response->info = |
| CommonCredentialInfo::New(); |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| auto request = GetTestPublicKeyCredentialRequestOptions(); |
| GetAssertionResult result = AuthenticatorGetAssertion(request->Clone()); |
| |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(request_proxy().observations().num_cancel, 0u); |
| EXPECT_EQ(request_proxy().observations().get_requests.size(), 1u); |
| |
| auto expected = request->Clone(); |
| expected->remote_desktop_client_override = RemoteDesktopClientOverride::New(); |
| expected->remote_desktop_client_override->origin = |
| url::Origin::Create(GURL(kTestOrigin1)); |
| expected->remote_desktop_client_override->same_origin_with_ancestors = true; |
| EXPECT_EQ(request_proxy().observations().get_requests.at(0), expected); |
| } |
| |
| // Tests that attempting to get an assertion when a request is already proxied |
| // fails with NotAllowedError. |
| TEST_F(AuthenticatorImplWithRequestProxyTest, GetAssertionAlreadyProxied) { |
| GURL origin(kCorpCrdOrigin); |
| test_client_.GetTestWebAuthenticationDelegate() |
| ->remote_desktop_client_override_origin = url::Origin::Create(origin); |
| NavigateAndCommit(origin); |
| auto request = GetTestPublicKeyCredentialRequestOptions(); |
| request->remote_desktop_client_override = |
| RemoteDesktopClientOverride::New(url::Origin::Create(origin), true); |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(request)); |
| |
| EXPECT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(request_proxy().observations().get_requests.size(), 0u); |
| } |
| |
| // Verify that Conditional UI requests are not proxied. |
| TEST_F(AuthenticatorImplWithRequestProxyTest, GetAssertionConditionalUI) { |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| auto request = GetTestPublicKeyCredentialRequestOptions(); |
| request->is_conditional = true; |
| GetAssertionResult result = AuthenticatorGetAssertion(std::move(request)); |
| |
| EXPECT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(request_proxy().observations().get_requests.size(), 0u); |
| } |
| |
| // Verify requests with an attached proxy run RP ID checks. |
| TEST_F(AuthenticatorImplWithRequestProxyTest, GetAssertionOriginAndRpIds) { |
| request_proxy().config().request_success = true; |
| request_proxy().config().get_assertion_response = |
| GetAssertionAuthenticatorResponse::New(); |
| request_proxy().config().get_assertion_response->info = |
| CommonCredentialInfo::New(); |
| |
| for (const OriginClaimedAuthorityPair& test_case : |
| kInvalidRelyingPartyTestCases) { |
| SCOPED_TRACE(std::string(test_case.claimed_authority) + " " + |
| std::string(test_case.origin)); |
| |
| NavigateAndCommit(GURL(test_case.origin)); |
| BrowserContext* context = main_rfh()->GetBrowserContext(); |
| ASSERT_TRUE( |
| test_client_.GetWebAuthenticationDelegate()->MaybeGetRequestProxy( |
| context, url::Origin::Create(GURL(test_case.origin)))); |
| |
| PublicKeyCredentialRequestOptionsPtr options = |
| GetTestPublicKeyCredentialRequestOptions(); |
| options->relying_party_id = test_case.claimed_authority; |
| |
| EXPECT_EQ(AuthenticatorGetAssertion(std::move(options)).status, |
| test_case.expected_status); |
| EXPECT_EQ(request_proxy().observations().get_requests.size(), 0u); |
| } |
| } |
| |
| TEST_F(AuthenticatorImplWithRequestProxyTest, GetAssertion_Timeout) { |
| request_proxy().config().resolve_callbacks = false; |
| request_proxy().config().request_success = true; |
| request_proxy().config().get_assertion_response = |
| GetAssertionAuthenticatorResponse::New(); |
| request_proxy().config().get_assertion_response->info = |
| CommonCredentialInfo::New(); |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| GetAssertionResult result = AuthenticatorGetAssertionAndWaitForTimeout( |
| GetTestPublicKeyCredentialRequestOptions()); |
| |
| EXPECT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(request_proxy().observations().get_requests.size(), 1u); |
| EXPECT_EQ(request_proxy().observations().num_cancel, 1u); |
| |
| // Proxy should not hold a pending request after cancellation. |
| EXPECT_FALSE(request_proxy().HasPendingRequest()); |
| } |
| |
| TEST_F(AuthenticatorImplWithRequestProxyTest, |
| VirtualAuthenticatorTakesPrecedence) { |
| // With the virtual authenticator enabled, no requests should hit the proxy. |
| content::AuthenticatorEnvironment::GetInstance() |
| ->EnableVirtualAuthenticatorFor( |
| static_cast<content::RenderFrameHostImpl*>(main_rfh()) |
| ->frame_tree_node(), |
| /*enable_ui=*/false); |
| test_client_.GetTestWebAuthenticationDelegate()->is_uvpaa_override = true; |
| |
| NavigateAndCommit(GURL(kTestOrigin1)); |
| ASSERT_TRUE( |
| request_proxy().IsActive(url::Origin::Create(GURL(kTestOrigin1)))); |
| |
| { |
| MakeCredentialResult result = AuthenticatorMakeCredential( |
| GetTestPublicKeyCredentialCreationOptions()); |
| EXPECT_EQ(result.status, AuthenticatorStatus::SUCCESS); |
| EXPECT_EQ(request_proxy().observations().create_requests.size(), 0u); |
| } |
| |
| { |
| GetAssertionResult result = |
| AuthenticatorGetAssertion(GetTestPublicKeyCredentialRequestOptions()); |
| EXPECT_EQ(result.status, AuthenticatorStatus::NOT_ALLOWED_ERROR); |
| EXPECT_EQ(request_proxy().observations().get_requests.size(), 0u); |
| } |
| |
| EXPECT_TRUE(AuthenticatorIsUvpaa()); |
| EXPECT_EQ(request_proxy().observations().num_isuvpaa, 0u); |
| EXPECT_TRUE(AuthenticatorIsConditionalMediationAvailable()); |
| EXPECT_EQ(request_proxy().observations().num_isuvpaa, 0u); |
| } |
| |
| } // namespace content |