blob: 5812b2e2f85a3377bfb9c0446c563b65e703cd2c [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/payments/content/secure_payment_confirmation_service.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "components/payments/content/browser_binding/browser_bound_key_store.h"
#include "components/payments/content/browser_binding/fake_browser_bound_key.h"
#include "components/payments/content/browser_binding/fake_browser_bound_key_store.h"
#include "components/payments/content/mock_web_payments_web_data_service.h"
#include "components/payments/content/web_payments_web_data_service.h"
#include "components/payments/core/features.h"
#include "components/webauthn/core/browser/internal_authenticator.h"
#include "components/webauthn/core/browser/mock_internal_authenticator.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_web_contents_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
namespace payments {
using ::testing::_;
using ::testing::AnyOf;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Field;
using ::testing::IsNull;
using ::testing::Pointee;
using ::testing::Pointer;
using payments::mojom::SecurePaymentConfirmationAvailabilityEnum;
namespace {
struct SecurePaymentConfirmationServiceDeleter {
void operator()(SecurePaymentConfirmationService* spc_service) {
spc_service->ResetAndDeleteThis();
}
};
#if BUILDFLAG(IS_ANDROID)
static const int32_t kAlgorithmIdentifier = 1;
static const int32_t kAnotherAlgorithmIdentifier = 2;
#endif
} // namespace
// Base class for unittests testing SecurePaymentConfirmationService, which
// provides general test environment setup and management.
//
// Tests that derive from this class should setup their feature flags as needed,
// and then call `InitializeSecurePaymentConfirmationService` to create the SPC
// service under test.
class SecurePaymentConfirmationServiceTestBase {
public:
SecurePaymentConfirmationServiceTestBase() = default;
protected:
void InitializeSecurePaymentConfirmationService(
bool with_authenticator = true,
bool is_off_the_record = false) {
context_.set_is_off_the_record(is_off_the_record);
web_contents_ = web_contents_factory_.CreateWebContents(&context_);
CHECK(!mock_internal_authenticator_);
CHECK(!spc_service_);
mojo::PendingRemote<mojom::SecurePaymentConfirmationService> remote;
mojo::PendingReceiver<mojom::SecurePaymentConfirmationService> receiver =
remote.InitWithNewPipeAndPassReceiver();
spc_service_ = std::unique_ptr<SecurePaymentConfirmationService,
SecurePaymentConfirmationServiceDeleter>(
new SecurePaymentConfirmationService(
*web_contents_->GetPrimaryMainFrame(),
/*receiver=*/std::move(receiver), mock_web_data_service_,
with_authenticator ? CreateMockInternalAuthenticator() : nullptr));
}
content::BrowserTaskEnvironment task_environment_;
content::TestBrowserContext context_;
content::TestWebContentsFactory web_contents_factory_;
raw_ptr<content::WebContents> web_contents_;
scoped_refptr<payments::MockWebPaymentsWebDataService>
mock_web_data_service_ =
base::MakeRefCounted<MockWebPaymentsWebDataService>();
// The `spc_service_` must be deleted after `mock_internal_authenticator_`, as
// it owns the underlying std::unique_ptr.
std::unique_ptr<SecurePaymentConfirmationService,
SecurePaymentConfirmationServiceDeleter>
spc_service_;
raw_ptr<webauthn::MockInternalAuthenticator> mock_internal_authenticator_;
base::MockCallback<mojom::SecurePaymentConfirmationService::
SecurePaymentConfirmationAvailabilityCallback>
mock_secure_payment_confirmation_availability_callback_;
private:
std::unique_ptr<webauthn::InternalAuthenticator>
CreateMockInternalAuthenticator() {
mock_internal_authenticator_ =
new webauthn::MockInternalAuthenticator(web_contents_);
return base::WrapUnique(static_cast<webauthn::InternalAuthenticator*>(
&*mock_internal_authenticator_));
}
};
class SecurePaymentConfirmationServiceTest
: public SecurePaymentConfirmationServiceTestBase,
public ::testing::Test {};
TEST_F(SecurePaymentConfirmationServiceTest,
SecurePaymentConfirmationAvailabilityAPI) {
base::test::ScopedFeatureList features;
features.InitWithFeatures(
{::features::kSecurePaymentConfirmation,
features::kSecurePaymentConfirmationUseCredentialStoreAPIs},
{});
InitializeSecurePaymentConfirmationService();
EXPECT_CALL(*mock_internal_authenticator_,
IsGetMatchingCredentialIdsSupported())
.WillRepeatedly(testing::Return(true));
EXPECT_CALL(*mock_internal_authenticator_,
IsUserVerifyingPlatformAuthenticatorAvailable(_))
.WillRepeatedly(base::test::RunOnceCallbackRepeatedly<0>(true));
EXPECT_CALL(mock_secure_payment_confirmation_availability_callback_,
Run(SecurePaymentConfirmationAvailabilityEnum::kAvailable));
spc_service_->SecurePaymentConfirmationAvailability(
mock_secure_payment_confirmation_availability_callback_.Get());
}
TEST_F(SecurePaymentConfirmationServiceTest,
SecurePaymentConfirmationAvailabilityAPI_FeatureDisabled) {
base::test::ScopedFeatureList features;
features.InitWithFeatures(
{}, {::features::kSecurePaymentConfirmation,
features::kSecurePaymentConfirmationUseCredentialStoreAPIs});
InitializeSecurePaymentConfirmationService();
EXPECT_CALL(mock_secure_payment_confirmation_availability_callback_,
Run(SecurePaymentConfirmationAvailabilityEnum::
kUnavailableFeatureNotEnabled));
spc_service_->SecurePaymentConfirmationAvailability(
mock_secure_payment_confirmation_availability_callback_.Get());
}
TEST_F(
SecurePaymentConfirmationServiceTest,
SecurePaymentConfirmationAvailabilityAPI_SecurePaymentConfirmationDebugMode) {
base::test::ScopedFeatureList features;
features.InitWithFeatures(
{::features::kSecurePaymentConfirmation,
features::kSecurePaymentConfirmationUseCredentialStoreAPIs,
::features::kSecurePaymentConfirmationDebug},
{});
InitializeSecurePaymentConfirmationService(/*with_authenticator=*/false);
// Here we haven't set up the authenticator, but since the debug flag is set
// that does not matter; the API should still return true.
EXPECT_CALL(mock_secure_payment_confirmation_availability_callback_,
Run(SecurePaymentConfirmationAvailabilityEnum::kAvailable));
spc_service_->SecurePaymentConfirmationAvailability(
mock_secure_payment_confirmation_availability_callback_.Get());
}
TEST_F(SecurePaymentConfirmationServiceTest,
SecurePaymentConfirmationAvailabilityAPI_NoAuthenticator) {
base::test::ScopedFeatureList features;
features.InitWithFeatures(
{::features::kSecurePaymentConfirmation,
features::kSecurePaymentConfirmationUseCredentialStoreAPIs},
{});
InitializeSecurePaymentConfirmationService(/*with_authenticator=*/false);
EXPECT_CALL(mock_secure_payment_confirmation_availability_callback_,
Run(SecurePaymentConfirmationAvailabilityEnum::
kUnavailableUnknownReason));
spc_service_->SecurePaymentConfirmationAvailability(
mock_secure_payment_confirmation_availability_callback_.Get());
}
TEST_F(
SecurePaymentConfirmationServiceTest,
SecurePaymentConfirmationAvailabilityAPI_GetMatchingCredentialIdsNotSupported) {
base::test::ScopedFeatureList features;
features.InitWithFeatures(
{::features::kSecurePaymentConfirmation,
features::kSecurePaymentConfirmationUseCredentialStoreAPIs},
{});
InitializeSecurePaymentConfirmationService();
EXPECT_CALL(*mock_internal_authenticator_,
IsGetMatchingCredentialIdsSupported())
.WillRepeatedly(testing::Return(false));
EXPECT_CALL(mock_secure_payment_confirmation_availability_callback_,
Run(SecurePaymentConfirmationAvailabilityEnum::
kUnavailableUnknownReason));
spc_service_->SecurePaymentConfirmationAvailability(
mock_secure_payment_confirmation_availability_callback_.Get());
}
TEST_F(
SecurePaymentConfirmationServiceTest,
SecurePaymentConfirmationAvailabilityAPI_AuthenticatorIsNotUserVerifying) {
base::test::ScopedFeatureList features;
features.InitWithFeatures(
{::features::kSecurePaymentConfirmation,
features::kSecurePaymentConfirmationUseCredentialStoreAPIs},
{});
InitializeSecurePaymentConfirmationService();
EXPECT_CALL(*mock_internal_authenticator_,
IsGetMatchingCredentialIdsSupported())
.WillRepeatedly(testing::Return(true));
EXPECT_CALL(*mock_internal_authenticator_,
IsUserVerifyingPlatformAuthenticatorAvailable(_))
.WillRepeatedly(base::test::RunOnceCallbackRepeatedly<0>(false));
EXPECT_CALL(mock_secure_payment_confirmation_availability_callback_,
Run(SecurePaymentConfirmationAvailabilityEnum::
kUnavailableNoUserVerifyingPlatformAuthenticator));
spc_service_->SecurePaymentConfirmationAvailability(
mock_secure_payment_confirmation_availability_callback_.Get());
}
#if BUILDFLAG(IS_ANDROID)
struct CredentialTestParams {
// The algorithm identifier supported by the fake browser bound key store.
FakeBrowserBoundKey fake_key;
std::vector<device::PublicKeyCredentialParams::CredentialInfo>
public_key_parameters;
std::optional<std::vector<device::PublicKeyCredentialParams::CredentialInfo>>
browser_bound_key_cred_params;
std::vector<uint8_t> expected_signature;
std::vector<uint8_t> expected_browser_bound_key;
std::string test_description;
};
class SecurePaymentConfirmationServiceCredentialTest
: public SecurePaymentConfirmationServiceTestBase,
public ::testing::TestWithParam<CredentialTestParams> {
public:
SecurePaymentConfirmationServiceCredentialTest() {
scoped_feature_list_.InitAndEnableFeature(
blink::features::kSecurePaymentConfirmationBrowserBoundKeys);
}
void SetUpTest(bool is_off_the_record) {
InitializeSecurePaymentConfirmationService(/*with_authenticator=*/true,
is_off_the_record);
auto passkey_browser_binder = std::make_unique<PasskeyBrowserBinder>(
fake_browser_bound_key_store_, mock_web_data_service_);
passkey_browser_binder->SetRandomBytesAsVectorCallbackForTesting(
base::BindRepeating(
[](size_t length) { return GetParam().fake_key.GetIdentifier(); }));
spc_service_->SetPasskeyBrowserBinderForTesting(
std::move(passkey_browser_binder));
}
protected:
const std::vector<uint8_t> fake_challenge_ = {0x01, 0x02, 0x03, 0x04};
const std::vector<uint8_t> fake_credential_id_ = {0x10, 0x11, 0x12, 0x13};
const std::vector<uint8_t> fake_client_data_json_ = {0x30, 0x31, 0x32, 0x33};
const std::string fake_relying_party_id_ = "relying-party.example";
::blink::mojom::PublicKeyCredentialCreationOptionsPtr
GetPublicKeyCredentialCreationOptions() {
auto creation_options =
::blink::mojom::PublicKeyCredentialCreationOptions::New();
creation_options->relying_party.id = fake_relying_party_id_;
creation_options->is_payment_credential_creation = true;
creation_options->challenge = fake_challenge_;
creation_options->public_key_parameters = GetParam().public_key_parameters;
creation_options->payment_browser_bound_key_parameters =
GetParam().browser_bound_key_cred_params;
return creation_options;
}
scoped_refptr<FakeBrowserBoundKeyStore> fake_browser_bound_key_store_ =
base::MakeRefCounted<FakeBrowserBoundKeyStore>();
base::MockCallback<
mojom::SecurePaymentConfirmationService::MakePaymentCredentialCallback>
mock_payment_credential_callback_;
base::test::ScopedFeatureList scoped_feature_list_;
};
static ::testing::Matcher<
::blink::mojom::MakeCredentialAuthenticatorResponsePtr>
AuthenticatorResponseWithBrowserBoundSignature(std::vector<uint8_t> signature) {
return Pointee(Field(
"payment", &::blink::mojom::MakeCredentialAuthenticatorResponse::payment,
Pointee(Field("browser_bound_signature",
&::blink::mojom::AuthenticationExtensionsPaymentResponse::
browser_bound_signature,
Eq(signature)))));
}
static ::testing::Matcher<
::blink::mojom::MakeCredentialAuthenticatorResponsePtr>
AuthenticatorResponseWithoutBrowserBoundSignature() {
return Pointee(Field(
"payment", &::blink::mojom::MakeCredentialAuthenticatorResponse::payment,
AnyOf(Pointer(nullptr),
Pointee(
Field("browser_bound_signature",
&::blink::mojom::AuthenticationExtensionsPaymentResponse::
browser_bound_signature,
ElementsAre())))));
}
INSTANTIATE_TEST_SUITE_P(
SecurePaymentConfirmationServiceCredentialTest,
SecurePaymentConfirmationServiceCredentialTest,
::testing::Values<CredentialTestParams>(
CredentialTestParams{
.fake_key = FakeBrowserBoundKey(
/*browser_bound_key_id=*/{0x60, 0x61, 0x62, 0x63},
/*public_key_as_cose_key=*/{0x50, 0x51, 0x52, 0x53},
/*signature=*/{0x20, 0x21, 0x22, 0x23},
kAlgorithmIdentifier,
/*expected_client_data=*/{0x30, 0x31, 0x32, 0x33}),
.public_key_parameters =
{device::PublicKeyCredentialParams::CredentialInfo(
device::CredentialType::kPublicKey,
kAnotherAlgorithmIdentifier)},
.browser_bound_key_cred_params =
{{device::PublicKeyCredentialParams::CredentialInfo(
device::CredentialType::kPublicKey,
kAlgorithmIdentifier)}},
.expected_signature = {0x20, 0x21, 0x22, 0x23},
.expected_browser_bound_key = {0x50, 0x51, 0x52, 0x53},
.test_description = "UsingPasskeyPubKeyCredParams",
},
CredentialTestParams{
.fake_key = FakeBrowserBoundKey(
/*browser_bound_key_id=*/{0x60, 0x61, 0x62, 0x63},
/*public_key_as_cose_key=*/{0x50, 0x51, 0x52, 0x53},
/*signature=*/{0x20, 0x21, 0x22, 0x23},
kAlgorithmIdentifier,
/*expected_client_data=*/{0x30, 0x31, 0x32, 0x33}),
.public_key_parameters =
{device::PublicKeyCredentialParams::CredentialInfo(
device::CredentialType::kPublicKey,
kAlgorithmIdentifier)},
.browser_bound_key_cred_params = std::nullopt,
.expected_signature = {0x20, 0x21, 0x22, 0x23},
.expected_browser_bound_key = {0x50, 0x51, 0x52, 0x53},
.test_description = "UsingBrowserBoundPubKeyCredParams",
}),
[](const ::testing::TestParamInfo<CredentialTestParams>& info) {
return info.param.test_description;
});
TEST_P(SecurePaymentConfirmationServiceCredentialTest,
MakePaymentCredentialAddsBrowserBoundKey) {
SetUpTest(/*is_off_the_record=*/false);
fake_browser_bound_key_store_->PutFakeKey(GetParam().fake_key);
::blink::mojom::PublicKeyCredentialCreationOptionsPtr creation_options =
GetPublicKeyCredentialCreationOptions();
auto fake_authenticator_response =
::blink::mojom::MakeCredentialAuthenticatorResponse::New();
fake_authenticator_response->info =
::blink::mojom::CommonCredentialInfo::New();
fake_authenticator_response->info->raw_id = fake_credential_id_;
fake_authenticator_response->info->client_data_json = fake_client_data_json_;
EXPECT_CALL(*mock_web_data_service_,
SetBrowserBoundKey(fake_credential_id_, fake_relying_party_id_,
GetParam().fake_key.GetIdentifier(), _));
::blink::mojom::PaymentOptionsPtr actual_payment_options;
EXPECT_CALL(*mock_internal_authenticator_, SetPaymentOptions(_))
.Times(1)
.WillOnce([&actual_payment_options](
::blink::mojom::PaymentOptionsPtr payment_options) {
actual_payment_options = payment_options.Clone();
});
EXPECT_CALL(*mock_internal_authenticator_,
MakeCredential(Eq(std::ref(creation_options)), _))
.WillRepeatedly(
[&fake_authenticator_response](
::blink::mojom::PublicKeyCredentialCreationOptionsPtr options,
::blink::mojom::Authenticator::MakeCredentialCallback callback) {
std::move(callback).Run(
::blink::mojom::AuthenticatorStatus::SUCCESS,
fake_authenticator_response.Clone(),
/*exception_details=*/nullptr);
});
EXPECT_CALL(mock_payment_credential_callback_,
Run(Eq(::blink::mojom::AuthenticatorStatus::SUCCESS),
AuthenticatorResponseWithBrowserBoundSignature(
GetParam().expected_signature),
_));
spc_service_->MakePaymentCredential(creation_options.Clone(),
mock_payment_credential_callback_.Get());
ASSERT_FALSE(actual_payment_options.is_null());
EXPECT_EQ(actual_payment_options->browser_bound_public_key,
GetParam().expected_browser_bound_key);
}
TEST_P(SecurePaymentConfirmationServiceCredentialTest,
MakePaymentCredentialDoesNotAddBrowserBoundKeyWhenOffTheRecord) {
SetUpTest(/*is_off_the_record=*/true);
fake_browser_bound_key_store_->PutFakeKey(GetParam().fake_key);
::blink::mojom::PublicKeyCredentialCreationOptionsPtr creation_options =
GetPublicKeyCredentialCreationOptions();
auto fake_authenticator_response =
::blink::mojom::MakeCredentialAuthenticatorResponse::New();
fake_authenticator_response->info =
::blink::mojom::CommonCredentialInfo::New();
fake_authenticator_response->info->raw_id = fake_credential_id_;
fake_authenticator_response->info->client_data_json = fake_client_data_json_;
EXPECT_CALL(*mock_web_data_service_, SetBrowserBoundKey).Times(0);
::blink::mojom::PaymentOptionsPtr actual_payment_options;
EXPECT_CALL(*mock_internal_authenticator_, SetPaymentOptions(_))
.Times(1)
.WillOnce([&actual_payment_options](
::blink::mojom::PaymentOptionsPtr payment_options) {
actual_payment_options = payment_options.Clone();
});
EXPECT_CALL(*mock_internal_authenticator_,
MakeCredential(Eq(std::ref(creation_options)), _))
.WillRepeatedly(
[&fake_authenticator_response](
::blink::mojom::PublicKeyCredentialCreationOptionsPtr options,
::blink::mojom::Authenticator::MakeCredentialCallback callback) {
std::move(callback).Run(
::blink::mojom::AuthenticatorStatus::SUCCESS,
fake_authenticator_response.Clone(),
/*exception_details=*/nullptr);
});
EXPECT_CALL(mock_payment_credential_callback_,
Run(Eq(::blink::mojom::AuthenticatorStatus::SUCCESS),
AuthenticatorResponseWithoutBrowserBoundSignature(), _));
spc_service_->MakePaymentCredential(creation_options.Clone(),
mock_payment_credential_callback_.Get());
ASSERT_FALSE(actual_payment_options.is_null());
EXPECT_FALSE(actual_payment_options->browser_bound_public_key.has_value());
}
#endif
} // namespace payments