#include "device/fido/fido_device_authenticator.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/rand_util.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_test_data.h"
#include "device/fido/fido_types.h"
#include "device/fido/large_blob.h"
#include "device/fido/pin.h"
#include "device/fido/virtual_ctap2_device.h"
#include "device/fido/virtual_fido_device.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace device {
namespace {
using GetAssertionFuture =
base::test::TestFuture<GetAssertionStatus,
std::vector<AuthenticatorGetAssertionResponse>>;
using PinFuture = base::test::TestFuture<CtapDeviceResponseCode,
std::optional<pin::TokenResponse>>;
using GarbageCollectionFuture = base::test::TestFuture<CtapDeviceResponseCode>;
using TouchFuture = base::test::TestFuture<void>;
const std::string kRpId = "galaxy.example.com";
const std::vector<uint8_t> kCredentialId1{1, 1, 1, 1};
const std::vector<uint8_t> kCredentialId2{2, 2, 2, 2};
const std::vector<uint8_t> kUserId1{7, 7, 7, 7};
const std::vector<uint8_t> kUserId2{8, 8, 8, 8};
const std::vector<uint8_t> kSmallBlob1{'r', 'o', 's', 'a'};
const std::vector<uint8_t> kSmallBlob2{'l', 'u', 'm', 'a'};
const std::vector<uint8_t> kSmallBlob3{'s', 't', 'a', 'r'};
constexpr size_t kLargeBlobStorageSize = 4096;
constexpr char kPin[] = "1234";
class FidoDeviceAuthenticatorTest : public testing::Test {
protected:
void SetUp() override {
VirtualCtap2Device::Config config;
config.pin_support = true;
config.large_blob_support = true;
config.resident_key_support = true;
config.available_large_blob_storage = kLargeBlobStorageSize;
config.pin_uv_auth_token_support = true;
config.ctap2_versions = {Ctap2Version::kCtap2_1};
config.credential_management_support = true;
config.return_err_no_credentials_on_empty_rp_enumeration = true;
SetUpAuthenticator(std::move(config));
}
protected:
void SetUpAuthenticator(VirtualCtap2Device::Config config) {
authenticator_state_ = base::MakeRefCounted<VirtualFidoDevice::State>();
auto virtual_device =
std::make_unique<VirtualCtap2Device>(authenticator_state_, config);
CHECK(virtual_device->mutable_state()->InjectResidentKey(
kCredentialId1, kRpId, kUserId1, "rosa", "Rosalina"));
virtual_device->mutable_state()
->registrations.at(kCredentialId1)
.large_blob_key = {{1}};
virtual_device_ = virtual_device.get();
authenticator_ =
std::make_unique<FidoDeviceAuthenticator>(std::move(virtual_device));
base::test::TestFuture<void> future;
authenticator_->InitializeAuthenticator(future.GetCallback());
EXPECT_TRUE(future.Wait());
}
cbor::Value::ArrayValue GetLargeBlobArray() {
LargeBlobArrayReader reader;
reader.Append(virtual_device_->mutable_state()->large_blob);
return *reader.Materialize();
}
pin::TokenResponse GetPINToken() {
virtual_device_->SetPin(kPin);
PinFuture pin_future;
authenticator_->GetPINToken(
kPin,
{pin::Permissions::kLargeBlobWrite, pin::Permissions::kGetAssertion},
kRpId, pin_future.GetCallback());
EXPECT_TRUE(pin_future.Wait());
DCHECK_EQ(std::get<0>(pin_future.Get()), CtapDeviceResponseCode::kSuccess);
return *std::get<1>(pin_future.Get());
}
std::vector<AuthenticatorGetAssertionResponse> GetAssertion(
CtapGetAssertionOptions options,
std::vector<std::vector<uint8_t>> credential_ids) {
CtapGetAssertionRequest request(kRpId, test_data::kClientDataJson);
for (const std::vector<uint8_t>& credential_id : credential_ids) {
request.allow_list.emplace_back(CredentialType::kPublicKey,
credential_id);
}
GetAssertionFuture future;
authenticator_->GetAssertion(std::move(request), std::move(options),
future.GetCallback());
EXPECT_TRUE(future.Wait());
CHECK_EQ(std::get<0>(future.Get()), GetAssertionStatus::kSuccess)
<< " get assertion returned "
<< static_cast<unsigned>(std::get<0>(future.Get()));
std::vector<AuthenticatorGetAssertionResponse> response =
std::get<1>(future.Take());
return response;
}
std::vector<AuthenticatorGetAssertionResponse> GetAssertionForRead(
std::vector<std::vector<uint8_t>> credential_ids = {kCredentialId1}) {
CtapGetAssertionOptions options;
options.large_blob_read = true;
return GetAssertion(std::move(options), std::move(credential_ids));
}
AuthenticatorGetAssertionResponse GetAssertionForWrite(
base::span<const uint8_t> blob,
std::vector<uint8_t> credential_id = kCredentialId1) {
CtapGetAssertionOptions options;
options.large_blob_write.emplace(blob.begin(), blob.end());
std::vector<std::vector<uint8_t>> credential_ids;
credential_ids.push_back(std::move(credential_id));
std::vector<AuthenticatorGetAssertionResponse> responses =
GetAssertion(std::move(options), std::move(credential_ids));
CHECK_EQ(responses.size(), 1u);
return std::move(responses.at(0));
}
scoped_refptr<VirtualFidoDevice::State> authenticator_state_;
std::unique_ptr<FidoDeviceAuthenticator> authenticator_;
raw_ptr<VirtualCtap2Device> virtual_device_;
private:
base::test::SingleThreadTaskEnvironment task_environment_;
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
};
TEST_F(FidoDeviceAuthenticatorTest, TestReadEmptyLargeBlob) {
std::vector<AuthenticatorGetAssertionResponse> assertions =
GetAssertionForRead();
EXPECT_EQ(assertions.size(), 1u);
EXPECT_FALSE(assertions.at(0).large_blob.has_value());
}
TEST_F(FidoDeviceAuthenticatorTest, TestReadInvalidLargeBlob) {
authenticator_state_->large_blob[0] += 1;
std::vector<AuthenticatorGetAssertionResponse> assertions =
GetAssertionForRead();
EXPECT_EQ(assertions.size(), 1u);
EXPECT_FALSE(assertions.at(0).large_blob.has_value());
}
TEST_F(FidoDeviceAuthenticatorTest, TestWriteSmallBlob) {
AuthenticatorGetAssertionResponse write = GetAssertionForWrite(kSmallBlob1);
EXPECT_TRUE(write.large_blob_written);
std::vector<AuthenticatorGetAssertionResponse> read = GetAssertionForRead();
EXPECT_EQ(read.at(0).large_blob, kSmallBlob1);
}
TEST_F(FidoDeviceAuthenticatorTest, TestWriteInvalidLargeBlob) {
authenticator_state_->large_blob[0] += 1;
AuthenticatorGetAssertionResponse write = GetAssertionForWrite(kSmallBlob1);
EXPECT_TRUE(write.large_blob_written);
std::vector<AuthenticatorGetAssertionResponse> read = GetAssertionForRead();
EXPECT_EQ(read.at(0).large_blob, kSmallBlob1);
}
TEST_F(FidoDeviceAuthenticatorTest,
TestWriteBlobDoesNotOverwriteNonStructuredData) {
virtual_device_->mutable_state()->InjectOpaqueLargeBlob(
cbor::Value("comet observatory"));
AuthenticatorGetAssertionResponse write = GetAssertionForWrite(kSmallBlob1);
EXPECT_TRUE(write.large_blob_written);
cbor::Value::ArrayValue large_blob_array = GetLargeBlobArray();
EXPECT_EQ(large_blob_array[0].GetString(), "comet observatory");
EXPECT_TRUE(LargeBlobData::Parse(large_blob_array[1]));
}
TEST_F(FidoDeviceAuthenticatorTest, TestWriteLargeBlob) {
std::array<uint8_t, 2048> large_blob;
base::RandBytes(large_blob);
AuthenticatorGetAssertionResponse write = GetAssertionForWrite(large_blob);
EXPECT_TRUE(write.large_blob_written);
std::vector<AuthenticatorGetAssertionResponse> read = GetAssertionForRead();
EXPECT_EQ(base::span(*read.at(0).large_blob), large_blob);
}
TEST_F(FidoDeviceAuthenticatorTest, TestWriteSmallBlobWithToken) {
pin::TokenResponse pin_token = GetPINToken();
{
CtapGetAssertionOptions options;
options.large_blob_write = kSmallBlob1;
options.pin_uv_auth_token = pin_token;
std::vector<AuthenticatorGetAssertionResponse> responses =
GetAssertion(std::move(options), {kCredentialId1});
EXPECT_EQ(responses.size(), 1u);
EXPECT_TRUE(responses.at(0).large_blob_written);
}
{
CtapGetAssertionOptions options;
options.large_blob_read = true;
options.pin_uv_auth_token = pin_token;
std::vector<AuthenticatorGetAssertionResponse> responses =
GetAssertion(std::move(options), {kCredentialId1});
EXPECT_EQ(responses.size(), 1u);
EXPECT_EQ(responses.at(0).large_blob, kSmallBlob1);
}
}
TEST_F(FidoDeviceAuthenticatorTest, TestUpdateLargeBlob) {
virtual_device_->mutable_state()->InjectResidentKey(kCredentialId2, kRpId,
kUserId2, "luma", "Luma");
virtual_device_->mutable_state()
->registrations.at(kCredentialId2)
.large_blob_key = {{2}};
{
AuthenticatorGetAssertionResponse write =
GetAssertionForWrite(kSmallBlob1, kCredentialId1);
EXPECT_TRUE(write.large_blob_written);
}
{
AuthenticatorGetAssertionResponse write =
GetAssertionForWrite(kSmallBlob2, kCredentialId2);
EXPECT_TRUE(write.large_blob_written);
}
{
AuthenticatorGetAssertionResponse write =
GetAssertionForWrite(kSmallBlob3, kCredentialId1);
EXPECT_TRUE(write.large_blob_written);
}
std::vector<AuthenticatorGetAssertionResponse> read =
GetAssertionForRead({});
ASSERT_EQ(read.size(), 2u);
auto first = std::ranges::find_if(read, [&](const auto& response) {
return response.credential->id == kCredentialId1;
});
ASSERT_NE(first, read.end());
EXPECT_EQ(first->large_blob, kSmallBlob3);
auto second = std::ranges::find_if(read, [&](const auto& response) {
return response.credential->id == kCredentialId2;
});
ASSERT_NE(second, read.end());
EXPECT_EQ(second->large_blob, kSmallBlob2);
}
TEST_F(FidoDeviceAuthenticatorTest, TestWriteLargeBlobTooLarge) {
{
AuthenticatorGetAssertionResponse write = GetAssertionForWrite(kSmallBlob1);
EXPECT_TRUE(write.large_blob_written);
}
std::vector<uint8_t> large_blob;
large_blob.resize(kLargeBlobStorageSize * 2);
base::RandBytes(large_blob);
AuthenticatorGetAssertionResponse write =
GetAssertionForWrite(std::move(large_blob));
EXPECT_FALSE(write.large_blob_written);
std::vector<AuthenticatorGetAssertionResponse> read =
GetAssertionForRead({});
ASSERT_EQ(read.size(), 1u);
EXPECT_EQ(read.at(0).large_blob, kSmallBlob1);
}
TEST_F(FidoDeviceAuthenticatorTest, TestWriteLargeBlobNoLargeBlobKey) {
for (auto& registration : virtual_device_->mutable_state()->registrations) {
registration.second.large_blob_key = std::nullopt;
}
AuthenticatorGetAssertionResponse write = GetAssertionForWrite(kSmallBlob1);
EXPECT_FALSE(write.large_blob_written);
}
TEST_F(FidoDeviceAuthenticatorTest, TestWriteLargeBlobCtapError) {
VirtualCtap2Device::Config config;
config.pin_support = true;
config.large_blob_support = true;
config.resident_key_support = true;
config.available_large_blob_storage = kLargeBlobStorageSize;
config.pin_uv_auth_token_support = true;
config.ctap2_versions = {Ctap2Version::kCtap2_1};
config.override_response_map[CtapRequestCommand::kAuthenticatorLargeBlobs] =
std::make_pair(device::CtapDeviceResponseCode::kCtap1ErrInvalidParameter,
std::nullopt);
SetUpAuthenticator(std::move(config));
AuthenticatorGetAssertionResponse write = GetAssertionForWrite(kSmallBlob1);
EXPECT_FALSE(write.large_blob_written);
}
TEST_F(FidoDeviceAuthenticatorTest, TestGarbageCollectLargeBlob) {
{
AuthenticatorGetAssertionResponse write = GetAssertionForWrite(kSmallBlob1);
EXPECT_TRUE(write.large_blob_written);
}
{
virtual_device_->mutable_state()->InjectResidentKey(
kCredentialId2, kRpId, kUserId2, "luma", "Luma");
virtual_device_->mutable_state()
->registrations.at(kCredentialId2)
.large_blob_key = {{2}};
AuthenticatorGetAssertionResponse write =
GetAssertionForWrite(kSmallBlob2, kCredentialId2);
EXPECT_TRUE(write.large_blob_written);
virtual_device_->mutable_state()->registrations.erase(kCredentialId2);
}
virtual_device_->mutable_state()->InjectOpaqueLargeBlob(
cbor::Value("comet observatory"));
cbor::Value::ArrayValue large_blob_array = GetLargeBlobArray();
ASSERT_EQ(large_blob_array.size(), 3u);
ASSERT_EQ(large_blob_array.at(2).GetString(), "comet observatory");
GarbageCollectionFuture garbage_collection_future;
authenticator_->GarbageCollectLargeBlob(
GetPINToken(), garbage_collection_future.GetCallback());
EXPECT_TRUE(garbage_collection_future.Wait());
EXPECT_EQ(garbage_collection_future.Get(), CtapDeviceResponseCode::kSuccess);
large_blob_array = GetLargeBlobArray();
ASSERT_EQ(large_blob_array.size(), 2u);
EXPECT_EQ(large_blob_array.at(1).GetString(), "comet observatory");
std::vector<AuthenticatorGetAssertionResponse> read = GetAssertionForRead();
ASSERT_EQ(read.size(), 1u);
EXPECT_EQ(read.at(0).large_blob, kSmallBlob1);
}
TEST_F(FidoDeviceAuthenticatorTest, TestGarbageCollectLargeBlobNoChanges) {
{
AuthenticatorGetAssertionResponse write = GetAssertionForWrite(kSmallBlob1);
EXPECT_TRUE(write.large_blob_written);
}
GarbageCollectionFuture gabarge_collection_future;
authenticator_->GarbageCollectLargeBlob(
GetPINToken(), gabarge_collection_future.GetCallback());
EXPECT_TRUE(gabarge_collection_future.Wait());
EXPECT_EQ(gabarge_collection_future.Get(), CtapDeviceResponseCode::kSuccess);
std::vector<AuthenticatorGetAssertionResponse> read = GetAssertionForRead();
ASSERT_EQ(read.size(), 1u);
EXPECT_EQ(read.at(0).large_blob, kSmallBlob1);
}
TEST_F(FidoDeviceAuthenticatorTest, TestGarbageCollectLargeBlobInvalid) {
std::vector<uint8_t> empty_large_blob = authenticator_state_->large_blob;
authenticator_state_->large_blob[0] += 1;
GarbageCollectionFuture gabarge_collection_future;
authenticator_->GarbageCollectLargeBlob(
GetPINToken(), gabarge_collection_future.GetCallback());
EXPECT_TRUE(gabarge_collection_future.Wait());
EXPECT_EQ(gabarge_collection_future.Get(), CtapDeviceResponseCode::kSuccess);
EXPECT_EQ(authenticator_state_->large_blob, empty_large_blob);
}
TEST_F(FidoDeviceAuthenticatorTest, TestGarbageCollectLargeBlobNoCredentials) {
{
AuthenticatorGetAssertionResponse write = GetAssertionForWrite(kSmallBlob1);
EXPECT_TRUE(write.large_blob_written);
virtual_device_->mutable_state()->registrations.clear();
}
cbor::Value::ArrayValue large_blob_array = GetLargeBlobArray();
ASSERT_EQ(large_blob_array.size(), 1u);
GarbageCollectionFuture garbage_collection_future;
authenticator_->GarbageCollectLargeBlob(
GetPINToken(), garbage_collection_future.GetCallback());
EXPECT_TRUE(garbage_collection_future.Wait());
EXPECT_EQ(garbage_collection_future.Get(), CtapDeviceResponseCode::kSuccess);
large_blob_array = GetLargeBlobArray();
ASSERT_EQ(large_blob_array.size(), 0u);
}
TEST_F(FidoDeviceAuthenticatorTest, TestGetTouch) {
for (Ctap2Version version :
{Ctap2Version::kCtap2_0, Ctap2Version::kCtap2_1}) {
SCOPED_TRACE(std::string("CTAP ") +
(version == Ctap2Version::kCtap2_0 ? "2.0" : "2.1"));
VirtualCtap2Device::Config config;
config.ctap2_versions = {version};
SetUpAuthenticator(std::move(config));
TouchFuture future;
bool touch_pressed = false;
authenticator_state_->simulate_press_callback =
base::BindLambdaForTesting([&](VirtualFidoDevice* device) {
touch_pressed = true;
return true;
});
authenticator_->GetTouch(future.GetCallback());
EXPECT_TRUE(future.Wait());
EXPECT_TRUE(touch_pressed);
}
}
}
}