blob: 29a5bcbc4e3a425a434363ab50e624d14f02923f [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 "content/browser/digital_credentials/cross_device_request_dispatcher.h"
#include "base/json/json_reader.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "content/browser/digital_credentials/cross_device_request_dispatcher.h"
#include "content/public/browser/cross_device_request_info.h"
#include "content/public/browser/digital_credentials_cross_device.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/fido/cable/fido_cable_discovery.h"
#include "device/fido/cable/v2_authenticator.h"
#include "device/fido/cable/v2_constants.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/cable/v2_test_util.h"
#include "device/fido/fido_constants.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/boringssl/src/include/openssl/ec.h"
#include "third_party/boringssl/src/include/openssl/nid.h"
#include "url/gurl.h"
#include "url/origin.h"
using base::JSONReader;
using testing::NiceMock;
namespace content::digital_credentials::cross_device {
namespace {
class DigitalCredentialsCrossDeviceRequestDispatcherTest
: public ::testing::TestWithParam<RequestInfo::RequestType> {
public:
void SetUp() override {
network_context_ = device::cablev2::NewMockTunnelServer(std::nullopt);
std::tie(ble_advert_callback_, ble_advert_events_) =
device::cablev2::Discovery::AdvertEventStream::New();
bssl::UniquePtr<EC_GROUP> p256(
EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
bssl::UniquePtr<EC_KEY> peer_identity(EC_KEY_derive_from_secret(
p256.get(), zero_seed_.data(), zero_seed_.size()));
CHECK_EQ(sizeof(peer_identity_x962_),
EC_POINT_point2oct(
p256.get(), EC_KEY_get0_public_key(peer_identity.get()),
POINT_CONVERSION_UNCOMPRESSED, peer_identity_x962_,
sizeof(peer_identity_x962_), /*ctx=*/nullptr));
mock_adapter_ =
base::MakeRefCounted<NiceMock<device::MockBluetoothAdapter>>();
device::BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter_);
}
protected:
base::expected<Response, RequestDispatcher::Error> Transact(
device::cablev2::PayloadType response_payload_type,
const std::string& response) {
{
auto callback_and_event_stream = device::cablev2::Discovery::EventStream<
std::unique_ptr<device::cablev2::Pairing>>::New();
auto discovery = std::make_unique<device::cablev2::Discovery>(
// This value isn't used since it's a QR-based transaction.
device::FidoRequestType::kGetAssertion,
base::BindLambdaForTesting([&]() { return network_context_.get(); }),
qr_generator_key_, std::move(ble_advert_events_),
std::move(callback_and_event_stream.second),
/*extension_contents=*/std::vector<device::CableDiscoveryData>(),
GetPairingCallback(), GetInvalidatedPairingCallback(),
GetEventCallback(),
/*must_support_ctap=*/false);
const GURL url("https://example.com");
base::Value::Dict request_value;
request_value.Set("foo", "bar");
RequestInfo request_info{/*request_type=*/GetParam(),
url::Origin::Create(url),
base::Value(std::move(request_value))};
base::test::TestFuture<base::expected<Response, RequestDispatcher::Error>>
callback;
auto request_handler = std::make_unique<RequestDispatcher>(
std::make_unique<device::FidoCableDiscovery>(
std::vector<device::CableDiscoveryData>()),
std::move(discovery), std::move(request_info),
callback.GetCallback());
std::unique_ptr<device::cablev2::authenticator::Transaction> transaction =
device::cablev2::authenticator::
TransactDigitalIdentityFromQRCodeForTesting(
device::cablev2::authenticator::NewMockPlatform(
std::move(ble_advert_callback_),
/*ctap2_device=*/nullptr,
/*observer=*/nullptr),
base::BindLambdaForTesting(
[&]() { return network_context_.get(); }),
zero_qr_secret_, peer_identity_x962_, response_payload_type,
std::vector<uint8_t>(response.begin(), response.end()));
return callback.Take();
}
}
base::RepeatingCallback<void(std::unique_ptr<device::cablev2::Pairing>)>
GetPairingCallback() {
return base::DoNothing();
}
base::RepeatingCallback<void(std::unique_ptr<device::cablev2::Pairing>)>
GetInvalidatedPairingCallback() {
return base::DoNothing();
}
base::RepeatingCallback<void(device::cablev2::Event)> GetEventCallback() {
return base::DoNothing();
}
std::unique_ptr<network::mojom::NetworkContext> network_context_;
const std::array<uint8_t, device::cablev2::kQRKeySize> qr_generator_key_ = {
0};
std::unique_ptr<device::cablev2::Discovery::AdvertEventStream>
ble_advert_events_;
device::cablev2::Discovery::AdvertEventStream::Callback ble_advert_callback_;
uint8_t peer_identity_x962_[device::kP256X962Length] = {};
const std::array<uint8_t, device::cablev2::kQRSecretSize> zero_qr_secret_ = {
0};
const std::array<uint8_t, device::cablev2::kRootSecretSize> root_secret_ = {
0};
const std::array<uint8_t, device::cablev2::kQRSeedSize> zero_seed_ = {0};
scoped_refptr<NiceMock<device::MockBluetoothAdapter>> mock_adapter_;
base::test::TaskEnvironment task_environment;
};
TEST_P(DigitalCredentialsCrossDeviceRequestDispatcherTest, ValidLegacyFormat) {
base::expected<Response, RequestDispatcher::Error> result = Transact(
device::cablev2::PayloadType::kJSON,
R"({"response": {"digital": {"data": {"vp_token" : "token"}}}})");
ASSERT_TRUE(result.has_value());
ASSERT_EQ(result.value()->data,
JSONReader::Read(R"({"vp_token":"token"})",
base::JSON_PARSE_CHROMIUM_EXTENSIONS)
.value());
EXPECT_FALSE(result.value()->protocol.has_value());
}
TEST_P(DigitalCredentialsCrossDeviceRequestDispatcherTest, InvalidJson) {
base::expected<Response, RequestDispatcher::Error> result =
Transact(device::cablev2::PayloadType::kJSON, "!");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
RequestDispatcher::Error(ProtocolError::kInvalidResponse));
}
TEST_P(DigitalCredentialsCrossDeviceRequestDispatcherTest, ErrorResponse) {
base::expected<Response, RequestDispatcher::Error> result =
Transact(device::cablev2::PayloadType::kJSON,
R"({"response": {"digital": {"error": "NO_CREDENTIAL"}}})");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
RequestDispatcher::Error(RemoteError::kNoCredential));
}
TEST_P(DigitalCredentialsCrossDeviceRequestDispatcherTest, OtherError) {
base::expected<Response, RequestDispatcher::Error> result =
Transact(device::cablev2::PayloadType::kJSON,
R"({"response": {"digital": {"error": "RANDOM_STUFF"}}})");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), RequestDispatcher::Error(RemoteError::kOther));
}
TEST_P(DigitalCredentialsCrossDeviceRequestDispatcherTest, ErrorIsNotAString) {
base::expected<Response, RequestDispatcher::Error> result =
Transact(device::cablev2::PayloadType::kJSON,
R"({"response": {"digital": {"error": 1}}})");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
RequestDispatcher::Error(ProtocolError::kInvalidResponse));
}
TEST_P(DigitalCredentialsCrossDeviceRequestDispatcherTest, InvalidStructure) {
base::expected<Response, RequestDispatcher::Error> result =
Transact(device::cablev2::PayloadType::kJSON, R"({"result": 1})");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
RequestDispatcher::Error(ProtocolError::kInvalidResponse));
}
TEST_P(DigitalCredentialsCrossDeviceRequestDispatcherTest, CTAPResponse) {
base::expected<Response, RequestDispatcher::Error> result =
Transact(device::cablev2::PayloadType::kCTAP, "");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
RequestDispatcher::Error(ProtocolError::kTransportError));
}
TEST_P(DigitalCredentialsCrossDeviceRequestDispatcherTest, NewResponseFormat) {
base::expected<Response, RequestDispatcher::Error> result =
Transact(device::cablev2::PayloadType::kJSON,
R"({
"response": {
"digital": {
"data": {
"data": {"key": "value"},
"protocol" : "ProtocolInResponse"
}
}
}
})");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value()->data,
JSONReader::Read(R"({"key":"value"})",
base::JSON_PARSE_CHROMIUM_EXTENSIONS)
.value());
EXPECT_EQ(result.value()->protocol, "ProtocolInResponse");
}
TEST_P(DigitalCredentialsCrossDeviceRequestDispatcherTest,
NewResponseFormatWithoutProtocol) {
base::expected<Response, RequestDispatcher::Error> result =
Transact(device::cablev2::PayloadType::kJSON,
R"({
"response": {
"digital": {
"data": {
"data": {"key": "value"}
}
}
}
})");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value()->data,
JSONReader::Read(R"({"key":"value"})",
base::JSON_PARSE_CHROMIUM_EXTENSIONS)
.value());
EXPECT_FALSE(result.value()->protocol.has_value());
}
INSTANTIATE_TEST_SUITE_P(,
DigitalCredentialsCrossDeviceRequestDispatcherTest,
::testing::Values(RequestInfo::RequestType::kGet,
RequestInfo::RequestType::kCreate));
} // namespace
} // namespace content::digital_credentials::cross_device