blob: d80e47a7a6e12fde93fa1976af4ab461729ebc3b [file] [log] [blame]
// 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/span.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/optional.h"
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/strings/string16.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 char kNewPIN[] = "5678";
struct TestExpectation {
pin::PINEntryReason reason;
pin::PINEntryError error = pin::PINEntryError::kNoError;
uint32_t min_pin_length = kMinPinLength;
int attempts = 8;
base::string16 pin = base::UTF8ToUTF16(kTestPIN);
};
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);
base::string16 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 = base::string16({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 = base::UTF8ToUTF16("まどか")},
{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 = base::UTF8ToUTF16(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 = base::UTF8ToUTF16(kNewPIN),
}}});
EXPECT_EQ(*delegate_->result(), AuthTokenRequester::Result::kSuccess);
EXPECT_TRUE(delegate_->response());
}
} // namespace
} // namespace device