blob: c6bb38c844587c4f9066ed116fb4016d32e66d05 [file] [log] [blame]
// 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