| // Copyright 2020 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 "device/fido/auth_token_requester.h" |
| |
| #include <list> |
| #include <string> |
| |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/span.h" |
| #include "base/logging.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/optional.h" |
| #include "base/run_loop.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/task_environment.h" |
| #include "device/fido/fido_constants.h" |
| #include "device/fido/fido_device_authenticator.h" |
| #include "device/fido/pin.h" |
| #include "device/fido/virtual_ctap2_device.h" |
| |
| namespace device { |
| namespace { |
| |
| using ::testing::ElementsAreArray; |
| |
| using ClientPinAvailability = |
| device::AuthenticatorSupportedOptions::ClientPinAvailability; |
| using UserVerificationAvailability = |
| device::AuthenticatorSupportedOptions::UserVerificationAvailability; |
| |
| constexpr char kTestPIN[] = "1234"; |
| constexpr char16_t kTestPIN16[] = u"1234"; |
| constexpr char16_t kNewPIN[] = u"5678"; |
| |
| struct TestExpectation { |
| pin::PINEntryReason reason; |
| pin::PINEntryError error = pin::PINEntryError::kNoError; |
| uint32_t min_pin_length = kMinPinLength; |
| int attempts = 8; |
| std::u16string pin = kTestPIN16; |
| }; |
| |
| struct TestCase { |
| ClientPinAvailability client_pin; |
| UserVerificationAvailability user_verification; |
| bool success; |
| std::list<TestExpectation> expectations; |
| }; |
| |
| class TestAuthTokenRequesterDelegate : public AuthTokenRequester::Delegate { |
| public: |
| explicit TestAuthTokenRequesterDelegate( |
| std::list<TestExpectation> expectations) |
| : expectations_(std::move(expectations)) {} |
| |
| void WaitForResult() { wait_for_result_loop_.Run(); } |
| base::Optional<AuthTokenRequester::Result>& result() { return result_; } |
| base::Optional<pin::TokenResponse>& response() { return response_; } |
| bool internal_uv_was_retried() { return internal_uv_num_retries_ > 0u; } |
| size_t internal_uv_num_retries() { return internal_uv_num_retries_; } |
| std::list<TestExpectation> expectations() { return expectations_; } |
| |
| private: |
| // AuthTokenRequester::Delegate: |
| void AuthenticatorSelectedForPINUVAuthToken( |
| FidoAuthenticator* authenticator) override { |
| authenticator_selected_ = true; |
| } |
| void CollectPIN(pin::PINEntryReason reason, |
| pin::PINEntryError error, |
| uint32_t min_pin_length, |
| int attempts, |
| ProvidePINCallback provide_pin_cb) override { |
| DCHECK(authenticator_selected_); |
| |
| DCHECK_NE(expectations_.size(), 0u); |
| DCHECK_EQ(reason, expectations_.front().reason); |
| DCHECK_EQ(error, expectations_.front().error); |
| DCHECK_EQ(min_pin_length, expectations_.front().min_pin_length); |
| DCHECK_EQ(attempts, expectations_.front().attempts); |
| |
| std::u16string pin = expectations_.front().pin; |
| expectations_.pop_front(); |
| std::move(provide_pin_cb).Run(pin); |
| } |
| void PromptForInternalUVRetry(int attempts) override { |
| DCHECK(authenticator_selected_); |
| internal_uv_num_retries_++; |
| } |
| void HavePINUVAuthTokenResultForAuthenticator( |
| FidoAuthenticator* authenticator, |
| AuthTokenRequester::Result result, |
| base::Optional<pin::TokenResponse> response) override { |
| if (!base::Contains( |
| std::vector<AuthTokenRequester::Result>{ |
| AuthTokenRequester::Result:: |
| kPreTouchAuthenticatorResponseInvalid, |
| AuthTokenRequester::Result::kPreTouchUnsatisfiableRequest}, |
| result)) { |
| DCHECK(authenticator_selected_); |
| } |
| DCHECK(!result_); |
| result_ = result; |
| response_ = std::move(response); |
| wait_for_result_loop_.Quit(); |
| } |
| |
| std::list<TestExpectation> expectations_; |
| |
| base::Optional<AuthTokenRequester::Result> result_; |
| base::Optional<pin::TokenResponse> response_; |
| |
| bool authenticator_selected_ = false; |
| size_t internal_uv_num_retries_ = 0u; |
| |
| base::RunLoop wait_for_result_loop_; |
| }; |
| |
| class AuthTokenRequesterTest : public ::testing::Test { |
| protected: |
| void SetUp() override {} |
| |
| void RunTestCase(VirtualCtap2Device::Config config, |
| scoped_refptr<VirtualFidoDevice::State> state, |
| const TestCase& test_case) { |
| state_ = state; |
| |
| switch (test_case.client_pin) { |
| case ClientPinAvailability::kNotSupported: |
| config.pin_support = false; |
| break; |
| case ClientPinAvailability::kSupportedButPinNotSet: |
| config.pin_support = true; |
| break; |
| case ClientPinAvailability::kSupportedAndPinSet: |
| config.pin_support = true; |
| state_->pin = kTestPIN; |
| break; |
| } |
| switch (test_case.user_verification) { |
| case UserVerificationAvailability::kNotSupported: |
| config.internal_uv_support = false; |
| break; |
| case UserVerificationAvailability::kSupportedButNotConfigured: |
| config.internal_uv_support = true; |
| break; |
| case UserVerificationAvailability::kSupportedAndConfigured: |
| config.internal_uv_support = true; |
| state_->fingerprints_enrolled = true; |
| break; |
| } |
| |
| auto authenticator = std::make_unique<FidoDeviceAuthenticator>( |
| std::make_unique<VirtualCtap2Device>(state_, std::move(config))); |
| |
| base::RunLoop init_loop; |
| authenticator->InitializeAuthenticator(init_loop.QuitClosure()); |
| init_loop.Run(); |
| |
| delegate_ = std::make_unique<TestAuthTokenRequesterDelegate>( |
| std::move(test_case.expectations)); |
| AuthTokenRequester::Options options; |
| options.token_permissions = {pin::Permissions::kMakeCredential}; |
| options.rp_id = "foobar.com"; |
| AuthTokenRequester requester(delegate_.get(), authenticator.get(), |
| std::move(options)); |
| requester.ObtainPINUVAuthToken(); |
| delegate_->WaitForResult(); |
| } |
| |
| void TearDown() override { EXPECT_EQ(delegate_->expectations().size(), 0u); } |
| |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| |
| scoped_refptr<VirtualFidoDevice::State> state_; |
| std::unique_ptr<TestAuthTokenRequesterDelegate> delegate_; |
| }; |
| |
| TEST_F(AuthTokenRequesterTest, AuthenticatorWithoutUVTokenSupport) { |
| const TestCase kTestCases[]{ |
| { |
| ClientPinAvailability::kNotSupported, |
| UserVerificationAvailability::kNotSupported, |
| false, |
| }, |
| { |
| ClientPinAvailability::kNotSupported, |
| UserVerificationAvailability::kSupportedButNotConfigured, |
| false, |
| }, |
| { |
| ClientPinAvailability::kNotSupported, |
| UserVerificationAvailability::kSupportedAndConfigured, |
| false, |
| }, |
| { |
| ClientPinAvailability::kSupportedButPinNotSet, |
| UserVerificationAvailability::kNotSupported, |
| true, |
| {{.reason = pin::PINEntryReason::kSet, .attempts = 0}}, |
| }, |
| { |
| ClientPinAvailability::kSupportedButPinNotSet, |
| UserVerificationAvailability::kSupportedButNotConfigured, |
| true, |
| {{.reason = pin::PINEntryReason::kSet, .attempts = 0}}, |
| }, |
| { |
| ClientPinAvailability::kSupportedButPinNotSet, |
| UserVerificationAvailability::kSupportedAndConfigured, |
| true, |
| {{.reason = pin::PINEntryReason::kSet, .attempts = 0}}, |
| }, |
| { |
| ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kNotSupported, |
| true, |
| {{pin::PINEntryReason::kChallenge}}, |
| }, |
| { |
| ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kSupportedButNotConfigured, |
| true, |
| {{pin::PINEntryReason::kChallenge}}, |
| }, |
| { |
| ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kSupportedAndConfigured, |
| true, |
| {{pin::PINEntryReason::kChallenge}}, |
| }, |
| }; |
| |
| int i = 0; |
| for (const TestCase& t : kTestCases) { |
| SCOPED_TRACE(i++); |
| VirtualCtap2Device::Config config; |
| config.pin_uv_auth_token_support = false; |
| RunTestCase(std::move(config), |
| base::MakeRefCounted<VirtualFidoDevice::State>(), t); |
| |
| if (t.success) { |
| EXPECT_EQ(*delegate_->result(), AuthTokenRequester::Result::kSuccess); |
| EXPECT_THAT(delegate_->response()->token_for_testing(), |
| ElementsAreArray(state_->pin_token)); |
| } else { |
| EXPECT_EQ(*delegate_->result(), |
| AuthTokenRequester::Result::kPreTouchUnsatisfiableRequest); |
| EXPECT_FALSE(delegate_->response()); |
| } |
| EXPECT_FALSE(delegate_->internal_uv_was_retried()); |
| } |
| } |
| |
| TEST_F(AuthTokenRequesterTest, AuthenticatorWithUVTokenSupport) { |
| const TestCase kTestCases[]{ |
| { |
| ClientPinAvailability::kNotSupported, |
| UserVerificationAvailability::kNotSupported, |
| false, |
| }, |
| { |
| ClientPinAvailability::kNotSupported, |
| UserVerificationAvailability::kSupportedButNotConfigured, |
| false, |
| }, |
| { |
| ClientPinAvailability::kNotSupported, |
| UserVerificationAvailability::kSupportedAndConfigured, |
| true, |
| }, |
| { |
| ClientPinAvailability::kSupportedButPinNotSet, |
| UserVerificationAvailability::kNotSupported, |
| true, |
| {{.reason = pin::PINEntryReason::kSet, .attempts = 0}}, |
| }, |
| { |
| ClientPinAvailability::kSupportedButPinNotSet, |
| UserVerificationAvailability::kSupportedButNotConfigured, |
| true, |
| {{.reason = pin::PINEntryReason::kSet, .attempts = 0}}, |
| }, |
| { |
| ClientPinAvailability::kSupportedButPinNotSet, |
| UserVerificationAvailability::kSupportedAndConfigured, |
| true, |
| }, |
| { |
| ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kNotSupported, |
| true, |
| {{pin::PINEntryReason::kChallenge}}, |
| }, |
| { |
| ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kSupportedButNotConfigured, |
| true, |
| {{pin::PINEntryReason::kChallenge}}, |
| }, |
| { |
| ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kSupportedAndConfigured, |
| true, |
| }, |
| }; |
| |
| int i = 0; |
| for (const TestCase& t : kTestCases) { |
| SCOPED_TRACE(i++); |
| |
| VirtualCtap2Device::Config config; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {std::begin(kCtap2Versions2_1), |
| std::end(kCtap2Versions2_1)}; |
| RunTestCase(std::move(config), |
| base::MakeRefCounted<VirtualFidoDevice::State>(), t); |
| |
| if (t.success) { |
| EXPECT_EQ(*delegate_->result(), AuthTokenRequester::Result::kSuccess); |
| EXPECT_EQ(state_->pin_uv_token_rpid, "foobar.com"); |
| EXPECT_EQ(state_->pin_uv_token_permissions, |
| static_cast<uint8_t>(pin::Permissions::kMakeCredential)); |
| EXPECT_THAT(delegate_->response()->token_for_testing(), |
| ElementsAreArray(state_->pin_token)); |
| EXPECT_FALSE(delegate_->internal_uv_was_retried()); |
| } else { |
| EXPECT_EQ(*delegate_->result(), |
| AuthTokenRequester::Result::kPreTouchUnsatisfiableRequest); |
| EXPECT_FALSE(delegate_->response()); |
| EXPECT_FALSE(delegate_->internal_uv_was_retried()); |
| } |
| } |
| } |
| |
| TEST_F(AuthTokenRequesterTest, PINSoftLock) { |
| VirtualCtap2Device::Config config; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {std::begin(kCtap2Versions2_1), |
| std::end(kCtap2Versions2_1)}; |
| auto state = base::MakeRefCounted<VirtualFidoDevice::State>(); |
| state->soft_locked = true; |
| |
| RunTestCase(std::move(config), state, |
| TestCase{ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kNotSupported, |
| false, |
| {{pin::PINEntryReason::kChallenge}}}); |
| |
| EXPECT_EQ(*delegate_->result(), |
| AuthTokenRequester::Result::kPostTouchAuthenticatorPINSoftLock); |
| EXPECT_FALSE(delegate_->response()); |
| EXPECT_FALSE(delegate_->internal_uv_was_retried()); |
| } |
| |
| TEST_F(AuthTokenRequesterTest, PINHardLock) { |
| VirtualCtap2Device::Config config; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {std::begin(kCtap2Versions2_1), |
| std::end(kCtap2Versions2_1)}; |
| auto state = base::MakeRefCounted<VirtualFidoDevice::State>(); |
| state->pin_retries = 0; |
| |
| RunTestCase(std::move(config), state, |
| TestCase{ |
| ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kNotSupported, |
| false, |
| }); |
| |
| EXPECT_EQ(*delegate_->result(), |
| AuthTokenRequester::Result::kPostTouchAuthenticatorPINHardLock); |
| EXPECT_FALSE(delegate_->response()); |
| EXPECT_FALSE(delegate_->internal_uv_was_retried()); |
| } |
| |
| TEST_F(AuthTokenRequesterTest, PINInvalid) { |
| VirtualCtap2Device::Config config; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {std::begin(kCtap2Versions2_1), |
| std::end(kCtap2Versions2_1)}; |
| auto state = base::MakeRefCounted<VirtualFidoDevice::State>(); |
| RunTestCase( |
| std::move(config), state, |
| TestCase{ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kNotSupported, |
| true, |
| {{.reason = pin::PINEntryReason::kChallenge, |
| .pin = std::u16string({0xd800, 0xd800, 0xd800, 0xd800})}, |
| {pin::PINEntryReason::kChallenge, |
| pin::PINEntryError::kInvalidCharacters}}}); |
| } |
| |
| TEST_F(AuthTokenRequesterTest, PINTooShort) { |
| VirtualCtap2Device::Config config; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {std::begin(kCtap2Versions2_1), |
| std::end(kCtap2Versions2_1)}; |
| auto state = base::MakeRefCounted<VirtualFidoDevice::State>(); |
| RunTestCase( |
| std::move(config), state, |
| TestCase{ |
| ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kNotSupported, |
| true, |
| {{.reason = pin::PINEntryReason::kChallenge, .pin = u"まどか"}, |
| {pin::PINEntryReason::kChallenge, pin::PINEntryError::kTooShort}}}); |
| } |
| |
| TEST_F(AuthTokenRequesterTest, UVLockedPINFallback) { |
| VirtualCtap2Device::Config config; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {std::begin(kCtap2Versions2_1), |
| std::end(kCtap2Versions2_1)}; |
| config.user_verification_succeeds = false; |
| auto state = base::MakeRefCounted<VirtualFidoDevice::State>(); |
| state->uv_retries = 3; |
| |
| RunTestCase(std::move(config), state, |
| TestCase{ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kSupportedAndConfigured, |
| true, |
| {{pin::PINEntryReason::kChallenge, |
| pin::PINEntryError::kInternalUvLocked}}}); |
| |
| EXPECT_EQ(*delegate_->result(), AuthTokenRequester::Result::kSuccess); |
| EXPECT_TRUE(delegate_->response()); |
| EXPECT_EQ(delegate_->internal_uv_num_retries(), 2u); |
| } |
| |
| TEST_F(AuthTokenRequesterTest, UVAlreadyLockedPINFallback) { |
| VirtualCtap2Device::Config config; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {std::begin(kCtap2Versions2_1), |
| std::end(kCtap2Versions2_1)}; |
| config.user_verification_succeeds = false; |
| auto state = base::MakeRefCounted<VirtualFidoDevice::State>(); |
| state->uv_retries = 0; |
| |
| RunTestCase(std::move(config), state, |
| TestCase{ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kSupportedAndConfigured, |
| true, |
| {{pin::PINEntryReason::kChallenge, |
| pin::PINEntryError::kInternalUvLocked}}}); |
| |
| EXPECT_EQ(*delegate_->result(), AuthTokenRequester::Result::kSuccess); |
| EXPECT_TRUE(delegate_->response()); |
| EXPECT_EQ(delegate_->internal_uv_num_retries(), 0u); |
| } |
| |
| TEST_F(AuthTokenRequesterTest, ForcePINChange) { |
| VirtualCtap2Device::Config config; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {std::begin(kCtap2Versions2_1), |
| std::end(kCtap2Versions2_1)}; |
| config.min_pin_length_support = true; |
| auto state = base::MakeRefCounted<VirtualFidoDevice::State>(); |
| state->force_pin_change = true; |
| |
| RunTestCase(std::move(config), state, |
| TestCase{ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kNotSupported, |
| true, |
| {{pin::PINEntryReason::kChallenge}, |
| { |
| .reason = pin::PINEntryReason::kChange, |
| .attempts = 0, |
| .pin = kNewPIN, |
| }}}); |
| |
| EXPECT_EQ(*delegate_->result(), AuthTokenRequester::Result::kSuccess); |
| EXPECT_TRUE(delegate_->response()); |
| } |
| |
| TEST_F(AuthTokenRequesterTest, ForcePINChangeSameAsCurrent) { |
| VirtualCtap2Device::Config config; |
| config.pin_uv_auth_token_support = true; |
| config.ctap2_versions = {std::begin(kCtap2Versions2_1), |
| std::end(kCtap2Versions2_1)}; |
| config.min_pin_length_support = true; |
| auto state = base::MakeRefCounted<VirtualFidoDevice::State>(); |
| state->force_pin_change = true; |
| |
| RunTestCase(std::move(config), state, |
| TestCase{ClientPinAvailability::kSupportedAndPinSet, |
| UserVerificationAvailability::kNotSupported, |
| true, |
| {{pin::PINEntryReason::kChallenge}, |
| { |
| .reason = pin::PINEntryReason::kChange, |
| .attempts = 0, |
| }, |
| { |
| .reason = pin::PINEntryReason::kChange, |
| .error = pin::PINEntryError::kSameAsCurrentPIN, |
| .attempts = 0, |
| .pin = kNewPIN, |
| }}}); |
| |
| EXPECT_EQ(*delegate_->result(), AuthTokenRequester::Result::kSuccess); |
| EXPECT_TRUE(delegate_->response()); |
| } |
| |
| } // namespace |
| } // namespace device |