| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "device/fido/credential_management_handler.h" |
| |
| #include <algorithm> |
| #include <array> |
| #include <cstdint> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/containers/flat_set.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/strings/strcat.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "device/fido/credential_management.h" |
| #include "device/fido/fido_constants.h" |
| #include "device/fido/fido_parsing_utils.h" |
| #include "device/fido/fido_transport_protocol.h" |
| #include "device/fido/fido_types.h" |
| #include "device/fido/large_blob.h" |
| #include "device/fido/public_key_credential_descriptor.h" |
| #include "device/fido/public_key_credential_rp_entity.h" |
| #include "device/fido/public_key_credential_user_entity.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" |
| |
| namespace device { |
| namespace { |
| |
| using testing::UnorderedElementsAreArray; |
| |
| constexpr char kPIN[] = "1234"; |
| constexpr uint8_t kCredentialID[] = {0xa, 0xa, 0xa, 0xa, 0xa, 0xa, 0xa, 0xa, |
| 0xa, 0xa, 0xa, 0xa, 0xa, 0xa, 0xa, 0xa}; |
| constexpr char kRPID[] = "example.com"; |
| constexpr char kRPName[] = "Example Corp"; |
| constexpr uint8_t kUserID[] = {0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, |
| 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}; |
| constexpr char kUserName[] = "alice@example.com"; |
| constexpr char kUserDisplayName[] = "Alice Example <alice@example.com>"; |
| |
| class CredentialManagementHandlerTest : public ::testing::Test { |
| protected: |
| std::unique_ptr<CredentialManagementHandler> MakeHandler() { |
| auto handler = std::make_unique<CredentialManagementHandler>( |
| &virtual_device_factory_, |
| base::flat_set<FidoTransportProtocol>{ |
| FidoTransportProtocol::kUsbHumanInterfaceDevice}, |
| ready_future_.GetCallback(), |
| base::BindRepeating(&CredentialManagementHandlerTest::GetPIN, |
| base::Unretained(this)), |
| finished_future_.GetCallback()); |
| return handler; |
| } |
| |
| void GetPIN(CredentialManagementHandler::AuthenticatorProperties |
| authenticator_properties, |
| base::OnceCallback<void(std::string)> provide_pin) { |
| std::move(provide_pin).Run(kPIN); |
| } |
| |
| base::test::TaskEnvironment task_environment_; |
| |
| base::test::TestFuture<void> ready_future_; |
| base::test::TestFuture< |
| CtapDeviceResponseCode, |
| std::optional<std::vector<AggregatedEnumerateCredentialsResponse>>, |
| std::optional<size_t>> |
| get_credentials_future_; |
| base::test::TestFuture<CtapDeviceResponseCode> delete_future_; |
| base::test::TestFuture<CtapDeviceResponseCode> update_user_info_future_; |
| base::test::TestFuture<CredentialManagementStatus> finished_future_; |
| test::VirtualFidoDeviceFactory virtual_device_factory_; |
| }; |
| |
| TEST_F(CredentialManagementHandlerTest, TestDeleteCredentials) { |
| VirtualCtap2Device::Config ctap_config; |
| ctap_config.pin_support = true; |
| ctap_config.resident_key_support = true; |
| ctap_config.credential_management_support = true; |
| ctap_config.resident_credential_storage = 100; |
| virtual_device_factory_.SetCtap2Config(ctap_config); |
| virtual_device_factory_.SetSupportedProtocol(device::ProtocolVersion::kCtap2); |
| virtual_device_factory_.mutable_state()->pin = kPIN; |
| virtual_device_factory_.mutable_state()->pin_retries = device::kMaxPinRetries; |
| |
| PublicKeyCredentialRpEntity rp(kRPID, kRPName); |
| PublicKeyCredentialUserEntity user(fido_parsing_utils::Materialize(kUserID), |
| kUserName, kUserDisplayName); |
| |
| ASSERT_TRUE(virtual_device_factory_.mutable_state()->InjectResidentKey( |
| kCredentialID, rp, user)); |
| |
| auto handler = MakeHandler(); |
| ASSERT_TRUE(ready_future_.Wait()); |
| |
| handler->GetCredentials(get_credentials_future_.GetCallback()); |
| EXPECT_TRUE(get_credentials_future_.Wait()); |
| |
| auto result = get_credentials_future_.Take(); |
| ASSERT_EQ(std::get<0>(result), CtapDeviceResponseCode::kSuccess); |
| auto opt_response = std::move(std::get<1>(result)); |
| ASSERT_TRUE(opt_response); |
| EXPECT_EQ(opt_response->size(), 1u); |
| EXPECT_EQ(opt_response->front().rp, rp); |
| ASSERT_EQ(opt_response->front().credentials.size(), 1u); |
| EXPECT_EQ(opt_response->front().credentials.front().user, user); |
| |
| auto num_remaining = std::get<2>(result); |
| ASSERT_TRUE(num_remaining); |
| EXPECT_EQ(*num_remaining, 99u); |
| |
| handler->DeleteCredentials( |
| {opt_response->front().credentials.front().credential_id}, |
| delete_future_.GetCallback()); |
| |
| EXPECT_TRUE(delete_future_.Wait()); |
| ASSERT_EQ(CtapDeviceResponseCode::kSuccess, delete_future_.Get()); |
| EXPECT_EQ(virtual_device_factory_.mutable_state()->registrations.size(), 0u); |
| EXPECT_FALSE(finished_future_.IsReady()); |
| } |
| |
| // Tests that the credential management handler performs garbage collection when |
| // starting up. |
| TEST_F(CredentialManagementHandlerTest, TestGarbageCollectLargeBlob_Startup) { |
| VirtualCtap2Device::Config ctap_config; |
| ctap_config.pin_support = true; |
| ctap_config.resident_key_support = true; |
| ctap_config.credential_management_support = true; |
| ctap_config.large_blob_support = true; |
| ctap_config.pin_uv_auth_token_support = true; |
| ctap_config.ctap2_versions = {Ctap2Version::kCtap2_1}; |
| virtual_device_factory_.SetCtap2Config(ctap_config); |
| virtual_device_factory_.SetSupportedProtocol(device::ProtocolVersion::kCtap2); |
| virtual_device_factory_.mutable_state()->pin = kPIN; |
| virtual_device_factory_.mutable_state()->pin_retries = device::kMaxPinRetries; |
| std::vector<uint8_t> empty_large_blob = |
| virtual_device_factory_.mutable_state()->large_blob; |
| |
| PublicKeyCredentialRpEntity rp(kRPID, kRPName); |
| PublicKeyCredentialUserEntity user(fido_parsing_utils::Materialize(kUserID), |
| kUserName, kUserDisplayName); |
| ASSERT_TRUE(virtual_device_factory_.mutable_state()->InjectResidentKey( |
| kCredentialID, rp, user)); |
| |
| std::vector<uint8_t> credential_id = |
| fido_parsing_utils::Materialize(kCredentialID); |
| LargeBlob blob(std::vector<uint8_t>{'b', 'l', 'o', 'b'}, 4); |
| virtual_device_factory_.mutable_state()->InjectLargeBlob( |
| &virtual_device_factory_.mutable_state()->registrations.at(credential_id), |
| std::move(blob)); |
| ASSERT_NE(virtual_device_factory_.mutable_state()->large_blob, |
| empty_large_blob); |
| |
| // Orphan the large blob by removing the credential. |
| virtual_device_factory_.mutable_state()->registrations.clear(); |
| |
| auto handler = MakeHandler(); |
| EXPECT_TRUE(ready_future_.Wait()); |
| EXPECT_EQ(virtual_device_factory_.mutable_state()->large_blob, |
| empty_large_blob); |
| } |
| |
| // Tests that CredentialManagementHandler::DeleteCredentials performs large blob |
| // garbage collection. |
| TEST_F(CredentialManagementHandlerTest, TestGarbageCollectLargeBlob_Delete) { |
| VirtualCtap2Device::Config ctap_config; |
| ctap_config.pin_support = true; |
| ctap_config.resident_key_support = true; |
| ctap_config.credential_management_support = true; |
| ctap_config.large_blob_support = true; |
| ctap_config.pin_uv_auth_token_support = true; |
| ctap_config.ctap2_versions = {Ctap2Version::kCtap2_1}; |
| virtual_device_factory_.SetCtap2Config(ctap_config); |
| virtual_device_factory_.SetSupportedProtocol(device::ProtocolVersion::kCtap2); |
| virtual_device_factory_.mutable_state()->pin = kPIN; |
| virtual_device_factory_.mutable_state()->pin_retries = device::kMaxPinRetries; |
| std::vector<uint8_t> empty_large_blob = |
| virtual_device_factory_.mutable_state()->large_blob; |
| |
| auto handler = MakeHandler(); |
| EXPECT_TRUE(ready_future_.Wait()); |
| |
| PublicKeyCredentialRpEntity rp(kRPID, kRPName); |
| PublicKeyCredentialUserEntity user(fido_parsing_utils::Materialize(kUserID), |
| kUserName, kUserDisplayName); |
| ASSERT_TRUE(virtual_device_factory_.mutable_state()->InjectResidentKey( |
| kCredentialID, rp, user)); |
| |
| std::vector<uint8_t> credential_id = |
| fido_parsing_utils::Materialize(kCredentialID); |
| LargeBlob blob(std::vector<uint8_t>{'b', 'l', 'o', 'b'}, 4); |
| virtual_device_factory_.mutable_state()->InjectLargeBlob( |
| &virtual_device_factory_.mutable_state()->registrations.at(credential_id), |
| std::move(blob)); |
| ASSERT_NE(virtual_device_factory_.mutable_state()->large_blob, |
| empty_large_blob); |
| |
| PublicKeyCredentialDescriptor credential(CredentialType::kPublicKey, |
| credential_id); |
| handler->DeleteCredentials({credential}, delete_future_.GetCallback()); |
| EXPECT_TRUE(delete_future_.Wait()); |
| ASSERT_EQ(CtapDeviceResponseCode::kSuccess, delete_future_.Get()); |
| EXPECT_EQ(virtual_device_factory_.mutable_state()->registrations.size(), 0u); |
| EXPECT_EQ(virtual_device_factory_.mutable_state()->large_blob, |
| empty_large_blob); |
| } |
| |
| // Tests that CredentialManagementHandler::DeleteCredentials does not attempt |
| // large blob garbage collection if there is an error deleting the credential. |
| TEST_F(CredentialManagementHandlerTest, |
| TestGarbageCollectLargeBlob_DeleteError) { |
| VirtualCtap2Device::Config ctap_config; |
| ctap_config.pin_support = true; |
| ctap_config.resident_key_support = true; |
| ctap_config.credential_management_support = true; |
| ctap_config.large_blob_support = true; |
| ctap_config.pin_uv_auth_token_support = true; |
| ctap_config.ctap2_versions = {Ctap2Version::kCtap2_1}; |
| virtual_device_factory_.SetCtap2Config(ctap_config); |
| virtual_device_factory_.SetSupportedProtocol(device::ProtocolVersion::kCtap2); |
| virtual_device_factory_.mutable_state()->pin = kPIN; |
| virtual_device_factory_.mutable_state()->pin_retries = device::kMaxPinRetries; |
| std::vector<uint8_t> empty_large_blob = |
| virtual_device_factory_.mutable_state()->large_blob; |
| |
| auto handler = MakeHandler(); |
| EXPECT_TRUE(ready_future_.Wait()); |
| |
| PublicKeyCredentialRpEntity rp(kRPID, kRPName); |
| PublicKeyCredentialUserEntity user(fido_parsing_utils::Materialize(kUserID), |
| kUserName, kUserDisplayName); |
| ASSERT_TRUE(virtual_device_factory_.mutable_state()->InjectResidentKey( |
| kCredentialID, rp, user)); |
| std::vector<uint8_t> credential_id = |
| fido_parsing_utils::Materialize(kCredentialID); |
| LargeBlob blob(std::vector<uint8_t>{'b', 'l', 'o', 'b'}, 4); |
| virtual_device_factory_.mutable_state()->InjectLargeBlob( |
| &virtual_device_factory_.mutable_state()->registrations.at(credential_id), |
| std::move(blob)); |
| ASSERT_NE(virtual_device_factory_.mutable_state()->large_blob, |
| empty_large_blob); |
| |
| // Delete the credential directly from the authenticator. |
| virtual_device_factory_.mutable_state()->registrations.clear(); |
| |
| // Trying to delete the credential again should fail, and it should not |
| // trigger garbage collection. |
| PublicKeyCredentialDescriptor credential(CredentialType::kPublicKey, |
| credential_id); |
| handler->DeleteCredentials({credential}, delete_future_.GetCallback()); |
| EXPECT_TRUE(delete_future_.Wait()); |
| ASSERT_EQ(CtapDeviceResponseCode::kCtap2ErrNoCredentials, |
| delete_future_.Get()); |
| EXPECT_NE(virtual_device_factory_.mutable_state()->large_blob, |
| empty_large_blob); |
| } |
| |
| TEST_F(CredentialManagementHandlerTest, TestUpdateUserInformation) { |
| VirtualCtap2Device::Config ctap_config; |
| ctap_config.pin_support = true; |
| ctap_config.resident_key_support = true; |
| ctap_config.credential_management_support = true; |
| ctap_config.resident_credential_storage = 100; |
| ctap_config.ctap2_versions = {device::Ctap2Version::kCtap2_1}; |
| virtual_device_factory_.SetCtap2Config(ctap_config); |
| virtual_device_factory_.SetSupportedProtocol(device::ProtocolVersion::kCtap2); |
| virtual_device_factory_.mutable_state()->pin = kPIN; |
| virtual_device_factory_.mutable_state()->pin_retries = device::kMaxPinRetries; |
| std::vector<uint8_t> credential_id = |
| fido_parsing_utils::Materialize(kCredentialID); |
| |
| PublicKeyCredentialRpEntity rp(kRPID, kRPName); |
| PublicKeyCredentialUserEntity user(fido_parsing_utils::Materialize(kUserID), |
| kUserName, kUserDisplayName); |
| |
| ASSERT_TRUE(virtual_device_factory_.mutable_state()->InjectResidentKey( |
| kCredentialID, rp, user)); |
| |
| auto handler = MakeHandler(); |
| EXPECT_TRUE(ready_future_.Wait()); |
| |
| PublicKeyCredentialUserEntity updated_user( |
| fido_parsing_utils::Materialize(kUserID), "bobbyr@example.com", |
| "Bobby R. Smith"); |
| |
| handler->UpdateUserInformation( |
| device::PublicKeyCredentialDescriptor(device::CredentialType::kPublicKey, |
| credential_id), |
| updated_user, update_user_info_future_.GetCallback()); |
| EXPECT_TRUE(update_user_info_future_.Wait()); |
| ASSERT_EQ(CtapDeviceResponseCode::kSuccess, update_user_info_future_.Get()); |
| |
| EXPECT_EQ(virtual_device_factory_.mutable_state() |
| ->registrations[credential_id] |
| .user, |
| updated_user); |
| EXPECT_FALSE(finished_future_.IsReady()); |
| } |
| |
| TEST_F(CredentialManagementHandlerTest, TestForcePINChange) { |
| virtual_device_factory_.mutable_state()->pin = kPIN; |
| virtual_device_factory_.mutable_state()->force_pin_change = true; |
| |
| VirtualCtap2Device::Config ctap_config; |
| ctap_config.pin_support = true; |
| ctap_config.resident_key_support = true; |
| ctap_config.credential_management_support = true; |
| ctap_config.min_pin_length_support = true; |
| ctap_config.pin_uv_auth_token_support = true; |
| ctap_config.ctap2_versions = {Ctap2Version::kCtap2_1}; |
| virtual_device_factory_.SetCtap2Config(ctap_config); |
| virtual_device_factory_.SetSupportedProtocol(device::ProtocolVersion::kCtap2); |
| |
| auto handler = MakeHandler(); |
| EXPECT_TRUE(finished_future_.Wait()); |
| ASSERT_EQ(finished_future_.Get(), |
| CredentialManagementStatus::kForcePINChange); |
| } |
| |
| TEST_F(CredentialManagementHandlerTest, |
| EnumerateCredentialResponse_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 |
| |
| VirtualCtap2Device::Config ctap_config; |
| ctap_config.pin_support = true; |
| ctap_config.resident_key_support = true; |
| ctap_config.credential_management_support = true; |
| ctap_config.resident_credential_storage = 100; |
| ctap_config.allow_invalid_utf8_in_credential_entities = true; |
| virtual_device_factory_.SetCtap2Config(ctap_config); |
| virtual_device_factory_.SetSupportedProtocol(device::ProtocolVersion::kCtap2); |
| virtual_device_factory_.mutable_state()->pin = kPIN; |
| virtual_device_factory_.mutable_state()->pin_retries = device::kMaxPinRetries; |
| |
| const std::string rp_name = base::StrCat({std::string(57, 'a'), "💣"}); |
| const std::string user_name = base::StrCat({std::string(57, 'b'), "💣"}); |
| const std::string display_name = base::StrCat({std::string(57, 'c'), "💣"}); |
| constexpr char kTruncatedUTF8[] = "\xf0\x9f\x92"; |
| |
| // Simulate a truncated rp and user entity strings by appending a partial |
| // UTF-8 sequence during InjectResidentKey(). The total string length |
| // including the trailing sequence will be 64 bytes. |
| DCHECK_EQ(rp_name.size(), 61u); |
| |
| ASSERT_TRUE(virtual_device_factory_.mutable_state()->InjectResidentKey( |
| kCredentialID, |
| PublicKeyCredentialRpEntity(kRPID, |
| base::StrCat({rp_name, kTruncatedUTF8})), |
| PublicKeyCredentialUserEntity( |
| fido_parsing_utils::Materialize(kUserID), |
| base::StrCat({user_name, kTruncatedUTF8}), |
| base::StrCat({display_name, kTruncatedUTF8})))); |
| |
| auto handler = MakeHandler(); |
| EXPECT_TRUE(ready_future_.Wait()); |
| handler->GetCredentials(get_credentials_future_.GetCallback()); |
| EXPECT_TRUE(get_credentials_future_.Wait()); |
| |
| auto result = get_credentials_future_.Take(); |
| ASSERT_EQ(std::get<0>(result), CtapDeviceResponseCode::kSuccess); |
| auto opt_response = std::move(std::get<1>(result)); |
| ASSERT_TRUE(opt_response); |
| ASSERT_EQ(opt_response->size(), 1u); |
| ASSERT_EQ(opt_response->front().credentials.size(), 1u); |
| EXPECT_EQ(opt_response->front().rp, |
| PublicKeyCredentialRpEntity(kRPID, rp_name)); |
| EXPECT_EQ( |
| opt_response->front().credentials.front().user, |
| PublicKeyCredentialUserEntity(fido_parsing_utils::Materialize(kUserID), |
| user_name, display_name)); |
| } |
| |
| TEST_F(CredentialManagementHandlerTest, EnumerateCredentialsMultipleRPs) { |
| VirtualCtap2Device::Config ctap_config; |
| ctap_config.pin_support = true; |
| ctap_config.resident_key_support = true; |
| ctap_config.credential_management_support = true; |
| ctap_config.resident_credential_storage = 100; |
| virtual_device_factory_.SetCtap2Config(ctap_config); |
| virtual_device_factory_.SetSupportedProtocol(device::ProtocolVersion::kCtap2); |
| virtual_device_factory_.mutable_state()->pin = kPIN; |
| virtual_device_factory_.mutable_state()->pin_retries = device::kMaxPinRetries; |
| |
| const PublicKeyCredentialRpEntity rps[] = { |
| {"foo.com", "foo"}, |
| {"bar.com", "bar"}, |
| {"foobar.com", "foobar"}, |
| }; |
| const PublicKeyCredentialUserEntity users[] = { |
| {{0}, "alice", "Alice"}, |
| {{1}, "bob", "Bob"}, |
| }; |
| |
| auto credential_id = std::to_array<uint8_t>({0}); |
| for (const auto& rp : rps) { |
| for (const auto& user : users) { |
| ASSERT_TRUE(virtual_device_factory_.mutable_state()->InjectResidentKey( |
| credential_id, rp, user)); |
| credential_id[0]++; |
| } |
| } |
| |
| auto handler = MakeHandler(); |
| EXPECT_TRUE(ready_future_.Wait()); |
| |
| handler->GetCredentials(get_credentials_future_.GetCallback()); |
| EXPECT_TRUE(get_credentials_future_.Wait()); |
| |
| auto result = get_credentials_future_.Take(); |
| ASSERT_EQ(std::get<0>(result), CtapDeviceResponseCode::kSuccess); |
| |
| std::vector<AggregatedEnumerateCredentialsResponse> responses = |
| std::move(*std::get<1>(result)); |
| ASSERT_EQ(responses.size(), 3u); |
| |
| PublicKeyCredentialRpEntity got_rps[3]; |
| std::ranges::transform(responses, std::begin(got_rps), |
| &AggregatedEnumerateCredentialsResponse::rp); |
| EXPECT_THAT(got_rps, UnorderedElementsAreArray(rps)); |
| |
| for (const AggregatedEnumerateCredentialsResponse& response : responses) { |
| ASSERT_EQ(response.credentials.size(), 2u); |
| PublicKeyCredentialUserEntity got_users[2]; |
| std::transform(response.credentials.begin(), response.credentials.end(), |
| std::begin(got_users), |
| [](const auto& credential) { return credential.user; }); |
| EXPECT_THAT(got_users, UnorderedElementsAreArray(users)); |
| } |
| } |
| |
| } // namespace |
| } // namespace device |