blob: 307add2587219c28ede4eaca4cdf8cf5db22bb17 [file] [log] [blame]
// Copyright 2019 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 <string>
#include <utility>
#include "base/i18n/char_iterator.h"
#include "base/strings/string_util.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "device/fido/fido_constants.h"
#include "device/fido/pin.h"
#include "device/fido/pin_internal.h"
#include "third_party/boringssl/src/include/openssl/aes.h"
#include "third_party/boringssl/src/include/openssl/bn.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/evp.h"
#include "third_party/boringssl/src/include/openssl/hmac.h"
#include "third_party/boringssl/src/include/openssl/obj.h"
#include "third_party/boringssl/src/include/openssl/sha.h"
namespace device {
namespace pin {
// HasAtLeastFourCodepoints returns true if |pin| is UTF-8 encoded and contains
// four or more code points. This reflects the "4 Unicode characters"
// requirement in CTAP2.
static bool HasAtLeastFourCodepoints(const std::string& pin) {
base::i18n::UTF8CharIterator it(&pin);
return it.Advance() && it.Advance() && it.Advance() && it.Advance();
}
bool IsValid(const std::string& pin) {
return pin.size() >= kMinBytes && pin.size() <= kMaxBytes &&
pin.back() != 0 && base::IsStringUTF8(pin) &&
HasAtLeastFourCodepoints(pin);
}
// EncodePINCommand returns a CTAP2 PIN command for the operation |subcommand|.
// Additional elements of the top-level CBOR map can be added with the optional
// |add_additional| callback.
static std::pair<CtapRequestCommand, base::Optional<cbor::Value>>
EncodePINCommand(
Subcommand subcommand,
std::function<void(cbor::Value::MapValue*)> add_additional = nullptr) {
cbor::Value::MapValue map;
map.emplace(static_cast<int>(RequestKey::kProtocol), kProtocolVersion);
map.emplace(static_cast<int>(RequestKey::kSubcommand),
static_cast<int>(subcommand));
if (add_additional) {
add_additional(&map);
}
return std::make_pair(CtapRequestCommand::kAuthenticatorClientPin,
cbor::Value(std::move(map)));
}
std::pair<CtapRequestCommand, base::Optional<cbor::Value>>
RetriesRequest::EncodeAsCBOR() const {
return EncodePINCommand(Subcommand::kGetRetries);
}
RetriesResponse::RetriesResponse() = default;
// static
base::Optional<RetriesResponse> RetriesResponse::Parse(
const base::Optional<cbor::Value>& cbor) {
if (!cbor || !cbor->is_map()) {
return base::nullopt;
}
const auto& response_map = cbor->GetMap();
auto it =
response_map.find(cbor::Value(static_cast<int>(ResponseKey::kRetries)));
if (it == response_map.end() || !it->second.is_unsigned()) {
return base::nullopt;
}
const int64_t retries = it->second.GetUnsigned();
if (retries > INT_MAX) {
return base::nullopt;
}
RetriesResponse ret;
ret.retries = static_cast<int>(retries);
return ret;
}
std::pair<CtapRequestCommand, base::Optional<cbor::Value>>
KeyAgreementRequest::EncodeAsCBOR() const {
return EncodePINCommand(Subcommand::kGetKeyAgreement);
}
KeyAgreementResponse::KeyAgreementResponse() = default;
// PointFromKeyAgreementResponse returns an |EC_POINT| that represents the same
// P-256 point as |response|. It returns |nullopt| if |response| encodes an
// invalid point.
base::Optional<bssl::UniquePtr<EC_POINT>> PointFromKeyAgreementResponse(
const EC_GROUP* group,
const KeyAgreementResponse& response) {
bssl::UniquePtr<EC_POINT> ret(EC_POINT_new(group));
BIGNUM x_bn, y_bn;
BN_init(&x_bn);
BN_init(&y_bn);
BN_bin2bn(response.x, sizeof(response.x), &x_bn);
BN_bin2bn(response.y, sizeof(response.y), &y_bn);
const bool on_curve =
EC_POINT_set_affine_coordinates_GFp(group, ret.get(), &x_bn, &y_bn,
nullptr /* ctx */) == 1;
BN_clear(&x_bn);
BN_clear(&y_bn);
if (!on_curve) {
return base::nullopt;
}
return ret;
}
// static
base::Optional<KeyAgreementResponse> KeyAgreementResponse::Parse(
const base::Optional<cbor::Value>& cbor) {
if (!cbor || !cbor->is_map()) {
return base::nullopt;
}
const auto& response_map = cbor->GetMap();
// The ephemeral key is encoded as a COSE structure.
auto it = response_map.find(
cbor::Value(static_cast<int>(ResponseKey::kKeyAgreement)));
if (it == response_map.end() || !it->second.is_map()) {
return base::nullopt;
}
const auto& cose_key = it->second.GetMap();
return ParseFromCOSE(cose_key);
}
// static
base::Optional<KeyAgreementResponse> KeyAgreementResponse::ParseFromCOSE(
const cbor::Value::MapValue& cose_key) {
// The COSE key must be a P-256 point. See
// https://tools.ietf.org/html/rfc8152#section-7.1
for (const auto& pair : std::vector<std::pair<int, int>>({
{1 /* key type */, 2 /* elliptic curve, uncompressed */},
{3 /* algorithm */, -25 /* ECDH, ephemeral–static, HKDF-SHA-256 */},
{-1 /* curve */, 1 /* P-256 */},
})) {
auto it = cose_key.find(cbor::Value(pair.first));
if (it == cose_key.end() || !it->second.is_integer() ||
it->second.GetInteger() != pair.second) {
return base::nullopt;
}
}
// See https://tools.ietf.org/html/rfc8152#section-13.1.1
const auto& x_it = cose_key.find(cbor::Value(-2));
const auto& y_it = cose_key.find(cbor::Value(-3));
if (x_it == cose_key.end() || y_it == cose_key.end() ||
!x_it->second.is_bytestring() || !y_it->second.is_bytestring()) {
return base::nullopt;
}
const auto& x = x_it->second.GetBytestring();
const auto& y = y_it->second.GetBytestring();
KeyAgreementResponse ret;
if (x.size() != sizeof(ret.x) || y.size() != sizeof(ret.y)) {
return base::nullopt;
}
memcpy(ret.x, x.data(), sizeof(ret.x));
memcpy(ret.y, y.data(), sizeof(ret.y));
bssl::UniquePtr<EC_GROUP> group(
EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
// Check that the point is on the curve.
auto point = PointFromKeyAgreementResponse(group.get(), ret);
if (!point) {
return base::nullopt;
}
return ret;
}
SetRequest::SetRequest(const std::string& pin,
const KeyAgreementResponse& peer_key)
: peer_key_(peer_key) {
DCHECK(IsValid(pin));
memset(pin_, 0, sizeof(pin_));
memcpy(pin_, pin.data(), pin.size());
}
// SHA256KDF implements CTAP2's KDF, which just runs SHA-256 on the x-coordinate
// of the result. The function signature is such that it fits into OpenSSL's
// ECDH API.
static void* SHA256KDF(const void* in,
size_t in_len,
void* out,
size_t* out_len) {
DCHECK_GE(*out_len, static_cast<size_t>(SHA256_DIGEST_LENGTH));
SHA256(reinterpret_cast<const uint8_t*>(in), in_len,
reinterpret_cast<uint8_t*>(out));
*out_len = SHA256_DIGEST_LENGTH;
return out;
}
// CalculateSharedKey writes the CTAP2 shared key between |key| and |peers_key|
// to |out_shared_key|.
void CalculateSharedKey(const EC_KEY* key,
const EC_POINT* peers_key,
uint8_t out_shared_key[SHA256_DIGEST_LENGTH]) {
CHECK_EQ(static_cast<int>(SHA256_DIGEST_LENGTH),
ECDH_compute_key(out_shared_key, SHA256_DIGEST_LENGTH, peers_key,
key, SHA256KDF));
}
// EncodeCOSEPublicKey returns the public part of |key| as a COSE structure.
cbor::Value::MapValue EncodeCOSEPublicKey(const EC_KEY* key) {
// X9.62 is the standard for serialising elliptic-curve points.
uint8_t x962[1 /* type byte */ + 32 /* x */ + 32 /* y */];
CHECK_EQ(
sizeof(x962),
EC_POINT_point2oct(EC_KEY_get0_group(key), EC_KEY_get0_public_key(key),
POINT_CONVERSION_UNCOMPRESSED, x962, sizeof(x962),
nullptr /* BN_CTX */));
cbor::Value::MapValue cose_key;
cose_key.emplace(1 /* key type */, 2 /* uncompressed elliptic curve */);
cose_key.emplace(3 /* algorithm */,
-25 /* ECDH, ephemeral–static, HKDF-SHA-256 */);
cose_key.emplace(-1 /* curve */, 1 /* P-256 */);
cose_key.emplace(-2 /* x */, base::span<const uint8_t>(x962 + 1, 32));
cose_key.emplace(-3 /* y */, base::span<const uint8_t>(x962 + 33, 32));
return cose_key;
}
// GenerateSharedKey generates and returns an ephemeral key, and writes the
// shared key between that ephemeral key and the authenticator's ephemeral key
// (from |peers_key|) to |out_shared_key|.
static cbor::Value::MapValue GenerateSharedKey(
const KeyAgreementResponse& peers_key,
uint8_t out_shared_key[SHA256_DIGEST_LENGTH]) {
bssl::UniquePtr<EC_KEY> key(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1));
CHECK(EC_KEY_generate_key(key.get()));
auto peers_point =
PointFromKeyAgreementResponse(EC_KEY_get0_group(key.get()), peers_key);
CalculateSharedKey(key.get(), peers_point->get(), out_shared_key);
return EncodeCOSEPublicKey(key.get());
}
// Encrypt encrypts |plaintext| using |key|, writing the ciphertext to
// |out_ciphertext|. |plaintext| must be a whole number of AES blocks.
void Encrypt(const uint8_t key[SHA256_DIGEST_LENGTH],
base::span<const uint8_t> plaintext,
uint8_t* out_ciphertext) {
DCHECK_EQ(0u, plaintext.size() % AES_BLOCK_SIZE);
EVP_CIPHER_CTX aes_ctx;
EVP_CIPHER_CTX_init(&aes_ctx);
const uint8_t kZeroIV[AES_BLOCK_SIZE] = {0};
CHECK(EVP_EncryptInit_ex(&aes_ctx, EVP_aes_256_cbc(), nullptr, key, kZeroIV));
CHECK(EVP_CIPHER_CTX_set_padding(&aes_ctx, 0 /* no padding */));
CHECK(
EVP_Cipher(&aes_ctx, out_ciphertext, plaintext.data(), plaintext.size()));
EVP_CIPHER_CTX_cleanup(&aes_ctx);
}
std::pair<CtapRequestCommand, base::Optional<cbor::Value>>
SetRequest::EncodeAsCBOR() const {
// See
// https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#settingNewPin
uint8_t shared_key[SHA256_DIGEST_LENGTH];
auto cose_key = GenerateSharedKey(peer_key_, shared_key);
static_assert((sizeof(pin_) % AES_BLOCK_SIZE) == 0,
"pin_ is not a multiple of the AES block size");
uint8_t encrypted_pin[sizeof(pin_)];
Encrypt(shared_key, pin_, encrypted_pin);
uint8_t pin_auth[SHA256_DIGEST_LENGTH];
unsigned hmac_bytes;
CHECK(HMAC(EVP_sha256(), shared_key, sizeof(shared_key), encrypted_pin,
sizeof(pin_), pin_auth, &hmac_bytes));
DCHECK_EQ(sizeof(pin_auth), static_cast<size_t>(hmac_bytes));
return EncodePINCommand(
Subcommand::kSetPIN,
[&cose_key, &encrypted_pin, &pin_auth](cbor::Value::MapValue* map) {
map->emplace(static_cast<int>(RequestKey::kKeyAgreement),
std::move(cose_key));
map->emplace(
static_cast<int>(RequestKey::kNewPINEnc),
base::span<const uint8_t>(encrypted_pin, sizeof(encrypted_pin)));
map->emplace(static_cast<int>(RequestKey::kPINAuth),
base::span<const uint8_t>(pin_auth));
});
}
ChangeRequest::ChangeRequest(const std::string& old_pin,
const std::string& new_pin,
const KeyAgreementResponse& peer_key)
: peer_key_(peer_key) {
uint8_t digest[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const uint8_t*>(old_pin.data()), old_pin.size(),
digest);
memcpy(old_pin_hash_, digest, sizeof(old_pin_hash_));
DCHECK(IsValid(new_pin));
memset(new_pin_, 0, sizeof(new_pin_));
memcpy(new_pin_, new_pin.data(), new_pin.size());
}
// static
base::Optional<EmptyResponse> EmptyResponse::Parse(
const base::Optional<cbor::Value>& cbor) {
// Yubikeys can return just the status byte, and no CBOR bytes, for the empty
// response, which will end up here with |cbor| being |nullopt|. This seems
// wrong, but is handled. (The response should, instead, encode an empty CBOR
// map.)
if (cbor && (!cbor->is_map() || !cbor->GetMap().empty())) {
return base::nullopt;
}
EmptyResponse ret;
return ret;
}
std::pair<CtapRequestCommand, base::Optional<cbor::Value>>
ChangeRequest::EncodeAsCBOR() const {
// See
// https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#changingExistingPin
uint8_t shared_key[SHA256_DIGEST_LENGTH];
auto cose_key = GenerateSharedKey(peer_key_, shared_key);
static_assert((sizeof(new_pin_) % AES_BLOCK_SIZE) == 0,
"new_pin_ is not a multiple of the AES block size");
uint8_t encrypted_pin[sizeof(new_pin_)];
Encrypt(shared_key, new_pin_, encrypted_pin);
static_assert((sizeof(old_pin_hash_) % AES_BLOCK_SIZE) == 0,
"old_pin_hash_ is not a multiple of the AES block size");
uint8_t old_pin_hash_enc[sizeof(old_pin_hash_)];
Encrypt(shared_key, old_pin_hash_, old_pin_hash_enc);
uint8_t ciphertexts_concat[sizeof(encrypted_pin) + sizeof(old_pin_hash_enc)];
memcpy(ciphertexts_concat, encrypted_pin, sizeof(encrypted_pin));
memcpy(ciphertexts_concat + sizeof(encrypted_pin), old_pin_hash_enc,
sizeof(old_pin_hash_enc));
uint8_t pin_auth[SHA256_DIGEST_LENGTH];
unsigned hmac_bytes;
CHECK(HMAC(EVP_sha256(), shared_key, sizeof(shared_key), ciphertexts_concat,
sizeof(ciphertexts_concat), pin_auth, &hmac_bytes));
DCHECK_EQ(sizeof(pin_auth), static_cast<size_t>(hmac_bytes));
return EncodePINCommand(
Subcommand::kChangePIN, [&cose_key, &encrypted_pin, &old_pin_hash_enc,
&pin_auth](cbor::Value::MapValue* map) {
map->emplace(static_cast<int>(RequestKey::kKeyAgreement),
std::move(cose_key));
map->emplace(static_cast<int>(RequestKey::kPINHashEnc),
base::span<const uint8_t>(old_pin_hash_enc,
sizeof(old_pin_hash_enc)));
map->emplace(
static_cast<int>(RequestKey::kNewPINEnc),
base::span<const uint8_t>(encrypted_pin, sizeof(encrypted_pin)));
map->emplace(static_cast<int>(RequestKey::kPINAuth),
base::span<const uint8_t>(pin_auth));
});
}
std::pair<CtapRequestCommand, base::Optional<cbor::Value>>
ResetRequest::EncodeAsCBOR() const {
return std::make_pair(CtapRequestCommand::kAuthenticatorReset, base::nullopt);
}
TokenRequest::TokenRequest(const std::string& pin,
const KeyAgreementResponse& peer_key)
: cose_key_(GenerateSharedKey(peer_key, shared_key_.data())) {
DCHECK_EQ(static_cast<size_t>(SHA256_DIGEST_LENGTH), shared_key_.size());
uint8_t digest[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const uint8_t*>(pin.data()), pin.size(), digest);
memcpy(pin_hash_, digest, sizeof(pin_hash_));
}
TokenRequest::~TokenRequest() = default;
TokenRequest::TokenRequest(TokenRequest&& other) = default;
const std::array<uint8_t, 32>& TokenRequest::shared_key() const {
return shared_key_;
}
std::pair<CtapRequestCommand, base::Optional<cbor::Value>>
TokenRequest::EncodeAsCBOR() const {
static_assert((sizeof(pin_hash_) % AES_BLOCK_SIZE) == 0,
"pin_hash_ is not a multiple of the AES block size");
uint8_t encrypted_pin[sizeof(pin_hash_)];
Encrypt(shared_key_.data(), pin_hash_, encrypted_pin);
return EncodePINCommand(
Subcommand::kGetPINToken,
[this, &encrypted_pin](cbor::Value::MapValue* map) {
map->emplace(static_cast<int>(RequestKey::kKeyAgreement),
std::move(this->cose_key_));
map->emplace(
static_cast<int>(RequestKey::kPINHashEnc),
base::span<const uint8_t>(encrypted_pin, sizeof(encrypted_pin)));
});
}
TokenResponse::TokenResponse() = default;
TokenResponse::~TokenResponse() = default;
TokenResponse::TokenResponse(const TokenResponse&) = default;
// Decrypt AES-256 CBC decrypts some number of whole blocks from |ciphertext|
// into |plaintext|, using |key|.
void Decrypt(const uint8_t key[SHA256_DIGEST_LENGTH],
base::span<const uint8_t> ciphertext,
uint8_t* out_plaintext) {
DCHECK_EQ(0u, ciphertext.size() % AES_BLOCK_SIZE);
EVP_CIPHER_CTX aes_ctx;
EVP_CIPHER_CTX_init(&aes_ctx);
const uint8_t kZeroIV[AES_BLOCK_SIZE] = {0};
CHECK(EVP_DecryptInit_ex(&aes_ctx, EVP_aes_256_cbc(), nullptr, key, kZeroIV));
CHECK(EVP_CIPHER_CTX_set_padding(&aes_ctx, 0 /* no padding */));
CHECK(EVP_Cipher(&aes_ctx, out_plaintext, ciphertext.data(),
ciphertext.size()));
EVP_CIPHER_CTX_cleanup(&aes_ctx);
}
base::Optional<TokenResponse> TokenResponse::Parse(
std::array<uint8_t, 32> shared_key,
const base::Optional<cbor::Value>& cbor) {
if (!cbor || !cbor->is_map()) {
return base::nullopt;
}
const auto& response_map = cbor->GetMap();
auto it =
response_map.find(cbor::Value(static_cast<int>(ResponseKey::kPINToken)));
if (it == response_map.end() || !it->second.is_bytestring()) {
return base::nullopt;
}
const auto& encrypted_token = it->second.GetBytestring();
if (encrypted_token.size() % AES_BLOCK_SIZE != 0) {
return base::nullopt;
}
TokenResponse ret;
ret.token_.resize(encrypted_token.size());
Decrypt(shared_key.data(), encrypted_token, ret.token_.data());
return ret;
}
std::vector<uint8_t> TokenResponse::PinAuth(
const std::array<uint8_t, 32> client_data_hash) {
std::vector<uint8_t> pin_auth;
pin_auth.resize(SHA256_DIGEST_LENGTH);
unsigned hmac_bytes;
CHECK(HMAC(EVP_sha256(), token_.data(), token_.size(),
client_data_hash.data(), client_data_hash.size(), pin_auth.data(),
&hmac_bytes));
DCHECK_EQ(pin_auth.size(), static_cast<size_t>(hmac_bytes));
pin_auth.resize(16);
return pin_auth;
}
} // namespace pin
} // namespace device