blob: 5a2bd99831578c1f8a100b96c2df01061678d03d [file]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "device/fido/get_assertion_task.h"
#include <array>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/containers/span.h"
#include "base/numerics/safe_conversions.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "components/device_event_log/device_event_log.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/device_response_converter.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/fido_test_data.h"
#include "device/fido/mock_fido_device.h"
#include "device/fido/public/fido_constants.h"
#include "device/fido/public/fido_types.h"
#include "device/fido/public/public_key_credential_descriptor.h"
#include "device/fido/virtual_ctap2_device.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
namespace device {
namespace {
using TestGetAssertionTaskFuture =
base::test::TestFuture<CtapDeviceResponseCode,
std::vector<AuthenticatorGetAssertionResponse>>;
class FidoGetAssertionTaskTest : public testing::Test {
public:
FidoGetAssertionTaskTest() = default;
TestGetAssertionTaskFuture& get_assertion_future() { return future_; }
private:
base::test::TaskEnvironment task_environment_;
TestGetAssertionTaskFuture future_;
};
TEST_F(FidoGetAssertionTaskTest, TestGetAssertionSuccess) {
auto device = MockFidoDevice::MakeCtap();
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
test_data::kTestGetAssertionResponse);
CtapGetAssertionRequest request_param(test_data::kRelyingPartyId,
test_data::kClientDataJson);
request_param.allow_list.emplace_back(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(
test_data::kTestGetAssertionCredentialId));
auto task = std::make_unique<GetAssertionTask>(
device.get(), std::move(request_param), CtapGetAssertionOptions(),
get_assertion_future().GetCallback());
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(CtapDeviceResponseCode::kSuccess,
std::get<0>(get_assertion_future().Get()));
EXPECT_EQ(std::get<1>(get_assertion_future().Get()).size(), 1u);
}
TEST_F(FidoGetAssertionTaskTest, RedactLog) {
// This is a base 64 encoded CTAP get assertion response with:
// * largeBlobKey
// * largeBlob extension
// * PRF extension
// All the values intended to be redacted are set to the string "secret".
// (The response is not valid, but that's okay here.)
constexpr char kTestCtapGetAssertionResponse[] =
"AKUBomJpZFBP92OYWX3ztmRWuLsC74ndZHR5cGVqcHVibGljLWtleQJYJcRs74KtG1Rkd1kd"
"AIsIdZ7D5tLstPOUdL/"
"qaWmSXQO3HQAAAAADZnNlY3JldAdmc2VjcmV0CKJjcHJmomdlbmFibGVk9GdyZXN1bHRzoWV"
"maXJzdGZzZWNyZXRpbGFyZ2VCbG9iZnNlY3JldA==";
auto device = MockFidoDevice::MakeCtap();
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion,
*base::Base64Decode(kTestCtapGetAssertionResponse));
device_event_log::Initialize(/*max_entries=*/0);
CtapGetAssertionRequest request_param(test_data::kRelyingPartyId,
test_data::kClientDataJson);
auto task = std::make_unique<GetAssertionTask>(
device.get(), std::move(request_param), CtapGetAssertionOptions(),
get_assertion_future().GetCallback());
EXPECT_TRUE(get_assertion_future().Wait());
// Signature.
std::string device_log = device_event_log::GetAsString(
device_event_log::NEWEST_FIRST, /*format=*/"level",
/*types=*/"fido",
/*max_level=*/device_event_log::LOG_LEVEL_EVENT, /*max_events=*/0);
EXPECT_THAT(device_log, testing::HasSubstr("3: \"[redacted]\""));
// Large blob key.
EXPECT_THAT(device_log, testing::HasSubstr("7: \"[redacted]\""));
// PRF extension.
EXPECT_THAT(
device_log,
testing::HasSubstr(
"8: {\"prf\": {\"enabled\": false, \"results\": \"[redacted]\""));
// Large blob extension.
EXPECT_THAT(device_log, testing::HasSubstr("\"largeBlob\": \"[redacted]\""));
// Verify that the data hasn't escaped redaction somehow.
EXPECT_THAT(device_log, testing::Not(testing::HasSubstr("secret")));
device_event_log::Shutdown();
}
TEST_F(FidoGetAssertionTaskTest, TestU2fSignSuccess) {
auto device = MockFidoDevice::MakeU2f();
device->ExpectWinkedAtLeastOnce();
device->ExpectRequestAndRespondWith(
test_data::kU2fSignCommandApdu,
test_data::kApduEncodedNoErrorSignResponse);
CtapGetAssertionRequest request_param(test_data::kRelyingPartyId,
test_data::kClientDataJson);
request_param.allow_list.emplace_back(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(test_data::kU2fSignKeyHandle));
auto task = std::make_unique<GetAssertionTask>(
device.get(), std::move(request_param), CtapGetAssertionOptions(),
get_assertion_future().GetCallback());
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(CtapDeviceResponseCode::kSuccess,
std::get<0>(get_assertion_future().Get()));
EXPECT_EQ(std::get<1>(get_assertion_future().Get()).size(), 1u);
}
TEST_F(FidoGetAssertionTaskTest, TestSignSuccessWithFake) {
static const uint8_t kCredentialId[] = {1, 2, 3, 4};
CtapGetAssertionRequest request_param(test_data::kRelyingPartyId,
test_data::kClientDataJson);
request_param.allow_list.emplace_back(PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(kCredentialId)));
auto device = std::make_unique<VirtualCtap2Device>();
ASSERT_TRUE(device->mutable_state()->InjectRegistration(
kCredentialId, test_data::kRelyingPartyId));
base::test::TestFuture<void> done;
device->DiscoverSupportedProtocolAndDeviceInfo(done.GetCallback());
EXPECT_TRUE(done.Wait());
auto task = std::make_unique<GetAssertionTask>(
device.get(), std::move(request_param), CtapGetAssertionOptions(),
get_assertion_future().GetCallback());
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(CtapDeviceResponseCode::kSuccess,
std::get<0>(get_assertion_future().Get()));
// Just a sanity check, we don't verify the actual signature.
ASSERT_GE(32u + 1u + 4u + 8u, // Minimal ECDSA signature is 8 bytes
std::get<1>(get_assertion_future().Get())
.at(0)
.authenticator_data.SerializeToByteArray()
.size());
EXPECT_EQ(0x01,
std::get<1>(get_assertion_future().Get())
.at(0)
.authenticator_data.SerializeToByteArray()[32]); // UP flag
// Counter starts at zero and is incremented for every sign request.
EXPECT_EQ(1, std::get<1>(get_assertion_future().Get())
.at(0)
.authenticator_data.SerializeToByteArray()[36]); // counter
}
TEST_F(FidoGetAssertionTaskTest, TestIncorrectGetAssertionResponse) {
auto device = MockFidoDevice::MakeCtap();
device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetAssertion, std::nullopt);
auto task = std::make_unique<GetAssertionTask>(
device.get(),
CtapGetAssertionRequest(test_data::kRelyingPartyId,
test_data::kClientDataJson),
CtapGetAssertionOptions(), get_assertion_future().GetCallback());
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(CtapDeviceResponseCode::kCtap2ErrOther,
std::get<0>(get_assertion_future().Get()));
EXPECT_TRUE(std::get<1>(get_assertion_future().Get()).empty());
}
TEST_F(FidoGetAssertionTaskTest, TestU2fSignRequestWithEmptyAllowedList) {
auto request = CtapGetAssertionRequest(test_data::kRelyingPartyId,
test_data::kClientDataJson);
auto device = MockFidoDevice::MakeU2f();
device->ExpectWinkedAtLeastOnce();
device->ExpectRequestAndRespondWith(
test_data::kU2fFakeRegisterCommand,
test_data::kApduEncodedNoErrorSignResponse);
auto task = std::make_unique<GetAssertionTask>(
device.get(), std::move(request), CtapGetAssertionOptions(),
get_assertion_future().GetCallback());
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(CtapDeviceResponseCode::kCtap2ErrNoCredentials,
std::get<0>(get_assertion_future().Get()));
EXPECT_TRUE(std::get<1>(get_assertion_future().Get()).empty());
}
// Checks that when device supports both CTAP2 and U2F protocol and when
// appId extension parameter is present, the browser first checks presence
// of valid credentials via silent authentication.
TEST_F(FidoGetAssertionTaskTest, TestSilentSignInWhenAppIdExtensionPresent) {
CtapGetAssertionRequest request(test_data::kRelyingPartyId,
test_data::kClientDataJson);
std::vector<PublicKeyCredentialDescriptor> allowed_list;
allowed_list.push_back(PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(test_data::kU2fSignKeyHandle)));
request.app_id = test_data::kAppId;
request.alternative_application_parameter =
fido_parsing_utils::Materialize(base::span<const uint8_t, 32>(
test_data::kAlternativeApplicationParameter));
request.allow_list = std::move(allowed_list);
auto device = MockFidoDevice::MakeCtap();
device->ExpectRequestAndRespondWith(test_data::kCtapSilentGetAssertionRequest,
test_data::kTestGetAssertionResponse);
device->ExpectRequestAndRespondWith(test_data::kCtapGetAssertionRequest,
test_data::kTestGetAssertionResponse);
auto task = std::make_unique<GetAssertionTask>(
device.get(), std::move(request), CtapGetAssertionOptions(),
get_assertion_future().GetCallback());
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(CtapDeviceResponseCode::kSuccess,
std::get<0>(get_assertion_future().Get()));
}
TEST_F(FidoGetAssertionTaskTest, TestU2fFallbackForAppIdExtension) {
CtapGetAssertionRequest request(test_data::kRelyingPartyId,
test_data::kClientDataJson);
std::vector<PublicKeyCredentialDescriptor> allowed_list;
allowed_list.push_back(PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(test_data::kU2fSignKeyHandle)));
request.app_id = test_data::kAppId;
request.alternative_application_parameter =
fido_parsing_utils::Materialize(base::span<const uint8_t, 32>(
test_data::kAlternativeApplicationParameter));
request.allow_list = std::move(allowed_list);
::testing::InSequence s;
auto device = MockFidoDevice::MakeCtap();
std::array<uint8_t, 1> error{{base::strict_cast<uint8_t>(
CtapDeviceResponseCode::kCtap2ErrNoCredentials)}};
// First, as the device supports both CTAP2 and U2F, the browser will attempt
// a CTAP2 GetAssertion.
device->ExpectRequestAndRespondWith(test_data::kCtapSilentGetAssertionRequest,
error);
// After falling back to U2F the request will use the alternative app_param,
// which will be rejected.
device->ExpectWinkedAtLeastOnce();
device->ExpectRequestAndRespondWith(
test_data::kU2fSignCommandApduWithAlternativeApplicationParameter,
test_data::kU2fWrongDataApduResponse);
// After the rejection, the U2F sign request with the primary application
// parameter should be tried.
device->ExpectWinkedAtLeastOnce();
device->ExpectRequestAndRespondWith(
test_data::kU2fSignCommandApdu,
test_data::kApduEncodedNoErrorSignResponse);
auto task = std::make_unique<GetAssertionTask>(
device.get(), std::move(request), CtapGetAssertionOptions(),
get_assertion_future().GetCallback());
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(CtapDeviceResponseCode::kSuccess,
std::get<0>(get_assertion_future().Get()));
}
TEST_F(FidoGetAssertionTaskTest, TestAvoidSilentSignInForCtapOnlyDevice) {
CtapGetAssertionRequest request(test_data::kRelyingPartyId,
test_data::kClientDataJson);
std::vector<PublicKeyCredentialDescriptor> allowed_list;
allowed_list.push_back(PublicKeyCredentialDescriptor(
CredentialType::kPublicKey,
fido_parsing_utils::Materialize(test_data::kU2fSignKeyHandle)));
request.app_id = test_data::kAppId;
request.alternative_application_parameter =
fido_parsing_utils::Materialize(base::span<const uint8_t, 32>(
test_data::kAlternativeApplicationParameter));
request.allow_list = std::move(allowed_list);
auto device = MockFidoDevice::MakeCtap(ReadCTAPGetInfoResponse(
test_data::kTestCtap2OnlyAuthenticatorGetInfoResponse));
std::array<uint8_t, 1> error{
{base::strict_cast<uint8_t>(CtapDeviceResponseCode::kCtap2ErrOther)}};
device->ExpectRequestAndRespondWith(test_data::kCtapGetAssertionRequest,
error);
auto task = std::make_unique<GetAssertionTask>(
device.get(), std::move(request), CtapGetAssertionOptions(),
get_assertion_future().GetCallback());
EXPECT_TRUE(get_assertion_future().Wait());
EXPECT_EQ(CtapDeviceResponseCode::kCtap2ErrOther,
std::get<0>(get_assertion_future().Get()));
}
} // namespace
} // namespace device