blob: 5e33bb6a4257f54ca6e99688ddca09710a456940 [file] [log] [blame]
// 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_handshake.h"
#include <inttypes.h>
#include <array>
#include <type_traits>
#include "base/base64url.h"
#include "base/bits.h"
#include "base/cxx17_backports.h" // for base::size
#include "base/numerics/safe_conversions.h"
#include "base/numerics/safe_math.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.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/aead.h"
#include "device/fido/cable/v2_constants.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_parsing_utils.h"
#include "third_party/boringssl/src/include/openssl/aes.h"
#include "third_party/boringssl/src/include/openssl/bytestring.h"
#include "third_party/boringssl/src/include/openssl/digest.h"
#include "third_party/boringssl/src/include/openssl/ec.h"
#include "third_party/boringssl/src/include/openssl/ec_key.h"
#include "third_party/boringssl/src/include/openssl/ecdh.h"
#include "third_party/boringssl/src/include/openssl/hkdf.h"
#include "third_party/boringssl/src/include/openssl/hmac.h"
#include "third_party/boringssl/src/include/openssl/mem.h"
#include "third_party/boringssl/src/include/openssl/obj.h"
#include "third_party/boringssl/src/include/openssl/sha.h"
#include "url/gurl.h"
namespace device {
namespace cablev2 {
namespace {
// Maximum value of a sequence number. Exceeding this causes all operations to
// return an error. This is assumed to be vastly larger than any caBLE exchange
// will ever reach.
constexpr uint32_t kMaxSequence = (1 << 24) - 1;
bool ConstructNonce(uint32_t counter, base::span<uint8_t, 12> out_nonce) {
if (counter > kMaxSequence) {
return false;
}
// Nonce is just a little-endian counter.
std::array<uint8_t, sizeof(counter)> counter_bytes;
memcpy(counter_bytes.data(), &counter, sizeof(counter));
std::copy(counter_bytes.begin(), counter_bytes.end(), out_nonce.begin());
std::fill(out_nonce.begin() + counter_bytes.size(), out_nonce.end(), 0);
return true;
}
std::array<uint8_t, 32> PairingSignature(
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) {
const EC_GROUP* const p256 = EC_KEY_get0_group(identity_key);
bssl::UniquePtr<EC_POINT> peer_public_key(EC_POINT_new(p256));
CHECK(EC_POINT_oct2point(p256, peer_public_key.get(),
peer_public_key_x962.data(),
peer_public_key_x962.size(),
/*ctx=*/nullptr));
uint8_t shared_secret[32];
CHECK(ECDH_compute_key(shared_secret, sizeof(shared_secret),
peer_public_key.get(), identity_key,
/*kdf=*/nullptr) == sizeof(shared_secret));
std::array<uint8_t, SHA256_DIGEST_LENGTH> expected_signature;
unsigned expected_signature_len = 0;
CHECK(HMAC(EVP_sha256(), /*key=*/shared_secret, sizeof(shared_secret),
handshake_hash.data(), handshake_hash.size(),
expected_signature.data(), &expected_signature_len) != nullptr);
CHECK_EQ(expected_signature_len, EXTENT(expected_signature));
return expected_signature;
}
// ReservedBitsAreZero returns true if the currently unused bits in |eid| are
// all set to zero.
bool ReservedBitsAreZero(const CableEidArray& eid) {
return eid[0] == 0;
}
bssl::UniquePtr<EC_KEY> ECKeyFromSeed(
base::span<const uint8_t, kQRSeedSize> seed) {
bssl::UniquePtr<EC_GROUP> p256(
EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
return bssl::UniquePtr<EC_KEY>(
EC_KEY_derive_from_secret(p256.get(), seed.data(), seed.size()));
}
} // namespace
namespace tunnelserver {
// kAssignedDomains is the list of defined tunnel server domains. These map
// to values 0..256.
static const char* kAssignedDomains[] = {"cable.ua5v.com"};
absl::optional<KnownDomainID> ToKnownDomainID(uint16_t domain) {
if (domain >= 256 || domain < base::size(kAssignedDomains)) {
return KnownDomainID(domain);
}
return absl::nullopt;
}
std::string DecodeDomain(KnownDomainID domain_id) {
const uint16_t domain = domain_id.value();
if (domain < 256) {
// The |KnownDomainID| type should only contain valid values for this but,
// just in case, CHECK it too.
CHECK_LT(domain, base::size(kAssignedDomains));
return kAssignedDomains[domain];
}
char templ[] = "caBLEv2 tunnel server domain\x00\x00";
memcpy(&templ[sizeof(templ) - 1 - sizeof(domain)], &domain, sizeof(domain));
uint8_t digest[SHA256_DIGEST_LENGTH];
// The input should be NUL-terminated, thus the trailing NUL in |templ| is
// included here.
SHA256(reinterpret_cast<const uint8_t*>(templ), sizeof(templ), digest);
uint64_t result;
static_assert(sizeof(result) <= sizeof(digest), "");
memcpy(&result, digest, sizeof(result));
static const char kBase32Chars[33] = "abcdefghijklmnopqrstuvwxyz234567";
const int tld_value = result & 3;
result >>= 2;
std::string ret = "cable.";
while (result != 0) {
ret.push_back(kBase32Chars[result & 31]);
result >>= 5;
}
ret.push_back('.');
static const char kTLDs[4][5] = {"com", "org", "net", "info"};
ret += kTLDs[tld_value];
return ret;
}
GURL GetNewTunnelURL(KnownDomainID domain, base::span<const uint8_t, 16> id) {
std::string ret = "wss://" + DecodeDomain(domain) + "/cable/new/";
ret += base::HexEncode(id);
const GURL url(ret);
DCHECK(url.is_valid());
return url;
}
GURL GetConnectURL(KnownDomainID domain,
std::array<uint8_t, kRoutingIdSize> routing_id,
base::span<const uint8_t, 16> id) {
std::string ret = "wss://" + DecodeDomain(domain) + "/cable/connect/";
ret += base::HexEncode(routing_id);
ret += "/";
ret += base::HexEncode(id);
const GURL url(ret);
DCHECK(url.is_valid());
return url;
}
GURL GetContactURL(const std::string& tunnel_server,
base::span<const uint8_t> contact_id) {
std::string contact_id_base64;
base::Base64UrlEncode(
base::StringPiece(reinterpret_cast<const char*>(contact_id.data()),
contact_id.size()),
base::Base64UrlEncodePolicy::OMIT_PADDING, &contact_id_base64);
GURL ret(std::string("wss://") + tunnel_server + "/cable/contact/" +
contact_id_base64);
DCHECK(ret.is_valid());
return ret;
}
} // namespace tunnelserver
namespace eid {
Components::Components() = default;
Components::~Components() = default;
Components::Components(const Components&) = default;
std::array<uint8_t, kAdvertSize> Encrypt(
const CableEidArray& eid,
base::span<const uint8_t, kEIDKeySize> key) {
// |eid| is encrypted as an AES block and a 4-byte HMAC is appended. The |key|
// is a pair of 256-bit keys, concatenated.
DCHECK(ReservedBitsAreZero(eid));
std::array<uint8_t, kAdvertSize> ret;
static_assert(EXTENT(ret) == AES_BLOCK_SIZE + 4, "");
AES_KEY aes_key;
static_assert(EXTENT(key) == 32 + 32, "");
CHECK(AES_set_encrypt_key(key.data(), /*bits=*/8 * 32, &aes_key) == 0);
static_assert(EXTENT(eid) == AES_BLOCK_SIZE, "EIDs are not AES blocks");
AES_encrypt(/*in=*/eid.data(), /*out=*/ret.data(), &aes_key);
uint8_t hmac[SHA256_DIGEST_LENGTH];
unsigned hmac_len;
CHECK(HMAC(EVP_sha256(), key.data() + 32, 32, ret.data(), AES_BLOCK_SIZE,
hmac, &hmac_len) != nullptr);
CHECK_EQ(hmac_len, sizeof(hmac));
static_assert(sizeof(hmac) >= 4, "");
memcpy(ret.data() + AES_BLOCK_SIZE, hmac, 4);
return ret;
}
absl::optional<CableEidArray> Decrypt(
const std::array<uint8_t, kAdvertSize>& advert,
base::span<const uint8_t, kEIDKeySize> key) {
// See |Encrypt| about the format.
static_assert(EXTENT(advert) == AES_BLOCK_SIZE + 4, "");
static_assert(EXTENT(key) == 32 + 32, "");
uint8_t calculated_hmac[SHA256_DIGEST_LENGTH];
unsigned calculated_hmac_len;
CHECK(HMAC(EVP_sha256(), key.data() + 32, 32, advert.data(), AES_BLOCK_SIZE,
calculated_hmac, &calculated_hmac_len) != nullptr);
CHECK_EQ(calculated_hmac_len, sizeof(calculated_hmac));
if (CRYPTO_memcmp(calculated_hmac, advert.data() + AES_BLOCK_SIZE, 4) != 0) {
return absl::nullopt;
}
AES_KEY aes_key;
CHECK(AES_set_decrypt_key(key.data(), /*bits=*/8 * 32, &aes_key) == 0);
CableEidArray plaintext;
static_assert(EXTENT(plaintext) == AES_BLOCK_SIZE, "EIDs are not AES blocks");
AES_decrypt(/*in=*/advert.data(), /*out=*/plaintext.data(), &aes_key);
// Ensure that reserved bits are zero. They might be used for new features in
// the future but support for those features must be advertised in the QR
// code, thus authenticators should not be unilaterally setting any of these
// bits.
if (!ReservedBitsAreZero(plaintext)) {
return absl::nullopt;
}
uint16_t tunnel_server_domain;
static_assert(EXTENT(plaintext) >= sizeof(tunnel_server_domain), "");
memcpy(&tunnel_server_domain,
&plaintext[EXTENT(plaintext) - sizeof(tunnel_server_domain)],
sizeof(tunnel_server_domain));
if (!tunnelserver::ToKnownDomainID(tunnel_server_domain)) {
return absl::nullopt;
}
return plaintext;
}
CableEidArray FromComponents(const Components& components) {
CableEidArray eid;
static_assert(EXTENT(components.nonce) == kNonceSize, "");
static_assert(EXTENT(eid) == 1 + kNonceSize + sizeof(components.routing_id) +
sizeof(components.tunnel_server_domain),
"");
eid[0] = 0;
memcpy(&eid[1], components.nonce.data(), kNonceSize);
memcpy(&eid[1 + kNonceSize], components.routing_id.data(),
sizeof(components.routing_id));
memcpy(&eid[1 + kNonceSize + sizeof(components.routing_id)],
&components.tunnel_server_domain,
sizeof(components.tunnel_server_domain));
return eid;
}
Components ToComponents(const CableEidArray& eid) {
Components ret;
static_assert(EXTENT(ret.nonce) == kNonceSize, "");
static_assert(EXTENT(eid) == 1 + kNonceSize + sizeof(ret.routing_id) +
sizeof(ret.tunnel_server_domain),
"");
memcpy(ret.nonce.data(), &eid[1], kNonceSize);
memcpy(ret.routing_id.data(), &eid[1 + kNonceSize], sizeof(ret.routing_id));
uint16_t tunnel_server_domain;
memcpy(&tunnel_server_domain, &eid[1 + kNonceSize + sizeof(ret.routing_id)],
sizeof(tunnel_server_domain));
// |eid| has been checked by |Decrypt| so the tunnel server domain must be
// valid.
ret.tunnel_server_domain =
*tunnelserver::ToKnownDomainID(tunnel_server_domain);
return ret;
}
} // namespace eid
namespace qr {
constexpr char kPrefix[] = "FIDO:/";
// DecompressPublicKey converts a compressed public key (from a scanned QR
// code) into a standard, uncompressed one.
static absl::optional<std::array<uint8_t, device::kP256X962Length>>
DecompressPublicKey(base::span<const uint8_t> compressed_public_key) {
if (compressed_public_key.size() !=
device::cablev2::kCompressedPublicKeySize) {
return absl::nullopt;
}
bssl::UniquePtr<EC_GROUP> p256(
EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
bssl::UniquePtr<EC_POINT> point(EC_POINT_new(p256.get()));
if (!EC_POINT_oct2point(p256.get(), point.get(), compressed_public_key.data(),
compressed_public_key.size(), /*ctx=*/nullptr)) {
return absl::nullopt;
}
std::array<uint8_t, device::kP256X962Length> ret;
CHECK_EQ(
ret.size(),
EC_POINT_point2oct(p256.get(), point.get(), POINT_CONVERSION_UNCOMPRESSED,
ret.data(), ret.size(), /*ctx=*/nullptr));
return ret;
}
static std::array<uint8_t, device::cablev2::kCompressedPublicKeySize>
SeedToCompressedPublicKey(base::span<const uint8_t, 32> seed) {
bssl::UniquePtr<EC_KEY> key = ECKeyFromSeed(seed);
const EC_POINT* public_key = EC_KEY_get0_public_key(key.get());
std::array<uint8_t, device::cablev2::kCompressedPublicKeySize> ret;
CHECK_EQ(ret.size(),
EC_POINT_point2oct(EC_KEY_get0_group(key.get()), public_key,
POINT_CONVERSION_COMPRESSED, ret.data(),
ret.size(), /*ctx=*/nullptr));
return ret;
}
// static
absl::optional<Components> Parse(const std::string& qr_url) {
if (qr_url.size() < sizeof(kPrefix) - 1 ||
base::CompareCaseInsensitiveASCII(
kPrefix, qr_url.substr(0, sizeof(kPrefix) - 1)) != 0) {
return absl::nullopt;
}
absl::optional<std::vector<uint8_t>> qr_bytes =
DigitsToBytes(qr_url.substr(sizeof(kPrefix) - 1));
if (!qr_bytes) {
return absl::nullopt;
}
absl::optional<cbor::Value> qr_contents = cbor::Reader::Read(*qr_bytes);
if (!qr_contents || !qr_contents->is_map()) {
return absl::nullopt;
}
const cbor::Value::MapValue& qr_contents_map(qr_contents->GetMap());
base::span<const uint8_t> values[2];
for (size_t i = 0; i < base::size(values); i++) {
const cbor::Value::MapValue::const_iterator it =
qr_contents_map.find(cbor::Value(static_cast<int>(i)));
if (it == qr_contents_map.end() || !it->second.is_bytestring()) {
return absl::nullopt;
}
values[i] = it->second.GetBytestring();
}
base::span<const uint8_t> compressed_public_key = values[0];
base::span<const uint8_t> qr_secret = values[1];
Components ret;
if (qr_secret.size() != ret.secret.size()) {
return absl::nullopt;
}
std::copy(qr_secret.begin(), qr_secret.end(), ret.secret.begin());
absl::optional<std::array<uint8_t, device::kP256X962Length>> peer_identity =
DecompressPublicKey(compressed_public_key);
if (!peer_identity) {
FIDO_LOG(ERROR) << "Invalid compressed public key in QR data";
return absl::nullopt;
}
ret.peer_identity = *peer_identity;
return ret;
}
std::string Encode(base::span<const uint8_t, kQRKeySize> qr_key) {
cbor::Value::MapValue qr_contents;
qr_contents.emplace(
0, SeedToCompressedPublicKey(
base::span<const uint8_t, device::cablev2::kQRSeedSize>(
qr_key.data(), device::cablev2::kQRSeedSize)));
qr_contents.emplace(1, qr_key.subspan(device::cablev2::kQRSeedSize));
qr_contents.emplace(
2, static_cast<int64_t>(base::size(tunnelserver::kAssignedDomains)));
qr_contents.emplace(3, static_cast<int64_t>(base::Time::Now().ToTimeT()));
const absl::optional<std::vector<uint8_t>> qr_data =
cbor::Writer::Write(cbor::Value(std::move(qr_contents)));
return std::string(kPrefix) + BytesToDigits(*qr_data);
}
// When converting between bytes and digits, chunks of 7 bytes are turned into
// 17 digits. See https://www.imperialviolet.org/2021/08/26/qrencoding.html.
constexpr size_t kChunkSize = 7;
constexpr size_t kChunkDigits = 17;
std::string BytesToDigits(base::span<const uint8_t> in) {
std::string ret;
ret.reserve(((in.size() + kChunkSize - 1) / kChunkSize) * kChunkDigits);
while (in.size() >= kChunkSize) {
uint64_t v = 0;
static_assert(sizeof(v) >= kChunkSize, "");
memcpy(&v, in.data(), kChunkSize);
char digits[kChunkDigits + 1];
static_assert(kChunkDigits == 17, "Need to change next line");
CHECK_LT(snprintf(digits, sizeof(digits), "%017" PRIu64, v),
static_cast<int>(sizeof(digits)));
ret += digits;
in = in.subspan(kChunkSize);
}
if (in.size()) {
char format[16];
// kPartialChunkDigits is the number of digits needed to encode each length
// of trailing data from 6 bytes down to zero. I.e. it's 15, 13, 10, 8, 5,
// 3, 0 written in hex.
constexpr uint32_t kPartialChunkDigits = 0x0fda8530;
CHECK_LT(snprintf(format, sizeof(format), "%%0%d" PRIu64,
15 & (kPartialChunkDigits >> (4 * in.size()))),
static_cast<int>(sizeof(format)));
uint64_t v = 0;
CHECK_LE(in.size(), sizeof(v));
memcpy(&v, in.data(), in.size());
char digits[kChunkDigits + 1];
CHECK_LT(snprintf(digits, sizeof(digits), format, v),
static_cast<int>(sizeof(digits)));
ret += digits;
}
return ret;
}
absl::optional<std::vector<uint8_t>> DigitsToBytes(base::StringPiece in) {
std::vector<uint8_t> ret;
ret.reserve(((in.size() + kChunkDigits - 1) / kChunkDigits) * kChunkSize);
while (in.size() >= kChunkDigits) {
uint64_t v;
if (!base::StringToUint64(in.substr(0, kChunkDigits), &v) ||
v >> (kChunkSize * 8) != 0) {
return absl::nullopt;
}
const uint8_t* const v_bytes = reinterpret_cast<uint8_t*>(&v);
ret.insert(ret.end(), v_bytes, v_bytes + kChunkSize);
in = in.substr(kChunkDigits);
}
if (in.size()) {
size_t remaining_bytes;
switch (in.size()) {
case 3:
remaining_bytes = 1;
break;
case 5:
remaining_bytes = 2;
break;
case 8:
remaining_bytes = 3;
break;
case 10:
remaining_bytes = 4;
break;
case 13:
remaining_bytes = 5;
break;
case 15:
remaining_bytes = 6;
break;
default:
return absl::nullopt;
}
uint64_t v;
if (!base::StringToUint64(in, &v) || v >> (remaining_bytes * 8) != 0) {
return absl::nullopt;
}
const uint8_t* const v_bytes = reinterpret_cast<uint8_t*>(&v);
ret.insert(ret.end(), v_bytes, v_bytes + remaining_bytes);
}
return ret;
}
} // namespace qr
namespace sync {
uint32_t IDNow() {
const base::Time now = base::Time::Now();
time_t utc_time = now.ToTimeT();
// The IDs, and thus Sync secret rotation, have a period of one day. These
// are lazily updated by the phone and don't cause additional Sync uploads.
utc_time /= 86400;
// A uint32_t can span about 11 million years.
return static_cast<uint32_t>(utc_time);
}
bool IDIsValid(uint32_t candidate) {
const uint32_t now = IDNow();
// Sync secrets are allowed to be, at most, 21 periods (~21 days) old. This is
// because the desktop accepts DeviceInfo records of phones that are 14 days
// old and the phone should be slightly laxer than that.
return candidate <= now && (now - candidate) < 21;
}
} // namespace sync
namespace internal {
void Derive(uint8_t* out,
size_t out_len,
base::span<const uint8_t> secret,
base::span<const uint8_t> nonce,
DerivedValueType type) {
static_assert(sizeof(DerivedValueType) <= sizeof(uint32_t), "");
const uint32_t type32 = static_cast<uint32_t>(type);
HKDF(out, out_len, EVP_sha256(), secret.data(), secret.size(),
/*salt=*/nonce.data(), nonce.size(),
/*info=*/reinterpret_cast<const uint8_t*>(&type32), sizeof(type32));
}
} // namespace internal
bssl::UniquePtr<EC_KEY> IdentityKey(base::span<const uint8_t, 32> root_secret) {
std::array<uint8_t, 32> seed;
seed = device::cablev2::Derive<EXTENT(seed)>(
root_secret, /*nonce=*/base::span<uint8_t>(),
device::cablev2::DerivedValueType::kIdentityKeySeed);
bssl::UniquePtr<EC_GROUP> p256(
EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
return bssl::UniquePtr<EC_KEY>(
EC_KEY_derive_from_secret(p256.get(), seed.data(), seed.size()));
}
absl::optional<std::vector<uint8_t>> EncodePaddedCBORMap(
cbor::Value::MapValue map) {
// The number of padding bytes is a uint16_t, so the granularity cannot be
// larger than that.
static_assert(kPostHandshakeMsgPaddingGranularity > 0, "");
static_assert(kPostHandshakeMsgPaddingGranularity - 1 <=
std::numeric_limits<uint16_t>::max());
// The granularity must also be a power of two.
static_assert((kPostHandshakeMsgPaddingGranularity &
(kPostHandshakeMsgPaddingGranularity - 1)) == 0);
absl::optional<std::vector<uint8_t>> cbor_bytes =
cbor::Writer::Write(cbor::Value(std::move(map)));
if (!cbor_bytes) {
return absl::nullopt;
}
base::CheckedNumeric<size_t> padded_size_checked = cbor_bytes->size();
padded_size_checked += sizeof(uint16_t); // padding-length bytes
padded_size_checked =
(padded_size_checked + kPostHandshakeMsgPaddingGranularity - 1) &
~(kPostHandshakeMsgPaddingGranularity - 1);
if (!padded_size_checked.IsValid()) {
return absl::nullopt;
}
const size_t padded_size = padded_size_checked.ValueOrDie();
DCHECK_GE(padded_size, cbor_bytes->size() + sizeof(uint16_t));
const size_t extra_bytes = padded_size - cbor_bytes->size();
const size_t num_padding_bytes =
extra_bytes - sizeof(uint16_t) /* length of padding length */;
cbor_bytes->resize(padded_size);
const uint16_t num_padding_bytes16 =
base::checked_cast<uint16_t>(num_padding_bytes);
memcpy(&cbor_bytes.value()[padded_size - sizeof(num_padding_bytes16)],
&num_padding_bytes16, sizeof(num_padding_bytes16));
return *cbor_bytes;
}
namespace {
// DecodePaddedCBORMap8 performs the actions of |DecodePaddedCBORMap| using the
// old padding format. We still support this format for backwards compatibility.
// See comment in |DecodePaddedCBORMap|.
//
// TODO(agl): remove support for this padding format. (Chromium started sending
// the new format with M99.)
absl::optional<cbor::Value> DecodePaddedCBORMap8(
const base::span<const uint8_t> input) {
if (input.empty()) {
return absl::nullopt;
}
const size_t padding_length = input.back();
if (padding_length + 1 > input.size()) {
return absl::nullopt;
}
auto unpadded_input = input.subspan(0, input.size() - padding_length - 1);
absl::optional<cbor::Value> payload = cbor::Reader::Read(unpadded_input);
if (!payload || !payload->is_map()) {
return absl::nullopt;
}
return payload;
}
// DecodePaddedCBORMap16 performs the actions of |DecodePaddedCBORMap| using the
// new padding format. See comment in |DecodePaddedCBORMap|.
absl::optional<cbor::Value> DecodePaddedCBORMap16(
base::span<const uint8_t> input) {
if (input.size() < sizeof(uint16_t)) {
return absl::nullopt;
}
uint16_t padding_length16;
memcpy(&padding_length16, &input[input.size() - sizeof(padding_length16)],
sizeof(padding_length16));
const size_t padding_length = padding_length16;
if (padding_length + sizeof(uint16_t) > input.size()) {
return absl::nullopt;
}
input = input.subspan(0, input.size() - padding_length - sizeof(uint16_t));
absl::optional<cbor::Value> payload = cbor::Reader::Read(input);
if (!payload || !payload->is_map()) {
return absl::nullopt;
}
return payload;
}
} // namespace
absl::optional<cbor::Value> DecodePaddedCBORMap(
base::span<const uint8_t> input) {
// Two padding formats are currently in use. They are unambiguous so we try
// each, new first. Eventually the old format can be removed once enough time
// has passed since M99.
absl::optional<cbor::Value> result = DecodePaddedCBORMap16(input);
if (!result) {
result = DecodePaddedCBORMap8(input);
}
if (!result) {
FIDO_LOG(DEBUG) << "Invalid padding in caBLE handshake message";
}
return result;
}
Crypter::Crypter(base::span<const uint8_t, 32> read_key,
base::span<const uint8_t, 32> write_key)
: read_key_(fido_parsing_utils::Materialize(read_key)),
write_key_(fido_parsing_utils::Materialize(write_key)) {}
Crypter::~Crypter() = default;
bool Crypter::Encrypt(std::vector<uint8_t>* message_to_encrypt) {
// Messages will be padded in order to round their length up to a multiple
// of kPaddingGranularity.
constexpr size_t kPaddingGranularity = 32;
static_assert(kPaddingGranularity < 256, "padding too large");
static_assert(base::bits::IsPowerOfTwo(kPaddingGranularity),
"padding must be a power of two");
// Padding consists of a some number of zero bytes appended to the message
// and the final byte in the message is the number of zeros.
base::CheckedNumeric<size_t> padded_size_checked = message_to_encrypt->size();
padded_size_checked += 1; // padding-length byte.
padded_size_checked = (padded_size_checked + kPaddingGranularity - 1) &
~(kPaddingGranularity - 1);
if (!padded_size_checked.IsValid()) {
NOTREACHED();
return false;
}
const size_t padded_size = padded_size_checked.ValueOrDie();
CHECK_GT(padded_size, message_to_encrypt->size());
const size_t num_zeros = padded_size - message_to_encrypt->size() - 1;
std::vector<uint8_t> padded_message(padded_size, 0);
memcpy(padded_message.data(), message_to_encrypt->data(),
message_to_encrypt->size());
// The number of added zeros has to fit in a single byte so it has to be
// less than 256.
DCHECK_LT(num_zeros, 256u);
padded_message[padded_message.size() - 1] = static_cast<uint8_t>(num_zeros);
std::array<uint8_t, 12> nonce;
if (!ConstructNonce(write_sequence_num_++, nonce)) {
return false;
}
crypto::Aead aes_key(crypto::Aead::AES_256_GCM);
aes_key.Init(write_key_);
DCHECK_EQ(nonce.size(), aes_key.NonceLength());
const uint8_t additional_data[1] = {/*version=*/2};
std::vector<uint8_t> ciphertext =
aes_key.Seal(padded_message, nonce, additional_data);
message_to_encrypt->swap(ciphertext);
return true;
}
bool Crypter::Decrypt(base::span<const uint8_t> ciphertext,
std::vector<uint8_t>* out_plaintext) {
std::array<uint8_t, 12> nonce;
if (!ConstructNonce(read_sequence_num_, nonce)) {
return false;
}
crypto::Aead aes_key(crypto::Aead::AES_256_GCM);
aes_key.Init(read_key_);
DCHECK_EQ(nonce.size(), aes_key.NonceLength());
const uint8_t additional_data[1] = {/*version=*/2};
absl::optional<std::vector<uint8_t>> plaintext =
aes_key.Open(ciphertext, nonce, additional_data);
if (!plaintext) {
return false;
}
read_sequence_num_++;
if (plaintext->empty()) {
FIDO_LOG(ERROR) << "Invalid caBLE message.";
return false;
}
const size_t padding_length = (*plaintext)[plaintext->size() - 1];
if (padding_length + 1 > plaintext->size()) {
FIDO_LOG(ERROR) << "Invalid caBLE message.";
return false;
}
plaintext->resize(plaintext->size() - padding_length - 1);
out_plaintext->swap(*plaintext);
return true;
}
bool Crypter::IsCounterpartyOfForTesting(const Crypter& other) const {
return read_key_ == other.write_key_ && write_key_ == other.read_key_;
}
HandshakeInitiator::HandshakeInitiator(
base::span<const uint8_t, 32> psk,
absl::optional<base::span<const uint8_t, kP256X962Length>> peer_identity,
absl::optional<base::span<const uint8_t, kQRSeedSize>> identity_seed)
: psk_(fido_parsing_utils::Materialize(psk)),
local_identity_(identity_seed ? ECKeyFromSeed(*identity_seed) : nullptr) {
DCHECK(peer_identity.has_value() ^ static_cast<bool>(local_identity_));
if (peer_identity) {
peer_identity_ =
fido_parsing_utils::Materialize<kP256X962Length>(*peer_identity);
}
}
HandshakeInitiator::~HandshakeInitiator() = default;
std::vector<uint8_t> HandshakeInitiator::BuildInitialMessage() {
uint8_t prologue[1];
if (peer_identity_) {
noise_.Init(Noise::HandshakeType::kNKpsk0);
prologue[0] = 0;
noise_.MixHash(prologue);
noise_.MixHash(*peer_identity_);
} else {
noise_.Init(Noise::HandshakeType::kKNpsk0);
prologue[0] = 1;
noise_.MixHash(prologue);
noise_.MixHashPoint(EC_KEY_get0_public_key(local_identity_.get()));
}
noise_.MixKeyAndHash(psk_);
ephemeral_key_.reset(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1));
const EC_GROUP* group = EC_KEY_get0_group(ephemeral_key_.get());
CHECK(EC_KEY_generate_key(ephemeral_key_.get()));
uint8_t ephemeral_key_public_bytes[kP256X962Length];
CHECK_EQ(sizeof(ephemeral_key_public_bytes),
EC_POINT_point2oct(
group, EC_KEY_get0_public_key(ephemeral_key_.get()),
POINT_CONVERSION_UNCOMPRESSED, ephemeral_key_public_bytes,
sizeof(ephemeral_key_public_bytes), /*ctx=*/nullptr));
noise_.MixHash(ephemeral_key_public_bytes);
noise_.MixKey(ephemeral_key_public_bytes);
if (peer_identity_) {
// If we know the identity of the peer from a previous interaction, NKpsk0
// is performed to ensure that other browsers, which may also know the PSK,
// cannot impersonate the authenticator.
bssl::UniquePtr<EC_POINT> peer_identity_point(EC_POINT_new(group));
uint8_t es_key[32];
CHECK(EC_POINT_oct2point(group, peer_identity_point.get(),
peer_identity_->data(), peer_identity_->size(),
/*ctx=*/nullptr));
CHECK(ECDH_compute_key(es_key, sizeof(es_key), peer_identity_point.get(),
ephemeral_key_.get(),
/*kdf=*/nullptr) == sizeof(es_key));
noise_.MixKey(es_key);
}
std::vector<uint8_t> ciphertext =
noise_.EncryptAndHash(base::span<const uint8_t>());
std::vector<uint8_t> handshake_message;
handshake_message.reserve(sizeof(ephemeral_key_public_bytes) +
ciphertext.size());
handshake_message.insert(
handshake_message.end(), ephemeral_key_public_bytes,
ephemeral_key_public_bytes + sizeof(ephemeral_key_public_bytes));
handshake_message.insert(handshake_message.end(), ciphertext.begin(),
ciphertext.end());
return handshake_message;
}
HandshakeResult HandshakeInitiator::ProcessResponse(
base::span<const uint8_t> response) {
if (response.size() < kP256X962Length) {
FIDO_LOG(DEBUG) << "Handshake response truncated (" << response.size()
<< " bytes)";
return absl::nullopt;
}
auto peer_point_bytes = response.subspan(0, kP256X962Length);
auto ciphertext = response.subspan(kP256X962Length);
bssl::UniquePtr<EC_POINT> peer_point(
EC_POINT_new(EC_KEY_get0_group(ephemeral_key_.get())));
uint8_t shared_key_ee[32];
const EC_GROUP* group = EC_KEY_get0_group(ephemeral_key_.get());
if (!EC_POINT_oct2point(group, peer_point.get(), peer_point_bytes.data(),
peer_point_bytes.size(), /*ctx=*/nullptr) ||
ECDH_compute_key(shared_key_ee, sizeof(shared_key_ee), peer_point.get(),
ephemeral_key_.get(),
/*kdf=*/nullptr) != sizeof(shared_key_ee)) {
FIDO_LOG(DEBUG) << "Peer's P-256 point not on curve.";
return absl::nullopt;
}
noise_.MixHash(peer_point_bytes);
noise_.MixKey(peer_point_bytes);
noise_.MixKey(shared_key_ee);
if (local_identity_) {
uint8_t shared_key_se[32];
if (ECDH_compute_key(shared_key_se, sizeof(shared_key_se), peer_point.get(),
local_identity_.get(),
/*kdf=*/nullptr) != sizeof(shared_key_se)) {
FIDO_LOG(DEBUG) << "ECDH_compute_key failed";
return absl::nullopt;
}
noise_.MixKey(shared_key_se);
}
auto plaintext = noise_.DecryptAndHash(ciphertext);
if (!plaintext || !plaintext->empty()) {
FIDO_LOG(DEBUG) << "Invalid caBLE handshake message";
return absl::nullopt;
}
std::array<uint8_t, 32> read_key, write_key;
std::tie(write_key, read_key) = noise_.traffic_keys();
return std::make_pair(std::make_unique<cablev2::Crypter>(read_key, write_key),
noise_.handshake_hash());
}
HandshakeResult RespondToHandshake(
base::span<const uint8_t, 32> psk,
bssl::UniquePtr<EC_KEY> identity,
absl::optional<base::span<const uint8_t, kP256X962Length>> peer_identity,
base::span<const uint8_t> in,
std::vector<uint8_t>* out_response) {
DCHECK(peer_identity.has_value() ^ static_cast<bool>(identity));
if (in.size() < kP256X962Length) {
FIDO_LOG(DEBUG) << "Handshake truncated (" << in.size() << " bytes)";
return absl::nullopt;
}
auto peer_point_bytes = in.subspan(0, kP256X962Length);
auto ciphertext = in.subspan(kP256X962Length);
Noise noise;
uint8_t prologue[1];
if (identity) {
noise.Init(device::Noise::HandshakeType::kNKpsk0);
prologue[0] = 0;
noise.MixHash(prologue);
noise.MixHashPoint(EC_KEY_get0_public_key(identity.get()));
} else {
noise.Init(device::Noise::HandshakeType::kKNpsk0);
prologue[0] = 1;
noise.MixHash(prologue);
noise.MixHash(*peer_identity);
}
noise.MixKeyAndHash(psk);
noise.MixHash(peer_point_bytes);
noise.MixKey(peer_point_bytes);
bssl::UniquePtr<EC_KEY> ephemeral_key(
EC_KEY_new_by_curve_name(NID_X9_62_prime256v1));
const EC_GROUP* group = EC_KEY_get0_group(ephemeral_key.get());
CHECK(EC_KEY_generate_key(ephemeral_key.get()));
bssl::UniquePtr<EC_POINT> peer_point(EC_POINT_new(group));
if (!EC_POINT_oct2point(group, peer_point.get(), peer_point_bytes.data(),
peer_point_bytes.size(),
/*ctx=*/nullptr)) {
FIDO_LOG(DEBUG) << "Peer's P-256 point not on curve.";
return absl::nullopt;
}
if (identity) {
uint8_t es_key[32];
if (ECDH_compute_key(es_key, sizeof(es_key), peer_point.get(),
identity.get(),
/*kdf=*/nullptr) != sizeof(es_key)) {
return absl::nullopt;
}
noise.MixKey(es_key);
}
auto plaintext = noise.DecryptAndHash(ciphertext);
if (!plaintext || !plaintext->empty()) {
FIDO_LOG(DEBUG) << "Failed to decrypt handshake ciphertext.";
return absl::nullopt;
}
uint8_t ephemeral_key_public_bytes[kP256X962Length];
CHECK_EQ(sizeof(ephemeral_key_public_bytes),
EC_POINT_point2oct(
group, EC_KEY_get0_public_key(ephemeral_key.get()),
POINT_CONVERSION_UNCOMPRESSED, ephemeral_key_public_bytes,
sizeof(ephemeral_key_public_bytes),
/*ctx=*/nullptr));
noise.MixHash(ephemeral_key_public_bytes);
noise.MixKey(ephemeral_key_public_bytes);
uint8_t shared_key_ee[32];
if (ECDH_compute_key(shared_key_ee, sizeof(shared_key_ee), peer_point.get(),
ephemeral_key.get(),
/*kdf=*/nullptr) != sizeof(shared_key_ee)) {
return absl::nullopt;
}
noise.MixKey(shared_key_ee);
if (peer_identity) {
bssl::UniquePtr<EC_POINT> peer_identity_point(EC_POINT_new(group));
CHECK(EC_POINT_oct2point(group, peer_identity_point.get(),
peer_identity->data(), peer_identity->size(),
/*ctx=*/nullptr));
uint8_t shared_key_se[32];
if (ECDH_compute_key(shared_key_se, sizeof(shared_key_se),
peer_identity_point.get(), ephemeral_key.get(),
/*kdf=*/nullptr) != sizeof(shared_key_se)) {
return absl::nullopt;
}
noise.MixKey(shared_key_se);
}
const std::vector<uint8_t> my_ciphertext =
noise.EncryptAndHash(base::span<const uint8_t>());
out_response->insert(
out_response->end(), ephemeral_key_public_bytes,
ephemeral_key_public_bytes + sizeof(ephemeral_key_public_bytes));
out_response->insert(out_response->end(), my_ciphertext.begin(),
my_ciphertext.end());
std::array<uint8_t, 32> read_key, write_key;
std::tie(read_key, write_key) = noise.traffic_keys();
return std::make_pair(std::make_unique<cablev2::Crypter>(read_key, write_key),
noise.handshake_hash());
}
bool VerifyPairingSignature(
base::span<const uint8_t, kQRSeedSize> 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) {
bssl::UniquePtr<EC_KEY> identity_key = ECKeyFromSeed(identity_seed);
std::array<uint8_t, SHA256_DIGEST_LENGTH> expected_signature =
PairingSignature(identity_key.get(), peer_public_key_x962,
handshake_hash);
return signature.size() == EXTENT(expected_signature) &&
CRYPTO_memcmp(expected_signature.data(), signature.data(),
EXTENT(expected_signature)) == 0;
}
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) {
std::array<uint8_t, SHA256_DIGEST_LENGTH> expected_signature =
PairingSignature(identity_key, peer_public_key_x962, handshake_hash);
return std::vector<uint8_t>(expected_signature.begin(),
expected_signature.end());
}
} // namespace cablev2
} // namespace device