| // Copyright 2020 The Chromium OS 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 "u2fd/webauthn_handler.h" |
| |
| #include <regex> // NOLINT(build/c++11) |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include <base/check.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <brillo/dbus/mock_dbus_method_response.h> |
| #include <chromeos/cbor/values.h> |
| #include <chromeos/cbor/writer.h> |
| #include <chromeos/dbus/service_constants.h> |
| #include <dbus/bus.h> |
| #include <dbus/mock_bus.h> |
| #include <dbus/mock_object_proxy.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| #include <metrics/metrics_library_mock.h> |
| #include <user_data_auth-client-test/user_data_auth/dbus-proxy-mocks.h> |
| |
| #include "u2fd/mock_allowlisting_util.h" |
| #include "u2fd/mock_u2f_command_processor.h" |
| #include "u2fd/mock_user_state.h" |
| #include "u2fd/mock_webauthn_storage.h" |
| #include "u2fd/util.h" |
| |
| namespace u2f { |
| namespace { |
| |
| using ::brillo::dbus_utils::MockDBusMethodResponse; |
| |
| using ::testing::_; |
| using ::testing::DoAll; |
| using ::testing::Matcher; |
| using ::testing::MatchesRegex; |
| using ::testing::Pointee; |
| using ::testing::Return; |
| using ::testing::SetArgPointee; |
| using ::testing::StrictMock; |
| using ::testing::Unused; |
| |
| // Dummy User State. |
| constexpr char kUserSecret[65] = {[0 ... 63] = 'E', '\0'}; |
| constexpr char kCredentialSecret[65] = {[0 ... 63] = 'F', '\0'}; |
| // Dummy RP id. |
| constexpr char kRpId[] = "example.com"; |
| // Wrong RP id is used to test app id extension path. |
| constexpr char kWrongRpId[] = "wrong.com"; |
| |
| // // Dummy cr50 U2F_SIGN_RESP. |
| const struct u2f_sign_resp kU2fSignResponse = {.sig_r = {[0 ... 31] = 0x12}, |
| .sig_s = {[0 ... 31] = 0x34}}; |
| |
| // AuthenticatorData field sizes, in bytes. |
| constexpr int kRpIdHashBytes = 32; |
| constexpr int kAuthenticatorDataFlagBytes = 1; |
| constexpr int kSignatureCounterBytes = 4; |
| constexpr int kAaguidBytes = 16; |
| constexpr int kCredentialIdLengthBytes = 2; |
| |
| std::vector<uint8_t> GetAuthTimeSecretHash() { |
| return std::vector<uint8_t>(32, 0x12); |
| } |
| |
| std::vector<uint8_t> GetRpIdHash() { |
| return util::Sha256(std::string(kRpId)); |
| } |
| |
| std::vector<uint8_t> GetWrongRpIdHash() { |
| return util::Sha256(std::string(kWrongRpId)); |
| } |
| |
| std::vector<uint8_t> GetCorrectUserSecret() { |
| return std::vector<uint8_t>(32, '\xee'); |
| } |
| |
| std::string GetClientDataHash() { |
| return std::string(SHA256_DIGEST_LENGTH, 0xcd); |
| } |
| |
| // // AAGUID for none attestation. |
| std::vector<uint8_t> GetAaguid() { |
| return std::vector<uint8_t>{0x84, 0x03, 0x98, 0x77, 0xa5, 0x4b, 0xdf, 0xbb, |
| 0x04, 0xa8, 0x2d, 0xf2, 0xfa, 0x2a, 0x11, 0x6e}; |
| } |
| |
| std::vector<uint8_t> GetCredId() { |
| return std::vector<uint8_t>(64, 0xFD); |
| } |
| |
| std::vector<uint8_t> GetVersionedCredId() { |
| return std::vector<uint8_t>(145, 0xFD); |
| } |
| |
| std::string GetCredIdString() { |
| auto cred_id = GetCredId(); |
| return std::string(cred_id.begin(), cred_id.end()); |
| } |
| |
| std::string GetVersionedCredIdString() { |
| auto cred_id = GetVersionedCredId(); |
| return std::string(cred_id.begin(), cred_id.end()); |
| } |
| |
| std::vector<uint8_t> GetCredPubKey() { |
| return std::vector<uint8_t>(65, 0xAB); |
| } |
| |
| std::vector<uint8_t> GetSignature() { |
| return *util::SignatureToDerBytes(kU2fSignResponse.sig_r, |
| kU2fSignResponse.sig_s); |
| } |
| |
| brillo::SecureBlob ArrayToSecureBlob(const char* array) { |
| brillo::SecureBlob blob; |
| CHECK(brillo::SecureBlob::HexStringToSecureBlob(array, &blob)); |
| return blob; |
| } |
| |
| brillo::Blob HexArrayToBlob(const char* array) { |
| brillo::Blob blob; |
| CHECK(base::HexStringToBytes(array, &blob)); |
| return blob; |
| } |
| |
| } // namespace |
| |
| // The base test fixture tests behaviors seen by general consumers. It |
| // disallows presence-only mode, because U2F isn't offered to general |
| // consumers. |
| class WebAuthnHandlerTestBase : public ::testing::Test { |
| public: |
| void SetUp() override { |
| PrepareMockBus(); |
| CreateHandler(U2fMode::kDisabled, /*allowlisting_util=*/nullptr); |
| PrepareMockStorage(); |
| // We use per-credential secret instead of the old user secret. |
| ExpectNoGetUserSecret(); |
| } |
| |
| protected: |
| void PrepareMockBus() { |
| dbus::Bus::Options options; |
| options.bus_type = dbus::Bus::SYSTEM; |
| mock_bus_ = new dbus::MockBus(options); |
| |
| mock_auth_dialog_proxy_ = new dbus::MockObjectProxy( |
| mock_bus_.get(), chromeos::kUserAuthenticationServiceName, |
| dbus::ObjectPath(chromeos::kUserAuthenticationServicePath)); |
| |
| // Set an expectation so that the MockBus will return our mock proxy. |
| EXPECT_CALL(*mock_bus_, |
| GetObjectProxy( |
| chromeos::kUserAuthenticationServiceName, |
| dbus::ObjectPath(chromeos::kUserAuthenticationServicePath))) |
| .WillOnce(Return(mock_auth_dialog_proxy_.get())); |
| } |
| |
| void CreateHandler(U2fMode u2f_mode, |
| std::unique_ptr<AllowlistingUtil> allowlisting_util) { |
| handler_ = std::make_unique<WebAuthnHandler>(); |
| PrepareMockCryptohome(); |
| handler_->Initialize( |
| mock_bus_.get(), /*tpm_proxy=*/nullptr, &mock_user_state_, u2f_mode, |
| /*request_presence=*/[]() {}, std::move(allowlisting_util), |
| &mock_metrics_); |
| auto mock_processor = std::make_unique<MockU2fCommandProcessor>(); |
| mock_processor_ = mock_processor.get(); |
| handler_->SetU2fCommandProcessorForTesting(std::move(mock_processor)); |
| } |
| |
| void PrepareMockCryptohome() { |
| auto mock_cryptohome_proxy = |
| std::make_unique<org::chromium::UserDataAuthInterfaceProxyMock>(); |
| mock_cryptohome_proxy_ = mock_cryptohome_proxy.get(); |
| handler_->SetCryptohomeInterfaceProxyForTesting( |
| std::move(mock_cryptohome_proxy)); |
| } |
| |
| void PrepareMockStorage() { |
| auto mock_storage = std::make_unique<MockWebAuthnStorage>(); |
| mock_webauthn_storage_ = mock_storage.get(); |
| handler_->SetWebAuthnStorageForTesting(std::move(mock_storage)); |
| mock_webauthn_storage_->set_allow_access(true); |
| } |
| |
| void ExpectUVFlowSuccess() { |
| mock_auth_dialog_response_ = dbus::Response::CreateEmpty(); |
| dbus::Response* response = mock_auth_dialog_response_.get(); |
| dbus::MessageWriter writer(response); |
| writer.AppendBool(true); |
| EXPECT_CALL(*mock_auth_dialog_proxy_, DoCallMethod(_, _, _)) |
| .WillOnce( |
| [response](Unused, Unused, |
| base::OnceCallback<void(dbus::Response*)>* callback) { |
| std::move(*callback).Run(response); |
| }); |
| } |
| |
| void ExpectNoGetUserSecret() { |
| EXPECT_CALL(mock_user_state_, GetUserSecret()).Times(0); |
| } |
| |
| void ExpectGetUserSecret() { ExpectGetUserSecretForTimes(1); } |
| |
| void ExpectGetUserSecretForTimes(int times) { |
| EXPECT_CALL(mock_user_state_, GetUserSecret()) |
| .Times(times) |
| .WillRepeatedly(Return(ArrayToSecureBlob(kUserSecret))); |
| } |
| |
| void ExpectGetCounter() { |
| static const std::vector<uint8_t> kSignatureCounter({42, 23, 42, 23}); |
| EXPECT_CALL(mock_user_state_, GetCounter()) |
| .WillOnce(Return(kSignatureCounter)); |
| } |
| |
| void ExpectIncrementCounter() { |
| EXPECT_CALL(mock_user_state_, IncrementCounter()).WillOnce(Return(true)); |
| } |
| |
| std::vector<uint8_t> MakeAuthenticatorData( |
| const std::vector<uint8_t>& credential_id, |
| const std::vector<uint8_t>& credential_public_key, |
| bool user_verified, |
| bool include_attested_credential_data, |
| bool is_u2f_authenticator_credential) { |
| base::Optional<std::vector<uint8_t>> authenticator_data = |
| handler_->MakeAuthenticatorData( |
| GetRpIdHash(), credential_id, credential_public_key, user_verified, |
| include_attested_credential_data, is_u2f_authenticator_credential); |
| DCHECK(authenticator_data); |
| return *authenticator_data; |
| } |
| |
| // Set up an auth-time secret hash as if a user has logged in. |
| void SetUpAuthTimeSecretHash() { |
| handler_->auth_time_secret_hash_ = |
| std::make_unique<brillo::Blob>(GetAuthTimeSecretHash()); |
| } |
| |
| StrictMock<MockUserState> mock_user_state_; |
| |
| std::unique_ptr<WebAuthnHandler> handler_; |
| MockWebAuthnStorage* mock_webauthn_storage_; |
| MockU2fCommandProcessor* mock_processor_; |
| |
| private: |
| scoped_refptr<dbus::MockBus> mock_bus_; |
| scoped_refptr<dbus::MockObjectProxy> mock_auth_dialog_proxy_; |
| std::unique_ptr<dbus::Response> mock_auth_dialog_response_; |
| org::chromium::UserDataAuthInterfaceProxyMock* mock_cryptohome_proxy_; |
| testing::NiceMock<MetricsLibraryMock> mock_metrics_; |
| }; |
| |
| namespace { |
| |
| TEST_F(WebAuthnHandlerTestBase, MakeCredentialUninitialized) { |
| // Use an uninitialized WebAuthnHandler object. |
| handler_.reset(new WebAuthnHandler()); |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<MakeCredentialResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const MakeCredentialResponse& resp) { |
| EXPECT_EQ(resp.status(), MakeCredentialResponse::INTERNAL_ERROR); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| MakeCredentialRequest request; |
| handler_->MakeCredential(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, MakeCredentialEmptyRpId) { |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<MakeCredentialResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const MakeCredentialResponse& resp) { |
| EXPECT_EQ(resp.status(), MakeCredentialResponse::INVALID_REQUEST); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| MakeCredentialRequest request; |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| handler_->MakeCredential(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, MakeCredentialNoAuthTimeSecretHash) { |
| MakeCredentialRequest request; |
| request.set_rp_id(kRpId); |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| |
| ExpectUVFlowSuccess(); |
| EXPECT_CALL(*mock_processor_, |
| U2fGenerate(GetRpIdHash(), _, PresenceRequirement::kNone, true, |
| nullptr, _, _)) |
| .WillRepeatedly(Return(MakeCredentialResponse::INTERNAL_ERROR)); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<MakeCredentialResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const MakeCredentialResponse& resp) { |
| EXPECT_EQ(resp.status(), MakeCredentialResponse::INTERNAL_ERROR); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| handler_->MakeCredential(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, MakeCredentialUPUpgradedToUV) { |
| MakeCredentialRequest request; |
| request.set_rp_id(kRpId); |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| |
| // Though it's going to be UV, we will still check if any exclude credential |
| // matches legacy credentials. |
| ExpectGetUserSecret(); |
| ExpectUVFlowSuccess(); |
| SetUpAuthTimeSecretHash(); |
| EXPECT_CALL(*mock_processor_, |
| U2fGenerate(GetRpIdHash(), _, PresenceRequirement::kNone, true, |
| Pointee(GetAuthTimeSecretHash()), _, _)) |
| .WillOnce(DoAll(SetArgPointee<5>(GetVersionedCredId()), |
| SetArgPointee<6>(GetCredPubKey()), |
| Return(MakeCredentialResponse::SUCCESS))); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<MakeCredentialResponse>>(); |
| handler_->MakeCredential(std::move(mock_method_response), request); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, MakeCredentialVerificationSuccess) { |
| MakeCredentialRequest request; |
| request.set_rp_id(kRpId); |
| request.set_verification_type( |
| VerificationType::VERIFICATION_USER_VERIFICATION); |
| |
| // Thought it's going to be UV, we will still check if any exclude credential |
| // matches legacy credentials. |
| ExpectGetUserSecret(); |
| ExpectUVFlowSuccess(); |
| |
| SetUpAuthTimeSecretHash(); |
| EXPECT_CALL(*mock_processor_, |
| U2fGenerate(GetRpIdHash(), _, PresenceRequirement::kNone, true, |
| Pointee(GetAuthTimeSecretHash()), _, _)) |
| .WillOnce(DoAll(SetArgPointee<5>(GetVersionedCredId()), |
| SetArgPointee<6>(GetCredPubKey()), |
| Return(MakeCredentialResponse::SUCCESS))); |
| // TODO(yichengli): Specify the parameter to WriteRecord. |
| EXPECT_CALL(*mock_webauthn_storage_, WriteRecord(_)).WillOnce(Return(true)); |
| |
| auto rp_id_hash = GetRpIdHash(); |
| auto aaguid = GetAaguid(); |
| |
| const std::string expected_authenticator_data_regex = |
| base::HexEncode(rp_id_hash.data(), |
| rp_id_hash.size()) + // RP ID hash |
| std::string( |
| "45" // Flag: user present, user verified, attested |
| // credential data included. |
| "(..){4}") + // Signature counter |
| base::HexEncode(aaguid.data(), aaguid.size()) + // AAGUID |
| std::string( |
| "0091" // Credential ID length |
| // Credential ID, from kU2fGenerateVersionedResponse: |
| "(FD){65}" // Versioned key handle header |
| "(FD){16}" // Authorization salt |
| "(FD){32}" // Hash of authorization secret |
| "(FD){32}" // Authorization hmac |
| // CBOR encoded credential public key: |
| "(AB){65}"); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<MakeCredentialResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const std::string& expected_authenticator_data, |
| const MakeCredentialResponse& resp) { |
| EXPECT_EQ(resp.status(), MakeCredentialResponse::SUCCESS); |
| EXPECT_THAT(base::HexEncode(resp.authenticator_data().data(), |
| resp.authenticator_data().size()), |
| MatchesRegex(expected_authenticator_data)); |
| EXPECT_EQ(resp.attestation_format(), "none"); |
| EXPECT_EQ(resp.attestation_statement(), "\xa0"); |
| *called_ptr = true; |
| }, |
| &called, expected_authenticator_data_regex)); |
| |
| handler_->MakeCredential(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, GetAssertionUninitialized) { |
| // Use an uninitialized WebAuthnHandler object. |
| handler_.reset(new WebAuthnHandler()); |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const GetAssertionResponse& resp) { |
| EXPECT_EQ(resp.status(), GetAssertionResponse::INTERNAL_ERROR); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| GetAssertionRequest request; |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, GetAssertionEmptyRpId) { |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const GetAssertionResponse& resp) { |
| EXPECT_EQ(resp.status(), GetAssertionResponse::INVALID_REQUEST); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| GetAssertionRequest request; |
| request.set_client_data_hash(GetClientDataHash()); |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, GetAssertionWrongClientDataHashLength) { |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const GetAssertionResponse& resp) { |
| EXPECT_EQ(resp.status(), GetAssertionResponse::INVALID_REQUEST); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| GetAssertionRequest request; |
| request.set_rp_id(kRpId); |
| request.set_client_data_hash(std::string(SHA256_DIGEST_LENGTH - 1, 0xcd)); |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| // Simulates the case where the KH doesn't match any record in daemon-store, or |
| // any legacy credential id. |
| TEST_F(WebAuthnHandlerTestBase, GetAssertionNoCredentialSecret) { |
| GetAssertionRequest request; |
| request.set_rp_id(kWrongRpId); |
| request.set_app_id(kWrongRpId); |
| request.set_client_data_hash(GetClientDataHash()); |
| request.add_allowed_credential_id(GetCredIdString()); |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetCredIdString())) |
| .WillOnce(Return(base::nullopt)); |
| ExpectGetUserSecret(); |
| |
| // We will check for legacy credentials, so two check-only calls to TPM. |
| EXPECT_CALL( |
| *mock_processor_, |
| U2fSignCheckOnly(GetWrongRpIdHash(), GetCredId(), GetCorrectUserSecret())) |
| .Times(2) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const GetAssertionResponse& resp) { |
| EXPECT_EQ(resp.status(), GetAssertionResponse::UNKNOWN_CREDENTIAL_ID); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| // Simulates the case where the KH matches a record in daemon-store but is not |
| // recognized by cr50. This is not very likely in reality unless daemon-store is |
| // compromised. |
| TEST_F(WebAuthnHandlerTestBase, GetAssertionInvalidKeyHandle) { |
| GetAssertionRequest request; |
| request.set_rp_id(kWrongRpId); |
| request.set_app_id(kWrongRpId); |
| request.set_client_data_hash(GetClientDataHash()); |
| request.add_allowed_credential_id(GetCredIdString()); |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetCredIdString())) |
| .WillOnce(Return(HexArrayToBlob(kCredentialSecret))); |
| ExpectGetUserSecret(); |
| // 3 calls to SignCheckOnly, one for each credential type. |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(GetWrongRpIdHash(), GetCredId(), _)) |
| .Times(3) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const GetAssertionResponse& resp) { |
| EXPECT_EQ(resp.status(), GetAssertionResponse::UNKNOWN_CREDENTIAL_ID); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, GetAssertionUPUpgradedToUV) { |
| // Needed for "InsertAuthTimeSecretHash" workaround. |
| SetUpAuthTimeSecretHash(); |
| |
| GetAssertionRequest request; |
| request.set_rp_id(kRpId); |
| request.set_client_data_hash(GetClientDataHash()); |
| request.add_allowed_credential_id(GetVersionedCredIdString()); |
| |
| request.set_verification_type( |
| VerificationType::VERIFICATION_USER_VERIFICATION); |
| |
| // Pass DoU2fSignCheckOnly so that we can get to UV flow. |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetVersionedCredIdString())) |
| .WillRepeatedly(Return(HexArrayToBlob(kCredentialSecret))); |
| ExpectGetUserSecret(); |
| |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(GetRpIdHash(), GetVersionedCredId(), |
| HexArrayToBlob(kCredentialSecret))) |
| .WillRepeatedly(Return(HasCredentialsResponse::SUCCESS)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(_, GetVersionedCredId(), GetCorrectUserSecret())) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSign(GetRpIdHash(), _, GetVersionedCredId(), |
| HexArrayToBlob(kCredentialSecret), |
| PresenceRequirement::kAuthorizationSecret, _)) |
| .WillOnce(Return(GetAssertionResponse::SUCCESS)); |
| |
| ExpectUVFlowSuccess(); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, GetAssertionVerificationSuccess) { |
| // Needed for "InsertAuthTimeSecretHash" workaround. |
| SetUpAuthTimeSecretHash(); |
| |
| GetAssertionRequest request; |
| request.set_rp_id(kRpId); |
| request.set_client_data_hash(GetClientDataHash()); |
| |
| request.add_allowed_credential_id(GetVersionedCredIdString()); |
| |
| request.set_verification_type( |
| VerificationType::VERIFICATION_USER_VERIFICATION); |
| |
| ExpectUVFlowSuccess(); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetVersionedCredIdString())) |
| .WillRepeatedly(Return(HexArrayToBlob(kCredentialSecret))); |
| |
| ExpectGetUserSecret(); |
| |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(GetRpIdHash(), GetVersionedCredId(), |
| HexArrayToBlob(kCredentialSecret))) |
| .WillRepeatedly(Return(HasCredentialsResponse::SUCCESS)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(_, GetVersionedCredId(), GetCorrectUserSecret())) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSign(GetRpIdHash(), _, GetVersionedCredId(), |
| HexArrayToBlob(kCredentialSecret), |
| PresenceRequirement::kAuthorizationSecret, _)) |
| .WillOnce(DoAll(SetArgPointee<5>(GetSignature()), |
| Return(GetAssertionResponse::SUCCESS))); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const std::string& expected_credential_id, |
| const GetAssertionResponse& resp) { |
| auto rp_id_hash = GetRpIdHash(); |
| EXPECT_EQ(resp.status(), GetAssertionResponse::SUCCESS); |
| ASSERT_EQ(resp.assertion_size(), 1); |
| auto assertion = resp.assertion(0); |
| EXPECT_EQ(assertion.credential_id(), expected_credential_id); |
| EXPECT_THAT( |
| base::HexEncode(assertion.authenticator_data().data(), |
| assertion.authenticator_data().size()), |
| MatchesRegex(base::HexEncode(rp_id_hash.data(), |
| rp_id_hash.size()) + // RP ID hash |
| std::string("05" // Flag: user present, user verified |
| "(..){4}"))); // Signature counter |
| EXPECT_EQ(util::ToVector(assertion.signature()), |
| util::SignatureToDerBytes(kU2fSignResponse.sig_r, |
| kU2fSignResponse.sig_s)); |
| *called_ptr = true; |
| }, |
| &called, GetVersionedCredIdString())); |
| |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, HasCredentialsNoMatch) { |
| HasCredentialsRequest request; |
| request.set_rp_id(kWrongRpId); |
| request.set_app_id(kWrongRpId); |
| request.add_credential_id(GetCredIdString()); |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetCredIdString())) |
| .WillRepeatedly(Return(base::nullopt)); |
| ExpectGetUserSecret(); |
| |
| EXPECT_CALL( |
| *mock_processor_, |
| U2fSignCheckOnly(GetWrongRpIdHash(), GetCredId(), GetCorrectUserSecret())) |
| .Times(2) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| |
| auto resp = handler_->HasCredentials(request); |
| EXPECT_EQ(resp.credential_id_size(), 0); |
| EXPECT_EQ(resp.status(), HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID); |
| } |
| |
| // Match first of the 3 types of credentials, i.e. a credential generated by the |
| // platform authenticator with a versioned key handle. |
| TEST_F(WebAuthnHandlerTestBase, HasCredentialsMatchPlatformAuthenticator) { |
| HasCredentialsRequest request; |
| request.set_rp_id(kRpId); |
| request.set_app_id(kRpId); |
| request.add_credential_id(GetVersionedCredIdString()); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetVersionedCredIdString())) |
| .WillOnce(Return(HexArrayToBlob(kCredentialSecret))); |
| ExpectGetUserSecret(); |
| |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(GetRpIdHash(), GetVersionedCredId(), |
| HexArrayToBlob(kCredentialSecret))) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(GetRpIdHash(), GetVersionedCredId(), |
| GetCorrectUserSecret())) |
| .Times(2) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| |
| auto resp = handler_->HasCredentials(request); |
| EXPECT_EQ(resp.credential_id_size(), 1); |
| EXPECT_EQ(resp.status(), HasCredentialsResponse::SUCCESS); |
| } |
| |
| // Match second of the 3 types of credentials: a legacy u2f credential created |
| // by WebAuthnHandler, scoped to an RP ID. |
| TEST_F(WebAuthnHandlerTestBase, HasCredentialsMatchU2fhidWebAuthn) { |
| HasCredentialsRequest request; |
| request.set_rp_id(kRpId); |
| request.add_credential_id(GetCredIdString()); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetCredIdString())) |
| .WillOnce(Return(base::nullopt)); |
| ExpectGetUserSecret(); |
| EXPECT_CALL(*mock_processor_, U2fSignCheckOnly(GetRpIdHash(), GetCredId(), |
| GetCorrectUserSecret())) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| |
| auto resp = handler_->HasCredentials(request); |
| EXPECT_EQ(resp.credential_id_size(), 1); |
| EXPECT_EQ(resp.status(), HasCredentialsResponse::SUCCESS); |
| } |
| |
| // Match third of the 3 types of credentials: a legacy credential created by |
| // u2fhid, scoped to an App ID. |
| TEST_F(WebAuthnHandlerTestBase, HasCredentialsMatchAppId) { |
| HasCredentialsRequest request; |
| request.set_rp_id(kWrongRpId); |
| request.set_app_id(kRpId); |
| request.add_credential_id(GetCredIdString()); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetCredIdString())) |
| .WillOnce(Return(base::nullopt)); |
| ExpectGetUserSecret(); |
| // Matching rp_id fails. |
| EXPECT_CALL( |
| *mock_processor_, |
| U2fSignCheckOnly(GetWrongRpIdHash(), GetCredId(), GetCorrectUserSecret())) |
| .WillOnce(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| // Matching app_id succeeds. |
| EXPECT_CALL(*mock_processor_, U2fSignCheckOnly(GetRpIdHash(), GetCredId(), |
| GetCorrectUserSecret())) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| |
| auto resp = handler_->HasCredentials(request); |
| EXPECT_EQ(resp.credential_id_size(), 1); |
| EXPECT_EQ(resp.status(), HasCredentialsResponse::SUCCESS); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, HasCredentialsSomeMatches) { |
| // Test that HasCredentials with a mix of correct and wrong length credential |
| // IDs succeeds. |
| HasCredentialsRequest request; |
| request.set_rp_id(kRpId); |
| request.set_app_id(kRpId); |
| request.add_credential_id(GetVersionedCredIdString()); |
| std::vector<uint8_t> unknown_credential_id(U2F_V0_KH_SIZE + 1, 0xab); |
| std::string unknown_credential_id_string(unknown_credential_id.begin(), |
| unknown_credential_id.end()); |
| request.add_credential_id(unknown_credential_id_string); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetVersionedCredIdString())) |
| .WillOnce(Return(HexArrayToBlob(kCredentialSecret))); |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(unknown_credential_id_string)) |
| .WillOnce(Return(base::nullopt)); |
| ExpectGetUserSecret(); |
| |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(GetRpIdHash(), GetVersionedCredId(), |
| HexArrayToBlob(kCredentialSecret))) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(GetRpIdHash(), GetVersionedCredId(), |
| GetCorrectUserSecret())) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(GetRpIdHash(), unknown_credential_id, |
| GetCorrectUserSecret())) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| |
| auto resp = handler_->HasCredentials(request); |
| EXPECT_EQ(resp.credential_id_size(), 1); |
| EXPECT_EQ(resp.credential_id()[0], GetVersionedCredIdString()); |
| EXPECT_EQ(resp.status(), HasCredentialsResponse::SUCCESS); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, HasLegacyCredentialsNoMatch) { |
| HasCredentialsRequest request; |
| request.set_rp_id(kWrongRpId); |
| request.set_app_id(kWrongRpId); |
| request.add_credential_id(GetCredIdString()); |
| |
| ExpectGetUserSecret(); |
| EXPECT_CALL( |
| *mock_processor_, |
| U2fSignCheckOnly(GetWrongRpIdHash(), GetCredId(), GetCorrectUserSecret())) |
| .Times(2) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| |
| auto resp = handler_->HasLegacyCredentials(request); |
| EXPECT_EQ(resp.credential_id_size(), 0); |
| EXPECT_EQ(resp.status(), HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID); |
| } |
| |
| // Match second of the 3 types of credentials. |
| // If rp_id matches, it's a legacy credential registered with u2fhid on WebAuthn |
| // API. |
| TEST_F(WebAuthnHandlerTestBase, HasLegacyCredentialsMatchU2fhidWebAuthn) { |
| HasCredentialsRequest request; |
| request.set_rp_id(kRpId); |
| request.add_credential_id(GetCredIdString()); |
| |
| ExpectGetUserSecret(); |
| EXPECT_CALL(*mock_processor_, U2fSignCheckOnly(GetRpIdHash(), GetCredId(), |
| GetCorrectUserSecret())) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| |
| auto resp = handler_->HasLegacyCredentials(request); |
| EXPECT_EQ(resp.credential_id_size(), 1); |
| EXPECT_EQ(resp.status(), HasCredentialsResponse::SUCCESS); |
| } |
| |
| // Match third of the 3 types of credentials. |
| // If app_id matches, it's a legacy credential registered with U2F API. |
| TEST_F(WebAuthnHandlerTestBase, HasLegacyCredentialsMatchAppId) { |
| HasCredentialsRequest request; |
| request.set_rp_id(kWrongRpId); |
| request.set_app_id(kRpId); |
| request.add_credential_id(GetCredIdString()); |
| |
| ExpectGetUserSecret(); |
| // Matching rp_id fails. |
| EXPECT_CALL( |
| *mock_processor_, |
| U2fSignCheckOnly(GetWrongRpIdHash(), GetCredId(), GetCorrectUserSecret())) |
| .WillOnce(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| // Matching app_id succeeds. |
| EXPECT_CALL(*mock_processor_, U2fSignCheckOnly(GetRpIdHash(), GetCredId(), |
| GetCorrectUserSecret())) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| |
| auto resp = handler_->HasLegacyCredentials(request); |
| EXPECT_EQ(resp.credential_id_size(), 1); |
| EXPECT_EQ(resp.status(), HasCredentialsResponse::SUCCESS); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, HasLegacyCredentialsSomeMatches) { |
| // Test that HasLegacyCredentials with a mix of correct and wrong length |
| // credential IDs succeeds. |
| HasCredentialsRequest request; |
| request.set_rp_id(kRpId); |
| request.set_app_id(kRpId); |
| request.add_credential_id(GetCredIdString()); |
| std::vector<uint8_t> unknown_credential_id(U2F_V0_KH_SIZE + 1, 0xab); |
| std::string unknown_credential_id_string(unknown_credential_id.begin(), |
| unknown_credential_id.end()); |
| request.add_credential_id(unknown_credential_id_string); |
| |
| ExpectGetUserSecret(); |
| EXPECT_CALL(*mock_processor_, U2fSignCheckOnly(GetRpIdHash(), GetCredId(), |
| GetCorrectUserSecret())) |
| .WillRepeatedly(Return(HasCredentialsResponse::SUCCESS)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(GetRpIdHash(), unknown_credential_id, |
| GetCorrectUserSecret())) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| |
| auto resp = handler_->HasLegacyCredentials(request); |
| EXPECT_EQ(resp.credential_id_size(), 1); |
| EXPECT_EQ(resp.credential_id()[0], GetCredIdString()); |
| EXPECT_EQ(resp.status(), HasCredentialsResponse::SUCCESS); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, MakeAuthenticatorDataWithAttestedCredData) { |
| const std::vector<uint8_t> cred_id(64, 0xAA); |
| const std::vector<uint8_t> cred_pubkey(65, 0xBB); |
| |
| std::vector<uint8_t> authenticator_data = |
| MakeAuthenticatorData(cred_id, cred_pubkey, /* user_verified = */ |
| false, |
| /* include_attested_credential_data = */ true, |
| /* is_u2f_authenticator_credential = */ false); |
| EXPECT_EQ(authenticator_data.size(), |
| kRpIdHashBytes + kAuthenticatorDataFlagBytes + |
| kSignatureCounterBytes + kAaguidBytes + |
| kCredentialIdLengthBytes + cred_id.size() + cred_pubkey.size()); |
| auto rp_id_hash = GetRpIdHash(); |
| auto aaguid = GetAaguid(); |
| const std::string rp_id_hash_hex = |
| base::HexEncode(rp_id_hash.data(), rp_id_hash.size()); |
| const std::string expected_authenticator_data_regex = |
| rp_id_hash_hex + // RP ID hash |
| std::string( |
| "41" // Flag: user present, attested credential data included |
| "(..){4}") + // Signature counter |
| base::HexEncode(aaguid.data(), aaguid.size()) + // AAGUID |
| std::string( |
| "0040" // Credential ID length |
| "(AA){64}" // Credential ID |
| "(BB){65}"); // Credential public key |
| EXPECT_THAT( |
| base::HexEncode(authenticator_data.data(), authenticator_data.size()), |
| MatchesRegex(expected_authenticator_data_regex)); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, MakeAuthenticatorDataNoAttestedCredData) { |
| std::vector<uint8_t> authenticator_data = |
| MakeAuthenticatorData(std::vector<uint8_t>(), std::vector<uint8_t>(), |
| /* user_verified = */ false, |
| /* include_attested_credential_data = */ false, |
| /* is_u2f_authenticator_credential = */ false); |
| EXPECT_EQ( |
| authenticator_data.size(), |
| kRpIdHashBytes + kAuthenticatorDataFlagBytes + kSignatureCounterBytes); |
| auto rp_id_hash = GetRpIdHash(); |
| const std::string rp_id_hash_hex = |
| base::HexEncode(rp_id_hash.data(), rp_id_hash.size()); |
| const std::string expected_authenticator_data_regex = |
| rp_id_hash_hex + // RP ID hash |
| std::string( |
| "01" // Flag: user present |
| "(..){4}"); // Signature counter |
| EXPECT_THAT( |
| base::HexEncode(authenticator_data.data(), authenticator_data.size()), |
| MatchesRegex(expected_authenticator_data_regex)); |
| } |
| |
| TEST_F(WebAuthnHandlerTestBase, |
| MakeAuthenticatorDataU2fAuthenticatorCredential) { |
| // For U2F authenticator credentials only, the counter comes from UserState. |
| ExpectGetCounter(); |
| ExpectIncrementCounter(); |
| |
| std::vector<uint8_t> authenticator_data = |
| MakeAuthenticatorData(std::vector<uint8_t>(), std::vector<uint8_t>(), |
| /* user_verified = */ false, |
| /* include_attested_credential_data = */ false, |
| /* is_u2f_authenticator_credential = */ true); |
| |
| EXPECT_EQ( |
| base::HexEncode(authenticator_data), |
| base::HexEncode(GetRpIdHash()) + |
| std::string("01" // Flag: user present |
| "2A172A17")); // kSignatureCounter in network byte order |
| } |
| |
| } // namespace |
| |
| // This test fixture tests the behavior when u2f is enabled on the device. |
| class WebAuthnHandlerTestU2fMode : public WebAuthnHandlerTestBase { |
| public: |
| void SetUp() override { |
| PrepareMockBus(); |
| CreateHandler(U2fMode::kU2f, /*allowlisting_util=*/nullptr); |
| PrepareMockStorage(); |
| } |
| }; |
| |
| namespace { |
| |
| TEST_F(WebAuthnHandlerTestU2fMode, MakeCredentialPresenceSuccess) { |
| MakeCredentialRequest request; |
| request.set_rp_id(kRpId); |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| |
| ExpectGetCounter(); |
| ExpectIncrementCounter(); |
| |
| // 1. LegacyCredential uses "user secret" instead of per credential secret. |
| // 2. We will still check if any exclude credential matches legacy |
| // credentials. |
| ExpectGetUserSecretForTimes(2); |
| SetUpAuthTimeSecretHash(); |
| EXPECT_CALL(*mock_processor_, |
| U2fGenerate(GetRpIdHash(), GetCorrectUserSecret(), |
| PresenceRequirement::kPowerButton, false, |
| Pointee(GetAuthTimeSecretHash()), _, _)) |
| .WillOnce(DoAll(SetArgPointee<5>(GetCredId()), |
| SetArgPointee<6>(GetCredPubKey()), |
| Return(MakeCredentialResponse::SUCCESS))); |
| |
| // Since this creates a legacy credential with legacy secret, we won't write |
| // to storage. |
| EXPECT_CALL(*mock_webauthn_storage_, WriteRecord(_)).Times(0); |
| |
| const std::string expected_authenticator_data_regex = |
| base::HexEncode(GetRpIdHash()) + |
| std::string( |
| "41" // Flag: user present, attested credential data included |
| "2A172A17" // kSignatureCounter in network byte order |
| "(00){16}" // AAGUID |
| "0040" // Credential ID length |
| // Credential ID, from kU2fGenerateResponse: |
| "(FD){64}" // (non-versioned) key handle |
| // CBOR encoded credential public key: |
| "(AB){65}"); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<MakeCredentialResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const std::string& expected_authenticator_data, |
| const MakeCredentialResponse& resp) { |
| EXPECT_EQ(resp.status(), MakeCredentialResponse::SUCCESS); |
| EXPECT_THAT(base::HexEncode(resp.authenticator_data().data(), |
| resp.authenticator_data().size()), |
| MatchesRegex(expected_authenticator_data)); |
| EXPECT_EQ(resp.attestation_format(), "fido-u2f"); |
| const std::string expected_attestation_statement = |
| "A2" // Start a CBOR map of 2 elements |
| "63" // Start CBOR text of 3 chars |
| "736967" // "sig" |
| ".+" // Random signature |
| "63" // Start CBOR text of 3 chars |
| "783563" // "x5c" |
| "81" // Start CBOR array of 1 element |
| ".+"; // Random x509 |
| EXPECT_THAT(base::HexEncode(resp.attestation_statement().data(), |
| resp.attestation_statement().size()), |
| MatchesRegex(expected_attestation_statement)); |
| *called_ptr = true; |
| }, |
| &called, expected_authenticator_data_regex)); |
| |
| handler_->MakeCredential(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestU2fMode, GetAssertionSignLegacyCredentialNoPresence) { |
| GetAssertionRequest request; |
| request.set_rp_id(kRpId); |
| request.set_client_data_hash(GetClientDataHash()); |
| request.add_allowed_credential_id(GetCredIdString()); |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| |
| ExpectGetCounter(); |
| ExpectIncrementCounter(); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetCredIdString())) |
| .Times(2) |
| .WillRepeatedly(Return(base::nullopt)); |
| // LegacyCredential uses "user secret" instead of per credential secret. |
| ExpectGetUserSecretForTimes(2); |
| |
| EXPECT_CALL(*mock_processor_, U2fSignCheckOnly(GetRpIdHash(), GetCredId(), |
| GetCorrectUserSecret())) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSign(GetRpIdHash(), _, GetCredId(), GetCorrectUserSecret(), |
| PresenceRequirement::kPowerButton, _)) |
| .WillOnce(Return(GetAssertionResponse::VERIFICATION_FAILED)); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const GetAssertionResponse& resp) { |
| EXPECT_EQ(resp.status(), GetAssertionResponse::VERIFICATION_FAILED); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestU2fMode, GetAssertionSignLegacyCredentialSuccess) { |
| GetAssertionRequest request; |
| request.set_rp_id(kRpId); |
| request.set_client_data_hash(GetClientDataHash()); |
| request.add_allowed_credential_id(GetCredIdString()); |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| |
| ExpectGetCounter(); |
| ExpectIncrementCounter(); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetCredIdString())) |
| .Times(2) |
| .WillRepeatedly(Return(base::nullopt)); |
| // LegacyCredential uses "user secret" instead of per credential secret. |
| ExpectGetUserSecretForTimes(2); |
| EXPECT_CALL(*mock_processor_, U2fSignCheckOnly(GetRpIdHash(), GetCredId(), |
| GetCorrectUserSecret())) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSign(GetRpIdHash(), _, GetCredId(), GetCorrectUserSecret(), |
| PresenceRequirement::kPowerButton, _)) |
| .WillOnce(DoAll(SetArgPointee<5>(GetSignature()), |
| Return(GetAssertionResponse::SUCCESS))); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const GetAssertionResponse& resp) { |
| EXPECT_EQ(resp.status(), GetAssertionResponse::SUCCESS); |
| ASSERT_EQ(resp.assertion_size(), 1); |
| auto assertion = resp.assertion(0); |
| EXPECT_EQ(assertion.credential_id(), GetCredIdString()); |
| EXPECT_EQ( |
| base::HexEncode(assertion.authenticator_data().data(), |
| assertion.authenticator_data().size()), |
| base::HexEncode(GetRpIdHash()) + |
| std::string( |
| "01" // Flag: user present |
| "2A172A17")); // kSignatureCounter in network byte order |
| EXPECT_EQ(util::ToVector(assertion.signature()), |
| util::SignatureToDerBytes(kU2fSignResponse.sig_r, |
| kU2fSignResponse.sig_s)); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestU2fMode, GetAssertionSignLegacyCredentialAppIdMatch) { |
| GetAssertionRequest request; |
| request.set_rp_id(kWrongRpId); |
| // Legacy credentials registered via U2F interface use the app id. |
| request.set_app_id(kRpId); |
| request.set_client_data_hash(GetClientDataHash()); |
| request.add_allowed_credential_id(GetCredIdString()); |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| |
| ExpectGetCounter(); |
| ExpectIncrementCounter(); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetCredIdString())) |
| .Times(2) |
| .WillRepeatedly(Return(base::nullopt)); |
| // LegacyCredential uses "user secret" instead of per credential secret. |
| ExpectGetUserSecretForTimes(2); |
| |
| // Rp id doesn't match. |
| EXPECT_CALL( |
| *mock_processor_, |
| U2fSignCheckOnly(GetWrongRpIdHash(), GetCredId(), GetCorrectUserSecret())) |
| .WillOnce(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| // App id matches. |
| EXPECT_CALL(*mock_processor_, U2fSignCheckOnly(GetRpIdHash(), GetCredId(), |
| GetCorrectUserSecret())) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSign(GetRpIdHash(), _, GetCredId(), GetCorrectUserSecret(), |
| PresenceRequirement::kPowerButton, _)) |
| .WillOnce(DoAll(SetArgPointee<5>(GetSignature()), |
| Return(GetAssertionResponse::SUCCESS))); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const GetAssertionResponse& resp) { |
| EXPECT_EQ(resp.status(), GetAssertionResponse::SUCCESS); |
| ASSERT_EQ(resp.assertion_size(), 1); |
| auto assertion = resp.assertion(0); |
| EXPECT_EQ(assertion.credential_id(), GetCredIdString()); |
| EXPECT_EQ( |
| base::HexEncode(assertion.authenticator_data().data(), |
| assertion.authenticator_data().size()), |
| base::HexEncode(GetRpIdHash()) + |
| std::string( |
| "01" // Flag: user present |
| "2A172A17")); // kSignatureCounter in network byte order |
| EXPECT_EQ(util::ToVector(assertion.signature()), |
| util::SignatureToDerBytes(kU2fSignResponse.sig_r, |
| kU2fSignResponse.sig_s)); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestU2fMode, |
| GetAssertionSignVersionedCredentialInUVMode) { |
| // Needed for "InsertAuthTimeSecretHash" workaround. |
| SetUpAuthTimeSecretHash(); |
| |
| GetAssertionRequest request; |
| request.set_rp_id(kRpId); |
| request.set_client_data_hash(GetClientDataHash()); |
| |
| request.add_allowed_credential_id(GetVersionedCredIdString()); |
| |
| request.set_verification_type( |
| VerificationType::VERIFICATION_USER_VERIFICATION); |
| |
| ExpectUVFlowSuccess(); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetVersionedCredIdString())) |
| .WillRepeatedly(Return(HexArrayToBlob(kCredentialSecret))); |
| ExpectGetUserSecret(); |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(GetRpIdHash(), GetVersionedCredId(), |
| HexArrayToBlob(kCredentialSecret))) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(_, GetVersionedCredId(), GetCorrectUserSecret())) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSign(GetRpIdHash(), _, GetVersionedCredId(), |
| HexArrayToBlob(kCredentialSecret), |
| PresenceRequirement::kAuthorizationSecret, _)) |
| .WillOnce(DoAll(SetArgPointee<5>(GetSignature()), |
| Return(GetAssertionResponse::SUCCESS))); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const std::string& expected_credential_id, |
| const GetAssertionResponse& resp) { |
| auto rp_id_hash = GetRpIdHash(); |
| EXPECT_EQ(resp.status(), GetAssertionResponse::SUCCESS); |
| ASSERT_EQ(resp.assertion_size(), 1); |
| auto assertion = resp.assertion(0); |
| EXPECT_EQ(assertion.credential_id(), expected_credential_id); |
| EXPECT_THAT( |
| base::HexEncode(assertion.authenticator_data().data(), |
| assertion.authenticator_data().size()), |
| MatchesRegex(base::HexEncode(rp_id_hash.data(), |
| rp_id_hash.size()) + // RP ID hash |
| std::string("05" // Flag: user present, user verified |
| "(..){4}"))); // Signature counter |
| EXPECT_EQ(util::ToVector(assertion.signature()), |
| util::SignatureToDerBytes(kU2fSignResponse.sig_r, |
| kU2fSignResponse.sig_s)); |
| *called_ptr = true; |
| }, |
| &called, GetVersionedCredIdString())); |
| |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| TEST_F(WebAuthnHandlerTestU2fMode, |
| GetAssertionWithTwoTypesOfAllowedCredentials) { |
| // Needed for "InsertAuthTimeSecretHash" workaround. |
| SetUpAuthTimeSecretHash(); |
| |
| GetAssertionRequest request; |
| request.set_rp_id(kRpId); |
| request.set_client_data_hash(GetClientDataHash()); |
| // Add a U2F credential to the allow list first. |
| request.add_allowed_credential_id(GetCredIdString()); |
| // Add a platform credential (second type). |
| request.add_allowed_credential_id(GetVersionedCredIdString()); |
| request.set_verification_type( |
| VerificationType::VERIFICATION_USER_VERIFICATION); |
| |
| ExpectUVFlowSuccess(); |
| |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetVersionedCredIdString())) |
| .WillRepeatedly(Return(HexArrayToBlob(kCredentialSecret))); |
| EXPECT_CALL(*mock_webauthn_storage_, |
| GetSecretByCredentialId(GetCredIdString())) |
| .WillRepeatedly(Return(base::nullopt)); |
| ExpectGetUserSecret(); |
| // Both credentials should pass U2fSignCheckOnly, but only the platform |
| // credential should go through U2fSign. |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(GetRpIdHash(), GetVersionedCredId(), |
| HexArrayToBlob(kCredentialSecret))) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| EXPECT_CALL(*mock_processor_, |
| U2fSignCheckOnly(_, GetVersionedCredId(), GetCorrectUserSecret())) |
| .WillRepeatedly(Return(HasCredentialsResponse::UNKNOWN_CREDENTIAL_ID)); |
| EXPECT_CALL(*mock_processor_, U2fSignCheckOnly(GetRpIdHash(), GetCredId(), |
| GetCorrectUserSecret())) |
| .WillOnce(Return(HasCredentialsResponse::SUCCESS)); |
| |
| EXPECT_CALL(*mock_processor_, |
| U2fSign(GetRpIdHash(), _, GetVersionedCredId(), |
| HexArrayToBlob(kCredentialSecret), |
| PresenceRequirement::kAuthorizationSecret, _)) |
| .WillOnce(DoAll(SetArgPointee<5>(GetSignature()), |
| Return(GetAssertionResponse::SUCCESS))); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<GetAssertionResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const std::string& expected_credential_id, |
| const GetAssertionResponse& resp) { |
| auto rp_id_hash = GetRpIdHash(); |
| EXPECT_EQ(resp.status(), GetAssertionResponse::SUCCESS); |
| ASSERT_EQ(resp.assertion_size(), 1); |
| auto assertion = resp.assertion(0); |
| EXPECT_EQ(assertion.credential_id(), expected_credential_id); |
| EXPECT_THAT( |
| base::HexEncode(assertion.authenticator_data().data(), |
| assertion.authenticator_data().size()), |
| MatchesRegex(base::HexEncode(rp_id_hash.data(), |
| rp_id_hash.size()) + // RP ID hash |
| std::string("05" // Flag: user present, user verified |
| "(..){4}"))); // Signature counter |
| EXPECT_EQ(util::ToVector(assertion.signature()), |
| util::SignatureToDerBytes(kU2fSignResponse.sig_r, |
| kU2fSignResponse.sig_s)); |
| *called_ptr = true; |
| }, |
| // The platform credential should appear in the assertion even though it |
| // comes second in the allowed credential list. |
| &called, GetVersionedCredIdString())); |
| |
| handler_->GetAssertion(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| } // namespace |
| |
| // This test fixture tests the behavior when g2f is enabled on the device. |
| class WebAuthnHandlerTestG2fMode : public WebAuthnHandlerTestU2fMode { |
| public: |
| void SetUp() override { |
| PrepareMockBus(); |
| mock_allowlisting_util_ = new StrictMock<MockAllowlistingUtil>(); |
| CreateHandler(U2fMode::kU2fExtended, |
| std::unique_ptr<AllowlistingUtil>(mock_allowlisting_util_)); |
| PrepareMockStorage(); |
| } |
| |
| protected: |
| StrictMock<MockAllowlistingUtil>* mock_allowlisting_util_; // Not Owned. |
| }; |
| |
| namespace { |
| |
| // Example of a cert that would be returned by cr50. |
| constexpr char kDummyG2fCert[] = |
| "308201363081DDA0030201020210442D32429223D041240350303716EE6B300A06082A8648" |
| "CE3D040302300F310D300B06035504031304637235303022180F3230303030313031303030" |
| "3030305A180F32303939313233313233353935395A300F310D300B06035504031304637235" |
| "303059301306072A8648CE3D020106082A8648CE3D030107034200045165719A9975F6FD30" |
| "CC2516C22FE841F65F9D2EE7B8B72F76807AEBD8CA3376005C7FA86453E4B10DB7BFAD5D2B" |
| "D00DB4A7C4845AD06D686ACD0252387618ECA31730153013060B2B0601040182E51C020101" |
| "040403020308300A06082A8648CE3D0403020348003045022100F09976F373920FEF8205C4" |
| "B1FB1DA21EB9F3F176B7DF433A1ADE0F3F38B721960220179D9B9051BFCCCC90BA6BB42B86" |
| "111D7A9C4FB56DFD39FB426081DD027AD609"; |
| |
| std::vector<uint8_t> GetDummyG2fCert() { |
| std::vector<uint8_t> cert; |
| base::HexStringToBytes(kDummyG2fCert, &cert); |
| return cert; |
| } |
| |
| TEST_F(WebAuthnHandlerTestG2fMode, MakeCredentialPresenceSuccess) { |
| MakeCredentialRequest request; |
| request.set_rp_id(kRpId); |
| request.set_verification_type(VerificationType::VERIFICATION_USER_PRESENCE); |
| request.set_attestation_conveyance_preference(MakeCredentialRequest::G2F); |
| |
| ExpectGetCounter(); |
| ExpectIncrementCounter(); |
| |
| // We will need user secret 3 times: |
| // first time for u2f_generate (legacy credential), |
| // second time for g2f attestation command, |
| // third time for checking if any exclude credential matches legacy |
| // credentials. |
| ExpectGetUserSecretForTimes(3); |
| SetUpAuthTimeSecretHash(); |
| EXPECT_CALL(*mock_processor_, |
| U2fGenerate(GetRpIdHash(), _, PresenceRequirement::kPowerButton, |
| false, Pointee(GetAuthTimeSecretHash()), _, _)) |
| .WillOnce(DoAll(SetArgPointee<5>(GetCredId()), |
| SetArgPointee<6>(GetCredPubKey()), |
| Return(MakeCredentialResponse::SUCCESS))); |
| |
| // Since this creates a legacy credential with legacy secret, we won't write |
| // to storage. |
| EXPECT_CALL(*mock_webauthn_storage_, WriteRecord(_)).Times(0); |
| |
| // G2f attestation mock. |
| EXPECT_CALL(*mock_processor_, GetG2fCert()) |
| .WillOnce(Return(GetDummyG2fCert())); |
| EXPECT_CALL(*mock_processor_, G2fAttest(_, _, _, _)) |
| .WillOnce(Return(MakeCredentialResponse::SUCCESS)); |
| EXPECT_CALL(*mock_allowlisting_util_, AppendDataToCert(_)) |
| .WillOnce(Return(true)); |
| |
| auto mock_method_response = |
| std::make_unique<MockDBusMethodResponse<MakeCredentialResponse>>(); |
| bool called = false; |
| mock_method_response->set_return_callback(base::Bind( |
| [](bool* called_ptr, const MakeCredentialResponse& resp) { |
| EXPECT_EQ(resp.status(), MakeCredentialResponse::SUCCESS); |
| EXPECT_EQ(resp.attestation_format(), "fido-u2f"); |
| const std::string expected_attestation_statement = |
| "A2" // Start a CBOR map of 2 elements |
| "63" // Start CBOR text of 3 chars |
| "736967" // "sig" |
| ".+" // Random signature |
| "63" // Start CBOR text of 3 chars |
| "783563" // "x5c" |
| "81" // Start CBOR array of 1 element |
| ".+"; // Random x509 |
| EXPECT_THAT(base::HexEncode(resp.attestation_statement().data(), |
| resp.attestation_statement().size()), |
| MatchesRegex(expected_attestation_statement)); |
| *called_ptr = true; |
| }, |
| &called)); |
| |
| handler_->MakeCredential(std::move(mock_method_response), request); |
| ASSERT_TRUE(called); |
| } |
| |
| } // namespace |
| } // namespace u2f |