#include "device/fido/cable/v2_authenticator.h"
#include <algorithm>
#include <string_view>
#include <variant>
#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/sequence_checker.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "components/cbor/diagnostic_writer.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/v2_constants.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/cable/websocket_adapter.h"
#include "device/fido/cbor_extract.h"
#include "device/fido/features.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/network_context_factory.h"
#include "device/fido/public_key_credential_descriptor.h"
#include "device/fido/public_key_credential_params.h"
#include "device/fido/public_key_credential_rp_entity.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "net/base/isolation_info.h"
#include "net/cookies/site_for_cookies.h"
#include "net/storage_access_api/status.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/mojom/client_security_state.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h"
#include "third_party/boringssl/src/include/openssl/aes.h"
#include "third_party/boringssl/src/include/openssl/ec_key.h"
#include "third_party/boringssl/src/include/openssl/obj.h"
namespace device::cablev2::authenticator {
using device::CtapDeviceResponseCode;
using device::CtapRequestCommand;
using device::cbor_extract::IntKey;
using device::cbor_extract::Is;
using device::cbor_extract::Map;
using device::cbor_extract::StepOrByte;
using device::cbor_extract::Stop;
using device::cbor_extract::StringKey;
namespace {
const int kTimeoutSeconds = 60;
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("cablev2_websocket_from_authenticator",
R"(semantics {
sender: "Phone as a Security Key"
description:
"Chrome on a phone can communicate with other devices for the "
"purpose of using the phone as a security key. This WebSocket "
"connection is made to a Google service that aids in the exchange "
"of data with the other device. The service carries only "
"end-to-end encrypted data where the keys are shared directly "
"between the two devices via QR code and Bluetooth broadcast."
trigger:
"The user scans a QR code, displayed on the other device, and "
"confirms their desire to communicate with it."
data: "Only encrypted data that the service does not have the keys "
"for."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting: "Not controlled by a setting because the operation is "
"triggered by significant user action."
policy_exception_justification:
"No policy provided because the operation is triggered by "
" significant user action. No background activity occurs."
})");
struct MakeCredRequest {
RAW_PTR_EXCLUSION const std::vector<uint8_t>* client_data_hash;
RAW_PTR_EXCLUSION const std::string* rp_id;
RAW_PTR_EXCLUSION const std::string* rp_name;
RAW_PTR_EXCLUSION const std::vector<uint8_t>* user_id;
RAW_PTR_EXCLUSION const std::string* user_name;
RAW_PTR_EXCLUSION const std::string* user_display_name;
RAW_PTR_EXCLUSION const cbor::Value::ArrayValue* cred_params;
RAW_PTR_EXCLUSION const cbor::Value::ArrayValue* excluded_credentials;
RAW_PTR_EXCLUSION const bool* resident_key;
RAW_PTR_EXCLUSION const cbor::Value* prf;
};
static constexpr StepOrByte<MakeCredRequest> kMakeCredParseSteps[] = {
ELEMENT(Is::kRequired, MakeCredRequest, client_data_hash),
IntKey<MakeCredRequest>(1),
Map<MakeCredRequest>(),
IntKey<MakeCredRequest>(2),
ELEMENT(Is::kRequired, MakeCredRequest, rp_id),
StringKey<MakeCredRequest>(), 'i', 'd', '\0',
ELEMENT(Is::kRequired, MakeCredRequest, rp_name),
StringKey<MakeCredRequest>(), 'n', 'a', 'm', 'e', '\0',
Stop<MakeCredRequest>(),
Map<MakeCredRequest>(),
IntKey<MakeCredRequest>(3),
ELEMENT(Is::kRequired, MakeCredRequest, user_id),
StringKey<MakeCredRequest>(), 'i', 'd', '\0',
ELEMENT(Is::kRequired, MakeCredRequest, user_name),
StringKey<MakeCredRequest>(), 'n', 'a', 'm', 'e', '\0',
ELEMENT(Is::kRequired, MakeCredRequest, user_display_name),
StringKey<MakeCredRequest>(), 'd', 'i', 's', 'p', 'l', 'a', 'y',
'N', 'a', 'm', 'e', '\0',
Stop<MakeCredRequest>(),
ELEMENT(Is::kRequired, MakeCredRequest, cred_params),
IntKey<MakeCredRequest>(4),
ELEMENT(Is::kOptional, MakeCredRequest, excluded_credentials),
IntKey<MakeCredRequest>(5),
Map<MakeCredRequest>(Is::kOptional),
IntKey<MakeCredRequest>(6),
ELEMENT(Is::kOptional, MakeCredRequest, prf),
StringKey<MakeCredRequest>(), 'p', 'r', 'f', '\0',
Stop<MakeCredRequest>(),
Map<MakeCredRequest>(Is::kOptional),
IntKey<MakeCredRequest>(7),
ELEMENT(Is::kOptional, MakeCredRequest, resident_key),
StringKey<MakeCredRequest>(), 'r', 'k', '\0',
Stop<MakeCredRequest>(),
Stop<MakeCredRequest>(),
};
struct AttestationObject {
RAW_PTR_EXCLUSION const std::string* fmt;
RAW_PTR_EXCLUSION const std::vector<uint8_t>* auth_data;
RAW_PTR_EXCLUSION const cbor::Value* statement;
};
static constexpr StepOrByte<AttestationObject> kAttObjParseSteps[] = {
ELEMENT(Is::kRequired, AttestationObject, fmt),
StringKey<AttestationObject>(), 'f', 'm', 't', '\0',
ELEMENT(Is::kRequired, AttestationObject, auth_data),
StringKey<AttestationObject>(), 'a', 'u', 't', 'h', 'D', 'a', 't', 'a',
'\0',
ELEMENT(Is::kRequired, AttestationObject, statement),
StringKey<AttestationObject>(), 'a', 't', 't', 'S', 't', 'm', 't', '\0',
Stop<AttestationObject>(),
};
struct GetAssertionRequest {
RAW_PTR_EXCLUSION const std::string* rp_id;
RAW_PTR_EXCLUSION const std::vector<uint8_t>* client_data_hash;
RAW_PTR_EXCLUSION const cbor::Value::ArrayValue* allowed_credentials;
RAW_PTR_EXCLUSION const std::vector<uint8_t>* prf_eval_first;
RAW_PTR_EXCLUSION const std::vector<uint8_t>* prf_eval_second;
RAW_PTR_EXCLUSION const cbor::Value* prf_eval_by_cred;
};
static constexpr StepOrByte<GetAssertionRequest> kGetAssertionParseSteps[] = {
ELEMENT(Is::kRequired, GetAssertionRequest, rp_id),
IntKey<GetAssertionRequest>(1),
ELEMENT(Is::kRequired, GetAssertionRequest, client_data_hash),
IntKey<GetAssertionRequest>(2),
ELEMENT(Is::kOptional, GetAssertionRequest, allowed_credentials),
IntKey<GetAssertionRequest>(3),
Map<GetAssertionRequest>(Is::kOptional),
IntKey<GetAssertionRequest>(4),
Map<GetAssertionRequest>(Is::kOptional),
StringKey<GetAssertionRequest>(), 'p', 'r', 'f', '\0',
Map<GetAssertionRequest>(Is::kOptional),
StringKey<GetAssertionRequest>(), 'e', 'v', 'a', 'l', '\0',
ELEMENT(Is::kRequired, GetAssertionRequest, prf_eval_first),
StringKey<GetAssertionRequest>(), 'f', 'i', 'r', 's', 't', '\0',
ELEMENT(Is::kOptional, GetAssertionRequest, prf_eval_second),
StringKey<GetAssertionRequest>(), 's', 'e', 'c', 'o', 'n', 'd', '\0',
Stop<GetAssertionRequest>(),
ELEMENT(Is::kOptional, GetAssertionRequest, prf_eval_by_cred),
StringKey<GetAssertionRequest>(), 'e', 'v', 'a', 'l', 'B', 'y', 'C',
'r', 'e', 'd', 'e', 'n', 't', 'i',
'a', 'l', '\0',
Stop<GetAssertionRequest>(),
Stop<GetAssertionRequest>(),
Stop<GetAssertionRequest>(),
};
std::vector<uint8_t> BuildGetInfoResponse() {
std::array<uint8_t, device::kAaguidLength> aaguid{};
std::vector<cbor::Value> versions;
versions.emplace_back("FIDO_2_0");
versions.emplace_back("FIDO_2_1");
cbor::Value::MapValue options;
options.emplace("uv", true);
options.emplace("rk", true);
std::vector<cbor::Value> transports;
transports.emplace_back("cable");
transports.emplace_back("hybrid");
transports.emplace_back("internal");
cbor::Value::ArrayValue extensions;
extensions.emplace_back("prf");
cbor::Value::MapValue response_map;
response_map.emplace(1, std::move(versions));
response_map.emplace(2, std::move(extensions));
response_map.emplace(3, aaguid);
response_map.emplace(4, std::move(options));
response_map.emplace(9, std::move(transports));
return cbor::Writer::Write(cbor::Value(std::move(response_map))).value();
}
std::array<uint8_t, device::cablev2::kNonceSize> RandomNonce() {
std::array<uint8_t, device::cablev2::kNonceSize> ret;
crypto::RandBytes(ret);
return ret;
}
using GeneratePairingDataCallback =
base::OnceCallback<std::optional<cbor::Value>(
base::span<const uint8_t, device::kP256X962Length> peer_public_key_x962,
device::cablev2::HandshakeHash)>;
class TunnelTransport : public Transport {
public:
TunnelTransport(
Platform* platform,
NetworkContextFactory network_context_factory,
base::span<const uint8_t> secret,
base::span<const uint8_t, device::kP256X962Length> peer_identity,
GeneratePairingDataCallback generate_pairing_data,
base::flat_set<Feature> features)
: platform_(platform),
tunnel_id_(device::cablev2::Derive<kTunnelIdSize>(
secret,
base::span<uint8_t>(),
DerivedValueType::kTunnelID)),
eid_key_(device::cablev2::Derive<kEIDKeySize>(
secret,
base::span<const uint8_t>(),
device::cablev2::DerivedValueType::kEIDKey)),
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)),
features_(std::move(features)) {
DCHECK_EQ(state_, State::kNone);
state_ = State::kConnecting;
websocket_client_ = std::make_unique<device::cablev2::WebSocketAdapter>(
base::BindOnce(&TunnelTransport::OnTunnelReady, base::Unretained(this)),
base::BindRepeating(&TunnelTransport::OnTunnelData,
base::Unretained(this)));
target_ = device::cablev2::tunnelserver::GetNewTunnelURL(kTunnelServer,
tunnel_id_);
}
TunnelTransport(
Platform* platform,
NetworkContextFactory network_context_factory,
base::span<const uint8_t> secret,
base::span<const uint8_t, device::cablev2::kClientNonceSize> client_nonce,
std::array<uint8_t, device::cablev2::kRoutingIdSize> routing_id,
base::span<const uint8_t, 16> tunnel_id,
bssl::UniquePtr<EC_KEY> local_identity)
: platform_(platform),
tunnel_id_(fido_parsing_utils::Materialize(tunnel_id)),
eid_key_(device::cablev2::Derive<kEIDKeySize>(
secret,
client_nonce,
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);
state_ = State::kConnectingPaired;
websocket_client_ = std::make_unique<device::cablev2::WebSocketAdapter>(
base::BindOnce(&TunnelTransport::OnTunnelReady, base::Unretained(this)),
base::BindRepeating(&TunnelTransport::OnTunnelData,
base::Unretained(this)));
target_ = device::cablev2::tunnelserver::GetConnectURL(
kTunnelServer, routing_id, tunnel_id);
}
void StartReading(
base::RepeatingCallback<void(Update)> update_callback) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!update_callback_);
update_callback_ = std::move(update_callback);
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&TunnelTransport::StartWebSocket,
weak_factory_.GetWeakPtr()),
base::Milliseconds(250));
}
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>(payload_type == PayloadType::kCTAP
? MessageType::kCTAP
: MessageType::kJSON));
if (!crypter_->Encrypt(&data)) {
FIDO_LOG(ERROR) << "Failed to encrypt response";
return;
}
websocket_client_->Write(data);
}
private:
enum State {
kNone,
kConnecting,
kConnectingPaired,
kConnected,
kConnectedPaired,
kReady,
};
void StartWebSocket() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
network_context_factory_.Run()->CreateWebSocket(
target_, {device::kCableWebSocketProtocol}, net::SiteForCookies(),
net::StorageAccessApiStatus::kNone, net::IsolationInfo(),
{}, network::mojom::kBrowserProcessId,
url::Origin::Create(target_),
network::mojom::ClientSecurityState::New(),
network::mojom::kWebSocketOptionBlockAllCookies,
net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation),
websocket_client_->BindNewHandshakeClientPipe(),
mojo::NullRemote(),
mojo::NullRemote(),
mojo::NullRemote(),
std::nullopt);
FIDO_LOG(DEBUG) << "Creating WebSocket to " << target_.spec();
}
void OnTunnelReady(
WebSocketAdapter::Result result,
std::optional<std::array<uint8_t, device::cablev2::kRoutingIdSize>>
routing_id,
WebSocketAdapter::ConnectSignalSupport) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(state_ == State::kConnecting || state_ == State::kConnectingPaired);
bool ok = (result == WebSocketAdapter::Result::OK);
if (ok && state_ == State::kConnecting && !routing_id) {
FIDO_LOG(ERROR) << "Tunnel server did not specify routing ID";
ok = false;
}
if (!ok) {
FIDO_LOG(ERROR) << "Failed to connect to tunnel server";
update_callback_.Run(Platform::Error::TUNNEL_SERVER_CONNECT_FAILED);
return;
}
FIDO_LOG(DEBUG) << "WebSocket connection established.";
CableEidArray plaintext_eid;
if (state_ == State::kConnecting) {
device::cablev2::eid::Components components;
components.tunnel_server_domain = kTunnelServer;
components.routing_id = *routing_id;
components.nonce = RandomNonce();
plaintext_eid = device::cablev2::eid::FromComponents(components);
state_ = State::kConnected;
} else {
DCHECK_EQ(state_, State::kConnectingPaired);
crypto::RandBytes(plaintext_eid);
plaintext_eid[0] = 0;
state_ = State::kConnectedPaired;
}
ble_advert_ =
platform_->SendBLEAdvert(eid::Encrypt(plaintext_eid, eid_key_));
psk_ = device::cablev2::Derive<kPSKSize>(
secret_, plaintext_eid, device::cablev2::DerivedValueType::kPSK);
update_callback_.Run(Platform::Status::TUNNEL_SERVER_CONNECT);
}
void OnTunnelData(std::optional<base::span<const uint8_t>> msg) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!msg) {
FIDO_LOG(DEBUG) << "WebSocket tunnel closed";
update_callback_.Run(Disconnected::kDisconnected);
return;
}
switch (state_) {
case State::kConnectedPaired:
case State::kConnected: {
std::vector<uint8_t> response;
HandshakeResult result = RespondToHandshake(
psk_, std::move(local_identity_), peer_identity_, *msg, &response);
if (!result) {
FIDO_LOG(ERROR) << "caBLE handshake failure";
update_callback_.Run(Platform::Error::HANDSHAKE_FAILED);
return;
}
FIDO_LOG(DEBUG) << "caBLE handshake complete";
update_callback_.Run(Platform::Status::HANDSHAKE_COMPLETE);
websocket_client_->Write(response);
crypter_ = std::move(result->first);
cbor::Value::MapValue post_handshake_msg;
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 =
cbor::Writer::Write(cbor::Value(std::move(post_handshake_msg)));
if (!post_handshake_msg_bytes) {
FIDO_LOG(ERROR) << "failed to encode post-handshake message";
return;
}
if (!crypter_->Encrypt(&post_handshake_msg_bytes.value())) {
FIDO_LOG(ERROR) << "failed to encrypt post-handshake message";
return;
}
websocket_client_->Write(*post_handshake_msg_bytes);
if (state_ == State::kConnected) {
std::optional<cbor::Value> pairing_data(
std::move(generate_pairing_data_)
.Run(*peer_identity_, result->second));
constexpr size_t kPaddingTarget = 512;
static_assert(kPaddingTarget % 32 == 0);
size_t padding_length =
kPaddingTarget - 16 -
1 -
3;
if (pairing_data) {
cbor::Value::MapValue update_msg_for_measurement;
update_msg_for_measurement.emplace(1, pairing_data->Clone());
std::optional<std::vector<uint8_t>> cbor_bytes =
cbor::Writer::Write(
cbor::Value(std::move(update_msg_for_measurement)));
if (cbor_bytes && cbor_bytes->size() < padding_length) {
padding_length -= cbor_bytes->size();
} else {
DCHECK(false) << cbor_bytes.has_value();
}
}
cbor::Value::MapValue update_msg;
update_msg.emplace(0, std::vector<uint8_t>(padding_length));
if (pairing_data) {
update_msg.emplace(1, std::move(*pairing_data));
}
std::optional<std::vector<uint8_t>> update_msg_bytes =
cbor::Writer::Write(cbor::Value(std::move(update_msg)));
if (!update_msg_bytes) {
FIDO_LOG(ERROR) << "failed to encode update message";
return;
}
update_msg_bytes->insert(update_msg_bytes->begin(),
static_cast<uint8_t>(MessageType::kUpdate));
if (!crypter_->Encrypt(&update_msg_bytes.value())) {
FIDO_LOG(ERROR) << "failed to encrypt update message";
return;
}
DCHECK_EQ(update_msg_bytes->size(),
kPaddingTarget + 16);
websocket_client_->Write(*update_msg_bytes);
}
state_ = State::kReady;
break;
}
case State::kReady: {
std::vector<uint8_t> plaintext;
if (!crypter_->Decrypt(*msg, &plaintext)) {
FIDO_LOG(ERROR) << "failed to decrypt caBLE message";
update_callback_.Run(Platform::Error::DECRYPT_FAILURE);
return;
}
if (plaintext.empty()) {
FIDO_LOG(ERROR) << "invalid empty message";
update_callback_.Run(Platform::Error::DECRYPT_FAILURE);
return;
}
const uint8_t message_type_byte = plaintext[0];
plaintext.erase(plaintext.begin());
if (message_type_byte > static_cast<uint8_t>(MessageType::kMaxValue)) {
FIDO_LOG(ERROR) << "unknown message type "
<< static_cast<int>(message_type_byte);
update_callback_.Run(Disconnected::kDisconnected);
return;
}
const MessageType message_type =
static_cast<MessageType>(message_type_byte);
switch (message_type) {
case MessageType::kShutdown: {
update_callback_.Run(Disconnected::kDisconnected);
return;
}
case MessageType::kCTAP:
case MessageType::kJSON:
break;
case MessageType::kUpdate:
if (!cbor::Reader::Read(plaintext)) {
FIDO_LOG(ERROR) << "invalid CBOR payload in update message";
update_callback_.Run(Disconnected::kDisconnected);
}
return;
}
if (first_message_) {
update_callback_.Run(Platform::Status::REQUEST_RECEIVED);
first_message_ = false;
}
update_callback_.Run(std::make_pair(message_type == MessageType::kJSON
? PayloadType::kJSON
: PayloadType::kCTAP,
std::move(plaintext)));
break;
}
default:
NOTREACHED();
}
}
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;
}
}
std::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_;
const std::array<uint8_t, kEIDKeySize> eid_key_;
std::unique_ptr<WebSocketAdapter> websocket_client_;
std::unique_ptr<Crypter> crypter_;
NetworkContextFactory network_context_factory_;
const std::optional<std::array<uint8_t, kP256X962Length>> peer_identity_;
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_;
base::RepeatingCallback<void(Update)> update_callback_;
bool first_message_ = true;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<TunnelTransport> weak_factory_{this};
};
class CTAP2Processor : public Transaction {
public:
CTAP2Processor(std::unique_ptr<Transport> transport,
std::unique_ptr<Platform> platform)
: transport_(std::move(transport)), platform_(std::move(platform)) {
transport_->StartReading(base::BindRepeating(
&CTAP2Processor::OnTransportUpdate, base::Unretained(this)));
}
private:
void OnTransportUpdate(Transport::Update update) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (have_completed_) {
return;
}
if (auto* error = std::get_if<Platform::Error>(&update)) {
have_completed_ = true;
platform_->OnCompleted(*error);
return;
} else if (auto* status = std::get_if<Platform::Status>(&update)) {
platform_->OnStatus(*status);
return;
} else if (std::get_if<Transport::Disconnected>(&update)) {
std::optional<Platform::Error> maybe_error;
if (!transaction_received_) {
maybe_error = Platform::Error::UNEXPECTED_EOF;
} else if (!transaction_done_) {
maybe_error = Platform::Error::EOF_WHILE_PROCESSING;
}
have_completed_ = true;
platform_->OnCompleted(maybe_error);
return;
}
auto& msg = std::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 std::variant<std::vector<uint8_t>, Platform::Error> result =
ProcessCTAPMessage(msg.second);
if (const auto* error = std::get_if<Platform::Error>(&result)) {
have_completed_ = true;
platform_->OnCompleted(*error);
return;
}
const std::vector<uint8_t>& response =
std::get<std::vector<uint8_t>>(result);
if (response.empty()) {
return;
}
transport_->Write(PayloadType::kCTAP, std::move(response));
}
std::variant<std::vector<uint8_t>, Platform::Error> ProcessCTAPMessage(
base::span<const uint8_t> message_bytes) {
if (message_bytes.empty()) {
return Platform::Error::INVALID_CTAP;
}
const auto [command, cbor_bytes] = message_bytes.split_at<1>();
std::optional<cbor::Value> payload;
if (!cbor_bytes.empty()) {
payload = cbor::Reader::Read(cbor_bytes);
if (!payload) {
FIDO_LOG(ERROR) << "CBOR decoding failed for "
<< base::HexEncode(cbor_bytes);
return Platform::Error::INVALID_CTAP;
}
FIDO_LOG(DEBUG) << "<- (" << base::HexEncode(command) << ") "
<< cbor::DiagnosticWriter::Write(*payload);
} else {
FIDO_LOG(DEBUG) << "<- (" << base::HexEncode(command) << ") <no payload>";
}
switch (command[0]) {
case static_cast<uint8_t>(
device::CtapRequestCommand::kAuthenticatorGetInfo): {
if (payload) {
FIDO_LOG(ERROR) << "getInfo command incorrectly contained payload";
return Platform::Error::INVALID_CTAP;
}
std::optional<std::vector<uint8_t>> response = BuildGetInfoResponse();
if (!response) {
return Platform::Error::INTERNAL_ERROR;
}
response->insert(
response->begin(),
static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess));
return *response;
}
case static_cast<uint8_t>(
device::CtapRequestCommand::kAuthenticatorMakeCredential): {
if (!payload || !payload->is_map()) {
FIDO_LOG(ERROR) << "Invalid makeCredential payload";
return Platform::Error::INVALID_CTAP;
}
MakeCredRequest make_cred_request;
if (!device::cbor_extract::Extract<MakeCredRequest>(
&make_cred_request, kMakeCredParseSteps, payload->GetMap())) {
FIDO_LOG(ERROR) << "Failed to parse makeCredential request: "
<< base::HexEncode(cbor_bytes);
return Platform::Error::INVALID_CTAP;
}
auto params = blink::mojom::PublicKeyCredentialCreationOptions::New();
params->challenge = *make_cred_request.client_data_hash;
params->timeout = base::Seconds(kTimeoutSeconds);
params->relying_party.id = *make_cred_request.rp_id;
params->relying_party.name = *make_cred_request.rp_name;
params->user.id = *make_cred_request.user_id;
params->user.name = *make_cred_request.user_name;
params->user.display_name = *make_cred_request.user_display_name;
const bool rk =
make_cred_request.resident_key && *make_cred_request.resident_key;
params->authenticator_selection.emplace(
device::AuthenticatorAttachment::kPlatform,
rk ? device::ResidentKeyRequirement::kRequired
: device::ResidentKeyRequirement::kDiscouraged,
device::UserVerificationRequirement::kRequired);
if (make_cred_request.prf) {
params->prf_enable = true;
}
if (!CopyCredIds(make_cred_request.excluded_credentials,
¶ms->exclude_credentials)) {
return Platform::Error::INTERNAL_ERROR;
}
if (!device::cbor_extract::ForEachPublicKeyEntry(
*make_cred_request.cred_params, cbor::Value("alg"),
base::BindRepeating(
[](std::vector<
device::PublicKeyCredentialParams::CredentialInfo>
* out,
const cbor::Value& value) -> bool {
if (!value.is_integer()) {
return false;
}
const int64_t alg = value.GetInteger();
if (alg > std::numeric_limits<int32_t>::max() ||
alg < std::numeric_limits<int32_t>::min()) {
return true;
}
device::PublicKeyCredentialParams::CredentialInfo info;
info.algorithm = static_cast<int32_t>(alg);
out->push_back(info);
return true;
},
base::Unretained(¶ms->public_key_parameters)))) {
return Platform::Error::INVALID_CTAP;
}
transaction_received_ = true;
platform_->MakeCredential(
std::move(params),
base::BindOnce(&CTAP2Processor::OnMakeCredentialResponse,
weak_factory_.GetWeakPtr(), rk));
return std::vector<uint8_t>();
}
case static_cast<uint8_t>(
device::CtapRequestCommand::kAuthenticatorGetAssertion): {
if (!payload || !payload->is_map()) {
FIDO_LOG(ERROR) << "Invalid makeCredential payload";
return Platform::Error::INVALID_CTAP;
}
GetAssertionRequest get_assertion_request;
if (!device::cbor_extract::Extract<GetAssertionRequest>(
&get_assertion_request, kGetAssertionParseSteps,
payload->GetMap())) {
FIDO_LOG(ERROR) << "Failed to parse getAssertion request";
return Platform::Error::INVALID_CTAP;
}
auto params = blink::mojom::PublicKeyCredentialRequestOptions::New();
params->extensions =
blink::mojom::AuthenticationExtensionsClientInputs::New();
params->challenge = *get_assertion_request.client_data_hash;
params->relying_party_id = *get_assertion_request.rp_id;
params->user_verification =
device::UserVerificationRequirement::kRequired;
params->timeout = base::Seconds(kTimeoutSeconds);
if (!CopyCredIds(get_assertion_request.allowed_credentials,
¶ms->allow_credentials)) {
return Platform::Error::INTERNAL_ERROR;
}
if (get_assertion_request.prf_eval_first) {
params->extensions->prf = true;
auto values = blink::mojom::PRFValues::New();
values->first = *get_assertion_request.prf_eval_first;
if (get_assertion_request.prf_eval_second) {
values->second = *get_assertion_request.prf_eval_second;
}
params->extensions->prf_inputs.emplace_back(std::move(values));
}
if (get_assertion_request.prf_eval_by_cred) {
params->extensions->prf = true;
if (!get_assertion_request.prf_eval_by_cred->is_map()) {
return Platform::Error::INVALID_CTAP;
}
const cbor::Value::MapValue& by_cred =
get_assertion_request.prf_eval_by_cred->GetMap();
for (const auto& element : by_cred) {
if (!element.first.is_bytestring() || !element.second.is_map()) {
return Platform::Error::INVALID_CTAP;
}
auto values = blink::mojom::PRFValues::New();
values->id = element.first.GetBytestring();
const cbor::Value::MapValue& eval_points = element.second.GetMap();
const auto first_it = eval_points.find(cbor::Value("first"));
if (first_it == eval_points.end() ||
!first_it->second.is_bytestring()) {
return Platform::Error::INVALID_CTAP;
}
values->first = first_it->second.GetBytestring();
const auto second_it = eval_points.find(cbor::Value("second"));
if (second_it != eval_points.end()) {
if (!second_it->second.is_bytestring()) {
return Platform::Error::INVALID_CTAP;
}
values->second = second_it->second.GetBytestring();
}
params->extensions->prf_inputs.emplace_back(std::move(values));
}
}
transaction_received_ = true;
const bool empty_allowlist = params->allow_credentials.empty();
platform_->GetAssertion(
std::move(params),
base::BindOnce(&CTAP2Processor::OnGetAssertionResponse,
weak_factory_.GetWeakPtr(), empty_allowlist));
return std::vector<uint8_t>();
}
case static_cast<uint8_t>(
device::CtapRequestCommand::kAuthenticatorSelection): {
if (payload) {
FIDO_LOG(ERROR) << "Invalid authenticatorSelection payload";
return Platform::Error::INVALID_CTAP;
}
return Platform::Error::AUTHENTICATOR_SELECTION_RECEIVED;
}
default:
FIDO_LOG(ERROR) << "Received unknown command "
<< static_cast<unsigned>(command[0]);
return Platform::Error::INVALID_CTAP;
}
}
void OnMakeCredentialResponse(
bool was_discoverable_credential_request,
uint32_t ctap_status,
base::span<const uint8_t> attestation_object_bytes,
bool prf_enabled) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_LE(ctap_status, 0xFFu);
std::vector<uint8_t> response = {base::checked_cast<uint8_t>(ctap_status)};
if (ctap_status == static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess)) {
std::optional<cbor::Value> cbor_attestation_object =
cbor::Reader::Read(attestation_object_bytes);
if (!cbor_attestation_object || !cbor_attestation_object->is_map()) {
FIDO_LOG(ERROR) << "invalid CBOR attestation object";
return;
}
AttestationObject attestation_object;
if (!device::cbor_extract::Extract<AttestationObject>(
&attestation_object, kAttObjParseSteps,
cbor_attestation_object->GetMap())) {
FIDO_LOG(ERROR) << "attestation object parse failed";
return;
}
cbor::Value::MapValue response_map;
response_map.emplace(1, std::string_view(*attestation_object.fmt));
response_map.emplace(
2, base::span<const uint8_t>(*attestation_object.auth_data));
response_map.emplace(3, attestation_object.statement->Clone());
cbor::Value::MapValue unsigned_extension_outputs;
if (prf_enabled) {
cbor::Value::MapValue prf;
prf.emplace(kExtensionPRFEnabled, true);
unsigned_extension_outputs.emplace(kExtensionPRF, std::move(prf));
}
if (!unsigned_extension_outputs.empty()) {
response_map.emplace(6, std::move(unsigned_extension_outputs));
}
std::optional<std::vector<uint8_t>> response_payload =
cbor::Writer::Write(cbor::Value(std::move(response_map)));
if (!response_payload) {
return;
}
response.insert(response.end(), response_payload->begin(),
response_payload->end());
} else if (was_discoverable_credential_request &&
ctap_status ==
static_cast<uint8_t>(
CtapDeviceResponseCode::kCtap2ErrUnsupportedOption)) {
have_completed_ = true;
platform_->OnCompleted(Platform::Error::DISCOVERABLE_CREDENTIALS_REQUEST);
return;
} else {
platform_->OnStatus(Platform::Status::CTAP_ERROR);
}
if (!transaction_done_) {
platform_->OnStatus(Platform::Status::FIRST_TRANSACTION_DONE);
transaction_done_ = true;
}
transport_->Write(PayloadType::kCTAP, std::move(response));
}
void OnGetAssertionResponse(
bool was_empty_allowlist_request,
uint32_t ctap_status,
blink::mojom::GetAssertionAuthenticatorResponsePtr auth_response) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_LE(ctap_status, 0xFFu);
if (auth_response && was_empty_allowlist_request &&
!auth_response->user_handle) {
FIDO_LOG(ERROR)
<< "missing user id in response to discoverable credential assertion";
ctap_status =
static_cast<uint32_t>(CtapDeviceResponseCode::kCtap2ErrOther);
}
std::vector<uint8_t> response = {base::checked_cast<uint8_t>(ctap_status)};
if (ctap_status == static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess)) {
cbor::Value::MapValue credential_descriptor;
credential_descriptor.emplace("type", device::kPublicKey);
credential_descriptor.emplace("id",
std::move(auth_response->info->raw_id));
cbor::Value::ArrayValue transports;
transports.emplace_back("internal");
transports.emplace_back("cable");
credential_descriptor.emplace("transports", std::move(transports));
cbor::Value::MapValue response_map;
response_map.emplace(1, std::move(credential_descriptor));
response_map.emplace(2,
std::move(auth_response->info->authenticator_data));
response_map.emplace(3, std::move(auth_response->signature));
if (was_empty_allowlist_request) {
cbor::Value::MapValue user_map;
user_map.emplace("id", std::move(*auth_response->user_handle));
user_map.emplace("name", "");
user_map.emplace("displayName", "");
response_map.emplace(4, std::move(user_map));
response_map.emplace(6, true);
}
cbor::Value::MapValue unsigned_extension_outputs;
if (auth_response->extensions->prf_results) {
cbor::Value::MapValue prf, results;
results.emplace(kExtensionPRFFirst,
auth_response->extensions->prf_results->first);
if (auth_response->extensions->prf_results->second) {
results.emplace(kExtensionPRFSecond,
*auth_response->extensions->prf_results->second);
}
prf.emplace(kExtensionPRFResults, std::move(results));
unsigned_extension_outputs.emplace(kExtensionPRF, std::move(prf));
}
if (!unsigned_extension_outputs.empty()) {
response_map.emplace(8, std::move(unsigned_extension_outputs));
}
std::optional<std::vector<uint8_t>> response_payload =
cbor::Writer::Write(cbor::Value(std::move(response_map)));
if (!response_payload) {
return;
}
response.insert(response.end(), response_payload->begin(),
response_payload->end());
} else if (was_empty_allowlist_request &&
ctap_status ==
static_cast<uint8_t>(
CtapDeviceResponseCode::kCtap2ErrNoCredentials)) {
have_completed_ = true;
platform_->OnCompleted(Platform::Error::DISCOVERABLE_CREDENTIALS_REQUEST);
return;
} else {
platform_->OnStatus(Platform::Status::CTAP_ERROR);
}
if (!transaction_done_) {
platform_->OnStatus(Platform::Status::FIRST_TRANSACTION_DONE);
transaction_done_ = true;
}
transport_->Write(PayloadType::kCTAP, std::move(response));
}
static bool CopyCredIds(const cbor::Value::ArrayValue* in,
std::vector<PublicKeyCredentialDescriptor>* out) {
if (!in) {
return true;
}
return device::cbor_extract::ForEachPublicKeyEntry(
*in, cbor::Value("id"),
base::BindRepeating(
[](std::vector<PublicKeyCredentialDescriptor>* out,
const cbor::Value& value) -> bool {
if (!value.is_bytestring()) {
return false;
}
out->emplace_back(device::CredentialType::kPublicKey,
value.GetBytestring(),
base::flat_set<device::FidoTransportProtocol>{
device::FidoTransportProtocol::kInternal});
return true;
},
base::Unretained(out)));
}
bool have_completed_ = false;
bool transaction_received_ = false;
bool transaction_done_ = false;
const std::unique_ptr<Transport> transport_;
const std::unique_ptr<Platform> platform_;
SEQUENCE_CHECKER(sequence_checker_);
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 = std::get_if<Platform::Error>(&update)) {
have_completed_ = true;
platform_->OnCompleted(*error);
return;
} else if (auto* status = std::get_if<Platform::Status>(&update)) {
platform_->OnStatus(*status);
return;
} else if (std::get_if<Transport::Disconnected>(&update)) {
have_completed_ = true;
platform_->OnCompleted(std::nullopt);
return;
}
auto& msg = std::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,
base::span<const uint8_t, kPairingIDSize> pairing_id) {
base::span<const uint8_t, kRootSecretSize> secret = root_secret;
std::array<uint8_t, kRootSecretSize> per_contact_id_secret;
if (contact_id) {
per_contact_id_secret =
device::cablev2::Derive<per_contact_id_secret.size()>(
root_secret, *contact_id,
device::cablev2::DerivedValueType::kPerContactIDSecret);
secret = per_contact_id_secret;
}
std::array<uint8_t, 32> paired_secret;
paired_secret = device::cablev2::Derive<paired_secret.size()>(
secret, pairing_id, device::cablev2::DerivedValueType::kPairedSecret);
return paired_secret;
}
class PairingDataGenerator {
public:
static GeneratePairingDataCallback GetClosure(
base::span<const uint8_t, kRootSecretSize> root_secret,
const std::string& name,
std::optional<std::vector<uint8_t>> contact_id) {
auto* generator =
new PairingDataGenerator(root_secret, name, std::move(contact_id));
return base::BindOnce(&PairingDataGenerator::Generate,
base::Owned(generator));
}
private:
PairingDataGenerator(base::span<const uint8_t, kRootSecretSize> root_secret,
const std::string& name,
std::optional<std::vector<uint8_t>> contact_id)
: root_secret_(fido_parsing_utils::Materialize(root_secret)),
name_(name),
contact_id_(std::move(contact_id)) {}
std::optional<cbor::Value> Generate(
base::span<const uint8_t, device::kP256X962Length> peer_public_key_x962,
device::cablev2::HandshakeHash handshake_hash) {
if (!contact_id_) {
return std::nullopt;
}
std::array<uint8_t, kPairingIDSize> pairing_id;
crypto::RandBytes(pairing_id);
const std::array<uint8_t, 32> paired_secret =
DerivePairedSecret(root_secret_, *contact_id_, pairing_id);
cbor::Value::MapValue map;
map.emplace(1, std::move(*contact_id_));
map.emplace(2, pairing_id);
map.emplace(3, paired_secret);
bssl::UniquePtr<EC_KEY> identity_key(IdentityKey(root_secret_));
device::CableAuthenticatorIdentityKey public_key;
CHECK_EQ(
public_key.size(),
EC_POINT_point2oct(EC_KEY_get0_group(identity_key.get()),
EC_KEY_get0_public_key(identity_key.get()),
POINT_CONVERSION_UNCOMPRESSED, public_key.data(),
public_key.size(), nullptr));
map.emplace(4, public_key);
map.emplace(5, name_);
map.emplace(6,
device::cablev2::CalculatePairingSignature(
identity_key.get(), peer_public_key_x962, handshake_hash));
return cbor::Value(std::move(map));
}
const std::array<uint8_t, kRootSecretSize> root_secret_;
const std::string name_;
std::optional<std::vector<uint8_t>> contact_id_;
};
}
Platform::BLEAdvert::~BLEAdvert() = default;
Platform::~Platform() = default;
Transport::~Transport() = default;
Transaction::~Transaction() = default;
std::unique_ptr<Transaction> TransactWithPlaintextTransport(
std::unique_ptr<Platform> platform,
std::unique_ptr<Transport> transport) {
return std::make_unique<CTAP2Processor>(std::move(transport),
std::move(platform));
}
std::unique_ptr<Transaction> TransactFromQRCode(
std::unique_ptr<Platform> platform,
NetworkContextFactory network_context_factory,
base::span<const uint8_t, kRootSecretSize> root_secret,
const std::string& authenticator_name,
base::span<const uint8_t, 16> qr_secret,
base::span<const uint8_t, kP256X962Length> peer_identity,
std::optional<std::vector<uint8_t>> contact_id) {
auto generate_pairing_data = PairingDataGenerator::GetClosure(
root_secret, authenticator_name, std::move(contact_id));
Platform* const platform_ptr = platform.get();
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),
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,
base::span<const uint8_t, kRootSecretSize> root_secret,
const std::string& authenticator_name,
base::span<const uint8_t, 16> qr_secret,
base::span<const uint8_t, kP256X962Length> peer_identity,
std::optional<std::vector<uint8_t>> contact_id) {
NetworkContextFactory factory = base::BindRepeating(
[](network::mojom::NetworkContext* network_context) {
return network_context;
},
network_context);
return TransactFromQRCode(std::move(platform), std::move(factory),
root_secret, authenticator_name, qr_secret,
peer_identity, std::move(contact_id));
}
std::unique_ptr<Transaction> TransactFromFCM(
std::unique_ptr<Platform> platform,
NetworkContextFactory network_context_factory,
base::span<const uint8_t, kRootSecretSize> root_secret,
std::array<uint8_t, kRoutingIdSize> routing_id,
base::span<const uint8_t, kTunnelIdSize> tunnel_id,
base::span<const uint8_t, kPairingIDSize> pairing_id,
base::span<const uint8_t, kClientNonceSize> client_nonce,
std::optional<base::span<const uint8_t>> contact_id) {
const std::array<uint8_t, 32> paired_secret =
DerivePairedSecret(root_secret, contact_id, pairing_id);
Platform* const platform_ptr = platform.get();
return std::make_unique<CTAP2Processor>(
std::make_unique<TunnelTransport>(
platform_ptr, std::move(network_context_factory), paired_secret,
client_nonce, routing_id, tunnel_id, IdentityKey(root_secret)),
std::move(platform));
}
std::unique_ptr<Transaction> TransactFromFCMDeprecated(
std::unique_ptr<Platform> platform,
network::mojom::NetworkContext* network_context,
base::span<const uint8_t, kRootSecretSize> root_secret,
std::array<uint8_t, kRoutingIdSize> routing_id,
base::span<const uint8_t, kTunnelIdSize> tunnel_id,
base::span<const uint8_t, kPairingIDSize> pairing_id,
base::span<const uint8_t, kClientNonceSize> client_nonce,
std::optional<base::span<const uint8_t>> contact_id) {
NetworkContextFactory factory = base::BindRepeating(
[](network::mojom::NetworkContext* network_context) {
return network_context;
},
network_context);
return TransactFromFCM(std::move(platform), std::move(factory), root_secret,
std::move(routing_id), tunnel_id, pairing_id,
client_nonce, std::move(contact_id));
}
}