| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "device/fido/fake_fido_discovery.h" |
| #include "device/fido/fido_constants.h" |
| #include "device/fido/fido_device.h" |
| #include "device/fido/fido_request_handler.h" |
| #include "device/fido/fido_task.h" |
| #include "device/fido/fido_test_data.h" |
| #include "device/fido/fido_transport_protocol.h" |
| #include "device/fido/mock_fido_device.h" |
| #include "device/fido/test_callback_receiver.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using ::testing::_; |
| |
| namespace device { |
| |
| namespace { |
| |
| using FakeTaskCallback = |
| base::OnceCallback<void(CtapDeviceResponseCode status_code, |
| base::Optional<std::vector<uint8_t>>)>; |
| using FakeHandlerCallback = base::OnceCallback<void( |
| FidoReturnCode status_code, |
| base::Optional<std::vector<uint8_t>> response_data)>; |
| using FakeHandlerCallbackReceiver = |
| test::StatusAndValueCallbackReceiver<FidoReturnCode, |
| base::Optional<std::vector<uint8_t>>>; |
| |
| enum class FakeTaskResponse : uint8_t { |
| kSuccess = 0x00, |
| kErrorReceivedAfterObtainingUserPresence = 0x01, |
| kProcessingError = 0x02, |
| }; |
| |
| // Fake FidoTask implementation that sends an empty byte array to the device |
| // when StartTask() is invoked. |
| class FakeFidoTask : public FidoTask { |
| public: |
| FakeFidoTask(FidoDevice* device, FakeTaskCallback callback) |
| : FidoTask(device), callback_(std::move(callback)), weak_factory_(this) {} |
| ~FakeFidoTask() override = default; |
| |
| void StartTask() override { |
| device()->DeviceTransact(std::vector<uint8_t>(), |
| base::BindOnce(&FakeFidoTask::CompletionCallback, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void CompletionCallback( |
| base::Optional<std::vector<uint8_t>> device_response) { |
| DCHECK(device_response && device_response->size() == 1); |
| switch (static_cast<FakeTaskResponse>(device_response->front())) { |
| case FakeTaskResponse::kSuccess: |
| std::move(callback_).Run(CtapDeviceResponseCode::kSuccess, |
| std::vector<uint8_t>()); |
| return; |
| |
| case FakeTaskResponse::kErrorReceivedAfterObtainingUserPresence: |
| std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrNoCredentials, |
| std::vector<uint8_t>()); |
| return; |
| |
| case FakeTaskResponse::kProcessingError: |
| default: |
| std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrOther, |
| base::nullopt); |
| return; |
| } |
| } |
| |
| private: |
| FakeTaskCallback callback_; |
| base::WeakPtrFactory<FakeFidoTask> weak_factory_; |
| }; |
| |
| class FakeFidoAuthenticator : public FidoDeviceAuthenticator { |
| public: |
| FakeFidoAuthenticator(FidoDevice* device) : FidoDeviceAuthenticator(device) {} |
| |
| void RunFakeTask(FakeTaskCallback callback) { |
| SetTaskForTesting( |
| std::make_unique<FakeFidoTask>(device(), std::move(callback))); |
| } |
| }; |
| |
| class FakeFidoRequestHandler : public FidoRequestHandler<std::vector<uint8_t>> { |
| public: |
| FakeFidoRequestHandler( |
| const base::flat_set<FidoTransportProtocol>& protocols, |
| FakeHandlerCallback callback, |
| AddPlatformAuthenticatorCallback add_platform_authenticator = |
| AddPlatformAuthenticatorCallback()) |
| : FidoRequestHandler(nullptr /* connector */, |
| protocols, |
| std::move(callback), |
| std::move(add_platform_authenticator)), |
| weak_factory_(this) { |
| Start(); |
| } |
| ~FakeFidoRequestHandler() override = default; |
| |
| void DispatchRequest(FidoAuthenticator* authenticator) override { |
| static_cast<FakeFidoAuthenticator*>(authenticator) |
| ->RunFakeTask( |
| base::BindOnce(&FakeFidoRequestHandler::OnAuthenticatorResponse, |
| weak_factory_.GetWeakPtr(), authenticator)); |
| } |
| |
| std::unique_ptr<FidoDeviceAuthenticator> CreateAuthenticatorFromDevice( |
| FidoDevice* device) override { |
| return std::make_unique<FakeFidoAuthenticator>(device); |
| } |
| |
| private: |
| base::WeakPtrFactory<FakeFidoRequestHandler> weak_factory_; |
| }; |
| |
| std::vector<uint8_t> CreateFakeSuccessDeviceResponse() { |
| return {base::strict_cast<uint8_t>(FakeTaskResponse::kSuccess)}; |
| } |
| |
| std::vector<uint8_t> CreateFakeUserPresenceVerifiedError() { |
| return {base::strict_cast<uint8_t>( |
| FakeTaskResponse::kErrorReceivedAfterObtainingUserPresence)}; |
| } |
| |
| std::vector<uint8_t> CreateFakeDeviceProcesssingError() { |
| return {base::strict_cast<uint8_t>(FakeTaskResponse::kProcessingError)}; |
| } |
| |
| } // namespace |
| |
| class FidoRequestHandlerTest : public ::testing::Test { |
| public: |
| void ForgeNextHidDiscovery() { |
| discovery_ = scoped_fake_discovery_factory_.ForgeNextHidDiscovery(); |
| } |
| |
| std::unique_ptr<FakeFidoRequestHandler> CreateFakeHandler() { |
| ForgeNextHidDiscovery(); |
| return std::make_unique<FakeFidoRequestHandler>( |
| base::flat_set<FidoTransportProtocol>( |
| {FidoTransportProtocol::kUsbHumanInterfaceDevice}), |
| cb_.callback()); |
| } |
| |
| std::unique_ptr<FakeFidoRequestHandler> |
| CreateFakeHandlerWithPlatformAuthenticatorCallback( |
| FidoRequestHandlerBase::AddPlatformAuthenticatorCallback |
| add_platform_authenticator) { |
| return std::make_unique<FakeFidoRequestHandler>( |
| base::flat_set<FidoTransportProtocol>(), cb_.callback(), |
| std::move(add_platform_authenticator)); |
| } |
| |
| test::FakeFidoDiscovery* discovery() const { return discovery_; } |
| FakeHandlerCallbackReceiver& callback() { return cb_; } |
| |
| protected: |
| base::test::ScopedTaskEnvironment scoped_task_environment_{ |
| base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME}; |
| test::ScopedFakeFidoDiscoveryFactory scoped_fake_discovery_factory_; |
| test::FakeFidoDiscovery* discovery_; |
| FakeHandlerCallbackReceiver cb_; |
| }; |
| |
| TEST_F(FidoRequestHandlerTest, TestSingleDeviceSuccess) { |
| auto request_handler = CreateFakeHandler(); |
| discovery()->WaitForCallToStartAndSimulateSuccess(); |
| |
| auto device = std::make_unique<MockFidoDevice>(); |
| device->ExpectCtap2CommandAndRespondWith( |
| CtapRequestCommand::kAuthenticatorGetInfo, base::nullopt); |
| EXPECT_CALL(*device, GetId()).WillRepeatedly(testing::Return("device0")); |
| // Device returns success response. |
| device->ExpectRequestAndRespondWith(std::vector<uint8_t>(), |
| CreateFakeSuccessDeviceResponse()); |
| |
| discovery()->AddDevice(std::move(device)); |
| callback().WaitForCallback(); |
| EXPECT_EQ(FidoReturnCode::kSuccess, callback().status()); |
| EXPECT_TRUE(request_handler->is_complete()); |
| } |
| |
| // Tests a scenario where two unresponsive authenticators are connected and |
| // cancel request has been sent either from the user or from the relying party |
| // (i.e. FidoRequestHandler object is destroyed.) Upon destruction, cancel |
| // command must be invoked to all connected authenticators. |
| TEST_F(FidoRequestHandlerTest, TestAuthenticatorHandlerReset) { |
| auto request_handler = CreateFakeHandler(); |
| discovery()->WaitForCallToStartAndSimulateSuccess(); |
| |
| auto device0 = std::make_unique<MockFidoDevice>(); |
| device0->ExpectCtap2CommandAndRespondWith( |
| CtapRequestCommand::kAuthenticatorGetInfo, |
| test_data::kTestAuthenticatorGetInfoResponse); |
| EXPECT_CALL(*device0, GetId()).WillRepeatedly(testing::Return("device0")); |
| device0->ExpectRequestAndDoNotRespond(std::vector<uint8_t>()); |
| EXPECT_CALL(*device0, Cancel()); |
| auto device1 = std::make_unique<MockFidoDevice>(); |
| device1->ExpectCtap2CommandAndRespondWith( |
| CtapRequestCommand::kAuthenticatorGetInfo, |
| test_data::kTestAuthenticatorGetInfoResponse); |
| EXPECT_CALL(*device1, GetId()).WillRepeatedly(testing::Return("device1")); |
| device1->ExpectRequestAndDoNotRespond(std::vector<uint8_t>()); |
| EXPECT_CALL(*device1, Cancel()); |
| |
| discovery()->AddDevice(std::move(device0)); |
| discovery()->AddDevice(std::move(device1)); |
| scoped_task_environment_.FastForwardUntilNoTasksRemain(); |
| request_handler.reset(); |
| } |
| |
| // Test a scenario where 2 devices are connected and a response is received from |
| // only a single device(device1) and the remaining device hangs. |
| TEST_F(FidoRequestHandlerTest, TestRequestWithMultipleDevices) { |
| auto request_handler = CreateFakeHandler(); |
| discovery()->WaitForCallToStartAndSimulateSuccess(); |
| |
| // Represents a connected device that hangs without a response. |
| auto device0 = std::make_unique<MockFidoDevice>(); |
| device0->ExpectCtap2CommandAndRespondWith( |
| CtapRequestCommand::kAuthenticatorGetInfo, |
| test_data::kTestAuthenticatorGetInfoResponse); |
| EXPECT_CALL(*device0, GetId()).WillRepeatedly(testing::Return("device0")); |
| // Device is unresponsive and cancel command is invoked afterwards. |
| device0->ExpectRequestAndDoNotRespond(std::vector<uint8_t>()); |
| EXPECT_CALL(*device0, Cancel()); |
| |
| // Represents a connected device that response successfully. |
| auto device1 = std::make_unique<MockFidoDevice>(); |
| device1->ExpectCtap2CommandAndRespondWith( |
| CtapRequestCommand::kAuthenticatorGetInfo, |
| test_data::kTestAuthenticatorGetInfoResponse); |
| EXPECT_CALL(*device1, GetId()).WillRepeatedly(testing::Return("device1")); |
| device1->ExpectRequestAndRespondWith(std::vector<uint8_t>(), |
| CreateFakeSuccessDeviceResponse()); |
| |
| discovery()->AddDevice(std::move(device0)); |
| discovery()->AddDevice(std::move(device1)); |
| |
| callback().WaitForCallback(); |
| EXPECT_TRUE(request_handler->is_complete()); |
| EXPECT_EQ(FidoReturnCode::kSuccess, callback().status()); |
| } |
| |
| // Test a scenario where 2 devices respond successfully with small time |
| // delay. Only the first received response should be passed on to the relying |
| // party, and cancel request should be sent to the other authenticator. |
| TEST_F(FidoRequestHandlerTest, TestRequestWithMultipleSuccessResponses) { |
| auto request_handler = CreateFakeHandler(); |
| discovery()->WaitForCallToStartAndSimulateSuccess(); |
| |
| // Represents a connected device that responds successfully after small time |
| // delay. |
| auto device0 = std::make_unique<MockFidoDevice>(); |
| device0->ExpectCtap2CommandAndRespondWith( |
| CtapRequestCommand::kAuthenticatorGetInfo, |
| test_data::kTestAuthenticatorGetInfoResponse); |
| EXPECT_CALL(*device0, GetId()).WillRepeatedly(testing::Return("device0")); |
| device0->ExpectRequestAndRespondWith(std::vector<uint8_t>(), |
| CreateFakeSuccessDeviceResponse(), |
| base::TimeDelta::FromMicroseconds(1)); |
| |
| // Represents a device that returns a success response after a longer time |
| // delay. |
| auto device1 = std::make_unique<MockFidoDevice>(); |
| device1->ExpectCtap2CommandAndRespondWith( |
| CtapRequestCommand::kAuthenticatorGetInfo, |
| test_data::kTestAuthenticatorGetInfoResponse); |
| EXPECT_CALL(*device1, GetId()).WillRepeatedly(testing::Return("device1")); |
| device1->ExpectRequestAndRespondWith(std::vector<uint8_t>(), |
| CreateFakeSuccessDeviceResponse(), |
| base::TimeDelta::FromMicroseconds(10)); |
| // Cancel command is invoked after receiving response from |device0|. |
| EXPECT_CALL(*device1, Cancel()); |
| |
| discovery()->AddDevice(std::move(device0)); |
| discovery()->AddDevice(std::move(device1)); |
| |
| scoped_task_environment_.FastForwardUntilNoTasksRemain(); |
| callback().WaitForCallback(); |
| EXPECT_TRUE(request_handler->is_complete()); |
| EXPECT_EQ(FidoReturnCode::kSuccess, callback().status()); |
| } |
| |
| // Test a scenario where 3 devices respond with a processing error, an UP(user |
| // presence) verified failure response with small time delay, and an UP verified |
| // failure response with big time delay, respectively. Request for device with |
| // processing error should be immediately dropped. Also, for UP verified |
| // failures, the first received response should be passed on to the relying |
| // party and cancel command should be sent to the remaining device. |
| TEST_F(FidoRequestHandlerTest, TestRequestWithMultipleFailureResponses) { |
| auto request_handler = CreateFakeHandler(); |
| discovery()->WaitForCallToStartAndSimulateSuccess(); |
| |
| // Represents a connected device that immediately responds with a processing |
| // error. |
| auto device0 = std::make_unique<MockFidoDevice>(); |
| device0->ExpectCtap2CommandAndRespondWith( |
| CtapRequestCommand::kAuthenticatorGetInfo, |
| test_data::kTestAuthenticatorGetInfoResponse); |
| EXPECT_CALL(*device0, GetId()).WillRepeatedly(testing::Return("device0")); |
| device0->ExpectRequestAndRespondWith(std::vector<uint8_t>(), |
| CreateFakeDeviceProcesssingError()); |
| |
| // Represents a device that returns an UP verified failure response after a |
| // small time delay. |
| auto device1 = std::make_unique<MockFidoDevice>(); |
| device1->ExpectCtap2CommandAndRespondWith( |
| CtapRequestCommand::kAuthenticatorGetInfo, |
| test_data::kTestAuthenticatorGetInfoResponse); |
| EXPECT_CALL(*device1, GetId()).WillRepeatedly(testing::Return("device1")); |
| device1->ExpectRequestAndRespondWith(std::vector<uint8_t>(), |
| CreateFakeUserPresenceVerifiedError(), |
| base::TimeDelta::FromMicroseconds(1)); |
| |
| // Represents a device that returns an UP verified failure response after a |
| // big time delay. |
| auto device2 = std::make_unique<MockFidoDevice>(); |
| device2->ExpectCtap2CommandAndRespondWith( |
| CtapRequestCommand::kAuthenticatorGetInfo, |
| test_data::kTestAuthenticatorGetInfoResponse); |
| EXPECT_CALL(*device2, GetId()).WillRepeatedly(testing::Return("device2")); |
| device2->ExpectRequestAndRespondWith(std::vector<uint8_t>(), |
| CreateFakeDeviceProcesssingError(), |
| base::TimeDelta::FromMicroseconds(10)); |
| EXPECT_CALL(*device2, Cancel()); |
| |
| discovery()->AddDevice(std::move(device0)); |
| discovery()->AddDevice(std::move(device1)); |
| discovery()->AddDevice(std::move(device2)); |
| |
| scoped_task_environment_.FastForwardUntilNoTasksRemain(); |
| callback().WaitForCallback(); |
| EXPECT_TRUE(request_handler->is_complete()); |
| EXPECT_EQ(FidoReturnCode::kUserConsentButCredentialNotRecognized, |
| callback().status()); |
| } |
| |
| // Requests should be dispatched to the authenticator returned from the |
| // AddPlatformAuthenticatorCallback if one is passed. |
| TEST_F(FidoRequestHandlerTest, TestPlatformAuthenticatorCallback) { |
| // A platform authenticator usually wouldn't usually use a FidoDevice, but |
| // that's not the point of the test here. The test is only trying to ensure |
| // the authenticator gets injected and used. |
| auto device = MockFidoDevice::MakeCtap(); |
| EXPECT_CALL(*device, GetId()).WillRepeatedly(testing::Return("device0")); |
| // Device returns success response. |
| device->ExpectRequestAndRespondWith(std::vector<uint8_t>(), |
| CreateFakeSuccessDeviceResponse()); |
| |
| FidoRequestHandlerBase::AddPlatformAuthenticatorCallback |
| make_platform_authenticator = base::BindOnce( |
| [](FidoDevice* device) -> std::unique_ptr<FidoAuthenticator> { |
| return std::make_unique<FakeFidoAuthenticator>(device); |
| }, |
| device.get()); |
| auto request_handler = CreateFakeHandlerWithPlatformAuthenticatorCallback( |
| std::move(make_platform_authenticator)); |
| |
| scoped_task_environment_.FastForwardUntilNoTasksRemain(); |
| callback().WaitForCallback(); |
| EXPECT_TRUE(request_handler->is_complete()); |
| EXPECT_EQ(FidoReturnCode::kSuccess, callback().status()); |
| } |
| |
| } // namespace device |