blob: 4a1666af9dbac0c29287f1d932f366837be1b937 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <cstdint>
#include <memory>
#include <optional>
#include <tuple>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/ctap_make_credential_request.h"
#include "device/fido/device_response_converter.h"
#include "device/fido/fake_fido_discovery.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_device_authenticator.h"
#include "device/fido/fido_discovery_base.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/fido_test_data.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/fido_types.h"
#include "device/fido/get_assertion_request_handler.h"
#include "device/fido/make_credential_task.h"
#include "device/fido/mock_fido_device.h"
#include "device/fido/public_key_credential_descriptor.h"
#include "device/fido/u2f_command_constructor.h"
#include "device/fido/virtual_ctap2_device.h"
#include "device/fido/virtual_fido_device.h"
#include "device/fido/virtual_fido_device_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(IS_WIN)
#include "device/fido/hid/fake_hid_impl_for_testing.h"
#include "device/fido/win/fake_webauthn_api.h"
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_CHROMEOS)
#include "chromeos/dbus/u2f/u2f_client.h"
#endif
namespace device {
namespace {
constexpr char kRequestTransportHistogram[] =
"WebAuthentication.GetAssertionRequestTransport";
constexpr char kResponseTransportHistogram[] =
"WebAuthentication.GetAssertionResponseTransport";
using TestGetAssertionRequestFuture = base::test::TestFuture<
GetAssertionStatus,
std::optional<std::vector<AuthenticatorGetAssertionResponse>>,
FidoAuthenticator*>;
} // namespace
using testing::_;
// FidoGetAssertionHandlerTest allows testing GetAssertionRequestHandler against
// MockFidoDevices injected via a FakeFidoDiscoveryFactory.
class FidoGetAssertionHandlerTest : public ::testing::Test {
public:
void SetUp() override {
bluetooth_config_->SetLESupported(true);
BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter_);
#if BUILDFLAG(IS_CHROMEOS)
chromeos::U2FClient::InitializeFake();
#endif
}
void TearDown() override {
#if BUILDFLAG(IS_CHROMEOS)
task_environment_.RunUntilIdle();
chromeos::U2FClient::Shutdown();
#endif
}
void ForgeDiscoveries() {
discovery_ = fake_discovery_factory_->ForgeNextHidDiscovery();
cable_discovery_ = fake_discovery_factory_->ForgeNextCableDiscovery();
nfc_discovery_ = fake_discovery_factory_->ForgeNextNfcDiscovery();
platform_discovery_ = fake_discovery_factory_->ForgeNextPlatformDiscovery();
}
CtapGetAssertionRequest CreateTestRequestWithCableExtension() {
CtapGetAssertionRequest request(test_data::kRelyingPartyId,
test_data::kClientDataJson);
request.cable_extension.emplace();
return request;
}
std::unique_ptr<GetAssertionRequestHandler> CreateGetAssertionHandlerU2f() {
CtapGetAssertionRequest request(test_data::kRelyingPartyId,
test_data::kClientDataJson);
request.allow_list = {PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(test_data::kU2fSignKeyHandle))};
return CreateGetAssertionHandlerWithRequest(std::move(request));
}
std::unique_ptr<GetAssertionRequestHandler> CreateGetAssertionHandlerCtap() {
CtapGetAssertionRequest request(test_data::kRelyingPartyId,
test_data::kClientDataJson);
request.allow_list = {PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(
test_data::kTestGetAssertionCredentialId))};
return CreateGetAssertionHandlerWithRequest(std::move(request));
}
std::unique_ptr<GetAssertionRequestHandler>
CreateGetAssertionHandlerWithRequest(CtapGetAssertionRequest request) {
ForgeDiscoveries();
auto handler = std::make_unique<GetAssertionRequestHandler>(
fake_discovery_factory_.get(),
std::vector<std::unique_ptr<FidoDiscoveryBase>>(),
supported_transports_, std::move(request), CtapGetAssertionOptions(),
/*allow_skipping_pin_touch=*/true, get_assertion_future_.GetCallback());
return handler;
}
std::unique_ptr<GetAssertionRequestHandler>
CreateGetAssertionHandlerWithRequestedTransports(
std::vector<std::vector<FidoTransportProtocol>> transports) {
CtapGetAssertionRequest request(test_data::kRelyingPartyId,
test_data::kClientDataJson);
for (uint8_t i = 0; i < transports.size(); ++i) {
request.allow_list.emplace_back(CredentialType::kPublicKey,
std::vector<uint8_t>{i});
request.allow_list.back().transports = transports[i];
}
return CreateGetAssertionHandlerWithRequest(std::move(request));
}
void ExpectAllowedTransportsForRequestAre(
GetAssertionRequestHandler* request_handler,
base::flat_set<FidoTransportProtocol> transports) {
using Transport = FidoTransportProtocol;
if (base::Contains(transports, Transport::kUsbHumanInterfaceDevice))
discovery()->WaitForCallToStartAndSimulateSuccess();
if (base::Contains(transports, Transport::kHybrid))
cable_discovery()->WaitForCallToStartAndSimulateSuccess();
if (base::Contains(transports, Transport::kNearFieldCommunication))
nfc_discovery()->WaitForCallToStartAndSimulateSuccess();
if (base::Contains(transports, Transport::kInternal))
platform_discovery()->WaitForCallToStartAndSimulateSuccess();
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_FALSE(get_assertion_future().IsReady());
if (!base::Contains(transports, Transport::kUsbHumanInterfaceDevice))
EXPECT_FALSE(discovery()->is_start_requested());
if (!base::Contains(transports, Transport::kHybrid))
EXPECT_FALSE(cable_discovery()->is_start_requested());
if (!base::Contains(transports, Transport::kNearFieldCommunication))
EXPECT_FALSE(nfc_discovery()->is_start_requested());
if (!base::Contains(transports, Transport::kInternal))
EXPECT_FALSE(platform_discovery()->is_start_requested());
EXPECT_THAT(
request_handler->transport_availability_info().available_transports,
::testing::UnorderedElementsAreArray(transports));
}
test::FakeFidoDiscovery* discovery() const { return discovery_; }
test::FakeFidoDiscovery* cable_discovery() const { return cable_discovery_; }
test::FakeFidoDiscovery* nfc_discovery() const { return nfc_discovery_; }
test::FakeFidoDiscovery* platform_discovery() const {
return platform_discovery_;
}
TestGetAssertionRequestFuture& get_assertion_future() {
return get_assertion_future_;
}
void set_supported_transports(
base::flat_set<FidoTransportProtocol> transports) {
supported_transports_ = std::move(transports);
}
protected:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
std::unique_ptr<test::FakeFidoDiscoveryFactory> fake_discovery_factory_ =
std::make_unique<test::FakeFidoDiscoveryFactory>();
raw_ptr<test::FakeFidoDiscovery, DanglingUntriaged> discovery_;
raw_ptr<test::FakeFidoDiscovery, DanglingUntriaged> cable_discovery_;
raw_ptr<test::FakeFidoDiscovery, DanglingUntriaged> nfc_discovery_;
raw_ptr<test::FakeFidoDiscovery, DanglingUntriaged> platform_discovery_;
scoped_refptr<::testing::NiceMock<MockBluetoothAdapter>> mock_adapter_ =
base::MakeRefCounted<::testing::NiceMock<MockBluetoothAdapter>>();
TestGetAssertionRequestFuture get_assertion_future_;
base::flat_set<FidoTransportProtocol> supported_transports_ = {
FidoTransportProtocol::kUsbHumanInterfaceDevice,
FidoTransportProtocol::kInternal,
FidoTransportProtocol::kNearFieldCommunication,
FidoTransportProtocol::kHybrid};
std::unique_ptr<BluetoothAdapterFactory::GlobalOverrideValues>
bluetooth_config_ =
BluetoothAdapterFactory::Get()->InitGlobalOverrideValues();
FidoRequestHandlerBase::ScopedAlwaysAllowBLECalls always_allow_ble_calls_;
};
TEST_F(FidoGetAssertionHandlerTest, TransportAvailabilityInfo) {
{
// Empty allow list.
auto request_handler = CreateGetAssertionHandlerWithRequestedTransports({});
EXPECT_EQ(FidoRequestType::kGetAssertion,
request_handler->transport_availability_info().request_type);
EXPECT_FALSE(request_handler->transport_availability_info()
.transport_list_did_include_internal);
EXPECT_FALSE(request_handler->transport_availability_info()
.transport_list_did_include_hybrid);
EXPECT_FALSE(request_handler->transport_availability_info()
.transport_list_did_include_security_key);
EXPECT_TRUE(
request_handler->transport_availability_info().has_empty_allow_list);
EXPECT_FALSE(request_handler->transport_availability_info()
.request_is_internal_only);
}
{
// Internal and a phone.
auto request_handler = CreateGetAssertionHandlerWithRequestedTransports(
{{FidoTransportProtocol::kInternal},
{FidoTransportProtocol::kInternal, FidoTransportProtocol::kHybrid}});
EXPECT_EQ(FidoRequestType::kGetAssertion,
request_handler->transport_availability_info().request_type);
EXPECT_TRUE(request_handler->transport_availability_info()
.transport_list_did_include_internal);
EXPECT_TRUE(request_handler->transport_availability_info()
.transport_list_did_include_hybrid);
EXPECT_FALSE(request_handler->transport_availability_info()
.transport_list_did_include_security_key);
EXPECT_FALSE(
request_handler->transport_availability_info().has_empty_allow_list);
EXPECT_FALSE(request_handler->transport_availability_info()
.request_is_internal_only);
}
{
// Internal, a phone, and USB.
auto request_handler = CreateGetAssertionHandlerWithRequestedTransports(
{{FidoTransportProtocol::kUsbHumanInterfaceDevice},
{FidoTransportProtocol::kInternal},
{FidoTransportProtocol::kInternal, FidoTransportProtocol::kHybrid}});
EXPECT_EQ(FidoRequestType::kGetAssertion,
request_handler->transport_availability_info().request_type);
EXPECT_TRUE(request_handler->transport_availability_info()
.transport_list_did_include_internal);
EXPECT_TRUE(request_handler->transport_availability_info()
.transport_list_did_include_hybrid);
EXPECT_TRUE(request_handler->transport_availability_info()
.transport_list_did_include_security_key);
EXPECT_FALSE(
request_handler->transport_availability_info().has_empty_allow_list);
EXPECT_FALSE(request_handler->transport_availability_info()
.request_is_internal_only);
}
{
// Only USB.
auto request_handler = CreateGetAssertionHandlerWithRequestedTransports(
{{FidoTransportProtocol::kUsbHumanInterfaceDevice}});
EXPECT_EQ(FidoRequestType::kGetAssertion,
request_handler->transport_availability_info().request_type);
EXPECT_FALSE(request_handler->transport_availability_info()
.transport_list_did_include_internal);
EXPECT_FALSE(request_handler->transport_availability_info()
.transport_list_did_include_hybrid);
EXPECT_TRUE(request_handler->transport_availability_info()
.transport_list_did_include_security_key);
EXPECT_FALSE(
request_handler->transport_availability_info().has_empty_allow_list);
EXPECT_FALSE(request_handler->transport_availability_info()
.request_is_internal_only);
}
{
// A phone and an unknown (empty) transport credential.
auto request_handler = CreateGetAssertionHandlerWithRequestedTransports(
{{}, {FidoTransportProtocol::kHybrid}});
EXPECT_EQ(FidoRequestType::kGetAssertion,
request_handler->transport_availability_info().request_type);
EXPECT_TRUE(request_handler->transport_availability_info()
.transport_list_did_include_internal);
EXPECT_TRUE(request_handler->transport_availability_info()
.transport_list_did_include_hybrid);
EXPECT_TRUE(request_handler->transport_availability_info()
.transport_list_did_include_security_key);
EXPECT_FALSE(
request_handler->transport_availability_info().has_empty_allow_list);
EXPECT_FALSE(request_handler->transport_availability_info()
.request_is_internal_only);
}
{
// Internal only.
auto request_handler = CreateGetAssertionHandlerWithRequestedTransports(
{{FidoTransportProtocol::kInternal},
{FidoTransportProtocol::kInternal}});
EXPECT_EQ(FidoRequestType::kGetAssertion,
request_handler->transport_availability_info().request_type);
EXPECT_TRUE(request_handler->transport_availability_info()
.transport_list_did_include_internal);
EXPECT_FALSE(request_handler->transport_availability_info()
.transport_list_did_include_hybrid);
EXPECT_FALSE(request_handler->transport_availability_info()
.transport_list_did_include_security_key);
EXPECT_FALSE(
request_handler->transport_availability_info().has_empty_allow_list);
EXPECT_TRUE(request_handler->transport_availability_info()
.request_is_internal_only);
}
}
TEST_F(FidoGetAssertionHandlerTest, CtapRequestOnSingleDevice) {
auto request_handler = CreateGetAssertionHandlerCtap();
discovery()->WaitForCallToStartAndSimulateSuccess();
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponse);
discovery()->AddDevice(std::move(device));
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(GetAssertionStatus::kSuccess,
std::get<0>(get_assertion_future().Get()));
EXPECT_TRUE(std::get<1>(get_assertion_future().Get()));
}
// Test a scenario where the connected authenticator is a U2F device.
TEST_F(FidoGetAssertionHandlerTest, TestU2fSign) {
auto request_handler = CreateGetAssertionHandlerU2f();
discovery()->WaitForCallToStartAndSimulateSuccess();
auto device = MockFidoDevice::MakeU2fWithGetInfoExpectation();
device->ExpectRequestAndRespondWith(
test_data::kU2fSignCommandApdu,
test_data::kApduEncodedNoErrorSignResponse);
discovery()->AddDevice(std::move(device));
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_EQ(GetAssertionStatus::kSuccess,
std::get<0>(get_assertion_future().Get()));
EXPECT_TRUE(std::get<1>(get_assertion_future().Get()));
}
TEST_F(FidoGetAssertionHandlerTest, TestIncompatibleUserVerificationSetting) {
auto request = CtapGetAssertionRequest(test_data::kRelyingPartyId,
test_data::kClientDataJson);
request.user_verification = UserVerificationRequirement::kRequired;
auto request_handler =
CreateGetAssertionHandlerWithRequest(std::move(request));
discovery()->WaitForCallToStartAndSimulateSuccess();
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
test_data::kTestGetInfoResponseWithoutUvSupport);
device->ExpectRequestAndRespondWith(
MockFidoDevice::EncodeCBORRequest(AsCTAPRequestValuePair(
MakeCredentialTask::GetTouchRequest(device.get()))),
test_data::kTestMakeCredentialResponse);
discovery()->AddDevice(std::move(device));
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_EQ(GetAssertionStatus::kAuthenticatorMissingUserVerification,
std::get<0>(get_assertion_future().Get()));
}
TEST_F(FidoGetAssertionHandlerTest,
TestU2fSignRequestWithUserVerificationRequired) {
auto request = CtapGetAssertionRequest(test_data::kRelyingPartyId,
test_data::kClientDataJson);
request.allow_list = {PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(test_data::kU2fSignKeyHandle))};
request.user_verification = UserVerificationRequirement::kRequired;
auto request_handler =
CreateGetAssertionHandlerWithRequest(std::move(request));
discovery()->WaitForCallToStartAndSimulateSuccess();
auto device = MockFidoDevice::MakeU2fWithGetInfoExpectation();
device->ExpectRequestAndRespondWith(
ConstructBogusU2fRegistrationCommand(),
test_data::kApduEncodedNoErrorRegisterResponse);
discovery()->AddDevice(std::move(device));
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_EQ(GetAssertionStatus::kAuthenticatorMissingUserVerification,
std::get<0>(get_assertion_future().Get()));
}
TEST_F(FidoGetAssertionHandlerTest, IncorrectRpIdHash) {
auto request_handler =
CreateGetAssertionHandlerWithRequest(CtapGetAssertionRequest(
test_data::kRelyingPartyId, test_data::kClientDataJson));
discovery()->WaitForCallToStartAndSimulateSuccess();
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponseWithIncorrectRpIdHash);
discovery()->AddDevice(std::move(device));
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(GetAssertionStatus::kAuthenticatorResponseInvalid,
std::get<0>(get_assertion_future().Get()));
}
// Tests a scenario where the authenticator responds with credential ID that
// is not included in the allowed list.
TEST_F(FidoGetAssertionHandlerTest, InvalidCredential) {
CtapGetAssertionRequest request(test_data::kRelyingPartyId,
test_data::kClientDataJson);
request.allow_list = {PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(test_data::kKeyHandleAlpha))};
auto request_handler =
CreateGetAssertionHandlerWithRequest(std::move(request));
discovery()->WaitForCallToStartAndSimulateSuccess();
// Resident Keys must be disabled, otherwise allow list check is skipped.
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
test_data::kTestGetInfoResponseWithoutResidentKeySupport);
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponse);
discovery()->AddDevice(std::move(device));
// The response with the invalid credential ID is considered to be an error at
// the task level and the request handler will drop the authenticator.
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_FALSE(get_assertion_future().IsReady());
}
// Tests a scenario where the authenticator responds with an empty credential.
// When GetAssertion request only has a single credential in the allow list,
// this is a valid response. Check that credential is set by the client before
// the response is returned to the relying party.
TEST_F(FidoGetAssertionHandlerTest, ValidEmptyCredential) {
auto request_handler = CreateGetAssertionHandlerCtap();
discovery()->WaitForCallToStartAndSimulateSuccess();
// Resident Keys must be disabled, otherwise allow list check is skipped.
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
test_data::kTestGetInfoResponseWithoutResidentKeySupport);
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponseWithEmptyCredential);
discovery()->AddDevice(std::move(device));
EXPECT_TRUE(get_assertion_future().Wait());
const auto& response = std::get<1>(get_assertion_future().Get());
EXPECT_EQ(GetAssertionStatus::kSuccess,
std::get<0>(get_assertion_future().Get()));
ASSERT_TRUE(response);
ASSERT_EQ(1u, response->size());
EXPECT_TRUE(response.value()[0].credential);
EXPECT_THAT(
response.value()[0].credential->id,
::testing::ElementsAreArray(test_data::kTestGetAssertionCredentialId));
}
TEST_F(FidoGetAssertionHandlerTest, TruncatedUTF8) {
// Webauthn says[1] that authenticators may truncate strings in user entities.
// Since authenticators aren't going to do UTF-8 processing, that means that
// they may truncate a multi-byte code point and thus produce an invalid
// string in the CBOR. This test exercises that case.
//
// [1] https://www.w3.org/TR/webauthn/#sctn-user-credential-params
auto request_handler = CreateGetAssertionHandlerCtap();
discovery()->WaitForCallToStartAndSimulateSuccess();
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
test_data::kTestCtap2OnlyAuthenticatorGetInfoResponse);
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponseWithTruncatedUTF8);
discovery()->AddDevice(std::move(device));
EXPECT_TRUE(get_assertion_future().Wait());
const auto& response = std::get<1>(get_assertion_future().Get());
EXPECT_EQ(GetAssertionStatus::kSuccess,
std::get<0>(get_assertion_future().Get()));
ASSERT_TRUE(response);
ASSERT_EQ(1u, response->size());
ASSERT_TRUE(response.value()[0].user_entity);
EXPECT_EQ(63u, response.value()[0].user_entity->name->size());
}
TEST_F(FidoGetAssertionHandlerTest, TruncatedAndInvalidUTF8) {
// This test exercises the case where a UTF-8 string is truncated in a
// response, and the UTF-8 string contains invalid code-points that
// |base::IsStringUTF8| will be unhappy with.
auto request_handler = CreateGetAssertionHandlerCtap();
discovery()->WaitForCallToStartAndSimulateSuccess();
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
test_data::kTestCtap2OnlyAuthenticatorGetInfoResponse);
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponseWithTruncatedAndInvalidUTF8);
discovery()->AddDevice(std::move(device));
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_FALSE(get_assertion_future().IsReady());
}
// Tests a scenario where authenticator responds without user entity in its
// response but client is expecting a resident key credential.
TEST_F(FidoGetAssertionHandlerTest, IncorrectUserEntity) {
// Use a GetAssertion request with an empty allow list.
auto request_handler =
CreateGetAssertionHandlerWithRequest(CtapGetAssertionRequest(
test_data::kRelyingPartyId, test_data::kClientDataJson));
discovery()->WaitForCallToStartAndSimulateSuccess();
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponse);
discovery()->AddDevice(std::move(device));
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(GetAssertionStatus::kAuthenticatorResponseInvalid,
std::get<0>(get_assertion_future().Get()));
}
TEST_F(FidoGetAssertionHandlerTest, SupportedTransportsAreOnlyNfc) {
const base::flat_set<FidoTransportProtocol> kNfc = {
FidoTransportProtocol::kNearFieldCommunication,
};
set_supported_transports(kNfc);
auto request_handler = CreateGetAssertionHandlerWithRequest(
CreateTestRequestWithCableExtension());
ExpectAllowedTransportsForRequestAre(request_handler.get(), kNfc);
}
TEST_F(FidoGetAssertionHandlerTest,
SupportedTransportsAreOnlyCableAndInternal) {
const base::flat_set<FidoTransportProtocol> kCableAndInternal = {
FidoTransportProtocol::kHybrid,
FidoTransportProtocol::kInternal,
};
EXPECT_CALL(*mock_adapter_, IsPresent()).WillOnce(::testing::Return(true));
set_supported_transports(kCableAndInternal);
auto request_handler = CreateGetAssertionHandlerWithRequest(
CreateTestRequestWithCableExtension());
ExpectAllowedTransportsForRequestAre(request_handler.get(),
kCableAndInternal);
}
TEST_F(FidoGetAssertionHandlerTest, SuccessWithOnlyUsbTransportAllowed) {
auto request = CreateTestRequestWithCableExtension();
request.allow_list = {
PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(
test_data::kTestGetAssertionCredentialId),
{FidoTransportProtocol::kUsbHumanInterfaceDevice}),
};
set_supported_transports({FidoTransportProtocol::kUsbHumanInterfaceDevice});
auto request_handler =
CreateGetAssertionHandlerWithRequest(std::move(request));
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponse);
discovery()->WaitForCallToStartAndSimulateSuccess();
discovery()->AddDevice(std::move(device));
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(GetAssertionStatus::kSuccess,
std::get<0>(get_assertion_future().Get()));
EXPECT_TRUE(std::get<1>(get_assertion_future().Get()));
EXPECT_THAT(
request_handler->transport_availability_info().available_transports,
::testing::UnorderedElementsAre(
FidoTransportProtocol::kUsbHumanInterfaceDevice));
}
TEST_F(FidoGetAssertionHandlerTest, SuccessWithOnlyNfcTransportAllowed) {
auto request = CreateTestRequestWithCableExtension();
request.allow_list = {
PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(
test_data::kTestGetAssertionCredentialId),
{FidoTransportProtocol::kNearFieldCommunication}),
};
set_supported_transports({FidoTransportProtocol::kNearFieldCommunication});
auto request_handler =
CreateGetAssertionHandlerWithRequest(std::move(request));
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
device->SetDeviceTransport(FidoTransportProtocol::kNearFieldCommunication);
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponse);
nfc_discovery()->WaitForCallToStartAndSimulateSuccess();
nfc_discovery()->AddDevice(std::move(device));
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(GetAssertionStatus::kSuccess,
std::get<0>(get_assertion_future().Get()));
EXPECT_TRUE(std::get<1>(get_assertion_future().Get()));
EXPECT_THAT(
request_handler->transport_availability_info().available_transports,
::testing::UnorderedElementsAre(
FidoTransportProtocol::kNearFieldCommunication));
}
TEST_F(FidoGetAssertionHandlerTest, SuccessWithOnlyInternalTransportAllowed) {
auto request = CreateTestRequestWithCableExtension();
request.allow_list = {
PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(
test_data::kTestGetAssertionCredentialId),
{FidoTransportProtocol::kInternal}),
};
set_supported_transports({FidoTransportProtocol::kInternal});
auto request_handler =
CreateGetAssertionHandlerWithRequest(std::move(request));
auto device = MockFidoDevice::MakeCtap(
ReadCTAPGetInfoResponse(test_data::kTestGetInfoResponsePlatformDevice));
EXPECT_CALL(*device, GetId()).WillRepeatedly(testing::Return("device0"));
device->SetDeviceTransport(FidoTransportProtocol::kInternal);
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetInfo,
test_data::kTestGetInfoResponsePlatformDevice);
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponse);
platform_discovery()->WaitForCallToStartAndSimulateSuccess();
platform_discovery()->AddDevice(std::move(device));
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(GetAssertionStatus::kSuccess,
std::get<0>(get_assertion_future().Get()));
EXPECT_TRUE(std::get<1>(get_assertion_future().Get()));
EXPECT_THAT(
request_handler->transport_availability_info().available_transports,
::testing::UnorderedElementsAre(FidoTransportProtocol::kInternal));
}
// If a device with transport type kInternal returns a
// CTAP2_ERR_OPERATION_DENIED error, the request should complete with
// GetAssertionStatus::kUserConsentDenied. Pending authenticators should be
// cancelled.
TEST_F(FidoGetAssertionHandlerTest,
TestRequestWithOperationDeniedErrorPlatform) {
auto request_handler = CreateGetAssertionHandlerCtap();
auto platform_device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
test_data::kTestGetInfoResponsePlatformDevice);
platform_device->SetDeviceTransport(FidoTransportProtocol::kInternal);
platform_device->ExpectCtap2CommandAndRespondWithError(
CtapRequestCommand::kAuthenticatorGetAssertion,
CtapDeviceResponseCode::kCtap2ErrOperationDenied, base::Microseconds(10));
platform_discovery()->WaitForCallToStartAndSimulateSuccess();
platform_discovery()->AddDevice(std::move(platform_device));
auto other_device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
other_device->ExpectCtap2CommandAndDoNotRespond(
CtapRequestCommand::kAuthenticatorGetAssertion);
EXPECT_CALL(*other_device, Cancel);
discovery()->WaitForCallToStartAndSimulateSuccess();
discovery()->AddDevice(std::move(other_device));
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_TRUE(get_assertion_future().IsReady());
EXPECT_EQ(GetAssertionStatus::kUserConsentDenied,
std::get<0>(get_assertion_future().Get()));
}
// Like |TestRequestWithOperationDeniedErrorPlatform|, but with a
// cross-platform device.
TEST_F(FidoGetAssertionHandlerTest,
TestRequestWithOperationDeniedErrorCrossPlatform) {
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
device->ExpectCtap2CommandAndRespondWithError(
CtapRequestCommand::kAuthenticatorGetAssertion,
CtapDeviceResponseCode::kCtap2ErrOperationDenied);
auto request_handler = CreateGetAssertionHandlerCtap();
discovery()->WaitForCallToStartAndSimulateSuccess();
discovery()->AddDevice(std::move(device));
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_TRUE(get_assertion_future().IsReady());
EXPECT_EQ(GetAssertionStatus::kUserConsentDenied,
std::get<0>(get_assertion_future().Get()));
}
// If a device returns CTAP2_ERR_PIN_AUTH_INVALID, the request should complete
// with GetAssertionStatus::kUserConsentDenied.
TEST_F(FidoGetAssertionHandlerTest, TestRequestWithPinAuthInvalid) {
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
device->ExpectCtap2CommandAndRespondWithError(
CtapRequestCommand::kAuthenticatorGetAssertion,
CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid);
auto request_handler = CreateGetAssertionHandlerCtap();
discovery()->WaitForCallToStartAndSimulateSuccess();
discovery()->AddDevice(std::move(device));
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_TRUE(get_assertion_future().IsReady());
EXPECT_EQ(GetAssertionStatus::kUserConsentDenied,
std::get<0>(get_assertion_future().Get()));
}
MATCHER_P(IsCtap2Command, expected_command, "") {
return !arg.empty() && arg[0] == base::strict_cast<uint8_t>(expected_command);
}
TEST_F(FidoGetAssertionHandlerTest, DeviceFailsImmediately) {
// Test that, when a device immediately returns an unexpected error, the
// request continues and waits for another device.
auto broken_device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
EXPECT_CALL(
*broken_device,
DeviceTransactPtr(
IsCtap2Command(CtapRequestCommand::kAuthenticatorGetAssertion), _))
.WillOnce(::testing::DoAll(
::testing::WithArg<1>(
[this](FidoDevice::DeviceCallback& callback) {
std::vector<uint8_t> response = {static_cast<uint8_t>(
CtapDeviceResponseCode::kCtap2ErrInvalidCBOR)};
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), std::move(response)));
auto working_device =
MockFidoDevice::MakeCtapWithGetInfoExpectation();
working_device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponse);
discovery()->AddDevice(std::move(working_device));
}),
::testing::Return(0)));
auto request_handler = CreateGetAssertionHandlerCtap();
discovery()->WaitForCallToStartAndSimulateSuccess();
discovery()->AddDevice(std::move(broken_device));
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(GetAssertionStatus::kSuccess,
std::get<0>(get_assertion_future().Get()));
}
TEST_F(FidoGetAssertionHandlerTest, PinUvAuthTokenPreTouchFailure) {
VirtualCtap2Device::Config config;
config.ctap2_versions = {Ctap2Version::kCtap2_1};
config.pin_uv_auth_token_support = true;
config.internal_uv_support = true;
config.override_response_map[CtapRequestCommand::kAuthenticatorClientPin] =
std::make_pair(device::CtapDeviceResponseCode::kCtap2ErrOther,
std::nullopt);
auto state = base::MakeRefCounted<VirtualFidoDevice::State>();
state->fingerprints_enrolled = true;
CtapGetAssertionRequest request(test_data::kRelyingPartyId,
test_data::kClientDataJson);
request.allow_list = {PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(
test_data::kTestGetAssertionCredentialId))};
request.user_verification = UserVerificationRequirement::kRequired;
auto request_handler =
CreateGetAssertionHandlerWithRequest(std::move(request));
discovery()->WaitForCallToStartAndSimulateSuccess();
discovery()->AddDevice(std::make_unique<VirtualCtap2Device>(
std::move(state), std::move(config)));
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_FALSE(get_assertion_future().IsReady());
}
TEST_F(FidoGetAssertionHandlerTest, ReportTransportMetric) {
base::HistogramTester histograms;
auto request_handler = CreateGetAssertionHandlerCtap();
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponse);
discovery()->AddDevice(std::move(device));
auto nfc_device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
nfc_device->SetDeviceTransport(
FidoTransportProtocol::kNearFieldCommunication);
nfc_device->ExpectCtap2CommandAndDoNotRespond(
CtapRequestCommand::kAuthenticatorGetAssertion);
EXPECT_CALL(*nfc_device, Cancel(_));
nfc_discovery()->AddDevice(std::move(nfc_device));
nfc_discovery()->WaitForCallToStartAndSimulateSuccess();
discovery()->WaitForCallToStartAndSimulateSuccess();
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(GetAssertionStatus::kSuccess,
std::get<0>(get_assertion_future().Get()));
histograms.ExpectBucketCount(kRequestTransportHistogram,
FidoTransportProtocol::kUsbHumanInterfaceDevice,
1);
histograms.ExpectBucketCount(kRequestTransportHistogram,
FidoTransportProtocol::kNearFieldCommunication,
1);
histograms.ExpectUniqueSample(kResponseTransportHistogram,
FidoTransportProtocol::kUsbHumanInterfaceDevice,
1);
}
MATCHER_P(GetAssertionRequestWithAllowlist, empty_list, "") {
if (arg.empty() ||
arg[0] != base::strict_cast<uint8_t>(
CtapRequestCommand::kAuthenticatorGetAssertion)) {
*result_listener << "not get assertion";
return false;
}
base::span<const uint8_t> param_bytes(arg);
param_bytes = param_bytes.subspan<1>();
const auto maybe_map = cbor::Reader::Read(param_bytes);
if (!maybe_map || !maybe_map->is_map()) {
*result_listener << "not a map";
return false;
}
const auto& map = maybe_map->GetMap();
const auto options_it = map.find(cbor::Value(3));
if (options_it == map.end() || !options_it->second.is_array()) {
return empty_list;
}
return empty_list == options_it->second.GetArray().empty();
}
TEST_F(FidoGetAssertionHandlerTest, CtapRequestUsesPreselectedAccount) {
auto request_handler =
CreateGetAssertionHandlerWithRequest(CtapGetAssertionRequest(
test_data::kRelyingPartyId, test_data::kClientDataJson));
discovery()->WaitForCallToStartAndSimulateSuccess();
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponseWithUserEntity, base::TimeDelta(),
GetAssertionRequestWithAllowlist(/*empty_list=*/false));
PublicKeyCredentialUserEntity user_entity(
fido_parsing_utils::Materialize(test_data::kUserId), test_data::kUsername,
test_data::kUserDisplayName);
DiscoverableCredentialMetadata preselected_account(
AuthenticatorType::kOther, test_data::kRelyingPartyId,
fido_parsing_utils::Materialize(test_data::kTestGetAssertionCredentialId),
std::move(user_entity),
/*provider_name=*/std::nullopt);
request_handler->PreselectAccount(std::move(preselected_account));
discovery()->AddDevice(std::move(device));
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(GetAssertionStatus::kSuccess,
std::get<0>(get_assertion_future().Get()));
EXPECT_TRUE(std::get<1>(get_assertion_future().Get()));
}
// See https://crbug.com/400761095 for context.
TEST_F(FidoGetAssertionHandlerTest,
CtapRequestIgnoresPreselectedAccountFromOtherAuthenticator) {
auto request_handler =
CreateGetAssertionHandlerWithRequest(CtapGetAssertionRequest(
test_data::kRelyingPartyId, test_data::kClientDataJson));
discovery()->WaitForCallToStartAndSimulateSuccess();
auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponseWithUserEntity, base::TimeDelta(),
GetAssertionRequestWithAllowlist(/*empty_list=*/true));
PublicKeyCredentialUserEntity user_entity(
fido_parsing_utils::Materialize(test_data::kUserId), test_data::kUsername,
test_data::kUserDisplayName);
DiscoverableCredentialMetadata preselected_account(
AuthenticatorType::kEnclave, test_data::kRelyingPartyId,
fido_parsing_utils::Materialize(test_data::kTestGetAssertionCredentialId),
std::move(user_entity),
/*provider_name=*/std::nullopt);
request_handler->PreselectAccount(std::move(preselected_account));
discovery()->AddDevice(std::move(device));
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(GetAssertionStatus::kSuccess,
std::get<0>(get_assertion_future().Get()));
EXPECT_TRUE(std::get<1>(get_assertion_future().Get()));
}
#if BUILDFLAG(IS_WIN)
// Verify that the request handler instantiates a HID device backed
// FidoDeviceAuthenticator or a WinNativeCrossPlatformAuthenticator, depending
// on API availability.
TEST(GetAssertionRequestHandlerWinTest, TestWinUsbDiscovery) {
base::test::TaskEnvironment task_environment;
for (const bool enable_api : {false, true}) {
SCOPED_TRACE(::testing::Message() << "enable_api=" << enable_api);
FakeWinWebAuthnApi api;
api.set_available(enable_api);
api.InjectNonDiscoverableCredential(
test_data::kTestGetAssertionCredentialId, test_data::kRelyingPartyId);
WinWebAuthnApi::ScopedOverride win_webauthn_api_override(&api);
// Simulate a connected HID device.
ScopedFakeFidoHidManager fake_hid_manager;
fake_hid_manager.AddFidoHidDevice("guid");
TestGetAssertionRequestFuture future;
FidoDiscoveryFactory fido_discovery_factory;
CtapGetAssertionRequest request(test_data::kRelyingPartyId,
test_data::kClientDataJson);
request.allow_list = {PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(
test_data::kTestGetAssertionCredentialId))};
auto handler = std::make_unique<GetAssertionRequestHandler>(
&fido_discovery_factory,
std::vector<std::unique_ptr<FidoDiscoveryBase>>(),
base::flat_set<FidoTransportProtocol>(
{FidoTransportProtocol::kUsbHumanInterfaceDevice}),
std::move(request), CtapGetAssertionOptions(),
/*allow_skipping_pin_touch=*/true, future.GetCallback());
task_environment.RunUntilIdle();
EXPECT_EQ(handler->AuthenticatorsForTesting().size(), 1u);
EXPECT_EQ(handler->AuthenticatorsForTesting().begin()->second->GetType() ==
AuthenticatorType::kWinNative,
enable_api);
}
}
#endif // BUILDFLAG(IS_WIN)
} // namespace device