123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161 |
- // Copyright 2020 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
- #include "device/fido/cable/v2_authenticator.h"
- #include "base/feature_list.h"
- #include "base/memory/raw_ptr.h"
- #include "base/memory/weak_ptr.h"
- #include "base/sequence_checker.h"
- #include "base/strings/string_number_conversions.h"
- #include "base/threading/sequenced_task_runner_handle.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_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/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/traffic_annotation/network_traffic_annotation.h"
- #include "services/network/public/mojom/network_context.mojom.h"
- #include "third_party/abseil-cpp/absl/types/variant.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 {
- namespace cablev2 {
- namespace 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 {
- // kTimeoutSeconds is the timeout that is put into the parameters that are
- // passed up to the platform.
- 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 {
- // All fields below are not a raw_ptr<T> because cbor_extract.cc would
- // cast the raw_ptr<T> to a void*, skipping an AddRef() call and causing a
- // ref-counting mismatch.
- 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;
- };
- static constexpr StepOrByte<MakeCredRequest> kMakeCredParseSteps[] = {
- // clang-format off
- 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>(7),
- ELEMENT(Is::kOptional, MakeCredRequest, resident_key),
- StringKey<MakeCredRequest>(), 'r', 'k', '\0',
- Stop<MakeCredRequest>(),
- Stop<MakeCredRequest>(),
- // clang-format on
- };
- struct AttestationObject {
- // All fields below are not a raw_ptr<T> because cbor_extract.cc would
- // cast the raw_ptr<T> to a void*, skipping an AddRef() call and causing a
- // ref-counting mismatch.
- 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[] = {
- // clang-format off
- 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>(),
- // clang-format on
- };
- struct GetAssertionRequest {
- // All fields below are not a raw_ptr<T> because cbor_extract.cc would
- // cast the raw_ptr<T> to a void*, skipping an AddRef() call and causing a
- // ref-counting mismatch.
- 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;
- };
- static constexpr StepOrByte<GetAssertionRequest> kGetAssertionParseSteps[] = {
- // clang-format off
- 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),
- Stop<GetAssertionRequest>(),
- // clang-format on
- };
- // BuildGetInfoResponse returns a CBOR-encoded getInfo response.
- 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;
- // This code is only invoked if a screen-lock (i.e. user verification) is
- // configured on the device. Therefore the 'uv' option is unconditionally
- // true.
- 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::MapValue response_map;
- response_map.emplace(1, std::move(versions));
- 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<absl::optional<cbor::Value>(
- base::span<const uint8_t, device::kP256X962Length> peer_public_key_x962,
- device::cablev2::HandshakeHash)>;
- // TunnelTransport is a transport that uses WebSockets to talk to a cloud
- // service and uses BLE adverts to show proximity.
- class TunnelTransport : public Transport {
- public:
- TunnelTransport(
- unsigned protocol_revision,
- Platform* platform,
- network::mojom::NetworkContext* network_context,
- base::span<const uint8_t> secret,
- base::span<const uint8_t, device::kP256X962Length> peer_identity,
- bool use_new_crypter_construction,
- GeneratePairingDataCallback generate_pairing_data)
- : protocol_revision_(protocol_revision),
- platform_(platform),
- tunnel_id_(device::cablev2::Derive<EXTENT(tunnel_id_)>(
- secret,
- base::span<uint8_t>(),
- DerivedValueType::kTunnelID)),
- eid_key_(device::cablev2::Derive<EXTENT(eid_key_)>(
- secret,
- base::span<const uint8_t>(),
- device::cablev2::DerivedValueType::kEIDKey)),
- network_context_(network_context),
- peer_identity_(device::fido_parsing_utils::Materialize(peer_identity)),
- generate_pairing_data_(std::move(generate_pairing_data)),
- secret_(fido_parsing_utils::Materialize(secret)),
- use_new_crypter_construction_(use_new_crypter_construction) {
- 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(
- unsigned protocol_revision,
- Platform* platform,
- network::mojom::NetworkContext* network_context,
- 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)
- : protocol_revision_(protocol_revision),
- platform_(platform),
- tunnel_id_(fido_parsing_utils::Materialize(tunnel_id)),
- eid_key_(device::cablev2::Derive<EXTENT(eid_key_)>(
- secret,
- client_nonce,
- device::cablev2::DerivedValueType::kEIDKey)),
- network_context_(network_context),
- secret_(fido_parsing_utils::Materialize(secret)),
- local_identity_(std::move(local_identity)),
- use_new_crypter_construction_(protocol_revision_ >= 1) {
- 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);
- }
- // Transport:
- 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);
- // Delay the WebSocket creation by 250ms. This to measure whether DNS
- // errors are reduced in UMA stats. If so, then the network errors that we
- // see are probably due to a start-up race.
- base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
- FROM_HERE,
- base::BindOnce(&TunnelTransport::StartWebSocket,
- weak_factory_.GetWeakPtr()),
- base::Milliseconds(250));
- }
- void Write(std::vector<uint8_t> data) override {
- DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
- DCHECK_EQ(state_, kReady);
- if (protocol_revision_ >= 1) {
- data.insert(data.begin(), static_cast<uint8_t>(MessageType::kCTAP));
- }
- 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_->CreateWebSocket(
- target_, {device::kCableWebSocketProtocol}, net::SiteForCookies(),
- net::IsolationInfo(), /*additional_headers=*/{},
- network::mojom::kBrowserProcessId, url::Origin::Create(target_),
- network::mojom::kWebSocketOptionBlockAllCookies,
- net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation),
- websocket_client_->BindNewHandshakeClientPipe(),
- /*url_loader_network_observer=*/mojo::NullRemote(),
- /*auth_handler=*/mojo::NullRemote(),
- /*header_client=*/mojo::NullRemote(),
- /*throttling_profile_id=*/absl::nullopt);
- FIDO_LOG(DEBUG) << "Creating WebSocket to " << target_.spec();
- }
- void OnTunnelReady(
- WebSocketAdapter::Result result,
- absl::optional<std::array<uint8_t, device::cablev2::kRoutingIdSize>>
- routing_id) {
- 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);
- // The first byte is reserved to ensure that the format can be changed in
- // the future.
- plaintext_eid[0] = 0;
- state_ = State::kConnectedPaired;
- }
- ble_advert_ =
- platform_->SendBLEAdvert(eid::Encrypt(plaintext_eid, eid_key_));
- psk_ = device::cablev2::Derive<EXTENT(psk_)>(
- secret_, plaintext_eid, device::cablev2::DerivedValueType::kPSK);
- update_callback_.Run(Platform::Status::TUNNEL_SERVER_CONNECT);
- }
- void OnTunnelData(absl::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);
- if (use_new_crypter_construction_) {
- crypter_->UseNewConstruction();
- }
- cbor::Value::MapValue post_handshake_msg;
- post_handshake_msg.emplace(1, BuildGetInfoResponse());
- if (state_ == State::kConnected && protocol_revision_ < 1) {
- // For revision zero, linking information (if any) is included in the
- // post-handshake message.
- absl::optional<cbor::Value> pairing_data(
- std::move(generate_pairing_data_)
- .Run(*peer_identity_, result->second));
- if (pairing_data) {
- post_handshake_msg.emplace(2, std::move(*pairing_data));
- }
- }
- absl::optional<std::vector<uint8_t>> post_handshake_msg_bytes;
- if (protocol_revision_ >= 1) {
- post_handshake_msg_bytes =
- cbor::Writer::Write(cbor::Value(std::move(post_handshake_msg)));
- } else {
- post_handshake_msg_bytes =
- EncodePaddedCBORMap(std::move(post_handshake_msg));
- // All post-handshake messages should fit into the same padding
- // bucket. It doesn't have to be the smallest one, but that's
- // currently true which yields this easy check:
- DCHECK_EQ(post_handshake_msg_bytes->size(),
- kPostHandshakeMsgPaddingGranularity);
- }
- 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 && protocol_revision_ >= 1) {
- // For revision one and greater, linking information can be sent at
- // any time. We always send it immediately after the post-handshake
- // message.
- absl::optional<cbor::Value> pairing_data(
- std::move(generate_pairing_data_)
- .Run(*peer_identity_, result->second));
- // padding_target is the expected size of the plaintext of the update
- // message.
- constexpr size_t kPaddingTarget = 512;
- // padding_length is the length of a bytestring of zeros, included
- // just to hit `kPaddingTarget`. `Encrypt` pads to 32 bytes so we can
- // be a little sloppy here and use simpler code. Thus we aim at 16
- // bytes shy of the target so that it'll be padded up by `Encrypt`.
- static_assert(kPaddingTarget % 32 == 0);
- size_t padding_length =
- kPaddingTarget - 16 -
- /* length of CBOR map key */ 1 -
- /* length of bytestring overhead, assuming a two-byte length */ 3;
- if (pairing_data) {
- cbor::Value::MapValue update_msg_for_measurement;
- update_msg_for_measurement.emplace(1, pairing_data->Clone());
- absl::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));
- }
- absl::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 + /* AES-GCM overhead */ 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 (protocol_revision_ >= 1) {
- 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:
- break;
- case MessageType::kUpdate:
- // The payload is ignored for now. Maybe there will be desktop
- // updates defined in the future. But we still check that the
- // payload is well-formed.
- 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::move(plaintext));
- break;
- }
- default:
- NOTREACHED();
- }
- }
- const unsigned protocol_revision_;
- const raw_ptr<Platform> 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_;
- const raw_ptr<network::mojom::NetworkContext> network_context_;
- const absl::optional<std::array<uint8_t, kP256X962Length>> peer_identity_;
- std::array<uint8_t, kPSKSize> psk_;
- GeneratePairingDataCallback generate_pairing_data_;
- const std::vector<uint8_t> secret_;
- bssl::UniquePtr<EC_KEY> local_identity_;
- const bool use_new_crypter_construction_;
- 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_) {
- // If the owner of this object doesn't destroy it immediately after an
- // error then the transport could continue to send updates. These should
- // not be passed through.
- return;
- }
- 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)) {
- absl::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;
- }
- std::vector<uint8_t>& msg = absl::get<std::vector<uint8_t>>(update);
- const absl::variant<std::vector<uint8_t>, Platform::Error> result =
- ProcessCTAPMessage(msg);
- if (const auto* error = absl::get_if<Platform::Error>(&result)) {
- have_completed_ = true;
- platform_->OnCompleted(*error);
- return;
- }
- const std::vector<uint8_t>& response =
- absl::get<std::vector<uint8_t>>(result);
- if (response.empty()) {
- // Response is pending.
- return;
- }
- transport_->Write(std::move(response));
- }
- absl::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 = message_bytes[0];
- const auto cbor_bytes = message_bytes.subspan(1);
- absl::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, 1) << ") "
- << cbor::DiagnosticWriter::Write(*payload);
- } else {
- FIDO_LOG(DEBUG) << "<- (" << base::HexEncode(&command, 1)
- << ") <no payload>";
- }
- switch (command) {
- case static_cast<uint8_t>(
- device::CtapRequestCommand::kAuthenticatorGetInfo): {
- if (payload) {
- FIDO_LOG(ERROR) << "getInfo command incorrectly contained payload";
- return Platform::Error::INVALID_CTAP;
- }
- absl::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 (!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()) {
- // This value cannot be represented in the `int32_t`
- // in the Mojo structure and thus is ignored.
- 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->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;
- }
- 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);
- return Platform::Error::INVALID_CTAP;
- }
- }
- void OnMakeCredentialResponse(
- bool was_discoverable_credential_request,
- uint32_t ctap_status,
- base::span<const uint8_t> attestation_object_bytes) {
- 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)) {
- // TODO: pass response parameters from the Java side.
- absl::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, base::StringPiece(*attestation_object.fmt));
- response_map.emplace(
- 2, base::span<const uint8_t>(*attestation_object.auth_data));
- response_map.emplace(3, attestation_object.statement->Clone());
- absl::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(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));
- // The `name` and `displayName` fields are not present in
- // `GetAssertionAuthenticatorResponse` because they aren't returned
- // at the WebAuthn level. CTAP 2.1 says that fields other than `id` are
- // only applicable "For multiple accounts per RP case, where the
- // authenticator does not have a display". But we assume that caBLE
- // devices do have a display and don't handle multiple GetAssertion
- // responses anyway.
- user_map.emplace("name", "");
- user_map.emplace("displayName", "");
- response_map.emplace(4, std::move(user_map));
- // This is the `userSelected` field, which indicates that additional
- // confirmation of the account selection isn't needed.
- response_map.emplace(6, true);
- }
- absl::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(std::move(response));
- }
- // CopyCredIds parses a series of `PublicKeyCredentialDescriptor`s from `in`
- // and appends them to `out`, returning true on success or false on error.
- 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};
- };
- static std::array<uint8_t, 32> DerivePairedSecret(
- base::span<const uint8_t, kRootSecretSize> root_secret,
- const absl::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) {
- // The root secret is not used directly to derive the paired secret because
- // we want the keys to change after an unlink. Unlinking invalidates and
- // replaces the contact ID therefore we derive paired secrets in two steps:
- // first using the contact ID to derive a secret from the root secret, and
- // then using the pairing ID to generate a secret from that.
- per_contact_id_secret =
- device::cablev2::Derive<EXTENT(per_contact_id_secret)>(
- 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<EXTENT(paired_secret)>(
- 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,
- absl::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,
- absl::optional<std::vector<uint8_t>> contact_id)
- : root_secret_(fido_parsing_utils::Materialize(root_secret)),
- name_(name),
- contact_id_(std::move(contact_id)) {}
- absl::optional<cbor::Value> Generate(
- base::span<const uint8_t, device::kP256X962Length> peer_public_key_x962,
- device::cablev2::HandshakeHash handshake_hash) {
- if (!contact_id_) {
- return absl::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(), /*ctx=*/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_;
- absl::optional<std::vector<uint8_t>> contact_id_;
- };
- } // namespace
- 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(
- unsigned protocol_revision,
- 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,
- absl::optional<std::vector<uint8_t>> contact_id,
- bool use_new_crypter_construction) {
- 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>(
- protocol_revision, platform_ptr, network_context, qr_secret,
- peer_identity, use_new_crypter_construction,
- std::move(generate_pairing_data)),
- std::move(platform));
- }
- std::unique_ptr<Transaction> TransactFromFCM(
- unsigned protocol_revision,
- 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,
- absl::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>(
- protocol_revision, platform_ptr, network_context, paired_secret,
- client_nonce, routing_id, tunnel_id, IdentityKey(root_secret)),
- std::move(platform));
- }
- } // namespace authenticator
- } // namespace cablev2
- } // namespace device
|