Commit 7255f0fd authored by Adam Langley's avatar Adam Langley Committed by Commit Bot

cablev2: client updates for state-assisted transactions.

This change mostly updates FidoTunnelDevice to add support for
state-assisted (i.e. paired) transactions. That also involves some
handshaking updates because the state information signs the claimed
public key and so the handshake needs to export a binding value to sign
over.

BUG=1002262

Change-Id: Ib84886baacef24a8f55014b1a9b33617d0122d11
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2406834
Commit-Queue: Adam Langley <agl@chromium.org>
Auto-Submit: Adam Langley <agl@chromium.org>
Reviewed-by: default avatarMartin Kreichgauer <martinkr@google.com>
Cr-Commit-Position: refs/heads/master@{#807621}
parent f08a7a54
......@@ -145,6 +145,10 @@ class Defragmenter {
bool expect_message_start_ = true;
};
typedef std::pair<std::array<uint8_t, device::cablev2::kNonceSize>,
std::array<uint8_t, device::kCableEphemeralIdSize>>
NonceAndEID;
// AuthenticatorState contains the keys for a caBLE v2 authenticator.
struct AuthenticatorState {
// pairing_data contains long-term keys, and information that is potentially
......@@ -156,13 +160,13 @@ struct AuthenticatorState {
// pairing_advert contains information about the BLE advert that is sent based
// on the long-term keys.
device::cablev2::NonceAndEID pairing_advert;
NonceAndEID pairing_advert;
// If doing a QR pairing, the following two members will be present.
// qr_advert contains information about the BLE advert that is sent based on
// QR pairing keys.
base::Optional<device::cablev2::NonceAndEID> qr_advert;
base::Optional<NonceAndEID> qr_advert;
// qr_psk_gen_key contains the PSK generating key derived from the QR secret.
base::Optional<device::CablePskGeneratorKey> qr_psk_gen_key;
// peer_identity is the public-key of the desktop from the scanned QR code.
......@@ -869,7 +873,7 @@ class CableInterface : public BLEClient::Delegate {
CableInterface() = default;
void StartAdvertising(const device::CableEidGeneratorKey& eid_gen_key,
device::cablev2::NonceAndEID* out_nonce_and_eid) {
NonceAndEID* out_nonce_and_eid) {
std::array<uint8_t, device::kCableNonceSize> nonce;
crypto::RandBytes(nonce);
......
......@@ -181,8 +181,10 @@ base::span<uint8_t> QRDataForCurrentTime(
uint8_t out_buf[QRCode::V5::kInputBytes],
base::span<const uint8_t, 32> qr_generator_key) {
const int64_t current_tick = device::CableDiscoveryData::CurrentTimeTick();
// TODO(agl): fix this. Currently doing this in order to split up CLs.
device::QRGeneratorKey temp_key = {0};
const device::CableQRData qr_data =
device::CableDiscoveryData::DeriveQRData(qr_generator_key, current_tick);
device::CableDiscoveryData::DeriveQRData(temp_key, current_tick);
std::string base64_qr_data;
base::Base64UrlEncode(
......
......@@ -362,7 +362,10 @@ class AuthenticatorRequestDialogModel {
}
base::span<const uint8_t, 32> qr_generator_key() const {
return *qr_generator_key_;
// TODO(agl): return the true generator key. This is currently broken to
// allow CLs to be split up reasonably.
static_assert(EXTENT(*qr_generator_key_) >= 32, "");
return base::span<const uint8_t, 32>(qr_generator_key_->data(), 32);
}
void CollectPIN(base::Optional<int> attempts,
......
......@@ -7,6 +7,7 @@
#include <cstring>
#include "base/time/time.h"
#include "components/cbor/values.h"
#include "crypto/random.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/fido_parsing_utils.h"
......@@ -26,7 +27,7 @@ enum class QRValue : uint8_t {
IDENTITY_KEY_SEED = 1,
};
void DeriveQRValue(base::span<const uint8_t, 32> qr_generator_key,
void DeriveQRValue(base::span<const uint8_t, kCableQRDataSize> qr_generator_key,
const int64_t tick,
QRValue type,
base::span<uint8_t> out) {
......@@ -157,7 +158,7 @@ int64_t CableDiscoveryData::CurrentTimeTick() {
// static
std::array<uint8_t, kCableQRSecretSize> CableDiscoveryData::DeriveQRSecret(
base::span<const uint8_t, 32> qr_generator_key,
base::span<const uint8_t, kCableQRDataSize> qr_generator_key,
const int64_t tick) {
std::array<uint8_t, kCableQRSecretSize> ret;
DeriveQRValue(qr_generator_key, tick, QRValue::QR_SECRET, ret);
......@@ -166,7 +167,7 @@ std::array<uint8_t, kCableQRSecretSize> CableDiscoveryData::DeriveQRSecret(
// static
CableIdentityKeySeed CableDiscoveryData::DeriveIdentityKeySeed(
base::span<const uint8_t, 32> qr_generator_key,
base::span<const uint8_t, kCableQRDataSize> qr_generator_key,
const int64_t tick) {
std::array<uint8_t, kCableIdentityKeySeedSize> ret;
DeriveQRValue(qr_generator_key, tick, QRValue::IDENTITY_KEY_SEED, ret);
......@@ -175,7 +176,7 @@ CableIdentityKeySeed CableDiscoveryData::DeriveIdentityKeySeed(
// static
CableQRData CableDiscoveryData::DeriveQRData(
base::span<const uint8_t, 32> qr_generator_key,
base::span<const uint8_t, kCableQRDataSize> qr_generator_key,
const int64_t tick) {
auto identity_key_seed = DeriveIdentityKeySeed(qr_generator_key, tick);
bssl::UniquePtr<EC_GROUP> p256(
......@@ -229,4 +230,59 @@ void CableDiscoveryData::InitFromQRSecret(
DCHECK(ok);
}
namespace cablev2 {
Pairing::Pairing() = default;
Pairing::~Pairing() = default;
// static
base::Optional<std::unique_ptr<Pairing>> Pairing::Parse(
const cbor::Value& cbor,
uint32_t tunnel_server_domain,
base::span<const uint8_t, kCableIdentityKeySeedSize> local_identity_seed,
base::span<const uint8_t, 32> handshake_hash) {
if (!cbor.is_map()) {
return base::nullopt;
}
const cbor::Value::MapValue& map = cbor.GetMap();
auto pairing = std::make_unique<Pairing>();
const std::array<cbor::Value::MapValue::const_iterator, 5> its = {
map.find(cbor::Value(1)), map.find(cbor::Value(2)),
map.find(cbor::Value(3)), map.find(cbor::Value(4)),
map.find(cbor::Value(6))};
const cbor::Value::MapValue::const_iterator name_it =
map.find(cbor::Value(5));
if (name_it == map.end() || !name_it->second.is_string() ||
std::any_of(
&its[0], &its[its.size()],
[&map](const cbor::Value::MapValue::const_iterator& it) -> bool {
return it == map.end() || !it->second.is_bytestring();
}) ||
its[3]->second.GetBytestring().size() !=
std::tuple_size<decltype(pairing->peer_public_key_x962)>::value) {
}
pairing->tunnel_server_domain =
tunnelserver::DecodeDomain(tunnel_server_domain),
pairing->contact_id = its[0]->second.GetBytestring();
pairing->id = its[1]->second.GetBytestring();
pairing->secret = its[2]->second.GetBytestring();
const std::vector<uint8_t>& peer_public_key = its[3]->second.GetBytestring();
std::copy(peer_public_key.begin(), peer_public_key.end(),
pairing->peer_public_key_x962.begin());
pairing->name = name_it->second.GetString();
if (!VerifyPairingSignature(local_identity_seed,
pairing->peer_public_key_x962, handshake_hash,
its[4]->second.GetBytestring())) {
return base::nullopt;
}
return pairing;
}
} // namespace cablev2
} // namespace device
......@@ -10,8 +10,13 @@
#include "base/component_export.h"
#include "base/containers/span.h"
#include "base/optional.h"
#include "device/fido/fido_constants.h"
namespace cbor {
class Value;
}
namespace device {
constexpr size_t kCableEphemeralIdSize = 16;
......@@ -26,10 +31,9 @@ constexpr size_t kCableQRDataSize =
using CableEidArray = std::array<uint8_t, kCableEphemeralIdSize>;
using CableSessionPreKeyArray = std::array<uint8_t, kCableSessionPreKeySize>;
// QRGeneratorKey is a random, AES-256 key that is used by
// |CableDiscoveryData::DeriveQRKeyMaterial| to encrypt a coarse timestamp and
// generate QR secrets, EIDs, etc.
using QRGeneratorKey = std::array<uint8_t, 32>;
// QRGeneratorKey is a hang-over from old code that hasn't been renamed yet.
// TODO(agl): remove
using QRGeneratorKey = std::array<uint8_t, kCableQRDataSize>;
// CableNonce is a nonce used in BLE handshaking.
using CableNonce = std::array<uint8_t, 8>;
// CableEidGeneratorKey is an AES-256 key that is used to encrypt a 64-bit nonce
......@@ -105,20 +109,20 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) CableDiscoveryData {
// DeriveQRKeyMaterial returns a QR-secret given a generating key and a
// timestamp.
static std::array<uint8_t, kCableQRSecretSize> DeriveQRSecret(
base::span<const uint8_t, 32> qr_generator_key,
base::span<const uint8_t, kCableQRDataSize> qr_generator_key,
const int64_t tick);
// DeriveIdentityKeySeed returns a seed that can be used to create a P-256
// identity key for a handshake using |EC_KEY_derive_from_secret|.
static CableIdentityKeySeed DeriveIdentityKeySeed(
base::span<const uint8_t, 32> qr_generator_key,
base::span<const uint8_t, kCableQRDataSize> qr_generator_key,
const int64_t tick);
// DeriveQRData returns the QR data, a combination of QR secret and public
// identity key. This is base64url-encoded and placed in a caBLE v2 QR code
// with a prefix prepended.
static CableQRData DeriveQRData(
base::span<const uint8_t, 32> qr_generator_key,
base::span<const uint8_t, kCableQRDataSize> qr_generator_key,
const int64_t tick);
// version indicates whether v1 or v2 data is contained in this object.
......@@ -156,6 +160,47 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) CableDiscoveryData {
base::span<const uint8_t, kCableQRSecretSize> qr_secret);
};
namespace cablev2 {
// Pairing represents information previously received from a caBLEv2
// authenticator that enables future interactions to skip scanning a QR code.
struct COMPONENT_EXPORT(DEVICE_FIDO) Pairing {
Pairing();
~Pairing();
Pairing(const Pairing&) = delete;
Pairing& operator=(const Pairing&) = delete;
// Parse builds a |Pairing| from an authenticator message. The signature
// within the structure is validated by using |local_identity_seed| and
// |handshake_hash|.
static base::Optional<std::unique_ptr<Pairing>> Parse(
const cbor::Value& cbor,
uint32_t tunnel_server_domain,
base::span<const uint8_t, kCableIdentityKeySeedSize> local_identity_seed,
base::span<const uint8_t, 32> handshake_hash);
// tunnel_server_domain is known to be a valid hostname as it's constructed
// from the 22-bit value in the BLE advert rather than being parsed as a
// string from the authenticator.
std::string tunnel_server_domain;
// contact_id is an opaque value that is sent to the tunnel service in order
// to identify the caBLEv2 authenticator.
std::vector<uint8_t> contact_id;
// id is an opaque identifier that is sent via the tunnel service, to the
// authenticator, to identify this specific pairing.
std::vector<uint8_t> id;
// secret is the shared secret that authenticates the desktop to the
// authenticator.
std::vector<uint8_t> secret;
// peer_public_key_x962 is the authenticator's public key.
std::array<uint8_t, kP256X962Length> peer_public_key_x962;
// name is a human-friendly name for the authenticator, specified by that
// authenticator. (For example "Pixel 3".)
std::string name;
};
} // namespace cablev2
} // namespace device
#endif // DEVICE_FIDO_CABLE_CABLE_DISCOVERY_DATA_H_
......@@ -526,9 +526,9 @@ void FidoCableDiscovery::CableDeviceFound(BluetoothAdapter* adapter,
!network_context_) {
return;
}
AddDevice(std::make_unique<cablev2::FidoTunnelDevice>(
network_context_, *result->discovery_data.v2, result->eid,
*result->decrypted_eid));
// Disabled in order to make each step of a multi-CL change compile.
// TODO(agl): reenable
// AddDevice(...);
break;
}
......
......@@ -343,8 +343,8 @@ class FakeFidoCableDiscovery : public FidoCableDiscovery {
device, nonce, discovery_data.v1->session_pre_key);
}
static std::array<uint8_t, 32> BogusQRGeneratorKey() {
std::array<uint8_t, 32> ret;
static std::array<uint8_t, kCableQRDataSize> BogusQRGeneratorKey() {
std::array<uint8_t, kCableQRDataSize> ret;
memset(ret.data(), 0, ret.size());
return ret;
}
......
This diff is collapsed.
......@@ -7,11 +7,12 @@
#include <vector>
#include "base/callback_forward.h"
#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"
#include "third_party/abseil-cpp/absl/types/variant.h"
namespace network {
namespace mojom {
......@@ -24,15 +25,30 @@ namespace cablev2 {
class Crypter;
class WebSocketAdapter;
struct Pairing;
class COMPONENT_EXPORT(DEVICE_FIDO) FidoTunnelDevice : public FidoDevice {
public:
// This constructor is used for QR-initiated connections.
FidoTunnelDevice(
network::mojom::NetworkContext* network_context,
base::OnceCallback<void(std::unique_ptr<Pairing>)> pairing_callback,
base::span<const uint8_t> secret,
base::span<const uint8_t, kCableIdentityKeySeedSize> local_identity_seed,
const CableEidArray& eid,
const CableEidArray& decrypted_eid);
// This constructor is used for pairing-initiated connections.
FidoTunnelDevice(network::mojom::NetworkContext* network_context,
const CableDiscoveryData::V2Data& v2data,
const CableEidArray& eid,
const CableEidArray& decrypted_eid);
std::unique_ptr<Pairing> pairing);
~FidoTunnelDevice() override;
// MatchEID is only valid for a pairing-initiated connection. It returns true
// if the given |eid| matched this pending tunnel and thus this device is now
// ready.
bool MatchEID(const CableEidArray& eid);
// FidoDevice:
CancelToken DeviceTransact(std::vector<uint8_t> command,
DeviceCallback callback) override;
......@@ -45,20 +61,54 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoTunnelDevice : public FidoDevice {
enum class State {
kConnecting,
kConnected,
kWaitingForEID,
kHandshakeProcessed,
kReady,
kError,
};
void OnTunnelReady(bool ok, base::Optional<uint8_t> shard_id);
struct QRInfo {
QRInfo();
~QRInfo();
QRInfo(const QRInfo&) = delete;
QRInfo& operator=(const QRInfo&) = delete;
CableEidArray eid;
std::array<uint8_t, 32> psk;
base::OnceCallback<void(std::unique_ptr<Pairing>)> pairing_callback;
std::array<uint8_t, kCableIdentityKeySeedSize> local_identity_seed;
uint32_t tunnel_server_domain;
base::Optional<HandshakeHash> handshake_hash;
};
struct PairedInfo {
PairedInfo();
~PairedInfo();
PairedInfo(const PairedInfo&) = delete;
PairedInfo& operator=(const PairedInfo&) = delete;
std::array<uint8_t, 32> eid_encryption_key;
std::array<uint8_t, kP256X962Length> peer_identity;
std::vector<uint8_t> secret;
base::Optional<CableEidArray> eid;
base::Optional<std::array<uint8_t, 32>> psk;
base::Optional<std::vector<uint8_t>> handshake_message;
};
// This is a dummy function to allow things to compile at each step of a
// multi-CL sequence.
void OnTunnelReady(bool ok, base::Optional<uint8_t> routing_id);
void OnTunnelReady_Future(
bool ok,
base::Optional<std::array<uint8_t, kRoutingIdSize>> routing_id);
void OnTunnelData(base::Optional<base::span<const uint8_t>> data);
void ProcessHandshake(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_;
absl::variant<QRInfo, PairedInfo> info_;
const std::array<uint8_t, 8> id_;
std::unique_ptr<WebSocketAdapter> websocket_client_;
std::unique_ptr<Crypter> crypter_;
std::vector<uint8_t> getinfo_response_bytes_;
......
......@@ -124,6 +124,10 @@ base::Optional<std::vector<uint8_t>> Noise::DecryptAndHash(
return plaintext;
}
std::array<uint8_t, 32> Noise::handshake_hash() const {
return h_;
}
void Noise::MixHashPoint(const EC_POINT* point) {
uint8_t x962[kP256X962Length];
bssl::UniquePtr<EC_GROUP> p256(
......
......@@ -41,6 +41,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) Noise {
std::vector<uint8_t> EncryptAndHash(base::span<const uint8_t> plaintext);
base::Optional<std::vector<uint8_t>> DecryptAndHash(
base::span<const uint8_t> ciphertext);
std::array<uint8_t, 32> handshake_hash() const;
// MaxHashPoint calls |MixHash| with the uncompressed, X9.62 serialization of
// |point|.
......
This diff is collapsed.
......@@ -16,6 +16,7 @@
#include "components/cbor/values.h"
#include "device/fido/cable/cable_discovery_data.h"
#include "device/fido/cable/noise.h"
#include "device/fido/cable/v2_constants.h"
#include "device/fido/fido_constants.h"
#include "third_party/boringssl/src/include/openssl/base.h"
......@@ -24,8 +25,6 @@ class GURL;
namespace device {
namespace cablev2 {
constexpr size_t kNonceSize = 10;
namespace tunnelserver {
// Base32Ord converts |c| into its base32 value, as defined in
......@@ -62,26 +61,41 @@ constexpr uint32_t EncodeDomain(const char label[5], TLD tld) {
tld_value;
}
// Action enumerates the two possible requests that can be made of a tunnel
// server: to create a new tunnel or to connect to an existing one.
enum class Action {
kNew,
kConnect,
};
// DecodeDomain converts a 22-bit tunnel server domain (as encoded by
// |EncodeDomain|) into a string in dotted form.
COMPONENT_EXPORT(DEVICE_FIDO) std::string DecodeDomain(uint32_t domain);
// GetNewTunnelURL converts a 22-bit tunnel server domain (as encoded by
// |EncodeDomain|), and a tunnel ID, into a WebSockets-based URL for creating a
// new tunnel.
COMPONENT_EXPORT(DEVICE_FIDO)
GURL GetNewTunnelURL(uint32_t domain, base::span<const uint8_t, 16> id);
// GetConnectURL converts a 22-bit tunnel server domain (as encoded by
// |EncodeDomain|), a routing-ID, and a tunnel ID, into a WebSockets-based URL
// for connecting to an existing tunnel.
COMPONENT_EXPORT(DEVICE_FIDO)
GURL GetConnectURL(uint32_t domain,
std::array<uint8_t, kRoutingIdSize> routing_id,
base::span<const uint8_t, 16> id);
// GetURL converts a 22-bit tunnel server domain (as encoded by |EncodeDomain|),
// an action, and a tunnel ID, into a WebSockets-based URL.
// GetContactURL gets a URL for contacting a previously-paired authenticator.
// The |tunnel_server| is assumed to be a valid domain name and should have been
// taken from a previous call to |DecodeDomain|.
COMPONENT_EXPORT(DEVICE_FIDO)
GURL GetURL(uint32_t domain, Action action, base::span<const uint8_t, 16> id);
GURL GetContactURL(const std::string& tunnel_server,
base::span<const uint8_t> contact_id);
} // namespace tunnelserver
namespace eid {
// TODO(agl): this could probably be a class.
// Components contains the parts of a decrypted EID.
struct Components {
uint8_t shard_id;
uint32_t tunnel_server_domain;
std::array<uint8_t, kRoutingIdSize> routing_id;
std::array<uint8_t, kNonceSize> nonce;
};
......@@ -102,6 +116,39 @@ Components ToComponents(const CableEidArray& eid);
} // namespace eid
// DerivedValueType enumerates the different types of values that might be
// derived in caBLEv2 from some secret. The values this this enum are protocol
// constants and thus must not change over time.
enum class DerivedValueType : uint32_t {
kEIDKey = 1,
kTunnelID = 2,
kPSK = 3,
kPairedSecret = 4,
kIdentityKeySeed = 5,
};
namespace internal {
COMPONENT_EXPORT(DEVICE_FIDO)
void Derive(uint8_t* out,
size_t out_len,
base::span<const uint8_t> secret,
base::span<const uint8_t> nonce,
DerivedValueType type);
} // namespace internal
// Derive derives a sub-secret from a secret and nonce. It is not possible to
// learn anything about |secret| from the value of the sub-secret, assuming that
// |secret| has sufficient size to prevent full enumeration of the
// possibilities.
template <size_t N>
std::array<uint8_t, N> Derive(base::span<const uint8_t> secret,
base::span<const uint8_t> nonce,
DerivedValueType type) {
std::array<uint8_t, N> ret;
internal::Derive(ret.data(), N, secret, nonce, type);
return ret;
}
// 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
// assumption that the returned bytes will be encrypted and the encoded size of
......@@ -117,12 +164,6 @@ COMPONENT_EXPORT(DEVICE_FIDO)
base::Optional<cbor::Value> DecodePaddedCBORMap(
base::span<const uint8_t> input);
// NonceAndEID contains both the random nonce chosen for an advert, as well as
// the EID that was generated from it.
typedef std::pair<std::array<uint8_t, kNonceSize>,
std::array<uint8_t, device::kCableEphemeralIdSize>>
NonceAndEID;
// Crypter handles the post-handshake encryption of CTAP2 messages.
class COMPONENT_EXPORT(DEVICE_FIDO) Crypter {
public:
......@@ -153,18 +194,20 @@ class COMPONENT_EXPORT(DEVICE_FIDO) Crypter {
uint32_t write_sequence_num_ = 0;
};
// HandshakeHash is the hashed transcript of a handshake. This can be used as a
// channel-binding value. See
// http://www.noiseprotocol.org/noise.html#channel-binding.
using HandshakeHash = std::array<uint8_t, 32>;
// HandshakeInitiator starts a caBLE v2 handshake and processes the single
// response message from the other party. The handshake is always initiated from
// the phone.
class COMPONENT_EXPORT(DEVICE_FIDO) HandshakeInitiator {
public:
HandshakeInitiator(
// psk_gen_key is either derived from QR-code secrets or comes from
// pairing data.
base::span<const uint8_t, 32> psk_gen_key,
// nonce is randomly generated per advertisement and ensures that BLE
// adverts are non-deterministic.
base::span<const uint8_t, kNonceSize> nonce,
// psk is derived from the connection nonce and either QR-code secrets
// pairing secrets.
base::span<const uint8_t, 32> psk,
// peer_identity, if not nullopt, specifies that this is a QR handshake
// and then contains a P-256 public key for the peer. Otherwise this is a
// paired handshake.
......@@ -190,9 +233,9 @@ class COMPONENT_EXPORT(DEVICE_FIDO) HandshakeInitiator {
// ProcessResponse processes the handshake response from the peer. If
// successful it returns a |Crypter| for protecting future messages on the
// connection.
base::Optional<std::unique_ptr<Crypter>> ProcessResponse(
base::span<const uint8_t> response);
// connection and a handshake transcript for signing over if needed.
base::Optional<std::pair<std::unique_ptr<Crypter>, HandshakeHash>>
ProcessResponse(base::span<const uint8_t> response);
private:
Noise noise_;
......@@ -203,17 +246,33 @@ class COMPONENT_EXPORT(DEVICE_FIDO) HandshakeInitiator {
bssl::UniquePtr<EC_KEY> ephemeral_key_;
};
// RespondToHandshake responds to a caBLE v2 handshake started by a peer. It
// returns a Crypter for encrypting and decrypting future messages, as well as
// the getInfo response from the phone.
// ResponderResult is the result of a successful handshake from the responder's
// side. It contains a Crypter for protecting future messages, the contents of
// the getInfo response given by the peer, and a hash of the handshake
// transcript.
struct COMPONENT_EXPORT(DEVICE_FIDO) ResponderResult {
ResponderResult(std::unique_ptr<Crypter>,
std::vector<uint8_t> getinfo_bytes,
HandshakeHash);
~ResponderResult();
ResponderResult(const ResponderResult&) = delete;
ResponderResult(ResponderResult&&);
ResponderResult& operator=(const ResponderResult&) = delete;
std::unique_ptr<Crypter> crypter;
std::vector<uint8_t> getinfo_bytes;
const HandshakeHash handshake_hash;
};
// RespondToHandshake responds to a caBLE v2 handshake started by a peer.
COMPONENT_EXPORT(DEVICE_FIDO)
base::Optional<std::pair<std::unique_ptr<Crypter>, std::vector<uint8_t>>>
RespondToHandshake(
// For the first two arguments see |HandshakeInitiator| comments about
// |psk_gen_key| and |nonce|, and the |BuildInitialMessage| comment about
// |eid|.
base::span<const uint8_t, 32> psk_gen_key,
const NonceAndEID& nonce_and_eid,
base::Optional<ResponderResult> RespondToHandshake(
// psk is derived from the connection nonce and either QR-code secrets or
// pairing secrets.
base::span<const uint8_t, 32> psk,
// eid is the EID that was advertised for this handshake. This is checked
// as part of the handshake.
base::span<const uint8_t, kCableEphemeralIdSize> eid,
// identity_seed, if not nullopt, specifies that this is a QR handshake and
// contains the seed for QR key for this client.
base::Optional<base::span<const uint8_t, kCableIdentityKeySeedSize>>
......@@ -226,6 +285,27 @@ RespondToHandshake(
// out_response is set to the response handshake message, if successful.
std::vector<uint8_t>* out_response);
// VerifyPairingSignature checks that |signature| is a valid signature of
// |handshake_hash| by |peer_public_key_x962|. This is used by a phone to prove
// possession of |peer_public_key_x962| since the |handshake_hash| encloses
// random values generated by the desktop and thus is a fresh value.
COMPONENT_EXPORT(DEVICE_FIDO)
bool VerifyPairingSignature(
base::span<const uint8_t, kCableIdentityKeySeedSize> identity_seed,
base::span<const uint8_t, kP256X962Length> peer_public_key_x962,
base::span<const uint8_t, std::tuple_size<HandshakeHash>::value>
handshake_hash,
base::span<const uint8_t> signature);
// CalculatePairingSignature generates a value that will satisfy
// |VerifyPairingSignature|.
COMPONENT_EXPORT(DEVICE_FIDO)
std::vector<uint8_t> CalculatePairingSignature(
const EC_KEY* identity_key,
base::span<const uint8_t, kP256X962Length> peer_public_key_x962,
base::span<const uint8_t, std::tuple_size<HandshakeHash>::value>
handshake_hash);
} // namespace cablev2
} // namespace device
......
......@@ -17,13 +17,11 @@ namespace device {
namespace {
constexpr std::array<uint8_t, 32> kTestPSKGeneratorKey = {
constexpr std::array<uint8_t, 32> kTestPSK = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
};
constexpr std::array<uint8_t, cablev2::kNonceSize> kTestNonce = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
constexpr std::array<uint8_t, 16> kTestEphemeralID = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
constexpr std::array<uint8_t, 65> kTestPeerIdentity = {
......@@ -66,16 +64,13 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* raw_data, size_t size) {
}
if (initiate) {
cablev2::HandshakeInitiator handshaker(kTestPSKGeneratorKey, kTestNonce,
peer_identity, std::move(local_key));
cablev2::HandshakeInitiator handshaker(kTestPSK, peer_identity,
std::move(local_key));
handshaker.BuildInitialMessage(kTestEphemeralID, kTestGetInfoBytes);
handshaker.ProcessResponse(input);
} else {
cablev2::NonceAndEID nonce_and_eid;
nonce_and_eid.first = kTestNonce;
nonce_and_eid.second = kTestEphemeralID;
std::vector<uint8_t> response;
cablev2::RespondToHandshake(kTestPSKGeneratorKey, nonce_and_eid, local_seed,
cablev2::RespondToHandshake(kTestPSK, kTestEphemeralID, local_seed,
peer_identity, input, &response);
}
......
......@@ -3,8 +3,8 @@
// found in the LICENSE file.
#include "device/fido/cable/v2_handshake.h"
#include "base/rand_util.h"
#include "components/cbor/values.h"
#include "crypto/random.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_key.h"
......@@ -21,21 +21,21 @@ TEST(CableV2Encoding, TunnelServerURLs) {
constexpr uint32_t encoded =
tunnelserver::EncodeDomain("abcd", tunnelserver::TLD::NET);
uint8_t tunnel_id[16] = {0};
const GURL url =
tunnelserver::GetURL(encoded, tunnelserver::Action::kNew, tunnel_id);
const GURL url = tunnelserver::GetNewTunnelURL(encoded, tunnel_id);
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());
components.routing_id = {9, 10, 11};
crypto::RandBytes(components.nonce);
CableEidArray eid = eid::FromComponents(components);
EXPECT_TRUE(eid::IsValid(eid));
eid::Components components2 = eid::ToComponents(eid);
EXPECT_EQ(components.shard_id, components2.shard_id);
EXPECT_EQ(components.routing_id, components2.routing_id);
EXPECT_EQ(components.tunnel_server_domain, components2.tunnel_server_domain);
EXPECT_EQ(components.nonce, components2.nonce);
......@@ -68,13 +68,56 @@ TEST(CableV2Encoding, PaddedCBOR) {
EXPECT_EQ(1u, decoded->GetMap().size());
}
std::array<uint8_t, kP256X962Length> PublicKeyOf(const EC_KEY* private_key) {
std::array<uint8_t, kP256X962Length> ret;
CHECK_EQ(ret.size(),
EC_POINT_point2oct(EC_KEY_get0_group(private_key),
EC_KEY_get0_public_key(private_key),
POINT_CONVERSION_UNCOMPRESSED, ret.data(),
ret.size(), /*ctx=*/nullptr));
return ret;
}
TEST(CableV2Encoding, HandshakeSignatures) {
static const uint8_t kSeed0[kCableIdentityKeySeedSize] = {0};
static const uint8_t kSeed1[kCableIdentityKeySeedSize] = {1};
bssl::UniquePtr<EC_GROUP> group(
EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
bssl::UniquePtr<EC_KEY> authenticator_key(
EC_KEY_derive_from_secret(group.get(), kSeed0, sizeof(kSeed0)));
bssl::UniquePtr<EC_KEY> client_key(
EC_KEY_derive_from_secret(group.get(), kSeed1, sizeof(kSeed1)));
const std::array<uint8_t, kP256X962Length> authenticator_public_key =
PublicKeyOf(authenticator_key.get());
const std::array<uint8_t, kP256X962Length> client_public_key =
PublicKeyOf(client_key.get());
HandshakeHash handshake_hash = {1};
std::vector<uint8_t> signature = CalculatePairingSignature(
authenticator_key.get(), client_public_key, handshake_hash);
EXPECT_TRUE(VerifyPairingSignature(kSeed1, authenticator_public_key,
handshake_hash, signature));
handshake_hash[0] ^= 1;
EXPECT_FALSE(VerifyPairingSignature(kSeed1, authenticator_public_key,
handshake_hash, signature));
handshake_hash[0] ^= 1;
signature[0] ^= 1;
EXPECT_FALSE(VerifyPairingSignature(kSeed1, authenticator_public_key,
handshake_hash, signature));
signature[0] ^= 1;
}
class CableV2HandshakeTest : public ::testing::Test {
public:
CableV2HandshakeTest() {
std::fill(psk_gen_key_.begin(), psk_gen_key_.end(), 0);
std::fill(nonce_and_eid_.first.begin(), nonce_and_eid_.first.end(), 1);
std::fill(nonce_and_eid_.second.begin(), nonce_and_eid_.second.end(), 2);
std::fill(identity_seed_.begin(), identity_seed_.end(), 3);
std::fill(psk_.begin(), psk_.end(), 0);
std::fill(eid_.begin(), eid_.end(), 1);
std::fill(identity_seed_.begin(), identity_seed_.end(), 2);
bssl::UniquePtr<EC_GROUP> group(
EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
......@@ -88,8 +131,8 @@ class CableV2HandshakeTest : public ::testing::Test {
}
protected:
std::array<uint8_t, 32> psk_gen_key_;
NonceAndEID nonce_and_eid_;
std::array<uint8_t, 32> psk_;
CableEidArray eid_;
bssl::UniquePtr<EC_KEY> identity_key_;
std::array<uint8_t, kP256X962Length> identity_public_;
std::array<uint8_t, kCableIdentityKeySeedSize> identity_seed_;
......@@ -123,34 +166,33 @@ TEST_F(CableV2HandshakeTest, MessageEncrytion) {
}
TEST_F(CableV2HandshakeTest, QRHandshake) {
std::array<uint8_t, 32> wrong_psk_gen_key = psk_gen_key_;
wrong_psk_gen_key[0] ^= 1;
std::array<uint8_t, 32> wrong_psk = psk_;
wrong_psk[0] ^= 1;
uint8_t kGetInfoBytes[] = {1, 2, 3, 4, 5};
for (const bool use_correct_key : {false, true}) {
HandshakeInitiator initiator(
use_correct_key ? psk_gen_key_ : wrong_psk_gen_key,
nonce_and_eid_.first, identity_public_,
/*local_identity=*/nullptr);
HandshakeInitiator initiator(use_correct_key ? psk_ : wrong_psk,
identity_public_,
/*local_identity=*/nullptr);
std::vector<uint8_t> message =
initiator.BuildInitialMessage(nonce_and_eid_.second, kGetInfoBytes);
initiator.BuildInitialMessage(eid_, kGetInfoBytes);
std::vector<uint8_t> response;
base::Optional<std::pair<std::unique_ptr<Crypter>, std::vector<uint8_t>>>
responder_result(RespondToHandshake(
psk_gen_key_, nonce_and_eid_, identity_seed_,
/*peer_identity=*/base::nullopt, message, &response));
base::Optional<ResponderResult> responder_result(RespondToHandshake(
psk_, eid_, identity_seed_,
/*peer_identity=*/base::nullopt, message, &response));
ASSERT_EQ(responder_result.has_value(), use_correct_key);
if (!use_correct_key) {
continue;
}
base::Optional<std::unique_ptr<Crypter>> initiator_result(
initiator.ProcessResponse(response));
base::Optional<std::pair<std::unique_ptr<Crypter>, HandshakeHash>>
initiator_result(initiator.ProcessResponse(response));
ASSERT_TRUE(initiator_result.has_value());
EXPECT_TRUE(responder_result->first->IsCounterpartyOfForTesting(
*initiator_result.value()));
ASSERT_EQ(responder_result->second.size(), sizeof(kGetInfoBytes));
EXPECT_EQ(0, memcmp(responder_result->second.data(), kGetInfoBytes,
EXPECT_EQ(initiator_result->second, responder_result->handshake_hash);
EXPECT_TRUE(responder_result->crypter->IsCounterpartyOfForTesting(
*initiator_result->first));
ASSERT_EQ(responder_result->getinfo_bytes.size(), sizeof(kGetInfoBytes));
EXPECT_EQ(0, memcmp(responder_result->getinfo_bytes.data(), kGetInfoBytes,
sizeof(kGetInfoBytes)));
}
}
......@@ -166,30 +208,28 @@ TEST_F(CableV2HandshakeTest, PairedHandshake) {
EC_KEY* const key = use_correct_key ? identity_key_.get() : wrong_key.get();
EC_KEY_up_ref(key);
HandshakeInitiator initiator(psk_gen_key_, nonce_and_eid_.first,
HandshakeInitiator initiator(psk_,
/*peer_identity=*/base::nullopt,
bssl::UniquePtr<EC_KEY>(key));
std::vector<uint8_t> message =
initiator.BuildInitialMessage(nonce_and_eid_.second, kGetInfoBytes);
initiator.BuildInitialMessage(eid_, kGetInfoBytes);
std::vector<uint8_t> response;
base::Optional<std::pair<std::unique_ptr<Crypter>, std::vector<uint8_t>>>
responder_result(RespondToHandshake(psk_gen_key_, nonce_and_eid_,
/*identity_seed=*/base::nullopt,
identity_public_, message,
&response));
base::Optional<ResponderResult> responder_result(RespondToHandshake(
psk_, eid_,
/*identity_seed=*/base::nullopt, identity_public_, message, &response));
ASSERT_EQ(responder_result.has_value(), use_correct_key);
if (!use_correct_key) {
continue;
}
base::Optional<std::unique_ptr<Crypter>> initiator_result(
initiator.ProcessResponse(response));
base::Optional<std::pair<std::unique_ptr<Crypter>, HandshakeHash>>
initiator_result(initiator.ProcessResponse(response));
ASSERT_TRUE(initiator_result.has_value());
EXPECT_TRUE(responder_result->first->IsCounterpartyOfForTesting(
*initiator_result.value()));
ASSERT_EQ(responder_result->second.size(), sizeof(kGetInfoBytes));
EXPECT_EQ(0, memcmp(responder_result->second.data(), kGetInfoBytes,
EXPECT_TRUE(responder_result->crypter->IsCounterpartyOfForTesting(
*initiator_result->first));
ASSERT_EQ(responder_result->getinfo_bytes.size(), sizeof(kGetInfoBytes));
EXPECT_EQ(0, memcmp(responder_result->getinfo_bytes.data(), kGetInfoBytes,
sizeof(kGetInfoBytes)));
}
}
......
......@@ -378,8 +378,19 @@ 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.
// TODO(agl): remove. Only being kept around to allow things to compile.
constexpr char kCableShardIdHeader[] = "X-caBLE-Shard";
// kCableRoutingIdHeader is the name of an HTTP header that is sent in the reply
// from the tunnel server and which specifies the server's chosen routing ID
// which other parties can use to reach the same tunnel server.
constexpr char kCableRoutingIdHeader[] = "X-caBLE-Routing-ID";
// kCableClientPayloadHeader is the name of an HTTP header that is to
// the tunnel server when performing a state-assisted handshake and which
// includes the client's nonce and pairing ID.
constexpr char kCableClientPayloadHeader[] = "X-caBLE-Client-Payload";
// Maximum wait time before client error outs on device.
COMPONENT_EXPORT(DEVICE_FIDO) extern const base::TimeDelta kDeviceTimeout;
......
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