device/fido: support digital identities over hybrid.
In the future it'll be possible to assert mobile driver's licenses (and other types of digital identity) over hybrid connections. This change adds some infrastructure to support this. It is not yet wired up to anything. Bug: 332562244 Change-Id: I045ad02eb8e6ba0f944290781a552db17f5ed8af Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5581160 Reviewed-by: Reilly Grant <reillyg@chromium.org> Commit-Queue: Adam Langley <agl@chromium.org> Reviewed-by: Avi Drissman <avi@chromium.org> Reviewed-by: Peter Kotwicz <pkotwicz@chromium.org> Cr-Commit-Position: refs/heads/main@{#1338014}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
f3317cc623
commit
eef90c2bc0
chrome/android/features/cablev2_authenticator/native
content
device/fido
cable
fido_tunnel_device.ccfido_tunnel_device.hv2_authenticator.ccv2_authenticator.hv2_constants.hv2_discovery.ccv2_discovery.hv2_test_util.cc
fido_authenticator.ccfido_authenticator.hfido_device.ccfido_device.hfido_device_authenticator.ccfido_device_authenticator.hfido_discovery_factory.ccfido_discovery_factory.h@ -361,6 +361,7 @@ class AndroidPlatform : public device::cablev2::authenticator::Platform {
|
||||
case Error::NO_SCREENLOCK:
|
||||
case Error::NO_BLUETOOTH_PERMISSION:
|
||||
case Error::QR_URI_ERROR:
|
||||
case Error::INVALID_JSON:
|
||||
result = CableV2MobileResult::kInternalError;
|
||||
break;
|
||||
}
|
||||
@ -436,7 +437,8 @@ class USBTransport : public device::cablev2::authenticator::Transport {
|
||||
Java_USBHandler_startReading(env_, usb_device_);
|
||||
}
|
||||
|
||||
void Write(std::vector<uint8_t> data) override {
|
||||
void Write(device::cablev2::PayloadType payload_type,
|
||||
std::vector<uint8_t> data) override {
|
||||
Java_USBHandler_write(env_, usb_device_, data);
|
||||
}
|
||||
|
||||
@ -445,7 +447,9 @@ class USBTransport : public device::cablev2::authenticator::Transport {
|
||||
if (!data) {
|
||||
callback_.Run(Disconnected::kDisconnected);
|
||||
} else {
|
||||
callback_.Run(device::fido_parsing_utils::Materialize(*data));
|
||||
callback_.Run(
|
||||
std::make_pair(device::cablev2::PayloadType::kCTAP,
|
||||
device::fido_parsing_utils::Materialize(*data)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3373,6 +3373,11 @@ source_set("browser") {
|
||||
"picture_in_picture/document_picture_in_picture_navigation_throttle.h",
|
||||
"picture_in_picture/document_picture_in_picture_window_controller_impl.cc",
|
||||
"picture_in_picture/document_picture_in_picture_window_controller_impl.h",
|
||||
|
||||
# Cross-device digital credentials flows are handled by Play Services on
|
||||
# Android, not the browser.
|
||||
"webid/digital_credentials/cross_device_request_dispatcher.cc",
|
||||
"webid/digital_credentials/cross_device_request_dispatcher.h",
|
||||
]
|
||||
|
||||
if (!is_fuchsia) {
|
||||
@ -3385,6 +3390,7 @@ source_set("browser") {
|
||||
}
|
||||
|
||||
deps += [
|
||||
"//components/device_event_log",
|
||||
"//components/soda:constants",
|
||||
"//components/soda:soda",
|
||||
"//components/soda:utils",
|
||||
|
@ -9338,7 +9338,7 @@ class AuthenticatorCableV2Test : public AuthenticatorImplRequestDelegateTest {
|
||||
/*contact_device_stream=*/nullptr,
|
||||
/*extension_contents=*/std::vector<device::CableDiscoveryData>(),
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(),
|
||||
GetEventCallback());
|
||||
GetEventCallback(), /*must_support_ctap=*/true);
|
||||
|
||||
ReplaceDiscoveryFactory(
|
||||
std::make_unique<DiscoveryFactory>(std::move(discovery)));
|
||||
@ -9380,7 +9380,7 @@ class AuthenticatorCableV2Test : public AuthenticatorImplRequestDelegateTest {
|
||||
std::move(callback_and_event_stream.second),
|
||||
/*extension_contents=*/std::vector<device::CableDiscoveryData>(),
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(),
|
||||
GetEventCallback());
|
||||
GetEventCallback(), /*must_support_ctap=*/true);
|
||||
|
||||
maybe_contact_phones_callback_ = base::BindLambdaForTesting([&]() {
|
||||
callback_and_event_stream.first.Run(
|
||||
@ -9486,8 +9486,8 @@ TEST_F(AuthenticatorCableV2Test, QRBasedWithNoPairing) {
|
||||
qr_generator_key_, std::move(ble_advert_events_),
|
||||
/*contact_device_stream=*/nullptr,
|
||||
/*extension_contents=*/std::vector<device::CableDiscoveryData>(),
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(),
|
||||
GetEventCallback());
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(), GetEventCallback(),
|
||||
/*must_support_ctap=*/true);
|
||||
|
||||
ReplaceDiscoveryFactory(
|
||||
std::make_unique<DiscoveryFactory>(std::move(discovery)));
|
||||
@ -9516,8 +9516,8 @@ TEST_F(AuthenticatorCableV2Test, HandshakeError) {
|
||||
qr_generator_key_, std::move(ble_advert_events_),
|
||||
/*contact_device_stream=*/nullptr,
|
||||
/*extension_contents=*/std::vector<device::CableDiscoveryData>(),
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(),
|
||||
GetEventCallback());
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(), GetEventCallback(),
|
||||
/*must_support_ctap=*/true);
|
||||
|
||||
ReplaceDiscoveryFactory(
|
||||
std::make_unique<DiscoveryFactory>(std::move(discovery)));
|
||||
@ -9557,8 +9557,8 @@ TEST_F(AuthenticatorCableV2Test, NetworkServiceCrash) {
|
||||
qr_generator_key_, std::move(ble_advert_events_),
|
||||
/*contact_device_stream=*/nullptr,
|
||||
/*extension_contents=*/std::vector<device::CableDiscoveryData>(),
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(),
|
||||
GetEventCallback());
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(), GetEventCallback(),
|
||||
/*must_support_ctap=*/true);
|
||||
|
||||
ReplaceDiscoveryFactory(
|
||||
std::make_unique<DiscoveryFactory>(std::move(discovery)));
|
||||
@ -9635,8 +9635,8 @@ TEST_F(AuthenticatorCableV2Test, ContactIDDisabled) {
|
||||
qr_generator_key_, std::move(ble_advert_events_),
|
||||
std::move(callback_and_event_stream.second),
|
||||
/*extension_contents=*/std::vector<device::CableDiscoveryData>(),
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(),
|
||||
GetEventCallback());
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(), GetEventCallback(),
|
||||
/*must_support_ctap=*/true);
|
||||
|
||||
ReplaceDiscoveryFactory(
|
||||
std::make_unique<DiscoveryFactory>(std::move(discovery)));
|
||||
@ -9709,7 +9709,8 @@ TEST_F(AuthenticatorCableV2Test, ServerLink) {
|
||||
base::BindLambdaForTesting([&]() { return network_context_.get(); }),
|
||||
qr_generator_key_, std::move(ble_advert_events_),
|
||||
/*contact_device_stream=*/nullptr, extension_values, GetPairingCallback(),
|
||||
GetInvalidatedPairingCallback(), GetEventCallback());
|
||||
GetInvalidatedPairingCallback(), GetEventCallback(),
|
||||
/*must_support_ctap=*/true);
|
||||
|
||||
ReplaceDiscoveryFactory(
|
||||
std::make_unique<DiscoveryFactory>(std::move(discovery)));
|
||||
@ -9742,8 +9743,8 @@ TEST_F(AuthenticatorCableV2Test, LateLinking) {
|
||||
qr_generator_key_, std::move(ble_advert_events_),
|
||||
/*contact_device_stream=*/nullptr,
|
||||
/*extension_contents=*/std::vector<device::CableDiscoveryData>(),
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(),
|
||||
GetEventCallback());
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(), GetEventCallback(),
|
||||
/*must_support_ctap=*/true);
|
||||
|
||||
ReplaceDiscoveryFactory(
|
||||
std::make_unique<DiscoveryFactory>(std::move(discovery)));
|
||||
@ -9786,7 +9787,7 @@ class AuthenticatorCableV2AuthenticatorTest
|
||||
/*contact_device_stream=*/nullptr,
|
||||
/*extension_contents=*/std::vector<device::CableDiscoveryData>(),
|
||||
GetPairingCallback(), GetInvalidatedPairingCallback(),
|
||||
GetEventCallback());
|
||||
GetEventCallback(), /*must_support_ctap=*/true);
|
||||
|
||||
ReplaceDiscoveryFactory(
|
||||
std::make_unique<DiscoveryFactory>(std::move(discovery)));
|
||||
|
6
content/browser/webid/digital_credentials/DEPS
Normal file
6
content/browser/webid/digital_credentials/DEPS
Normal file
@ -0,0 +1,6 @@
|
||||
# Similar to //content/browser/webauth, this code interacts with
|
||||
# external devices to implement Web Platform features.
|
||||
include_rules = [
|
||||
"+device/fido",
|
||||
"+components/device_event_log",
|
||||
]
|
@ -1,2 +1,5 @@
|
||||
pkotwicz@chromium.org
|
||||
file://content/browser/webid/OWNERS
|
||||
|
||||
# For maintainence of interactions with //device/fido
|
||||
file://device/fido/OWNERS
|
||||
|
@ -0,0 +1,174 @@
|
||||
// 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/webid/digital_credentials/cross_device_request_dispatcher.h"
|
||||
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/json/json_reader.h"
|
||||
#include "base/json/json_writer.h"
|
||||
#include "components/device_event_log/device_event_log.h"
|
||||
#include "device/fido/cable/fido_tunnel_device.h"
|
||||
#include "device/fido/fido_authenticator.h"
|
||||
#include "device/fido/fido_discovery_base.h"
|
||||
#include "device/fido/fido_types.h"
|
||||
|
||||
namespace content::digital_credentials::cross_device {
|
||||
|
||||
namespace {
|
||||
|
||||
RemoteError ErrorStringToRemoteError(const std::string& error_str) {
|
||||
if (error_str == "USER_CANCELED") {
|
||||
return RemoteError::kUserCanceled;
|
||||
} else if (error_str == "DEVICE_ABORTED") {
|
||||
return RemoteError::kDeviceAborted;
|
||||
} else if (error_str == "NO_CREDENTIAL") {
|
||||
return RemoteError::kNoCredential;
|
||||
}
|
||||
return RemoteError::kOther;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> RequestToJSONBytes(const url::Origin& origin,
|
||||
base::Value request) {
|
||||
base::Value::Dict digital;
|
||||
digital.Set("digital", std::move(request));
|
||||
|
||||
base::Value::Dict toplevel;
|
||||
toplevel.Set("origin", origin.Serialize());
|
||||
toplevel.Set("requestType", "credential.get");
|
||||
toplevel.Set("request", std::move(digital));
|
||||
|
||||
std::optional<std::string> json = base::WriteJson(toplevel);
|
||||
// WriteJson must not fail in this context.
|
||||
return std::vector<uint8_t>(json->begin(), json->end());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RequestDispatcher::RequestDispatcher(
|
||||
std::unique_ptr<device::FidoDiscoveryBase> discovery,
|
||||
url::Origin origin,
|
||||
base::Value request,
|
||||
CompletionCallback callback)
|
||||
: discovery_(std::move(discovery)),
|
||||
origin_(std::move(origin)),
|
||||
request_(std::move(request)),
|
||||
callback_(std::move(callback)) {
|
||||
FIDO_LOG(EVENT) << "Starting digital identity flow";
|
||||
discovery_->set_observer(this);
|
||||
discovery_->Start();
|
||||
}
|
||||
|
||||
RequestDispatcher::~RequestDispatcher() = default;
|
||||
|
||||
void RequestDispatcher::AuthenticatorAdded(
|
||||
device::FidoDiscoveryBase* discovery,
|
||||
device::FidoAuthenticator* authenticator) {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
|
||||
|
||||
if (!callback_) {
|
||||
return;
|
||||
}
|
||||
authenticator->InitializeAuthenticator(
|
||||
base::BindOnce(&RequestDispatcher::OnAuthenticatorReady,
|
||||
weak_factory_.GetWeakPtr(), authenticator));
|
||||
}
|
||||
|
||||
void RequestDispatcher::OnAuthenticatorReady(
|
||||
device::FidoAuthenticator* authenticator) {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
|
||||
|
||||
if (!callback_) {
|
||||
return;
|
||||
}
|
||||
device::cablev2::FidoTunnelDevice* tunnel_device =
|
||||
authenticator->GetTunnelDevice();
|
||||
if (!tunnel_device) {
|
||||
// Presumably all discovered FidoAuthenticators will be of the same type and
|
||||
// so there's no point in waiting for more.
|
||||
FIDO_LOG(ERROR) << "Non-tunnel device discovered";
|
||||
std::move(callback_).Run(
|
||||
base::unexpected(ProtocolError::kIncompatibleDevice));
|
||||
return;
|
||||
}
|
||||
if (!tunnel_device->features().contains(
|
||||
device::cablev2::Feature::kDigitialIdentities)) {
|
||||
FIDO_LOG(ERROR)
|
||||
<< "Hybrid device doesn't advertise support for digital identities";
|
||||
std::move(callback_).Run(
|
||||
base::unexpected(ProtocolError::kIncompatibleDevice));
|
||||
return;
|
||||
}
|
||||
tunnel_device->DeviceTransactJSON(
|
||||
RequestToJSONBytes(origin_, std::move(request_)),
|
||||
base::BindOnce(&RequestDispatcher::OnComplete,
|
||||
weak_factory_.GetWeakPtr()));
|
||||
}
|
||||
|
||||
void RequestDispatcher::AuthenticatorRemoved(
|
||||
device::FidoDiscoveryBase* discovery,
|
||||
device::FidoAuthenticator* authenticator) {}
|
||||
|
||||
void RequestDispatcher::OnComplete(
|
||||
std::optional<std::vector<uint8_t>> response) {
|
||||
if (!response) {
|
||||
FIDO_LOG(ERROR) << "No response for digital credential request";
|
||||
std::move(callback_).Run(base::unexpected(ProtocolError::kTransportError));
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<base::Value> json = base::JSONReader::Read(
|
||||
std::string_view(reinterpret_cast<const char*>(response->data()),
|
||||
response->size()),
|
||||
base::JSON_PARSE_RFC);
|
||||
if (!json || !json->is_dict()) {
|
||||
FIDO_LOG(ERROR) << "Invalid JSON response: " << base::HexEncode(*response);
|
||||
std::move(callback_).Run(base::unexpected(ProtocolError::kInvalidResponse));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string reserialized;
|
||||
base::JSONWriter::WriteWithOptions(
|
||||
*json, base::JsonOptions::OPTIONS_PRETTY_PRINT, &reserialized);
|
||||
FIDO_LOG(EVENT) << "-> " << reserialized;
|
||||
|
||||
const base::Value::Dict& dict = json->GetDict();
|
||||
const base::Value::Dict* response_dict = dict.FindDict("response");
|
||||
if (!response_dict) {
|
||||
FIDO_LOG(ERROR) << "no 'response' element in response";
|
||||
std::move(callback_).Run(base::unexpected(ProtocolError::kInvalidResponse));
|
||||
return;
|
||||
}
|
||||
|
||||
const base::Value::Dict* digital = response_dict->FindDict("digital");
|
||||
if (!digital) {
|
||||
FIDO_LOG(ERROR) << "no 'digital' element in response";
|
||||
std::move(callback_).Run(base::unexpected(ProtocolError::kInvalidResponse));
|
||||
return;
|
||||
}
|
||||
|
||||
const base::Value* error = digital->Find("error");
|
||||
if (error) {
|
||||
const std::string* error_str = error->GetIfString();
|
||||
if (!error_str) {
|
||||
FIDO_LOG(ERROR) << "error is not a string";
|
||||
std::move(callback_).Run(
|
||||
base::unexpected(ProtocolError::kInvalidResponse));
|
||||
return;
|
||||
}
|
||||
std::move(callback_).Run(
|
||||
base::unexpected(ErrorStringToRemoteError(*error_str)));
|
||||
return;
|
||||
}
|
||||
|
||||
const base::Value* data = digital->Find("data");
|
||||
if (!data) {
|
||||
FIDO_LOG(ERROR) << "response missing both 'error' and 'data'";
|
||||
std::move(callback_).Run(base::unexpected(ProtocolError::kInvalidResponse));
|
||||
return;
|
||||
}
|
||||
|
||||
std::move(callback_).Run(Response(data->Clone()));
|
||||
}
|
||||
|
||||
} // namespace content::digital_credentials::cross_device
|
@ -0,0 +1,66 @@
|
||||
// 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.
|
||||
|
||||
#ifndef CONTENT_BROWSER_WEBID_DIGITAL_CREDENTIALS_CROSS_DEVICE_REQUEST_DISPATCHER_H_
|
||||
#define CONTENT_BROWSER_WEBID_DIGITAL_CREDENTIALS_CROSS_DEVICE_REQUEST_DISPATCHER_H_
|
||||
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "base/functional/callback.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/sequence_checker.h"
|
||||
#include "base/types/expected.h"
|
||||
#include "content/common/content_export.h"
|
||||
#include "content/public/browser/digital_credentials_cross_device.h"
|
||||
#include "device/fido/fido_discovery_base.h"
|
||||
#include "third_party/abseil-cpp/absl/types/variant.h"
|
||||
|
||||
namespace device {
|
||||
class FidoAuthenticator;
|
||||
}
|
||||
|
||||
namespace content::digital_credentials::cross_device {
|
||||
|
||||
// A RequestDispatcher fetches an identity document from a mobile
|
||||
// device.
|
||||
class CONTENT_EXPORT RequestDispatcher : device::FidoDiscoveryBase::Observer {
|
||||
public:
|
||||
using Error = absl::variant<ProtocolError, RemoteError>;
|
||||
using CompletionCallback =
|
||||
base::OnceCallback<void(base::expected<Response, Error>)>;
|
||||
|
||||
RequestDispatcher(std::unique_ptr<device::FidoDiscoveryBase> discovery,
|
||||
url::Origin origin,
|
||||
base::Value request,
|
||||
CompletionCallback callback);
|
||||
|
||||
RequestDispatcher(const RequestDispatcher&) = delete;
|
||||
RequestDispatcher& operator=(const RequestDispatcher&) = delete;
|
||||
|
||||
~RequestDispatcher() override;
|
||||
|
||||
private:
|
||||
// FidoDiscoveryBase::Observer:
|
||||
void AuthenticatorAdded(device::FidoDiscoveryBase* discovery,
|
||||
device::FidoAuthenticator* authenticator) override;
|
||||
void AuthenticatorRemoved(device::FidoDiscoveryBase* discovery,
|
||||
device::FidoAuthenticator* authenticator) override;
|
||||
|
||||
void OnAuthenticatorReady(device::FidoAuthenticator* authenticator);
|
||||
void OnComplete(std::optional<std::vector<uint8_t>> response);
|
||||
|
||||
const std::unique_ptr<device::FidoDiscoveryBase> discovery_;
|
||||
const url::Origin origin_;
|
||||
base::Value request_;
|
||||
CompletionCallback callback_;
|
||||
|
||||
SEQUENCE_CHECKER(my_sequence_checker_);
|
||||
base::WeakPtrFactory<RequestDispatcher> weak_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace content::digital_credentials::cross_device
|
||||
|
||||
#endif // CONTENT_BROWSER_WEBID_DIGITAL_CREDENTIALS_CROSS_DEVICE_REQUEST_DISPATCHER_H_
|
177
content/browser/webid/digital_credentials/cross_device_request_dispatcher_unittest.cc
Normal file
177
content/browser/webid/digital_credentials/cross_device_request_dispatcher_unittest.cc
Normal file
@ -0,0 +1,177 @@
|
||||
// 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/webid/digital_credentials/cross_device_request_dispatcher.h"
|
||||
|
||||
#include "base/test/bind.h"
|
||||
#include "base/test/task_environment.h"
|
||||
#include "base/test/test_future.h"
|
||||
#include "content/browser/webid/digital_credentials/cross_device_request_dispatcher.h"
|
||||
#include "content/public/browser/digital_credentials_cross_device.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/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"
|
||||
|
||||
namespace content::digital_credentials::cross_device {
|
||||
namespace {
|
||||
|
||||
class DigitalCredentialsCrossDeviceRequestDispatcherTest
|
||||
: public ::testing::Test {
|
||||
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));
|
||||
}
|
||||
|
||||
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");
|
||||
url::Origin origin(url::Origin::Create(url));
|
||||
base::Value::Dict request_value;
|
||||
request_value.Set("foo", "bar");
|
||||
base::test::TestFuture<base::expected<Response, RequestDispatcher::Error>>
|
||||
callback;
|
||||
auto request_handler = std::make_unique<RequestDispatcher>(
|
||||
std::move(discovery), std::move(origin),
|
||||
base::Value(std::move(request_value)), 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] = {0};
|
||||
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};
|
||||
|
||||
base::test::TaskEnvironment task_environment;
|
||||
};
|
||||
|
||||
TEST_F(DigitalCredentialsCrossDeviceRequestDispatcherTest, Valid) {
|
||||
base::expected<Response, RequestDispatcher::Error> result =
|
||||
Transact(device::cablev2::PayloadType::kJSON,
|
||||
R"({"response": {"digital": {"data": "ok"}}})");
|
||||
ASSERT_TRUE(result.has_value());
|
||||
ASSERT_TRUE(result.value()->is_string());
|
||||
ASSERT_EQ(result.value()->GetString(), "ok");
|
||||
}
|
||||
|
||||
TEST_F(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_F(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_F(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_F(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_F(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_F(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));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace content::digital_credentials::cross_device
|
@ -152,6 +152,7 @@ source_set("browser_sources") {
|
||||
"devtools_manager_delegate.h",
|
||||
"devtools_network_transaction_factory.h",
|
||||
"devtools_socket_factory.h",
|
||||
"digital_credentials_cross_device.h",
|
||||
"digital_identity_interstitial_type.h",
|
||||
"digital_identity_provider.cc",
|
||||
"digital_identity_provider.h",
|
||||
|
@ -42,6 +42,10 @@ specific_include_rules = {
|
||||
"+device/fido",
|
||||
],
|
||||
|
||||
"digital_credentials_cross_device\.h": [
|
||||
"+device/fido",
|
||||
],
|
||||
|
||||
"desktop_capture\.h": [
|
||||
# desktop_capture.h creates a DesktopCaptureOptions to share between
|
||||
# content/browser and chrome/browser.
|
||||
|
115
content/public/browser/digital_credentials_cross_device.h
Normal file
115
content/public/browser/digital_credentials_cross_device.h
Normal file
@ -0,0 +1,115 @@
|
||||
// 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.
|
||||
|
||||
#ifndef CONTENT_PUBLIC_BROWSER_DIGITAL_CREDENTIALS_CROSS_DEVICE_H_
|
||||
#define CONTENT_PUBLIC_BROWSER_DIGITAL_CREDENTIALS_CROSS_DEVICE_H_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "base/types/expected.h"
|
||||
#include "base/types/strong_alias.h"
|
||||
#include "base/values.h"
|
||||
#include "content/common/content_export.h"
|
||||
#include "device/fido/cable/v2_constants.h"
|
||||
#include "device/fido/network_context_factory.h"
|
||||
#include "url/origin.h"
|
||||
|
||||
namespace content::digital_credentials::cross_device {
|
||||
|
||||
// SystemErrors result from issues with the local computer that prevent a
|
||||
// transaction from completing.
|
||||
enum class SystemError {
|
||||
// This is returned if a macOS process hasn't been launched as "self
|
||||
// responsible". Without this, the Bluetooth permission is based on the
|
||||
// launching application, often a terminal. If the terminal didn't happen to
|
||||
// confingure itself for Bluetooth permission then the Chrome process is
|
||||
// killed by the kernel.
|
||||
kNotSelfResponsible,
|
||||
// There's no Bluetooth adaptor, or it doesn't support BLE.
|
||||
kNoBleSupport,
|
||||
// Returned if BLE permission has been denied to the point where it cannot
|
||||
// be requested. The user will need to update their system settings.
|
||||
kPermissionDenied,
|
||||
// BLE was powered off during the transaction.
|
||||
kLostPower,
|
||||
};
|
||||
|
||||
// ProtocolErrors result from issues communicating with a remote device where
|
||||
// the remote device is at fault.
|
||||
enum class ProtocolError {
|
||||
kIncompatibleDevice,
|
||||
kTransportError,
|
||||
kInvalidResponse,
|
||||
};
|
||||
|
||||
// RemoteErrors are reported by the remote device. These values are taken from
|
||||
// the CTAP 2.2 spec.
|
||||
enum class RemoteError {
|
||||
kUserCanceled,
|
||||
kDeviceAborted,
|
||||
kNoCredential,
|
||||
kOther, // All unknown error values are mapped to this.
|
||||
};
|
||||
|
||||
enum class SystemEvent {
|
||||
// The BLE adapter is off. `Transaction::PowerBluetoothAdapter` can be called
|
||||
// to turn it on, but the user should have indicated that they wish to first.
|
||||
kBluetoothNotPowered,
|
||||
// The user has been prompted for Bluetooth permission. The system will show
|
||||
// a dialog to them for them to approve. No action is needed by the caller:
|
||||
// either the user will grant permission and the `kRunning` event will be
|
||||
// reported, or the user will deny permission and the transaction will fail
|
||||
// with `SystemError::kPermissionDenied`.
|
||||
kNeedPermission,
|
||||
// The system is listening for BLE adverts.
|
||||
kRunning,
|
||||
};
|
||||
|
||||
using Error = absl::variant<SystemError, ProtocolError, RemoteError>;
|
||||
|
||||
// Events either come from the underlying hybrid connection, or are
|
||||
// SystemEvents.
|
||||
using Event = absl::variant<device::cablev2::Event, SystemEvent>;
|
||||
|
||||
// A Response is the response to a cross-device request. At this level of
|
||||
// abstraction it's an opaque `base::Value` taken from the JSON reply.
|
||||
using Response = base::StrongAlias<class CrossDeviceResponseTag, base::Value>;
|
||||
|
||||
// A Transaction performs a cross-device digital identity transaction by
|
||||
// listening for mobile devices that have scanned a QR code and thus are
|
||||
// broadcasting a BLE message. An encrypted tunnel is set up between this device
|
||||
// and the mobile device and the digital identity is exchanged.
|
||||
class CONTENT_EXPORT Transaction {
|
||||
public:
|
||||
using EventCallback = base::RepeatingCallback<void(Event)>;
|
||||
using CompletionCallback =
|
||||
base::OnceCallback<void(base::expected<Response, Error>)>;
|
||||
|
||||
static std::unique_ptr<Transaction> New(
|
||||
// The origin of the requesting page.
|
||||
url::Origin origin,
|
||||
// The request, as would be found in place of "$1" in the following
|
||||
// Javascript: `navigator.identity.get({digital: $1});`
|
||||
base::Value request,
|
||||
// A secret key that was used to generate the generated QR code. Any
|
||||
// mobile devices will have to prove that they know this secret because
|
||||
// they scanned the QR code.
|
||||
std::array<uint8_t, device::cablev2::kQRKeySize> qr_generator_key,
|
||||
device::NetworkContextFactory network_context_factory,
|
||||
// This callback may be called multiple times as the process advances.
|
||||
EventCallback event_callback,
|
||||
// This callback will be called exactly once. After which, no more events
|
||||
// will be reported.
|
||||
CompletionCallback callback);
|
||||
|
||||
virtual ~Transaction();
|
||||
|
||||
// Turn on the Bluetooth adapter. Should only be called if
|
||||
// `kBluetoothNotPowered` is reported and the user wishes to.
|
||||
virtual void PowerBluetoothAdapter() = 0;
|
||||
};
|
||||
|
||||
} // namespace content::digital_credentials::cross_device
|
||||
|
||||
#endif // CONTENT_PUBLIC_BROWSER_DIGITAL_CREDENTIALS_CROSS_DEVICE_H_
|
@ -2934,6 +2934,7 @@ test("content_unittests") {
|
||||
sources += [
|
||||
"../browser/compute_pressure/pressure_service_for_frame_unittest.cc",
|
||||
"../browser/compute_pressure/pressure_service_for_worker_unittest.cc",
|
||||
"../browser/webid/digital_credentials/cross_device_request_dispatcher_unittest.cc",
|
||||
]
|
||||
}
|
||||
|
||||
@ -3371,6 +3372,8 @@ test("content_unittests") {
|
||||
]
|
||||
deps += [
|
||||
"//components/speech:speech",
|
||||
"//device/fido:cablev2_authenticator",
|
||||
"//device/fido:cablev2_test_util",
|
||||
"//media/mojo/mojom:web_speech_recognition",
|
||||
]
|
||||
|
||||
|
@ -7,16 +7,20 @@
|
||||
#include "base/metrics/histogram_functions.h"
|
||||
#include "base/strings/string_number_conversions.h"
|
||||
#include "base/task/sequenced_task_runner.h"
|
||||
#include "base/task/single_thread_task_runner.h"
|
||||
#include "components/cbor/reader.h"
|
||||
#include "components/cbor/values.h"
|
||||
#include "components/cbor/writer.h"
|
||||
#include "components/device_event_log/device_event_log.h"
|
||||
#include "crypto/random.h"
|
||||
#include "device/fido/cable/cable_discovery_data.h"
|
||||
#include "device/fido/cable/v2_constants.h"
|
||||
#include "device/fido/cbor_extract.h"
|
||||
#include "device/fido/features.h"
|
||||
#include "device/fido/fido_constants.h"
|
||||
#include "device/fido/fido_device.h"
|
||||
#include "device/fido/fido_parsing_utils.h"
|
||||
#include "device/fido/fido_types.h"
|
||||
#include "device/fido/network_context_factory.h"
|
||||
#include "net/storage_access_api/status.h"
|
||||
#include "net/traffic_annotation/network_traffic_annotation.h"
|
||||
@ -29,8 +33,7 @@ using device::cbor_extract::Is;
|
||||
using device::cbor_extract::StepOrByte;
|
||||
using device::cbor_extract::Stop;
|
||||
|
||||
namespace device {
|
||||
namespace cablev2 {
|
||||
namespace device::cablev2 {
|
||||
|
||||
namespace {
|
||||
|
||||
@ -104,10 +107,12 @@ FidoTunnelDevice::FidoTunnelDevice(
|
||||
std::optional<base::RepeatingCallback<void(Event)>> event_callback,
|
||||
base::span<const uint8_t> secret,
|
||||
base::span<const uint8_t, kQRSeedSize> local_identity_seed,
|
||||
const CableEidArray& decrypted_eid)
|
||||
const CableEidArray& decrypted_eid,
|
||||
bool must_support_ctap)
|
||||
: info_(std::in_place_type<QRInfo>),
|
||||
id_(RandomId()),
|
||||
event_callback_(std::move(event_callback)) {
|
||||
event_callback_(std::move(event_callback)),
|
||||
must_support_ctap_(must_support_ctap) {
|
||||
const eid::Components components = eid::ToComponents(decrypted_eid);
|
||||
|
||||
QRInfo& info = absl::get<QRInfo>(info_);
|
||||
@ -153,7 +158,9 @@ FidoTunnelDevice::FidoTunnelDevice(
|
||||
std::optional<base::RepeatingCallback<void(Event)>> event_callback)
|
||||
: info_(std::in_place_type<PairedInfo>),
|
||||
id_(RandomId()),
|
||||
event_callback_(std::move(event_callback)) {
|
||||
event_callback_(std::move(event_callback)),
|
||||
// Paired connections always use CTAP.
|
||||
must_support_ctap_(true) {
|
||||
uint8_t client_nonce[kClientNonceSize];
|
||||
crypto::RandBytes(client_nonce);
|
||||
|
||||
@ -234,25 +241,40 @@ bool FidoTunnelDevice::MatchAdvert(
|
||||
return true;
|
||||
}
|
||||
|
||||
void FidoTunnelDevice::DiscoverSupportedProtocolAndDeviceInfo(
|
||||
base::OnceClosure done) {
|
||||
if (features_.has_value() && features_->contains(Feature::kCTAP)) {
|
||||
FidoDevice::DiscoverSupportedProtocolAndDeviceInfo(std::move(done));
|
||||
} else if (!features_.has_value() && state_ != State::kError) {
|
||||
// The post-handshake message hasn't been processed yet.
|
||||
discover_callback_ = std::move(done);
|
||||
} else {
|
||||
// Either this is a non-CTAP2 device, in which case making a getInfo request
|
||||
// doesn't make sense, or else `state_` is `kError` and we can't.
|
||||
//
|
||||
// Here CTAP2 is set to satisfy other parts of the stack, but
|
||||
// `DoTransact` CHECKs that it is never used.
|
||||
const std::array<uint8_t, kAaguidLength> kZeroAaguid = {0};
|
||||
supported_protocol_ = ProtocolVersion::kCtap2;
|
||||
device_info_.emplace(
|
||||
base::flat_set<ProtocolVersion>{ProtocolVersion::kCtap2},
|
||||
base::flat_set<Ctap2Version>{Ctap2Version::kCtap2_0}, kZeroAaguid);
|
||||
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
|
||||
FROM_HERE, std::move(done));
|
||||
}
|
||||
}
|
||||
|
||||
FidoDevice::CancelToken FidoTunnelDevice::DeviceTransact(
|
||||
std::vector<uint8_t> command,
|
||||
DeviceCallback callback) {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||
return DoTransact(MessageType::kCTAP, std::move(command),
|
||||
std::move(callback));
|
||||
}
|
||||
|
||||
if (state_ == State::kError) {
|
||||
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
|
||||
FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
|
||||
} else if (state_ != State::kReady) {
|
||||
DCHECK(!pending_callback_);
|
||||
pending_message_ = std::move(command);
|
||||
pending_callback_ = std::move(callback);
|
||||
} else {
|
||||
DeviceTransactReady(std::move(command), std::move(callback));
|
||||
}
|
||||
|
||||
// TODO: cancelation would be useful, but it depends on the GMSCore action
|
||||
// being cancelable on Android, which it currently is not.
|
||||
return kInvalidCancelToken + 1;
|
||||
FidoDevice::CancelToken FidoTunnelDevice::DeviceTransactJSON(
|
||||
std::vector<uint8_t> json,
|
||||
DeviceCallback callback) {
|
||||
return DoTransact(MessageType::kJSON, std::move(json), std::move(callback));
|
||||
}
|
||||
|
||||
void FidoTunnelDevice::Cancel(CancelToken token) {
|
||||
@ -269,11 +291,59 @@ FidoTransportProtocol FidoTunnelDevice::DeviceTransport() const {
|
||||
return FidoTransportProtocol::kHybrid;
|
||||
}
|
||||
|
||||
FidoTunnelDevice* FidoTunnelDevice::GetTunnelDevice() {
|
||||
return this;
|
||||
}
|
||||
|
||||
base::flat_set<cablev2::Feature> FidoTunnelDevice::features() const {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||
DCHECK(features_.has_value())
|
||||
<< "features() was called before it was ready. This should never "
|
||||
"happen because no requests should be sent prior the post-handshake "
|
||||
"message.";
|
||||
return *features_;
|
||||
}
|
||||
|
||||
base::WeakPtr<FidoDevice> FidoTunnelDevice::GetWeakPtr() {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||
return weak_factory_.GetWeakPtr();
|
||||
}
|
||||
|
||||
FidoDevice::CancelToken FidoTunnelDevice::DoTransact(MessageType msg_type,
|
||||
std::vector<uint8_t> msg,
|
||||
DeviceCallback callback) {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||
|
||||
if (state_ == State::kError) {
|
||||
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
|
||||
FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
|
||||
return kInvalidCancelToken + 1;
|
||||
}
|
||||
CHECK_EQ(state_, State::kReady);
|
||||
CHECK(msg_type != MessageType::kCTAP || features_->contains(Feature::kCTAP));
|
||||
|
||||
if (msg_type != MessageType::kCTAP || msg.size() != 1 ||
|
||||
msg[0] !=
|
||||
static_cast<uint8_t>(CtapRequestCommand::kAuthenticatorGetInfo)) {
|
||||
established_connection_->Transact(msg_type, std::move(msg),
|
||||
std::move(callback));
|
||||
// TODO: cancelation would be useful, but it depends on the GMSCore action
|
||||
// being cancelable on Android, which it currently is not.
|
||||
return kInvalidCancelToken + 1;
|
||||
}
|
||||
|
||||
CHECK(!getinfo_response_bytes_.empty());
|
||||
std::vector<uint8_t> reply;
|
||||
reply.reserve(1 + getinfo_response_bytes_.size());
|
||||
reply.push_back(static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess));
|
||||
reply.insert(reply.end(), getinfo_response_bytes_.begin(),
|
||||
getinfo_response_bytes_.end());
|
||||
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
|
||||
FROM_HERE, base::BindOnce(std::move(callback), std::move(reply)));
|
||||
|
||||
return kInvalidCancelToken + 1;
|
||||
}
|
||||
|
||||
void FidoTunnelDevice::OnTunnelReady(
|
||||
WebSocketAdapter::Result result,
|
||||
std::optional<std::array<uint8_t, kRoutingIdSize>> routing_id,
|
||||
@ -415,17 +485,62 @@ void FidoTunnelDevice::OnTunnelData(
|
||||
}
|
||||
const cbor::Value::MapValue& map = payload->GetMap();
|
||||
|
||||
const cbor::Value::MapValue::const_iterator getinfo_it =
|
||||
map.find(cbor::Value(1));
|
||||
if (getinfo_it == map.end() || !getinfo_it->second.is_bytestring()) {
|
||||
FIDO_LOG(ERROR)
|
||||
<< GetId()
|
||||
<< ": caBLE post-handshake message missing getInfo response";
|
||||
features_.emplace();
|
||||
const cbor::Value::MapValue::const_iterator features_it =
|
||||
map.find(cbor::Value(3));
|
||||
if (features_it != map.end()) {
|
||||
if (!features_it->second.is_array()) {
|
||||
FIDO_LOG(ERROR)
|
||||
<< GetId()
|
||||
<< ": invalid features data in caBLE post-handshake message";
|
||||
RecordEvent(CableV2TunnelEvent::kPostHandshakeFailed);
|
||||
OnError();
|
||||
return;
|
||||
}
|
||||
const cbor::Value::ArrayValue& array = features_it->second.GetArray();
|
||||
for (const auto& capability : array) {
|
||||
if (!capability.is_string()) {
|
||||
continue;
|
||||
}
|
||||
if (capability.GetString() == "dc") {
|
||||
features_->insert(Feature::kDigitialIdentities);
|
||||
} else if (capability.GetString() == "ctap") {
|
||||
features_->insert(Feature::kCTAP);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the peer doesn't advertise any features then we assume that CTAP
|
||||
// is supported.
|
||||
features_->insert(Feature::kCTAP);
|
||||
}
|
||||
|
||||
if (must_support_ctap_ && !features_->contains(Feature::kCTAP)) {
|
||||
FIDO_LOG(ERROR) << GetId() << ": caBLE device doesn't support CTAP";
|
||||
RecordEvent(CableV2TunnelEvent::kPostHandshakeFailed);
|
||||
OnError();
|
||||
return;
|
||||
}
|
||||
|
||||
const cbor::Value::MapValue::const_iterator getinfo_it =
|
||||
map.find(cbor::Value(1));
|
||||
if (features_->contains(Feature::kCTAP)) {
|
||||
if (getinfo_it == map.end() || !getinfo_it->second.is_bytestring()) {
|
||||
FIDO_LOG(ERROR)
|
||||
<< GetId()
|
||||
<< ": caBLE post-handshake message missing getInfo response";
|
||||
RecordEvent(CableV2TunnelEvent::kPostHandshakeFailed);
|
||||
OnError();
|
||||
return;
|
||||
}
|
||||
getinfo_response_bytes_ = getinfo_it->second.GetBytestring();
|
||||
} else if (getinfo_it != map.end()) {
|
||||
FIDO_LOG(ERROR) << GetId()
|
||||
<< ": caBLE post-handshake message contained getInfo "
|
||||
"response but didn't advertise CTAP support.";
|
||||
RecordEvent(CableV2TunnelEvent::kPostHandshakeFailed);
|
||||
OnError();
|
||||
return;
|
||||
}
|
||||
getinfo_response_bytes_ = getinfo_it->second.GetBytestring();
|
||||
|
||||
// Linking information is always optional. Currently it is ignored outside
|
||||
// of a QR handshake but, in future, we may need to be able to update
|
||||
@ -475,10 +590,11 @@ void FidoTunnelDevice::OnTunnelData(
|
||||
std::move(websocket_client_), GetId(), protocol_revision,
|
||||
std::move(crypter_), *handshake_hash_, absl::get_if<QRInfo>(&info_));
|
||||
|
||||
if (pending_callback_) {
|
||||
DeviceTransactReady(std::move(pending_message_),
|
||||
std::move(pending_callback_));
|
||||
if (discover_callback_) {
|
||||
CHECK(features_.has_value());
|
||||
DiscoverSupportedProtocolAndDeviceInfo(std::move(discover_callback_));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@ -496,39 +612,17 @@ void FidoTunnelDevice::OnError() {
|
||||
state_ = State::kError;
|
||||
|
||||
if (previous_state == State::kReady) {
|
||||
DCHECK(!pending_callback_);
|
||||
DCHECK(!websocket_client_);
|
||||
established_connection_->Close();
|
||||
established_connection_.reset();
|
||||
} else {
|
||||
websocket_client_.reset();
|
||||
if (pending_callback_) {
|
||||
std::move(pending_callback_).Run(std::nullopt);
|
||||
if (discover_callback_) {
|
||||
DiscoverSupportedProtocolAndDeviceInfo(std::move(discover_callback_));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FidoTunnelDevice::DeviceTransactReady(std::vector<uint8_t> command,
|
||||
DeviceCallback callback) {
|
||||
DCHECK_EQ(state_, State::kReady);
|
||||
|
||||
if (command.size() != 1 ||
|
||||
command[0] !=
|
||||
static_cast<uint8_t>(CtapRequestCommand::kAuthenticatorGetInfo)) {
|
||||
established_connection_->Transact(std::move(command), std::move(callback));
|
||||
return;
|
||||
}
|
||||
|
||||
DCHECK(!getinfo_response_bytes_.empty());
|
||||
std::vector<uint8_t> reply;
|
||||
reply.reserve(1 + getinfo_response_bytes_.size());
|
||||
reply.push_back(static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess));
|
||||
reply.insert(reply.end(), getinfo_response_bytes_.begin(),
|
||||
getinfo_response_bytes_.end());
|
||||
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
|
||||
FROM_HERE, base::BindOnce(std::move(callback), std::move(reply)));
|
||||
}
|
||||
|
||||
bool FidoTunnelDevice::ProcessConnectSignal(base::span<const uint8_t> data) {
|
||||
if (data.size() != 1 || data[0] != 0) {
|
||||
return false;
|
||||
@ -578,13 +672,14 @@ FidoTunnelDevice::EstablishedConnection::~EstablishedConnection() {
|
||||
}
|
||||
|
||||
void FidoTunnelDevice::EstablishedConnection::Transact(
|
||||
MessageType msg_type,
|
||||
std::vector<uint8_t> message,
|
||||
DeviceCallback callback) {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||
DCHECK(state_ == State::kRunning || state_ == State::kRemoteShutdown);
|
||||
|
||||
if (protocol_revision_ >= 1) {
|
||||
message.insert(message.begin(), static_cast<uint8_t>(MessageType::kCTAP));
|
||||
message.insert(message.begin(), static_cast<uint8_t>(msg_type));
|
||||
}
|
||||
|
||||
if (state_ == State::kRemoteShutdown || !crypter_->Encrypt(&message)) {
|
||||
@ -594,6 +689,7 @@ void FidoTunnelDevice::EstablishedConnection::Transact(
|
||||
}
|
||||
|
||||
DCHECK(!callback_);
|
||||
expected_reply_type_ = msg_type;
|
||||
callback_ = std::move(callback);
|
||||
websocket_client_->Write(message);
|
||||
}
|
||||
@ -683,6 +779,13 @@ void FidoTunnelDevice::EstablishedConnection::OnTunnelData(
|
||||
return;
|
||||
|
||||
case MessageType::kCTAP:
|
||||
case MessageType::kJSON:
|
||||
if (message_type != expected_reply_type_.value()) {
|
||||
FIDO_LOG(ERROR) << "incorrect message type in reply";
|
||||
OnRemoteClose();
|
||||
// `this` may be invalid now.
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case MessageType::kUpdate: {
|
||||
@ -777,5 +880,4 @@ void FidoTunnelDevice::EstablishedConnection::OnTimeout() {
|
||||
OnRemoteClose();
|
||||
}
|
||||
|
||||
} // namespace cablev2
|
||||
} // namespace device
|
||||
} // namespace device::cablev2
|
||||
|
@ -36,7 +36,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoTunnelDevice : public FidoDevice {
|
||||
std::optional<base::RepeatingCallback<void(Event)>> event_callback,
|
||||
base::span<const uint8_t> secret,
|
||||
base::span<const uint8_t, kQRSeedSize> local_identity_seed,
|
||||
const CableEidArray& decrypted_eid);
|
||||
const CableEidArray& decrypted_eid,
|
||||
// If true, the peer device must support processing CTAP messages
|
||||
// otherwise a handshake error results. If false, the user commits to
|
||||
// checking `features()` first.
|
||||
bool must_support_ctap);
|
||||
|
||||
// This constructor is used for pairing-initiated connections. If the given
|
||||
// |Pairing| is reported by the tunnel server to be invalid (which can happen
|
||||
@ -60,11 +64,16 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoTunnelDevice : public FidoDevice {
|
||||
bool MatchAdvert(const std::array<uint8_t, kAdvertSize>& advert);
|
||||
|
||||
// FidoDevice:
|
||||
void DiscoverSupportedProtocolAndDeviceInfo(base::OnceClosure done) override;
|
||||
CancelToken DeviceTransact(std::vector<uint8_t> command,
|
||||
DeviceCallback callback) override;
|
||||
CancelToken DeviceTransactJSON(std::vector<uint8_t> json,
|
||||
DeviceCallback callback);
|
||||
void Cancel(CancelToken token) override;
|
||||
std::string GetId() const override;
|
||||
FidoTransportProtocol DeviceTransport() const override;
|
||||
FidoTunnelDevice* GetTunnelDevice() override;
|
||||
base::flat_set<Feature> features() const;
|
||||
base::WeakPtr<FidoDevice> GetWeakPtr() override;
|
||||
|
||||
// GetNumEstablishedConnectionInstancesForTesting returns the current number
|
||||
@ -171,7 +180,9 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoTunnelDevice : public FidoDevice {
|
||||
EstablishedConnection(const EstablishedConnection&) = delete;
|
||||
EstablishedConnection& operator=(const EstablishedConnection&) = delete;
|
||||
|
||||
void Transact(std::vector<uint8_t> message, DeviceCallback callback);
|
||||
void Transact(MessageType msg_type,
|
||||
std::vector<uint8_t> message,
|
||||
DeviceCallback callback);
|
||||
void Close();
|
||||
|
||||
private:
|
||||
@ -206,28 +217,32 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoTunnelDevice : public FidoDevice {
|
||||
|
||||
base::OneShotTimer timer_;
|
||||
DeviceCallback callback_;
|
||||
std::optional<MessageType> expected_reply_type_;
|
||||
SEQUENCE_CHECKER(sequence_checker_);
|
||||
};
|
||||
|
||||
CancelToken DoTransact(MessageType type,
|
||||
std::vector<uint8_t> msg,
|
||||
DeviceCallback callback);
|
||||
void OnTunnelReady(
|
||||
WebSocketAdapter::Result result,
|
||||
std::optional<std::array<uint8_t, kRoutingIdSize>> routing_id,
|
||||
WebSocketAdapter::ConnectSignalSupport connect_signal_support);
|
||||
void OnTunnelData(std::optional<base::span<const uint8_t>> data);
|
||||
void OnError();
|
||||
void DeviceTransactReady(std::vector<uint8_t> command,
|
||||
DeviceCallback callback);
|
||||
bool ProcessConnectSignal(base::span<const uint8_t> data);
|
||||
|
||||
State state_ = State::kConnecting;
|
||||
absl::variant<QRInfo, PairedInfo> info_;
|
||||
const std::array<uint8_t, 8> id_;
|
||||
const std::optional<base::RepeatingCallback<void(Event)>> event_callback_;
|
||||
std::vector<uint8_t> pending_message_;
|
||||
DeviceCallback pending_callback_;
|
||||
const bool must_support_ctap_;
|
||||
std::optional<base::flat_set<Feature>> features_;
|
||||
base::OnceClosure discover_callback_;
|
||||
std::optional<HandshakeInitiator> handshake_;
|
||||
std::optional<HandshakeHash> handshake_hash_;
|
||||
std::vector<uint8_t> getinfo_response_bytes_;
|
||||
std::optional<bool> supports_json_;
|
||||
|
||||
// These fields are |nullptr| when in state |kReady|.
|
||||
std::unique_ptr<WebSocketAdapter> websocket_client_;
|
||||
|
@ -6,10 +6,13 @@
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "base/containers/flat_set.h"
|
||||
#include "base/feature_list.h"
|
||||
#include "base/json/json_reader.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/memory/raw_ptr_exclusion.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/ranges/algorithm.h"
|
||||
#include "base/sequence_checker.h"
|
||||
#include "base/strings/string_number_conversions.h"
|
||||
#include "base/task/sequenced_task_runner.h"
|
||||
@ -20,6 +23,7 @@
|
||||
#include "components/cbor/writer.h"
|
||||
#include "components/device_event_log/device_event_log.h"
|
||||
#include "crypto/random.h"
|
||||
#include "device/fido/cable/v2_constants.h"
|
||||
#include "device/fido/cable/v2_handshake.h"
|
||||
#include "device/fido/cable/websocket_adapter.h"
|
||||
#include "device/fido/cbor_extract.h"
|
||||
@ -269,7 +273,8 @@ class TunnelTransport : public Transport {
|
||||
NetworkContextFactory network_context_factory,
|
||||
base::span<const uint8_t> secret,
|
||||
base::span<const uint8_t, device::kP256X962Length> peer_identity,
|
||||
GeneratePairingDataCallback generate_pairing_data)
|
||||
GeneratePairingDataCallback generate_pairing_data,
|
||||
base::flat_set<Feature> features)
|
||||
: platform_(platform),
|
||||
tunnel_id_(device::cablev2::Derive<EXTENT(tunnel_id_)>(
|
||||
secret,
|
||||
@ -282,7 +287,8 @@ class TunnelTransport : public Transport {
|
||||
network_context_factory_(std::move(network_context_factory)),
|
||||
peer_identity_(device::fido_parsing_utils::Materialize(peer_identity)),
|
||||
generate_pairing_data_(std::move(generate_pairing_data)),
|
||||
secret_(fido_parsing_utils::Materialize(secret)) {
|
||||
secret_(fido_parsing_utils::Materialize(secret)),
|
||||
features_(std::move(features)) {
|
||||
DCHECK_EQ(state_, State::kNone);
|
||||
state_ = State::kConnecting;
|
||||
|
||||
@ -310,6 +316,7 @@ class TunnelTransport : public Transport {
|
||||
device::cablev2::DerivedValueType::kEIDKey)),
|
||||
network_context_factory_(network_context_factory),
|
||||
secret_(fido_parsing_utils::Materialize(secret)),
|
||||
features_({Feature::kCTAP}),
|
||||
local_identity_(std::move(local_identity)) {
|
||||
DCHECK_EQ(state_, State::kNone);
|
||||
|
||||
@ -342,11 +349,14 @@ class TunnelTransport : public Transport {
|
||||
base::Milliseconds(250));
|
||||
}
|
||||
|
||||
void Write(std::vector<uint8_t> data) override {
|
||||
void Write(PayloadType payload_type, std::vector<uint8_t> data) override {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||
DCHECK_EQ(state_, kReady);
|
||||
|
||||
data.insert(data.begin(), static_cast<uint8_t>(MessageType::kCTAP));
|
||||
data.insert(data.begin(),
|
||||
static_cast<uint8_t>(payload_type == PayloadType::kCTAP
|
||||
? MessageType::kCTAP
|
||||
: MessageType::kJSON));
|
||||
if (!crypter_->Encrypt(&data)) {
|
||||
FIDO_LOG(ERROR) << "Failed to encrypt response";
|
||||
return;
|
||||
@ -456,7 +466,11 @@ class TunnelTransport : public Transport {
|
||||
crypter_ = std::move(result->first);
|
||||
|
||||
cbor::Value::MapValue post_handshake_msg;
|
||||
post_handshake_msg.emplace(1, BuildGetInfoResponse());
|
||||
post_handshake_msg.emplace(3, ToCBOR(features_));
|
||||
|
||||
if (features_.contains(Feature::kCTAP)) {
|
||||
post_handshake_msg.emplace(1, BuildGetInfoResponse());
|
||||
}
|
||||
|
||||
std::optional<std::vector<uint8_t>> post_handshake_msg_bytes;
|
||||
post_handshake_msg_bytes =
|
||||
@ -568,6 +582,7 @@ class TunnelTransport : public Transport {
|
||||
}
|
||||
|
||||
case MessageType::kCTAP:
|
||||
case MessageType::kJSON:
|
||||
break;
|
||||
|
||||
case MessageType::kUpdate:
|
||||
@ -585,7 +600,10 @@ class TunnelTransport : public Transport {
|
||||
update_callback_.Run(Platform::Status::REQUEST_RECEIVED);
|
||||
first_message_ = false;
|
||||
}
|
||||
update_callback_.Run(std::move(plaintext));
|
||||
update_callback_.Run(std::make_pair(message_type == MessageType::kJSON
|
||||
? PayloadType::kJSON
|
||||
: PayloadType::kCTAP,
|
||||
std::move(plaintext)));
|
||||
break;
|
||||
}
|
||||
|
||||
@ -594,6 +612,24 @@ class TunnelTransport : public Transport {
|
||||
}
|
||||
}
|
||||
|
||||
static cbor::Value ToCBOR(const base::flat_set<Feature>& features) {
|
||||
cbor::Value::ArrayValue ret;
|
||||
for (const auto feature : features) {
|
||||
switch (feature) {
|
||||
case Feature::kCTAP:
|
||||
ret.emplace_back("ctap");
|
||||
break;
|
||||
case Feature::kDigitialIdentities:
|
||||
ret.emplace_back("dc");
|
||||
break;
|
||||
}
|
||||
}
|
||||
base::ranges::sort(ret, [](const auto& a, const auto& b) {
|
||||
return a.GetString() < b.GetString();
|
||||
});
|
||||
return cbor::Value(std::move(ret));
|
||||
}
|
||||
|
||||
const raw_ptr<Platform, DanglingUntriaged> platform_;
|
||||
State state_ = State::kNone;
|
||||
const std::array<uint8_t, kTunnelIdSize> tunnel_id_;
|
||||
@ -605,6 +641,7 @@ class TunnelTransport : public Transport {
|
||||
std::array<uint8_t, kPSKSize> psk_;
|
||||
GeneratePairingDataCallback generate_pairing_data_;
|
||||
const std::vector<uint8_t> secret_;
|
||||
const base::flat_set<Feature> features_;
|
||||
bssl::UniquePtr<EC_KEY> local_identity_;
|
||||
GURL target_;
|
||||
std::unique_ptr<Platform::BLEAdvert> ble_advert_;
|
||||
@ -654,9 +691,14 @@ class CTAP2Processor : public Transaction {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t>& msg = absl::get<std::vector<uint8_t>>(update);
|
||||
auto& msg = absl::get<std::pair<PayloadType, std::vector<uint8_t>>>(update);
|
||||
if (msg.first != PayloadType::kCTAP) {
|
||||
have_completed_ = true;
|
||||
platform_->OnCompleted(Platform::Error::INVALID_CTAP);
|
||||
return;
|
||||
}
|
||||
const absl::variant<std::vector<uint8_t>, Platform::Error> result =
|
||||
ProcessCTAPMessage(msg);
|
||||
ProcessCTAPMessage(msg.second);
|
||||
if (const auto* error = absl::get_if<Platform::Error>(&result)) {
|
||||
have_completed_ = true;
|
||||
platform_->OnCompleted(*error);
|
||||
@ -670,7 +712,7 @@ class CTAP2Processor : public Transaction {
|
||||
return;
|
||||
}
|
||||
|
||||
transport_->Write(std::move(response));
|
||||
transport_->Write(PayloadType::kCTAP, std::move(response));
|
||||
}
|
||||
|
||||
absl::variant<std::vector<uint8_t>, Platform::Error> ProcessCTAPMessage(
|
||||
@ -762,8 +804,8 @@ class CTAP2Processor : public Transaction {
|
||||
*make_cred_request.cred_params, cbor::Value("alg"),
|
||||
base::BindRepeating(
|
||||
[](std::vector<
|
||||
device::PublicKeyCredentialParams::CredentialInfo>*
|
||||
out,
|
||||
device::PublicKeyCredentialParams::CredentialInfo>
|
||||
* out,
|
||||
const cbor::Value& value) -> bool {
|
||||
if (!value.is_integer()) {
|
||||
return false;
|
||||
@ -960,7 +1002,7 @@ class CTAP2Processor : public Transaction {
|
||||
platform_->OnStatus(Platform::Status::FIRST_TRANSACTION_DONE);
|
||||
transaction_done_ = true;
|
||||
}
|
||||
transport_->Write(std::move(response));
|
||||
transport_->Write(PayloadType::kCTAP, std::move(response));
|
||||
}
|
||||
|
||||
void OnGetAssertionResponse(
|
||||
@ -1051,7 +1093,7 @@ class CTAP2Processor : public Transaction {
|
||||
platform_->OnStatus(Platform::Status::FIRST_TRANSACTION_DONE);
|
||||
transaction_done_ = true;
|
||||
}
|
||||
transport_->Write(std::move(response));
|
||||
transport_->Write(PayloadType::kCTAP, std::move(response));
|
||||
}
|
||||
|
||||
// CopyCredIds parses a series of `PublicKeyCredentialDescriptor`s from `in`
|
||||
@ -1088,6 +1130,66 @@ class CTAP2Processor : public Transaction {
|
||||
base::WeakPtrFactory<CTAP2Processor> weak_factory_{this};
|
||||
};
|
||||
|
||||
class DigitalIdentityProcessor : public Transaction {
|
||||
public:
|
||||
DigitalIdentityProcessor(std::unique_ptr<Transport> transport,
|
||||
std::unique_ptr<Platform> platform,
|
||||
PayloadType response_payload_type,
|
||||
std::vector<uint8_t> response)
|
||||
: transport_(std::move(transport)),
|
||||
platform_(std::move(platform)),
|
||||
response_payload_type_(response_payload_type),
|
||||
response_(std::move(response)) {
|
||||
transport_->StartReading(base::BindRepeating(
|
||||
&DigitalIdentityProcessor::OnTransportUpdate, base::Unretained(this)));
|
||||
}
|
||||
|
||||
private:
|
||||
void OnTransportUpdate(Transport::Update update) {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||
CHECK(!have_completed_);
|
||||
|
||||
if (auto* error = absl::get_if<Platform::Error>(&update)) {
|
||||
have_completed_ = true;
|
||||
platform_->OnCompleted(*error);
|
||||
return;
|
||||
} else if (auto* status = absl::get_if<Platform::Status>(&update)) {
|
||||
platform_->OnStatus(*status);
|
||||
return;
|
||||
} else if (absl::get_if<Transport::Disconnected>(&update)) {
|
||||
have_completed_ = true;
|
||||
platform_->OnCompleted(std::nullopt);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& msg = absl::get<std::pair<PayloadType, std::vector<uint8_t>>>(update);
|
||||
if (msg.first != PayloadType::kJSON) {
|
||||
have_completed_ = true;
|
||||
platform_->OnCompleted(Platform::Error::INVALID_JSON);
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<base::Value> json = base::JSONReader::Read(
|
||||
std::string_view(reinterpret_cast<const char*>(msg.second.data()),
|
||||
msg.second.size()),
|
||||
base::JSON_PARSE_RFC);
|
||||
if (!json) {
|
||||
have_completed_ = true;
|
||||
platform_->OnCompleted(Platform::Error::INVALID_JSON);
|
||||
return;
|
||||
}
|
||||
|
||||
transport_->Write(response_payload_type_, response_);
|
||||
}
|
||||
|
||||
const std::unique_ptr<Transport> transport_;
|
||||
const std::unique_ptr<Platform> platform_;
|
||||
const PayloadType response_payload_type_;
|
||||
const std::vector<uint8_t> response_;
|
||||
bool have_completed_ = false;
|
||||
SEQUENCE_CHECKER(sequence_checker_);
|
||||
};
|
||||
|
||||
static std::array<uint8_t, 32> DerivePairedSecret(
|
||||
base::span<const uint8_t, kRootSecretSize> root_secret,
|
||||
const std::optional<base::span<const uint8_t>>& contact_id,
|
||||
@ -1205,10 +1307,34 @@ std::unique_ptr<Transaction> TransactFromQRCode(
|
||||
return std::make_unique<CTAP2Processor>(
|
||||
std::make_unique<TunnelTransport>(
|
||||
platform_ptr, std::move(network_context_factory), qr_secret,
|
||||
peer_identity, std::move(generate_pairing_data)),
|
||||
peer_identity, std::move(generate_pairing_data),
|
||||
base::flat_set<Feature>{Feature::kCTAP}),
|
||||
std::move(platform));
|
||||
}
|
||||
|
||||
std::unique_ptr<Transaction> TransactDigitalIdentityFromQRCodeForTesting(
|
||||
std::unique_ptr<Platform> platform,
|
||||
NetworkContextFactory network_context_factory,
|
||||
base::span<const uint8_t, 16> qr_secret,
|
||||
base::span<const uint8_t, kP256X962Length> peer_identity,
|
||||
PayloadType response_payload_type,
|
||||
std::vector<uint8_t> response) {
|
||||
auto no_pairing_data = base::BindOnce(
|
||||
[](base::span<const uint8_t, device::kP256X962Length>
|
||||
peer_public_key_x962,
|
||||
device::cablev2::HandshakeHash) -> std::optional<cbor::Value> {
|
||||
return std::nullopt;
|
||||
});
|
||||
|
||||
Platform* const platform_ptr = platform.get();
|
||||
return std::make_unique<DigitalIdentityProcessor>(
|
||||
std::make_unique<TunnelTransport>(
|
||||
platform_ptr, std::move(network_context_factory), qr_secret,
|
||||
peer_identity, std::move(no_pairing_data),
|
||||
base::flat_set<Feature>{Feature::kDigitialIdentities}),
|
||||
std::move(platform), response_payload_type, std::move(response));
|
||||
}
|
||||
|
||||
std::unique_ptr<Transaction> TransactFromQRCodeDeprecated(
|
||||
std::unique_ptr<Platform> platform,
|
||||
network::mojom::NetworkContext* network_context,
|
||||
|
@ -65,12 +65,13 @@ class Platform {
|
||||
EOF_WHILE_PROCESSING = 113,
|
||||
AUTHENTICATOR_SELECTION_RECEIVED = 114,
|
||||
DISCOVERABLE_CREDENTIALS_REQUEST = 115,
|
||||
INVALID_JSON = 116,
|
||||
};
|
||||
|
||||
using MakeCredentialCallback = base::OnceCallback<void(
|
||||
uint32_t status,
|
||||
base::span<const uint8_t> attestation_obj,
|
||||
bool prf_enabled)>;
|
||||
using MakeCredentialCallback =
|
||||
base::OnceCallback<void(uint32_t status,
|
||||
base::span<const uint8_t> attestation_obj,
|
||||
bool prf_enabled)>;
|
||||
|
||||
virtual void MakeCredential(
|
||||
blink::mojom::PublicKeyCredentialCreationOptionsPtr params,
|
||||
@ -107,7 +108,7 @@ class Transport {
|
||||
// report. The first element is a message from the peer. |Disconnected| is
|
||||
// handled separately because it's context dependent whether that is an error
|
||||
// or not.
|
||||
using Update = absl::variant<std::vector<uint8_t>,
|
||||
using Update = absl::variant<std::pair<PayloadType, std::vector<uint8_t>>,
|
||||
Platform::Error,
|
||||
Platform::Status,
|
||||
Disconnected>;
|
||||
@ -117,7 +118,7 @@ class Transport {
|
||||
// arrives from the peer, an error occurs, or the status of the link changes.
|
||||
virtual void StartReading(
|
||||
base::RepeatingCallback<void(Update)> update_callback) = 0;
|
||||
virtual void Write(std::vector<uint8_t> data) = 0;
|
||||
virtual void Write(PayloadType payload_type, std::vector<uint8_t> data) = 0;
|
||||
};
|
||||
|
||||
// A Transaction is a handle to an ongoing caBLEv2 transaction with a peer.
|
||||
@ -144,6 +145,17 @@ std::unique_ptr<Transaction> TransactFromQRCode(
|
||||
base::span<const uint8_t, kP256X962Length> peer_identity,
|
||||
std::optional<std::vector<uint8_t>> contact_id);
|
||||
|
||||
// TransactDigitalIdentityFromQRCodeForTesting starts a network-based
|
||||
// transaction that expects a JSON request, ignores it, and replies with the
|
||||
// given response.
|
||||
std::unique_ptr<Transaction> TransactDigitalIdentityFromQRCodeForTesting(
|
||||
std::unique_ptr<Platform> platform,
|
||||
NetworkContextFactory network_context_factory,
|
||||
base::span<const uint8_t, 16> qr_secret,
|
||||
base::span<const uint8_t, kP256X962Length> peer_identity,
|
||||
PayloadType response_payload_type,
|
||||
std::vector<uint8_t> response);
|
||||
|
||||
// Deprecated, kept around while Android cable code is cleaned up. Use
|
||||
// TransactFromQRCode instead.
|
||||
std::unique_ptr<Transaction> TransactFromQRCodeDeprecated(
|
||||
|
@ -84,8 +84,9 @@ enum class MessageType : uint8_t {
|
||||
kShutdown = 0,
|
||||
kCTAP = 1,
|
||||
kUpdate = 2,
|
||||
kJSON = 3,
|
||||
|
||||
kMaxValue = 2,
|
||||
kMaxValue = 3,
|
||||
};
|
||||
|
||||
enum class Event {
|
||||
@ -99,6 +100,20 @@ enum class Event {
|
||||
kReady,
|
||||
};
|
||||
|
||||
// PayloadType enumerates the types of application-level payloads carried over a
|
||||
// hybrid connection.
|
||||
enum class PayloadType {
|
||||
kCTAP,
|
||||
kJSON,
|
||||
};
|
||||
|
||||
// Feature enumerates the features that a hybrid device can support.
|
||||
enum class Feature {
|
||||
kCTAP,
|
||||
// Digital identity requests, e.g. mobile driver's licenses.
|
||||
kDigitialIdentities,
|
||||
};
|
||||
|
||||
} // namespace cablev2
|
||||
} // namespace device
|
||||
|
||||
|
@ -58,7 +58,8 @@ Discovery::Discovery(
|
||||
pairing_callback,
|
||||
std::optional<base::RepeatingCallback<void(std::unique_ptr<Pairing>)>>
|
||||
invalidated_pairing_callback,
|
||||
std::optional<base::RepeatingCallback<void(Event)>> event_callback)
|
||||
std::optional<base::RepeatingCallback<void(Event)>> event_callback,
|
||||
bool must_support_ctap)
|
||||
: FidoDeviceDiscovery(FidoTransportProtocol::kHybrid),
|
||||
request_type_(request_type),
|
||||
network_context_factory_(std::move(network_context_factory)),
|
||||
@ -68,7 +69,8 @@ Discovery::Discovery(
|
||||
contact_device_stream_(std::move(contact_device_stream)),
|
||||
pairing_callback_(std::move(pairing_callback)),
|
||||
invalidated_pairing_callback_(std::move(invalidated_pairing_callback)),
|
||||
event_callback_(std::move(event_callback)) {
|
||||
event_callback_(std::move(event_callback)),
|
||||
must_support_ctap_(must_support_ctap) {
|
||||
static_assert(EXTENT(*qr_generator_key) == kQRSecretSize + kQRSeedSize, "");
|
||||
advert_stream_->Connect(
|
||||
base::BindRepeating(&Discovery::OnBLEAdvertSeen, base::Unretained(this)));
|
||||
@ -161,7 +163,8 @@ void Discovery::OnBLEAdvertSeen(base::span<const uint8_t, kAdvertSize> advert) {
|
||||
}
|
||||
AddDevice(std::make_unique<cablev2::FidoTunnelDevice>(
|
||||
network_context_factory_, pairing_callback_, event_callback_,
|
||||
qr_keys_->qr_secret, qr_keys_->local_identity_seed, *plaintext));
|
||||
qr_keys_->qr_secret, qr_keys_->local_identity_seed, *plaintext,
|
||||
must_support_ctap_));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -177,7 +180,8 @@ void Discovery::OnBLEAdvertSeen(base::span<const uint8_t, kAdvertSize> advert) {
|
||||
device_committed_ = true;
|
||||
AddDevice(std::make_unique<cablev2::FidoTunnelDevice>(
|
||||
network_context_factory_, base::DoNothing(), event_callback_,
|
||||
extension.qr_secret, extension.local_identity_seed, *plaintext));
|
||||
extension.qr_secret, extension.local_identity_seed, *plaintext,
|
||||
must_support_ctap_));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,8 @@ class COMPONENT_EXPORT(DEVICE_FIDO) Discovery : public FidoDeviceDiscovery {
|
||||
std::optional<base::RepeatingCallback<void(std::unique_ptr<Pairing>)>>
|
||||
invalidated_pairing_callback,
|
||||
// event_callback receives updates on cablev2 events.
|
||||
std::optional<base::RepeatingCallback<void(Event)>> event_callback);
|
||||
std::optional<base::RepeatingCallback<void(Event)>> event_callback,
|
||||
bool must_support_ctap);
|
||||
~Discovery() override;
|
||||
Discovery(const Discovery&) = delete;
|
||||
Discovery& operator=(const Discovery&) = delete;
|
||||
@ -91,6 +92,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) Discovery : public FidoDeviceDiscovery {
|
||||
const std::optional<base::RepeatingCallback<void(std::unique_ptr<Pairing>)>>
|
||||
invalidated_pairing_callback_;
|
||||
const std::optional<base::RepeatingCallback<void(Event)>> event_callback_;
|
||||
const bool must_support_ctap_;
|
||||
std::vector<std::unique_ptr<FidoTunnelDevice>> tunnels_pending_advert_;
|
||||
base::flat_set<std::array<uint8_t, kAdvertSize>> observed_adverts_;
|
||||
bool started_ = false;
|
||||
|
@ -20,6 +20,7 @@
|
||||
#include "base/functional/callback.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/notreached.h"
|
||||
#include "base/strings/string_number_conversions.h"
|
||||
#include "base/task/sequenced_task_runner.h"
|
||||
#include "components/cbor/reader.h"
|
||||
@ -784,6 +785,9 @@ class LateLinkingDevice : public authenticator::Transaction {
|
||||
break;
|
||||
}
|
||||
|
||||
case MessageType::kJSON:
|
||||
NOTREACHED_NORETURN();
|
||||
|
||||
case MessageType::kShutdown:
|
||||
state_ = State::kShutdownReceived;
|
||||
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include "base/functional/callback.h"
|
||||
#include "base/notreached.h"
|
||||
#include "device/fido/cable/fido_tunnel_device.h"
|
||||
#include "device/fido/ctap_make_credential_request.h"
|
||||
#include "device/fido/fido_constants.h"
|
||||
|
||||
@ -190,6 +191,10 @@ AuthenticatorType FidoAuthenticator::GetType() const {
|
||||
return AuthenticatorType::kOther;
|
||||
}
|
||||
|
||||
cablev2::FidoTunnelDevice* FidoAuthenticator::GetTunnelDevice() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string FidoAuthenticator::GetDisplayName() const {
|
||||
return GetId();
|
||||
}
|
||||
|
@ -34,6 +34,10 @@ struct CtapGetAssertionOptions;
|
||||
struct CtapMakeCredentialRequest;
|
||||
struct MakeCredentialOptions;
|
||||
|
||||
namespace cablev2 {
|
||||
class FidoTunnelDevice;
|
||||
}
|
||||
|
||||
namespace pin {
|
||||
struct RetriesResponse;
|
||||
struct EmptyResponse;
|
||||
@ -333,6 +337,10 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoAuthenticator {
|
||||
// GetType returns the type of the authenticator.
|
||||
virtual AuthenticatorType GetType() const;
|
||||
|
||||
// Returns this object, as a tunnel device, or null if this object isn't of
|
||||
// the correct type.
|
||||
virtual cablev2::FidoTunnelDevice* GetTunnelDevice();
|
||||
|
||||
// GetId returns a unique string representing this device. This string should
|
||||
// be distinct from all other devices concurrently discovered.
|
||||
virtual std::string GetId() const = 0;
|
||||
|
@ -26,6 +26,10 @@ std::string FidoDevice::GetDisplayName() const {
|
||||
return GetId();
|
||||
}
|
||||
|
||||
cablev2::FidoTunnelDevice* FidoDevice::GetTunnelDevice() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void FidoDevice::DiscoverSupportedProtocolAndDeviceInfo(
|
||||
base::OnceClosure done) {
|
||||
// Set the protocol version to CTAP2 for the purpose of sending the GetInfo
|
||||
@ -49,8 +53,9 @@ void FidoDevice::OnDeviceInfoReceived(
|
||||
base::OnceClosure done,
|
||||
std::optional<std::vector<uint8_t>> response) {
|
||||
// TODO(hongjunchoi): Add tests that verify this behavior.
|
||||
if (state_ == FidoDevice::State::kDeviceError)
|
||||
if (state_ == FidoDevice::State::kDeviceError) {
|
||||
return;
|
||||
}
|
||||
|
||||
state_ = FidoDevice::State::kReady;
|
||||
std::optional<AuthenticatorGetInfoResponse> get_info_response =
|
||||
|
@ -20,6 +20,10 @@
|
||||
|
||||
namespace device {
|
||||
|
||||
namespace cablev2 {
|
||||
class FidoTunnelDevice;
|
||||
}
|
||||
|
||||
// Device abstraction for an individual CTAP1.0/CTAP2.0 device.
|
||||
//
|
||||
// Devices are instantiated with an unknown protocol version. Users should call
|
||||
@ -90,6 +94,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDevice {
|
||||
// same VID:PID. It defaults to returning the value of |GetId|.
|
||||
virtual std::string GetDisplayName() const;
|
||||
virtual FidoTransportProtocol DeviceTransport() const = 0;
|
||||
virtual cablev2::FidoTunnelDevice* GetTunnelDevice();
|
||||
|
||||
// NoSilentRequests returns true if this device does not support up=false
|
||||
// requests.
|
||||
|
@ -1511,6 +1511,10 @@ AuthenticatorType FidoDeviceAuthenticator::GetType() const {
|
||||
return AuthenticatorType::kOther;
|
||||
}
|
||||
|
||||
cablev2::FidoTunnelDevice* FidoDeviceAuthenticator::GetTunnelDevice() {
|
||||
return device_->GetTunnelDevice();
|
||||
}
|
||||
|
||||
std::string FidoDeviceAuthenticator::GetId() const {
|
||||
return device_->GetId();
|
||||
}
|
||||
|
@ -126,6 +126,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDeviceAuthenticator
|
||||
void Reset(ResetCallback callback) override;
|
||||
void Cancel() override;
|
||||
AuthenticatorType GetType() const override;
|
||||
cablev2::FidoTunnelDevice* GetTunnelDevice() override;
|
||||
std::string GetId() const override;
|
||||
std::string GetDisplayName() const override;
|
||||
ProtocolVersion SupportedProtocol() const override;
|
||||
|
@ -91,7 +91,7 @@ std::vector<std::unique_ptr<FidoDiscoveryBase>> FidoDiscoveryFactory::Create(
|
||||
cable_data_.value_or(std::vector<CableDiscoveryData>()),
|
||||
std::move(cable_pairing_callback_),
|
||||
std::move(cable_invalidated_pairing_callback_),
|
||||
std::move(cable_event_callback_)));
|
||||
std::move(cable_event_callback_), cable_must_support_ctap_));
|
||||
}
|
||||
|
||||
ret.emplace_back(std::move(v1_discovery));
|
||||
|
@ -185,6 +185,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDiscoveryFactory {
|
||||
cable_invalidated_pairing_callback_;
|
||||
std::optional<base::RepeatingCallback<void(cablev2::Event)>>
|
||||
cable_event_callback_;
|
||||
bool cable_must_support_ctap_ = true;
|
||||
#if BUILDFLAG(IS_CHROMEOS)
|
||||
base::RepeatingCallback<std::string()> generate_request_id_callback_;
|
||||
bool require_legacy_cros_authenticator_ = false;
|
||||
|
Reference in New Issue
Block a user