Commit eb882fc3 authored by Adam Langley's avatar Adam Langley Committed by Commit Bot

device/fido: add FidoTunnelDevice.

|FidoTunnelDevice| performs a caBLEv2 handshake, and passes CTAP2
messages, across a WebSocket.

BUG=1002262

Change-Id: I0d6e89019aca292a1795b380a9b87b59971733b9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2340388
Commit-Queue: Adam Langley <agl@chromium.org>
Reviewed-by: default avatarMartin Šrámek <msramek@chromium.org>
Reviewed-by: default avatarReilly Grant <reillyg@chromium.org>
Reviewed-by: default avatarMartin Kreichgauer <martinkr@google.com>
Cr-Commit-Position: refs/heads/master@{#799759}
parent 1cbbd34e
...@@ -111,6 +111,10 @@ component("fido") { ...@@ -111,6 +111,10 @@ component("fido") {
"cable/fido_cable_discovery.h", "cable/fido_cable_discovery.h",
"cable/fido_cable_handshake_handler.cc", "cable/fido_cable_handshake_handler.cc",
"cable/fido_cable_handshake_handler.h", "cable/fido_cable_handshake_handler.h",
"cable/fido_tunnel_device.cc",
"cable/fido_tunnel_device.h",
"cable/websocket_adapter.cc",
"cable/websocket_adapter.h",
"client_data.cc", "client_data.cc",
"client_data.h", "client_data.h",
"credential_management.cc", "credential_management.cc",
...@@ -190,6 +194,7 @@ component("fido") { ...@@ -190,6 +194,7 @@ component("fido") {
"//services/device/public/cpp/usb", "//services/device/public/cpp/usb",
"//services/device/public/mojom", "//services/device/public/mojom",
"//services/device/public/mojom:usb", "//services/device/public/mojom:usb",
"//services/network/public/mojom",
] ]
} }
......
...@@ -5,8 +5,10 @@ include_rules = [ ...@@ -5,8 +5,10 @@ include_rules = [
"+dbus", "+dbus",
"+net/base", "+net/base",
"+net/cert", "+net/cert",
"+ui/base/l10n", "+net/traffic_annotation",
"+services/network",
"+third_party/boringssl/src/include", "+third_party/boringssl/src/include",
"+third_party/cros_system_api", "+third_party/cros_system_api",
"+third_party/microsoft_webauthn", "+third_party/microsoft_webauthn",
"+ui/base/l10n",
] ]
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include "base/time/time.h" #include "base/time/time.h"
#include "crypto/random.h" #include "crypto/random.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/fido_parsing_utils.h" #include "device/fido/fido_parsing_utils.h"
#include "third_party/boringssl/src/include/openssl/aes.h" #include "third_party/boringssl/src/include/openssl/aes.h"
#include "third_party/boringssl/src/include/openssl/digest.h" #include "third_party/boringssl/src/include/openssl/digest.h"
...@@ -138,7 +139,7 @@ bool CableDiscoveryData::MatchV2(const CableEidArray& eid, ...@@ -138,7 +139,7 @@ bool CableDiscoveryData::MatchV2(const CableEidArray& eid,
static_assert(kCableEphemeralIdSize == AES_BLOCK_SIZE, static_assert(kCableEphemeralIdSize == AES_BLOCK_SIZE,
"EIDs are not AES blocks"); "EIDs are not AES blocks");
AES_decrypt(/*in=*/eid.data(), /*out=*/out.data(), &key); AES_decrypt(/*in=*/eid.data(), /*out=*/out.data(), &key);
return (out[3] & 0xc0) == 0 && out[4] == 0 && out[5] == 0; return cablev2::eid::IsValid(out);
} }
// static // static
......
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
#include "device/bluetooth/public/cpp/bluetooth_uuid.h" #include "device/bluetooth/public/cpp/bluetooth_uuid.h"
#include "device/fido/cable/fido_ble_uuids.h" #include "device/fido/cable/fido_ble_uuids.h"
#include "device/fido/cable/fido_cable_handshake_handler.h" #include "device/fido/cable/fido_cable_handshake_handler.h"
#include "device/fido/cable/fido_tunnel_device.h"
#include "device/fido/features.h" #include "device/fido/features.h"
#include "device/fido/fido_parsing_utils.h" #include "device/fido/fido_parsing_utils.h"
...@@ -161,7 +162,8 @@ FidoCableDiscovery::FidoCableDiscovery( ...@@ -161,7 +162,8 @@ FidoCableDiscovery::FidoCableDiscovery(
FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy), FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy),
discovery_data_(std::move(discovery_data)), discovery_data_(std::move(discovery_data)),
qr_generator_key_(std::move(qr_generator_key)), qr_generator_key_(std::move(qr_generator_key)),
pairing_callback_(std::move(pairing_callback)) { pairing_callback_(std::move(pairing_callback)),
network_context_(network_context) {
// Windows currently does not support multiple EIDs, thus we ignore any extra // Windows currently does not support multiple EIDs, thus we ignore any extra
// discovery data. // discovery data.
// TODO(https://crbug.com/837088): Add support for multiple EIDs on Windows. // TODO(https://crbug.com/837088): Add support for multiple EIDs on Windows.
...@@ -523,10 +525,13 @@ void FidoCableDiscovery::CableDeviceFound(BluetoothAdapter* adapter, ...@@ -523,10 +525,13 @@ void FidoCableDiscovery::CableDeviceFound(BluetoothAdapter* adapter,
} }
case CableDiscoveryData::Version::V2: { case CableDiscoveryData::Version::V2: {
if (!base::FeatureList::IsEnabled(device::kWebAuthPhoneSupport)) { if (!base::FeatureList::IsEnabled(device::kWebAuthPhoneSupport) ||
!network_context_) {
return; return;
} }
FIDO_LOG(DEBUG) << "caBLEv2 request being dropped during transition."; AddDevice(std::make_unique<cablev2::FidoTunnelDevice>(
network_context_, *result->discovery_data.v2, result->eid,
*result->decrypted_eid));
break; break;
} }
......
...@@ -191,6 +191,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoCableDiscovery ...@@ -191,6 +191,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoCableDiscovery
base::Optional< base::Optional<
base::RepeatingCallback<void(std::unique_ptr<CableDiscoveryData>)>> base::RepeatingCallback<void(std::unique_ptr<CableDiscoveryData>)>>
pairing_callback_; pairing_callback_;
network::mojom::NetworkContext* const network_context_;
// observed_devices_ caches the information from observed caBLE devices so // observed_devices_ caches the information from observed caBLE devices so
// that the device-log isn't spammed. // that the device-log isn't spammed.
......
// 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/fido_tunnel_device.h"
#include "base/strings/string_number_conversions.h"
#include "components/device_event_log/device_event_log.h"
#include "crypto/random.h"
#include "device/fido/fido_constants.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "third_party/boringssl/src/include/openssl/digest.h"
#include "third_party/boringssl/src/include/openssl/hkdf.h"
namespace device {
namespace cablev2 {
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("cablev2_websocket_from_client", R"(
semantics {
sender: "Phone as a Security Key"
description:
"Chrome can communicate with a phone for the purpose of using "
"the phone as a security key. This WebSocket connection is made to "
"a rendezvous service of the phone's choosing. Mostly likely that "
"is a Google service because the phone-side is being handled by "
"Chrome on that device. The service carries only end-to-end "
"encrypted data where the keys are shared directly between the "
"client and phone via QR code and Bluetooth broadcast."
trigger:
"A web-site initiates a WebAuthn request and the user scans a QR "
"code with their phone."
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."
})");
FidoTunnelDevice::FidoTunnelDevice(
network::mojom::NetworkContext* network_context,
const CableDiscoveryData::V2Data& v2data,
const CableEidArray& eid,
const CableEidArray& decrypted_eid)
: v2data_(v2data) {
DCHECK(eid::IsValid(decrypted_eid));
crypto::RandBytes(id_);
const eid::Components components = eid::ToComponents(decrypted_eid);
nonce_and_eid_.first = components.nonce;
nonce_and_eid_.second = eid;
std::array<uint8_t, 16> tunnel_id;
bool ok = HKDF(tunnel_id.data(), tunnel_id.size(), EVP_sha256(),
v2data_.tunnel_id_gen_key.data(),
v2data_.tunnel_id_gen_key.size(), components.nonce.data(),
components.nonce.size(), /*info=*/nullptr, 0);
DCHECK(ok);
const GURL url(cablev2::tunnelserver::GetURL(
components.tunnel_server_domain, cablev2::tunnelserver::Action::kConnect,
tunnel_id));
FIDO_LOG(DEBUG) << "Connecting caBLEv2 tunnel: " << url
<< " shard: " << static_cast<int>(components.shard_id);
websocket_client_ = std::make_unique<device::cablev2::WebSocketAdapter>(
base::BindOnce(&FidoTunnelDevice::OnTunnelReady, base::Unretained(this)),
base::BindRepeating(&FidoTunnelDevice::OnTunnelData,
base::Unretained(this)));
network_context->CreateWebSocket(
url, {kCableWebSocketProtocol}, net::SiteForCookies(),
net::IsolationInfo(), /*additional_headers=*/{},
network::mojom::kBrowserProcessId,
/*render_frame_id=*/0, url::Origin::Create(url),
network::mojom::kWebSocketOptionBlockAllCookies,
net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation),
websocket_client_->BindNewHandshakeClientPipe(), mojo::NullRemote(),
mojo::NullRemote());
}
FidoTunnelDevice::~FidoTunnelDevice() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
FidoDevice::CancelToken FidoTunnelDevice::DeviceTransact(
std::vector<uint8_t> command,
DeviceCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!callback_);
pending_message_ = std::move(command);
callback_ = std::move(callback);
if (state_ == State::kHandshakeProcessed || state_ == State::kReady) {
MaybeFlushPendingMessage();
}
// TODO: cancelation would be useful, but it depends on the GMSCore action
// being cancelable on Android, which it currently is not.
return kInvalidCancelToken + 1;
}
void FidoTunnelDevice::Cancel(CancelToken token) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
std::string FidoTunnelDevice::GetId() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return "tunnel-" + base::HexEncode(id_);
}
FidoTransportProtocol FidoTunnelDevice::DeviceTransport() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy;
}
base::WeakPtr<FidoDevice> FidoTunnelDevice::GetWeakPtr() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return weak_factory_.GetWeakPtr();
}
void FidoTunnelDevice::OnTunnelReady(bool ok,
base::Optional<uint8_t> shard_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(State::kConnecting, state_);
if (!ok) {
OnError();
return;
}
state_ = State::kConnected;
}
void FidoTunnelDevice::OnTunnelData(
base::Optional<base::span<const uint8_t>> data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!data) {
OnError();
return;
}
switch (state_) {
case State::kError:
case State::kConnecting:
NOTREACHED();
break;
case State::kConnected: {
std::vector<uint8_t> response;
base::Optional<std::pair<std::unique_ptr<Crypter>, std::vector<uint8_t>>>
result(cablev2::RespondToHandshake(
v2data_.psk_gen_key, nonce_and_eid_, v2data_.local_identity_seed,
base::nullopt, *data, &response));
if (!result || result->second.empty()) {
FIDO_LOG(ERROR) << "caBLEv2 handshake failed";
OnError();
return;
}
FIDO_LOG(DEBUG) << "caBLEv2 handshake successful";
websocket_client_->Write(response);
crypter_ = std::move(result->first);
getinfo_response_bytes_ = std::move(result->second);
state_ = State::kHandshakeProcessed;
MaybeFlushPendingMessage();
break;
}
case State::kHandshakeProcessed: {
// This is the post-handshake message that optionally contains pairing
// information.
std::vector<uint8_t> decrypted;
if (!crypter_->Decrypt(*data, &decrypted)) {
FIDO_LOG(ERROR) << "decryption failed for caBLE pairing message";
OnError();
return;
}
base::Optional<cbor::Value> payload = DecodePaddedCBORMap(decrypted);
if (!payload) {
FIDO_LOG(ERROR) << "decode failed for caBLE pairing message";
OnError();
return;
}
// TODO: pairing not yet handled.
state_ = State::kReady;
break;
}
case State::kReady: {
if (!callback_) {
OnError();
return;
}
std::vector<uint8_t> plaintext;
if (!crypter_->Decrypt(*data, &plaintext)) {
FIDO_LOG(ERROR) << "decryption failed for caBLE message";
OnError();
return;
}
std::move(callback_).Run(std::move(plaintext));
break;
}
}
}
void FidoTunnelDevice::OnError() {
state_ = State::kError;
websocket_client_.reset();
if (callback_) {
std::move(callback_).Run(base::nullopt);
}
}
void FidoTunnelDevice::MaybeFlushPendingMessage() {
if (pending_message_.empty()) {
return;
}
std::vector<uint8_t> pending(std::move(pending_message_));
if (pending.size() == 1 &&
pending[0] ==
static_cast<uint8_t>(CtapRequestCommand::kAuthenticatorGetInfo)) {
DCHECK(!getinfo_response_bytes_.empty());
std::vector<uint8_t> reply;
reply.reserve(1 + getinfo_response_bytes_.size());
reply.push_back(static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess));
reply.insert(reply.end(), getinfo_response_bytes_.begin(),
getinfo_response_bytes_.end());
std::move(callback_).Run(std::move(reply));
} else if (crypter_->Encrypt(&pending)) {
websocket_client_->Write(pending);
}
}
} // namespace cablev2
} // namespace device
// 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.
#ifndef DEVICE_FIDO_CABLE_FIDO_TUNNEL_DEVICE_H_
#define DEVICE_FIDO_CABLE_FIDO_TUNNEL_DEVICE_H_
#include <vector>
#include "base/sequence_checker.h"
#include "device/fido/cable/cable_discovery_data.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/cable/websocket_adapter.h"
#include "device/fido/fido_device.h"
namespace network {
namespace mojom {
class NetworkContext;
}
} // namespace network
namespace device {
namespace cablev2 {
class Crypter;
class WebSocketAdapter;
class COMPONENT_EXPORT(DEVICE_FIDO) FidoTunnelDevice : public FidoDevice {
public:
FidoTunnelDevice(network::mojom::NetworkContext* network_context,
const CableDiscoveryData::V2Data& v2data,
const CableEidArray& eid,
const CableEidArray& decrypted_eid);
~FidoTunnelDevice() override;
// FidoDevice:
CancelToken DeviceTransact(std::vector<uint8_t> command,
DeviceCallback callback) override;
void Cancel(CancelToken token) override;
std::string GetId() const override;
FidoTransportProtocol DeviceTransport() const override;
base::WeakPtr<FidoDevice> GetWeakPtr() override;
private:
enum class State {
kConnecting,
kConnected,
kHandshakeProcessed,
kReady,
kError,
};
void OnTunnelReady(bool ok, base::Optional<uint8_t> shard_id);
void OnTunnelData(base::Optional<base::span<const uint8_t>> data);
void OnError();
void MaybeFlushPendingMessage();
State state_ = State::kConnecting;
std::array<uint8_t, 8> id_;
const CableDiscoveryData::V2Data v2data_;
cablev2::NonceAndEID nonce_and_eid_;
std::unique_ptr<WebSocketAdapter> websocket_client_;
std::unique_ptr<Crypter> crypter_;
std::vector<uint8_t> getinfo_response_bytes_;
std::vector<uint8_t> pending_message_;
DeviceCallback callback_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<FidoTunnelDevice> weak_factory_{this};
DISALLOW_COPY_AND_ASSIGN(FidoTunnelDevice);
};
} // namespace cablev2
} // namespace device
#endif // DEVICE_FIDO_CABLE_FIDO_TUNNEL_DEVICE_H_
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
#include "device/fido/cable/v2_handshake.h" #include "device/fido/cable/v2_handshake.h"
#include <array>
#include <type_traits>
#include "base/bits.h" #include "base/bits.h"
#include "base/numerics/safe_math.h" #include "base/numerics/safe_math.h"
#include "base/strings/string_number_conversions.h" #include "base/strings/string_number_conversions.h"
...@@ -79,6 +82,54 @@ GURL GetURL(uint32_t domain, Action action, base::span<const uint8_t, 16> id) { ...@@ -79,6 +82,54 @@ GURL GetURL(uint32_t domain, Action action, base::span<const uint8_t, 16> id) {
} }
} // namespace tunnelserver } // namespace tunnelserver
namespace eid {
CableEidArray FromComponents(const Components& components) {
DCHECK_EQ(components.tunnel_server_domain >> 22, 0u);
DCHECK_EQ(components.shard_id >> 6, 0);
const uint32_t header = components.tunnel_server_domain |
(static_cast<uint32_t>(components.shard_id) << 22);
CableEidArray eid;
constexpr size_t eid_size =
std::tuple_size<std::remove_reference<decltype(eid)>::type>::value;
memset(eid.data(), 0, eid.size());
static_assert(eid_size >= sizeof(header), "EID too small");
memcpy(eid.data(), &header, sizeof(header));
static_assert(eid_size == 6 + kNonceSize, "EID wrong size");
static_assert(
std::tuple_size<decltype(components.nonce)>::value == kNonceSize,
"Nonce wrong size");
memcpy(eid.data() + 6, components.nonce.data(), kNonceSize);
return eid;
}
bool IsValid(const CableEidArray& eid) {
static_assert(
std::tuple_size<std::remove_reference<decltype(eid)>::type>::value >= 6,
"EID too small");
return (eid[3] & 0xc0) == 0 && eid[4] == 0 && eid[5] == 0;
}
Components ToComponents(const CableEidArray& eid) {
DCHECK(IsValid(eid));
constexpr size_t eid_size =
std::tuple_size<std::remove_reference<decltype(eid)>::type>::value;
Components ret;
uint32_t header;
static_assert(eid_size >= sizeof(header), "EID too small");
memcpy(&header, eid.data(), sizeof(header));
ret.shard_id = (header >> 22) & 0x3f;
ret.tunnel_server_domain = header & 0x3fffff;
static_assert(eid_size == 6 + std::tuple_size<decltype(ret.nonce)>::value,
"EID too small");
memcpy(ret.nonce.data(), eid.data() + 6, ret.nonce.size());
return ret;
}
} // namespace eid
base::Optional<std::vector<uint8_t>> EncodePaddedCBORMap( base::Optional<std::vector<uint8_t>> EncodePaddedCBORMap(
cbor::Value::MapValue map) { cbor::Value::MapValue map) {
base::Optional<std::vector<uint8_t>> cbor_bytes = base::Optional<std::vector<uint8_t>> cbor_bytes =
......
...@@ -24,6 +24,8 @@ class GURL; ...@@ -24,6 +24,8 @@ class GURL;
namespace device { namespace device {
namespace cablev2 { namespace cablev2 {
constexpr size_t kNonceSize = 10;
namespace tunnelserver { namespace tunnelserver {
// Base32Ord converts |c| into its base32 value, as defined in // Base32Ord converts |c| into its base32 value, as defined in
...@@ -74,6 +76,32 @@ GURL GetURL(uint32_t domain, Action action, base::span<const uint8_t, 16> id); ...@@ -74,6 +76,32 @@ GURL GetURL(uint32_t domain, Action action, base::span<const uint8_t, 16> id);
} // namespace tunnelserver } // namespace tunnelserver
namespace eid {
// Components contains the parts of a decrypted EID.
struct Components {
uint8_t shard_id;
uint32_t tunnel_server_domain;
std::array<uint8_t, kNonceSize> nonce;
};
// FromComponents constructs a valid EID from the given components. |IsValid|
// will be true of the result.
COMPONENT_EXPORT(DEVICE_FIDO)
CableEidArray FromComponents(const Components& components);
// IsValid returns true if |eid| could have been produced by |FromComponents|.
COMPONENT_EXPORT(DEVICE_FIDO)
bool IsValid(const CableEidArray& eid);
// ToComponents explodes a decrypted EID into its components. It's the
// inverse of |ComponentsToEID|. |IsValid| must be true for the given EID before
// calling this function.
COMPONENT_EXPORT(DEVICE_FIDO)
Components ToComponents(const CableEidArray& eid);
} // namespace eid
// EncodePaddedCBORMap encodes the given map and pads it to 256 bytes in such a // EncodePaddedCBORMap encodes the given map and pads it to 256 bytes in such a
// way that |DecodePaddedCBORMap| can decode it. The padding is done on the // way that |DecodePaddedCBORMap| can decode it. The padding is done on the
// assumption that the returned bytes will be encrypted and the encoded size of // assumption that the returned bytes will be encrypted and the encoded size of
...@@ -91,7 +119,6 @@ base::Optional<cbor::Value> DecodePaddedCBORMap( ...@@ -91,7 +119,6 @@ base::Optional<cbor::Value> DecodePaddedCBORMap(
// NonceAndEID contains both the random nonce chosen for an advert, as well as // NonceAndEID contains both the random nonce chosen for an advert, as well as
// the EID that was generated from it. // the EID that was generated from it.
constexpr size_t kNonceSize = 10;
typedef std::pair<std::array<uint8_t, kNonceSize>, typedef std::pair<std::array<uint8_t, kNonceSize>,
std::array<uint8_t, device::kCableEphemeralIdSize>> std::array<uint8_t, device::kCableEphemeralIdSize>>
NonceAndEID; NonceAndEID;
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
#include "device/fido/cable/v2_handshake.h" #include "device/fido/cable/v2_handshake.h"
#include "base/rand_util.h"
#include "components/cbor/values.h" #include "components/cbor/values.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
#include "third_party/boringssl/src/include/openssl/ec.h" #include "third_party/boringssl/src/include/openssl/ec.h"
...@@ -25,6 +26,26 @@ TEST(CableV2Encoding, TunnelServerURLs) { ...@@ -25,6 +26,26 @@ TEST(CableV2Encoding, TunnelServerURLs) {
EXPECT_TRUE(url.spec().find("//abcd.net/") != std::string::npos) << url; EXPECT_TRUE(url.spec().find("//abcd.net/") != std::string::npos) << url;
} }
TEST(CableV2Encoding, EIDs) {
eid::Components components;
components.tunnel_server_domain = 0x010203;
components.shard_id = 42;
base::RandBytes(components.nonce.data(), components.nonce.size());
CableEidArray eid = eid::FromComponents(components);
eid::Components components2 = eid::ToComponents(eid);
EXPECT_EQ(components.shard_id, components2.shard_id);
EXPECT_EQ(components.tunnel_server_domain, components2.tunnel_server_domain);
EXPECT_EQ(components.nonce, components2.nonce);
for (size_t i = 0; i < eid.size(); i++) {
eid[i] ^= 0xff;
}
EXPECT_FALSE(eid::IsValid(eid));
}
TEST(CableV2Encoding, PaddedCBOR) { TEST(CableV2Encoding, PaddedCBOR) {
cbor::Value::MapValue map; cbor::Value::MapValue map;
base::Optional<std::vector<uint8_t>> encoded = base::Optional<std::vector<uint8_t>> encoded =
......
// 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/websocket_adapter.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "components/device_event_log/device_event_log.h"
#include "device/fido/fido_constants.h"
namespace device {
namespace cablev2 {
// kMaxIncomingMessageSize is the maximum number of bytes in a single message
// from a WebSocket. This is set to be far larger than any plausible CTAP2
// message and exists to prevent a run away server from using up all memory.
static constexpr size_t kMaxIncomingMessageSize = 1 << 20;
WebSocketAdapter::WebSocketAdapter(TunnelReadyCallback on_tunnel_ready,
TunnelDataCallback on_tunnel_data)
: on_tunnel_ready_(std::move(on_tunnel_ready)),
on_tunnel_data_(std::move(on_tunnel_data)) {}
WebSocketAdapter::~WebSocketAdapter() = default;
mojo::PendingRemote<network::mojom::WebSocketHandshakeClient>
WebSocketAdapter::BindNewHandshakeClientPipe() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto ret = handshake_receiver_.BindNewPipeAndPassRemote();
handshake_receiver_.set_disconnect_handler(base::BindOnce(
&WebSocketAdapter::OnMojoPipeDisconnect, base::Unretained(this)));
return ret;
}
bool WebSocketAdapter::Write(base::span<const uint8_t> data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (closed_ || data.size() > std::numeric_limits<uint32_t>::max()) {
return false;
}
socket_remote_->SendMessage(network::mojom::WebSocketMessageType::BINARY,
data.size());
uint32_t num_bytes = static_cast<uint32_t>(data.size());
MojoResult result = write_pipe_->WriteData(data.data(), &num_bytes,
MOJO_WRITE_DATA_FLAG_ALL_OR_NONE);
DCHECK(result != MOJO_RESULT_OK ||
data.size() == static_cast<size_t>(num_bytes));
return result == MOJO_RESULT_OK;
}
void WebSocketAdapter::OnOpeningHandshakeStarted(
network::mojom::WebSocketHandshakeRequestPtr request) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void WebSocketAdapter::OnConnectionEstablished(
mojo::PendingRemote<network::mojom::WebSocket> socket,
mojo::PendingReceiver<network::mojom::WebSocketClient> client_receiver,
network::mojom::WebSocketHandshakeResponsePtr response,
mojo::ScopedDataPipeConsumerHandle readable,
mojo::ScopedDataPipeProducerHandle writable) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (response->selected_protocol != kCableWebSocketProtocol) {
FIDO_LOG(ERROR) << "Tunnel server didn't select cable protocol";
return;
}
base::Optional<uint8_t> shard_id;
for (const auto& header : response->headers) {
if (base::EqualsCaseInsensitiveASCII(header->name.c_str(),
kCableShardIdHeader)) {
unsigned u;
if (!base::StringToUint(header->value, &u) || shard_id > 63) {
FIDO_LOG(ERROR) << "Invalid shard ID from tunnel server";
return;
}
shard_id = u;
break;
}
}
socket_remote_.Bind(std::move(socket));
read_pipe_ = std::move(readable);
write_pipe_ = std::move(writable);
client_receiver_.Bind(std::move(client_receiver));
socket_remote_->StartReceiving();
std::move(on_tunnel_ready_).Run(true, shard_id);
}
void WebSocketAdapter::OnDataFrame(bool finish,
network::mojom::WebSocketMessageType type,
uint64_t data_len) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const size_t old_size = pending_message_.size();
const size_t new_size = old_size + data_len;
if (type != network::mojom::WebSocketMessageType::BINARY ||
data_len > std::numeric_limits<uint32_t>::max() || new_size < old_size ||
new_size > kMaxIncomingMessageSize) {
FIDO_LOG(ERROR) << "invalid WebSocket frame";
Close();
return;
}
if (data_len > 0) {
pending_message_.resize(new_size);
uint32_t data_len_32 = data_len;
if (read_pipe_->ReadData(&pending_message_.data()[old_size], &data_len_32,
MOJO_READ_DATA_FLAG_ALL_OR_NONE) !=
MOJO_RESULT_OK) {
FIDO_LOG(ERROR) << "reading WebSocket frame failed";
Close();
return;
}
DCHECK_EQ(static_cast<size_t>(data_len_32), data_len);
}
if (finish) {
on_tunnel_data_.Run(pending_message_);
pending_message_.resize(0);
}
}
void WebSocketAdapter::OnDropChannel(bool was_clean,
uint16_t code,
const std::string& reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Close();
}
void WebSocketAdapter::OnClosingHandshake() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void WebSocketAdapter::OnMojoPipeDisconnect() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// If disconnection happens before |OnConnectionEstablished| then report a
// failure to establish the tunnel.
if (on_tunnel_ready_) {
std::move(on_tunnel_ready_).Run(false, base::nullopt);
return;
}
// Otherwise, act as if the TLS connection was closed.
if (!closed_) {
Close();
}
}
void WebSocketAdapter::Close() {
DCHECK(!closed_);
closed_ = true;
client_receiver_.reset();
on_tunnel_data_.Run(base::nullopt);
}
} // namespace cablev2
} // namespace device
// 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.
#ifndef DEVICE_FIDO_CABLE_WEBSOCKET_ADAPTER_H_
#define DEVICE_FIDO_CABLE_WEBSOCKET_ADAPTER_H_
#include <vector>
#include "base/callback_forward.h"
#include "base/component_export.h"
#include "base/containers/span.h"
#include "base/optional.h"
#include "base/sequence_checker.h"
#include "services/network/public/mojom/network_context.mojom.h"
namespace device {
namespace cablev2 {
// WebSocketAdapter implements several network::mojom interfaces needed to
// create a WebSocket connection and translates the Mojo interface into a
// callback-based one.
class COMPONENT_EXPORT(DEVICE_FIDO) WebSocketAdapter
: public network::mojom::WebSocketHandshakeClient,
network::mojom::WebSocketClient {
public:
using TunnelReadyCallback =
base::OnceCallback<void(bool, base::Optional<uint8_t>)>;
using TunnelDataCallback =
base::RepeatingCallback<void(base::Optional<base::span<const uint8_t>>)>;
WebSocketAdapter(
// on_tunnel_ready is called once with a boolean that indicates whether
// the WebSocket successfully connected and an optional shard ID taken
// from the X-caBLE-Shard header in the HTTP response, if any.
TunnelReadyCallback on_tunnel_ready,
// on_tunnel_ready is called repeatedly, after successful connection, with
// the contents of WebSocket messages. Framing is preserved so a single
// message written by the server will result in a single callback.
TunnelDataCallback on_tunnel_data);
~WebSocketAdapter() override;
WebSocketAdapter(const WebSocketAdapter&) = delete;
WebSocketAdapter& operator=(const WebSocketAdapter&) = delete;
mojo::PendingRemote<network::mojom::WebSocketHandshakeClient>
BindNewHandshakeClientPipe();
// Write writes data to the WebSocket server. The amount of data that can be
// written at once is limited by the size of an internal Mojo buffer which
// defaults to 64KiB. Exceeding that will cause the function to return false.
bool Write(base::span<const uint8_t> data);
// WebSocketHandshakeClient:
void OnOpeningHandshakeStarted(
network::mojom::WebSocketHandshakeRequestPtr request) override;
void OnConnectionEstablished(
mojo::PendingRemote<network::mojom::WebSocket> socket,
mojo::PendingReceiver<network::mojom::WebSocketClient> client_receiver,
network::mojom::WebSocketHandshakeResponsePtr response,
mojo::ScopedDataPipeConsumerHandle readable,
mojo::ScopedDataPipeProducerHandle writable) override;
// WebSocketClient:
void OnDataFrame(bool finish,
network::mojom::WebSocketMessageType type,
uint64_t data_len) override;
void OnDropChannel(bool was_clean,
uint16_t code,
const std::string& reason) override;
void OnClosingHandshake() override;
private:
void OnMojoPipeDisconnect();
void Close();
bool closed_ = false;
// pending_message_ contains a partial message that is being reassembled.
std::vector<uint8_t> pending_message_;
TunnelReadyCallback on_tunnel_ready_;
const TunnelDataCallback on_tunnel_data_;
mojo::Receiver<network::mojom::WebSocketHandshakeClient> handshake_receiver_{
this};
mojo::Receiver<network::mojom::WebSocketClient> client_receiver_{this};
mojo::Remote<network::mojom::WebSocket> socket_remote_;
mojo::ScopedDataPipeConsumerHandle read_pipe_;
mojo::ScopedDataPipeProducerHandle write_pipe_;
SEQUENCE_CHECKER(sequence_checker_);
};
} // namespace cablev2
} // namespace device
#endif // DEVICE_FIDO_CABLE_WEBSOCKET_ADAPTER_H_
...@@ -362,6 +362,14 @@ constexpr size_t kMaxKeyHandleLength = 255; ...@@ -362,6 +362,14 @@ constexpr size_t kMaxKeyHandleLength = 255;
// [1] https://source.android.com/devices/accessories/aoa // [1] https://source.android.com/devices/accessories/aoa
constexpr char kCableOverAOAVersion[] = "12eba9f901039b36"; constexpr char kCableOverAOAVersion[] = "12eba9f901039b36";
// kCableWebSocketProtocol is the name of the WebSocket subprotocol used by
// caBLEv2. See https://tools.ietf.org/html/rfc6455#section-1.9.
constexpr char kCableWebSocketProtocol[] = "fido.cable";
// kCableShardIdHeader is the name of an HTTP header that is sent in the reply
// from the tunnel server and which specifies the server's chosen shard number.
constexpr char kCableShardIdHeader[] = "X-caBLE-Shard";
// Maximum wait time before client error outs on device. // Maximum wait time before client error outs on device.
COMPONENT_EXPORT(DEVICE_FIDO) extern const base::TimeDelta kDeviceTimeout; COMPONENT_EXPORT(DEVICE_FIDO) extern const base::TimeDelta kDeviceTimeout;
......
...@@ -30,6 +30,7 @@ Refer to README.md for content description and update process. ...@@ -30,6 +30,7 @@ Refer to README.md for content description and update process.
<item id="bluetooth_socket" hash_code="94099818" type="0" content_hash_code="30932349" os_list="linux,windows" file_path="device/bluetooth/bluetooth_socket_net.cc"/> <item id="bluetooth_socket" hash_code="94099818" type="0" content_hash_code="30932349" os_list="linux,windows" file_path="device/bluetooth/bluetooth_socket_net.cc"/>
<item id="brandcode_config" hash_code="109679553" type="0" content_hash_code="128843792" os_list="linux,windows" file_path="chrome/browser/profile_resetter/brandcode_config_fetcher.cc"/> <item id="brandcode_config" hash_code="109679553" type="0" content_hash_code="128843792" os_list="linux,windows" file_path="chrome/browser/profile_resetter/brandcode_config_fetcher.cc"/>
<item id="browser_switcher_ieem_sitelist" hash_code="97159948" type="0" content_hash_code="129062966" os_list="linux,windows" file_path="chrome/browser/browser_switcher/browser_switcher_service.cc"/> <item id="browser_switcher_ieem_sitelist" hash_code="97159948" type="0" content_hash_code="129062966" os_list="linux,windows" file_path="chrome/browser/browser_switcher/browser_switcher_service.cc"/>
<item id="cablev2_websocket_from_client" hash_code="3464399" type="0" content_hash_code="46324469" os_list="windows,linux" file_path="device/fido/cable/fido_tunnel_device.cc"/>
<item id="captive_portal_service" hash_code="88754904" type="0" content_hash_code="70737580" os_list="linux,windows" file_path="components/captive_portal/content/captive_portal_service.cc"/> <item id="captive_portal_service" hash_code="88754904" type="0" content_hash_code="70737580" os_list="linux,windows" file_path="components/captive_portal/content/captive_portal_service.cc"/>
<item id="cast_channel_send" hash_code="103172229" type="0" deprecated="2018-08-23" content_hash_code="33946302" file_path=""/> <item id="cast_channel_send" hash_code="103172229" type="0" deprecated="2018-08-23" content_hash_code="33946302" file_path=""/>
<item id="cast_keep_alive_delegate" hash_code="134755844" type="0" deprecated="2018-08-23" content_hash_code="66118796" file_path=""/> <item id="cast_keep_alive_delegate" hash_code="134755844" type="0" deprecated="2018-08-23" content_hash_code="66118796" file_path=""/>
......
...@@ -383,5 +383,8 @@ hidden="true" so that these annotations don't show up in the document. ...@@ -383,5 +383,8 @@ hidden="true" so that these annotations don't show up in the document.
<sender name="Assist Rank"> <sender name="Assist Rank">
<traffic_annotation unique_id="ranker_url_fetcher"/> <traffic_annotation unique_id="ranker_url_fetcher"/>
</sender> </sender>
<sender name="Phone as a Security Key">
<traffic_annotation unique_id="cablev2_websocket_from_client"/>
</sender>
</group> </group>
</groups> </groups>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment