| // 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/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 "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/fido_constants.h" |
| #include "device/fido/fido_parsing_utils.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/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 { |
| |
| 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<int64_t>, because ELEMENT() treats the |
| // raw_ptr<T> as a void*, skipping AddRef() call and causing a ref-counting |
| // mismatch. |
| const std::vector<uint8_t>* client_data_hash; |
| const std::string* rp_id; |
| const std::vector<uint8_t>* user_id; |
| const cbor::Value::ArrayValue* cred_params; |
| const cbor::Value::ArrayValue* excluded_credentials; |
| }; |
| |
| 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', |
| Stop<MakeCredRequest>(), |
| |
| Map<MakeCredRequest>(), |
| IntKey<MakeCredRequest>(3), |
| ELEMENT(Is::kRequired, MakeCredRequest, user_id), |
| StringKey<MakeCredRequest>(), 'i', 'd', '\0', |
| Stop<MakeCredRequest>(), |
| |
| ELEMENT(Is::kRequired, MakeCredRequest, cred_params), |
| IntKey<MakeCredRequest>(4), |
| ELEMENT(Is::kOptional, MakeCredRequest, excluded_credentials), |
| IntKey<MakeCredRequest>(5), |
| |
| Stop<MakeCredRequest>(), |
| // clang-format on |
| }; |
| |
| struct AttestationObject { |
| // All the fields below are not a raw_ptr<,,,>, because ELEMENT() treats the |
| // raw_ptr<T> as a void*, skipping AddRef() call and causing a ref-counting |
| // mismatch. |
| const std::string* fmt; |
| const std::vector<uint8_t>* auth_data; |
| 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 the fields below are not a raw_ptr<,,,>, because ELEMENT() treats the |
| // raw_ptr<T> as a void*, skipping AddRef() call and causing a ref-counting |
| // mismatch. |
| const std::string* rp_id; |
| const std::vector<uint8_t>* client_data_hash; |
| 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"); |
| // TODO: should be based on whether a screen-lock is enabled. |
| cbor::Value::MapValue options; |
| options.emplace("uv", true); |
| |
| cbor::Value::MapValue response_map; |
| response_map.emplace(1, std::move(versions)); |
| response_map.emplace(3, aaguid); |
| response_map.emplace(4, std::move(options)); |
| |
| 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( |
| Platform* platform, |
| network::mojom::NetworkContext* network_context, |
| base::span<const uint8_t> secret, |
| base::span<const uint8_t, device::kP256X962Length> peer_identity, |
| GeneratePairingDataCallback generate_pairing_data) |
| : 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)) { |
| 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, |
| 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) |
| : 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)) { |
| 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 (!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); |
| |
| cbor::Value::MapValue post_handshake_msg; |
| post_handshake_msg.emplace(1, BuildGetInfoResponse()); |
| |
| if (state_ == State::kConnected) { |
| 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( |
| EncodePaddedCBORMap(std::move(post_handshake_msg))); |
| if (!post_handshake_msg_bytes) { |
| FIDO_LOG(ERROR) << "failed to encode post-handshake message"; |
| return; |
| } |
| |
| // It should be the case that all post-handshake messages fall into |
| // a single padding bucket. (It doesn't have to be the smallest one.) |
| // |
| // This check should be: |
| // DCHECK_EQ(post_handshake_msg_bytes->size(), |
| // kPostHandshakeMsgPaddingGranularity); |
| // |
| // ... but we're waiting to roll out a protocol change that allows it. |
| // For now, check that the messages fit within the future padding |
| // granularity, which will also highlight this when that constant is |
| // rename to remove "Future". |
| DCHECK_LE(post_handshake_msg_bytes->size(), |
| kFuturePostHandshakeMsgPaddingGranularity); |
| |
| 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); |
| |
| 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 (first_message_) { |
| update_callback_.Run(Platform::Status::REQUEST_RECEIVED); |
| first_message_ = false; |
| } |
| update_callback_.Run(std::move(plaintext)); |
| break; |
| } |
| |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| 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_; |
| 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 (auto* error = absl::get_if<Platform::Error>(&update)) { |
| 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; |
| } |
| platform_->OnCompleted(maybe_error); |
| return; |
| } |
| |
| std::vector<uint8_t>& msg = absl::get<std::vector<uint8_t>>(update); |
| absl::optional<std::vector<uint8_t>> response = ProcessCTAPMessage(msg); |
| if (!response) { |
| // TODO(agl): expose more error information from |ProcessCTAPMessage|. |
| platform_->OnCompleted(Platform::Error::INVALID_CTAP); |
| return; |
| } |
| |
| if (response->empty()) { |
| // Response is pending. |
| return; |
| } |
| |
| transport_->Write(std::move(*response)); |
| } |
| |
| absl::optional<std::vector<uint8_t>> ProcessCTAPMessage( |
| base::span<const uint8_t> message_bytes) { |
| if (message_bytes.empty()) { |
| return absl::nullopt; |
| } |
| 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 absl::nullopt; |
| } |
| 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 absl::nullopt; |
| } |
| |
| absl::optional<std::vector<uint8_t>> response = BuildGetInfoResponse(); |
| if (!response) { |
| return absl::nullopt; |
| } |
| 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 absl::nullopt; |
| } |
| |
| 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 absl::nullopt; |
| } |
| |
| auto params = std::make_unique<Platform::MakeCredentialParams>(); |
| params->client_data_hash = *make_cred_request.client_data_hash; |
| params->rp_id = *make_cred_request.rp_id; |
| params->user_id = *make_cred_request.user_id; |
| params->callback = |
| base::BindOnce(&CTAP2Processor::OnMakeCredentialResponse, |
| weak_factory_.GetWeakPtr()); |
| |
| if (!device::cbor_extract::ForEachPublicKeyEntry( |
| *make_cred_request.cred_params, cbor::Value("alg"), |
| base::BindRepeating( |
| [](std::vector<int>* out, |
| const cbor::Value& value) -> bool { |
| if (!value.is_integer()) { |
| return false; |
| } |
| const int64_t alg = value.GetInteger(); |
| |
| if (alg > std::numeric_limits<int>::max() || |
| alg < std::numeric_limits<int>::min()) { |
| return false; |
| } |
| out->push_back(static_cast<int>(alg)); |
| return true; |
| }, |
| base::Unretained(¶ms->algorithms)))) { |
| return absl::nullopt; |
| } |
| |
| if (make_cred_request.excluded_credentials && |
| !device::cbor_extract::ForEachPublicKeyEntry( |
| *make_cred_request.excluded_credentials, cbor::Value("id"), |
| base::BindRepeating( |
| [](std::vector<std::vector<uint8_t>>* out, |
| const cbor::Value& value) -> bool { |
| if (!value.is_bytestring()) { |
| return false; |
| } |
| out->push_back(value.GetBytestring()); |
| return true; |
| }, |
| base::Unretained(¶ms->excluded_cred_ids)))) { |
| return absl::nullopt; |
| } |
| |
| // TODO: plumb the rk flag through once GmsCore supports resident |
| // keys. This will require support for optional maps in |Extract|. |
| transaction_received_ = true; |
| platform_->MakeCredential(std::move(params)); |
| 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 absl::nullopt; |
| } |
| |
| 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 absl::nullopt; |
| } |
| |
| auto params = std::make_unique<Platform::GetAssertionParams>(); |
| params->client_data_hash = *get_assertion_request.client_data_hash; |
| params->rp_id = *get_assertion_request.rp_id; |
| params->callback = |
| base::BindOnce(&CTAP2Processor::OnGetAssertionResponse, |
| weak_factory_.GetWeakPtr()); |
| |
| if (get_assertion_request.allowed_credentials && |
| !device::cbor_extract::ForEachPublicKeyEntry( |
| *get_assertion_request.allowed_credentials, cbor::Value("id"), |
| base::BindRepeating( |
| [](std::vector<std::vector<uint8_t>>* out, |
| const cbor::Value& value) -> bool { |
| if (!value.is_bytestring()) { |
| return false; |
| } |
| out->push_back(value.GetBytestring()); |
| return true; |
| }, |
| base::Unretained(¶ms->allowed_cred_ids)))) { |
| return absl::nullopt; |
| } |
| |
| transaction_received_ = true; |
| platform_->GetAssertion(std::move(params)); |
| return std::vector<uint8_t>(); |
| } |
| |
| default: |
| FIDO_LOG(ERROR) << "Received unknown command " |
| << static_cast<unsigned>(command); |
| return absl::nullopt; |
| } |
| } |
| |
| void OnMakeCredentialResponse( |
| 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 { |
| 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(uint32_t ctap_status, |
| base::span<const uint8_t> credential_id, |
| base::span<const uint8_t> authenticator_data, |
| base::span<const uint8_t> signature) { |
| 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)) { |
| cbor::Value::MapValue credential_descriptor; |
| credential_descriptor.emplace("type", device::kPublicKey); |
| credential_descriptor.emplace("id", credential_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, authenticator_data); |
| response_map.emplace(3, signature); |
| // TODO: add user entity to support resident keys. |
| |
| 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 { |
| platform_->OnStatus(Platform::Status::CTAP_ERROR); |
| } |
| |
| if (!transaction_done_) { |
| platform_->OnStatus(Platform::Status::FIRST_TRANSACTION_DONE); |
| transaction_done_ = true; |
| } |
| transport_->Write(std::move(response)); |
| } |
| |
| 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::MakeCredentialParams::MakeCredentialParams() = default; |
| Platform::MakeCredentialParams::~MakeCredentialParams() = default; |
| Platform::GetAssertionParams::GetAssertionParams() = default; |
| Platform::GetAssertionParams::~GetAssertionParams() = 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, |
| 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) { |
| 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, network_context, |
| qr_secret, peer_identity, |
| std::move(generate_pairing_data)), |
| std::move(platform)); |
| } |
| |
| std::unique_ptr<Transaction> TransactFromFCM( |
| 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>(platform_ptr, network_context, |
| paired_secret, client_nonce, routing_id, |
| tunnel_id, IdentityKey(root_secret)), |
| std::move(platform)); |
| } |
| |
| } // namespace authenticator |
| } // namespace cablev2 |
| } // namespace device |