0

Reduce caBLE v2 native code size.

The C++ classes that reflect the CTAP2 protocol produce a lot of cruft.
This change removes their use on Android in favour of some more
hair-raising code.

BUG=1002262

Change-Id: I6b039651ea291b14cbd53cb7e153e9a5833d9328
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2116942
Commit-Queue: Adam Langley <agl@chromium.org>
Reviewed-by: Martin Kreichgauer <martinkr@google.com>
Cr-Commit-Position: refs/heads/master@{#754109}
This commit is contained in:
Adam Langley
2020-03-27 19:09:44 +00:00
committed by Commit Bot
parent da77fd7e43
commit 9fe2bf40e8
7 changed files with 867 additions and 87 deletions
chrome/android/features/cablev2_authenticator/internal/native
device

@ -14,19 +14,11 @@
#include "components/device_event_log/device_event_log.h"
#include "crypto/aead.h"
#include "crypto/random.h"
#include "device/fido/attestation_object.h"
#include "device/fido/attestation_statement.h"
#include "device/fido/authenticator_data.h"
#include "device/fido/authenticator_get_info_response.h"
#include "device/fido/authenticator_make_credential_response.h"
#include "device/fido/authenticator_supported_options.h"
#include "device/fido/cable/cable_discovery_data.h"
#include "device/fido/cable/v2_handshake.h"
#include "device/fido/ctap_make_credential_request.h"
#include "device/fido/ec_public_key.h"
#include "device/fido/cbor_extract.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/fido_test_data.h"
#include "device/fido/fido_transport_protocol.h"
#include "third_party/boringssl/src/include/openssl/aes.h"
#include "third_party/boringssl/src/include/openssl/bytestring.h"
@ -50,17 +42,14 @@ using base::android::ToJavaArrayOfByteArray;
using base::android::ToJavaByteArray;
using base::android::ToJavaIntArray;
using device::AttestationObject;
using device::AttestedCredentialData;
using device::AuthenticatorData;
using device::AuthenticatorMakeCredentialResponse;
using device::CtapDeviceResponseCode;
using device::CtapMakeCredentialRequest;
using device::CtapRequestCommand;
using device::ECPublicKey;
using device::FidoTransportProtocol;
using device::NoneAttestationStatement;
using device::PublicKeyCredentialDescriptor;
using device::cbor_extract::IntKey;
using device::cbor_extract::Is;
using device::cbor_extract::Map;
using device::cbor_extract::StepOrByte;
using device::cbor_extract::Stop;
using device::cbor_extract::StringKey;
using device::fido_parsing_utils::CopyCBORBytestring;
namespace {
@ -179,14 +168,74 @@ struct AuthenticatorState {
base::Optional<bssl::UniquePtr<EC_POINT>> qr_peer_identity;
};
struct MakeCredRequest {
const std::vector<uint8_t>* client_data_hash;
const std::string* rp_id;
const std::vector<uint8_t>* user_id;
const cbor::Value::ArrayValue* cred_params;
const cbor::Value::ArrayValue* excluded_credentials;
};
static constexpr StepOrByte<MakeCredRequest> kMakeCredParseSteps[] = {
// clang-format off
ELEMENT(Is::kRequired, MakeCredRequest, client_data_hash),
IntKey<MakeCredRequest>(1),
Map<MakeCredRequest>(),
IntKey<MakeCredRequest>(2),
ELEMENT(Is::kRequired, MakeCredRequest, rp_id),
StringKey<MakeCredRequest>(), 'i', 'd', '\0',
Stop<MakeCredRequest>(),
Map<MakeCredRequest>(),
IntKey<MakeCredRequest>(3),
ELEMENT(Is::kRequired, MakeCredRequest, user_id),
StringKey<MakeCredRequest>(), 'i', 'd', '\0',
Stop<MakeCredRequest>(),
ELEMENT(Is::kRequired, MakeCredRequest, cred_params),
IntKey<MakeCredRequest>(4),
ELEMENT(Is::kOptional, MakeCredRequest, excluded_credentials),
IntKey<MakeCredRequest>(5),
Stop<MakeCredRequest>(),
// clang-format on
};
struct AttestationObject {
const std::string* fmt;
const std::vector<uint8_t>* auth_data;
const cbor::Value* statement;
};
static constexpr StepOrByte<AttestationObject> kAttObjParseSteps[] = {
// clang-format off
ELEMENT(Is::kRequired, AttestationObject, fmt),
StringKey<AttestationObject>(), 'f', 'm', 't', '\0',
ELEMENT(Is::kRequired, AttestationObject, auth_data),
StringKey<AttestationObject>(), 'a', 'u', 't', 'h', 'D', 'a', 't', 'a', '\0',
ELEMENT(Is::kRequired, AttestationObject, statement),
StringKey<AttestationObject>(), 'a', 't', 't', 'S', 't', 'm', 't', '\0',
Stop<AttestationObject>(),
// clang-format on
};
// Client represents the state of a single BLE peer.
class Client {
public:
class Delegate {
public:
virtual ~Delegate() = default;
virtual void OnMakeCredential(uint64_t client_addr,
CtapMakeCredentialRequest request) = 0;
virtual void OnMakeCredential(
uint64_t client_addr,
base::span<const uint8_t> client_data_hash,
const std::string& rp_id,
base::span<const uint8_t> user_id,
base::span<const int> algorithms,
std::vector<std::vector<uint8_t>> excluded_credential_ids,
bool resident_key_required) = 0;
};
Client(uint64_t addr,
@ -341,14 +390,21 @@ class Client {
}
std::array<uint8_t, device::kAaguidLength> aaguid{};
device::AuthenticatorGetInfoResponse get_info(
{device::ProtocolVersion::kCtap2}, aaguid);
std::vector<cbor::Value> versions;
versions.emplace_back("FIDO_2_0");
// TODO: should be based on whether a screen-lock is enabled.
get_info.options.user_verification_availability =
device::AuthenticatorSupportedOptions::
UserVerificationAvailability::kSupportedAndConfigured;
response =
device::AuthenticatorGetInfoResponse::EncodeToCBOR(get_info);
cbor::Value::MapValue options;
options.emplace("uv", true);
cbor::Value::MapValue response_map;
response_map.emplace(1, std::move(versions));
response_map.emplace(3, aaguid);
response_map.emplace(4, std::move(options));
base::Optional<std::vector<uint8_t>> response_bytes(
cbor::Writer::Write(cbor::Value(std::move(response_map))));
response = std::move(*response_bytes);
response.insert(response.begin(), 0);
break;
}
@ -359,13 +415,61 @@ class Client {
FIDO_LOG(ERROR) << "Invalid makeCredential payload";
return false;
}
base::Optional<CtapMakeCredentialRequest> request =
CtapMakeCredentialRequest::Parse(payload->GetMap());
if (!request) {
FIDO_LOG(ERROR) << "CtapMakeCredentialRequest::Parse() failed";
MakeCredRequest make_cred_request;
if (!device::cbor_extract::Extract<MakeCredRequest>(
&make_cred_request, kMakeCredParseSteps,
payload->GetMap())) {
LOG(ERROR) << "Failed to parse makeCredential request";
return false;
}
delegate_->OnMakeCredential(addr_, std::move(*request));
std::vector<int> algorithms;
if (!device::cbor_extract::ForEachPublicKeyEntry(
*make_cred_request.cred_params, cbor::Value("alg"),
base::BindRepeating(
[](std::vector<int>* out,
const cbor::Value& value) -> bool {
if (!value.is_integer()) {
return false;
}
const int64_t alg = value.GetInteger();
if (alg > std::numeric_limits<int>::max() ||
alg < std::numeric_limits<int>::min()) {
return false;
}
out->push_back(static_cast<int>(alg));
return true;
},
base::Unretained(&algorithms)))) {
return false;
}
std::vector<std::vector<uint8_t>> excluded_credential_ids;
if (make_cred_request.excluded_credentials &&
!device::cbor_extract::ForEachPublicKeyEntry(
*make_cred_request.excluded_credentials, cbor::Value("id"),
base::BindRepeating(
[](std::vector<std::vector<uint8_t>>* out,
const cbor::Value& value) -> bool {
if (!value.is_bytestring()) {
return false;
}
out->push_back(value.GetBytestring());
return true;
},
base::Unretained(&excluded_credential_ids)))) {
return false;
}
// TODO: plumb the rk flag through once GmsCore supports resident
// keys. This will require support for optional maps in |Extract|.
delegate_->OnMakeCredential(
addr_, *make_cred_request.client_data_hash,
*make_cred_request.rp_id, *make_cred_request.user_id,
algorithms, excluded_credential_ids,
/*resident_key=*/false);
return true;
}
@ -560,31 +664,23 @@ class CableInterface : public Client::Delegate {
env_, response_fragments ? *response_fragments : kEmptyFragments);
}
void OnMakeCredential(uint64_t client_addr,
CtapMakeCredentialRequest request) override {
std::vector<int> algorithms;
for (const auto& cred_info :
request.public_key_credential_params.public_key_credential_params()) {
if (cred_info.type == device::CredentialType::kPublicKey) {
algorithms.push_back(cred_info.algorithm);
}
}
std::vector<std::vector<uint8_t>> excluded_credential_ids;
for (const PublicKeyCredentialDescriptor& desc : request.exclude_list) {
if (desc.credential_type() == device::CredentialType::kPublicKey) {
excluded_credential_ids.emplace_back(desc.id());
}
}
void OnMakeCredential(
uint64_t client_addr,
base::span<const uint8_t> client_data_hash,
const std::string& rp_id,
base::span<const uint8_t> user_id,
base::span<const int> algorithms,
std::vector<std::vector<uint8_t>> excluded_credential_ids,
bool resident_key_required) override {
// TODO: Add extension support if necessary.
Java_BLEHandler_makeCredential(
env_, ble_handler_, client_addr,
ToJavaByteArray(env_, request.client_data_hash),
ConvertUTF8ToJavaString(env_, request.rp.id),
ToJavaByteArray(env_, client_data_hash),
ConvertUTF8ToJavaString(env_, rp_id),
// TODO: Pass full user entity once resident key support is added.
ToJavaByteArray(env_, request.user.id),
ToJavaIntArray(env_, algorithms),
ToJavaByteArray(env_, user_id), ToJavaIntArray(env_, algorithms),
ToJavaArrayOfByteArray(env_, excluded_credential_ids),
request.resident_key_required);
resident_key_required);
}
void OnMakeCredentialResponse(uint64_t client_addr,
@ -602,22 +698,32 @@ class CableInterface : public Client::Delegate {
// TODO: pass response parameters from the Java side.
base::Optional<cbor::Value> cbor_attestation_object =
cbor::Reader::Read(attestation_object);
if (!cbor_attestation_object) {
if (!cbor_attestation_object || !cbor_attestation_object->is_map()) {
FIDO_LOG(ERROR) << "invalid CBOR attestation object";
return;
}
base::Optional<AttestationObject> attestation_object =
AttestationObject::Parse(std::move(*cbor_attestation_object));
if (!attestation_object) {
FIDO_LOG(ERROR) << "AttestationObject::Parse() failed";
AttestationObject attestation_object;
if (!device::cbor_extract::Extract<AttestationObject>(
&attestation_object, kAttObjParseSteps,
cbor_attestation_object->GetMap())) {
FIDO_LOG(ERROR) << "attestation object parse failed";
return;
}
std::vector<uint8_t> ctap_response =
AsCTAPStyleCBORBytes(AuthenticatorMakeCredentialResponse(
FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy,
std::move(*attestation_object)));
response.insert(response.end(), ctap_response.begin(),
ctap_response.end());
cbor::Value::MapValue response_map;
response_map.emplace(1, base::StringPiece(*attestation_object.fmt));
response_map.emplace(
2, base::span<const uint8_t>(*attestation_object.auth_data));
response_map.emplace(3, attestation_object.statement->Clone());
base::Optional<std::vector<uint8_t>> response_payload =
cbor::Writer::Write(cbor::Value(std::move(response_map)));
if (!response_payload) {
return;
}
response.insert(response.end(), response_payload->begin(),
response_payload->end());
}
base::Optional<std::vector<std::vector<uint8_t>>> response_fragments =

@ -83,6 +83,7 @@ test("device_unittests") {
"fido/cable/fido_cable_discovery_unittest.cc",
"fido/cable/fido_cable_handshake_handler_unittest.cc",
"fido/cable/v2_handshake_unittest.cc",
"fido/cbor_extract_unittest.cc",
"fido/credential_management_handler_unittest.cc",
"fido/ctap_request_unittest.cc",
"fido/ctap_response_unittest.cc",

@ -61,6 +61,7 @@ component("fido") {
"cable/noise.h",
"cable/v2_handshake.cc",
"cable/v2_handshake.h",
"cbor_extract.cc",
"client_data.cc",
"client_data.h",
"credential_management.cc",

@ -30,26 +30,6 @@ std::tuple<std::array<uint8_t, 32>, std::array<uint8_t, 32>> HKDF2(
return std::make_tuple(a, b);
}
// HKDF3 implements the functions with the same name from Noise[1],
// specialized to the case where |num_outputs| is three.
//
// [1] https://www.noiseprotocol.org/noise.html#hash-functions
std::tuple<std::array<uint8_t, 32>,
std::array<uint8_t, 32>,
std::array<uint8_t, 32>>
HKDF3(base::span<const uint8_t, 32> ck, base::span<const uint8_t> ikm) {
uint8_t output[32 * 3];
HKDF(output, sizeof(output), EVP_sha256(), ikm.data(), ikm.size(), ck.data(),
ck.size(), /*info=*/nullptr, 0);
std::array<uint8_t, 32> a, b, c;
memcpy(a.data(), &output[0], 32);
memcpy(b.data(), &output[32], 32);
memcpy(c.data(), &output[64], 32);
return std::make_tuple(a, b, c);
}
} // namespace
namespace device {
@ -102,10 +82,13 @@ void Noise::MixKey(base::span<const uint8_t> ikm) {
void Noise::MixKeyAndHash(base::span<const uint8_t> ikm) {
// See https://www.noiseprotocol.org/noise.html#the-symmetricstate-object
std::array<uint8_t, 32> temp_h, temp_k;
std::tie(chaining_key_, temp_h, temp_k) = HKDF3(chaining_key_, ikm);
MixHash(temp_h);
InitializeKey(temp_k);
uint8_t output[32 * 3];
HKDF(output, sizeof(output), EVP_sha256(), ikm.data(), ikm.size(),
chaining_key_.data(), chaining_key_.size(), /*info=*/nullptr, 0);
DCHECK_EQ(chaining_key_.size(), 32u);
memcpy(chaining_key_.data(), output, 32);
MixHash(base::span<const uint8_t>(&output[32], 32));
InitializeKey(base::span<const uint8_t, 32>(&output[64], 32));
}
std::vector<uint8_t> Noise::EncryptAndHash(

212
device/fido/cbor_extract.cc Normal file

@ -0,0 +1,212 @@
// 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/cbor_extract.h"
#include <type_traits>
#include "base/callback.h"
#include "base/logging.h"
#include "components/cbor/values.h"
namespace device {
namespace cbor_extract {
namespace {
using internal::Type;
static_assert(sizeof(StepOrByte<void>) == 1,
"things should fit into a single byte");
const bool kTrue = true;
const bool kFalse = false;
constexpr uint8_t CBORTypeToBitfield(const cbor::Value::Type type) {
const unsigned type_u = static_cast<unsigned>(type);
if (type_u >= 8) {
__builtin_unreachable();
}
return 1u << type_u;
}
// ASSERT_TYPE_IS asserts that the type of |a| is |b|. This is used to ensure
// that the documented output types for elements of |Type| are correct.
#define ASSERT_TYPE_IS(a, b) \
static_assert( \
std::is_same<decltype(&a), decltype(reinterpret_cast<b*>(0))>::value, \
"types need updating");
class Extractor {
public:
Extractor(base::span<const void*> outputs,
base::span<const StepOrByte<void>> steps)
: outputs_(outputs), steps_(steps) {}
bool ValuesFromMap(const cbor::Value::MapValue& map) {
for (;;) {
// steps_[] emits a CHECK, and we don't want the code-size hit. Thus
// bounds are DCHECKed but then steps_.data() is dereferenced.
DCHECK_LT(step_i_, steps_.size());
const internal::Step step = steps_.data()[step_i_++].step;
const Type value_type = static_cast<Type>(step.value_type);
if (value_type == Type::kStop) {
return true;
}
DCHECK_LT(step_i_, steps_.size());
const uint8_t key_or_string_indicator = steps_.data()[step_i_++].u8;
cbor::Value::MapValue::const_iterator map_it;
if (key_or_string_indicator == StepOrByte<void>::STRING_KEY) {
DCHECK_LT(step_i_, steps_.size());
std::string key(&steps_.data()[step_i_].c);
step_i_ += key.size() + 1;
map_it = map.find(cbor::Value(std::move(key)));
} else {
map_it = map.find(
cbor::Value(static_cast<int64_t>(key_or_string_indicator)));
}
const void** output = nullptr;
if (value_type != Type::kMap) {
DCHECK_LT(step.output_index, outputs_.size());
output = &outputs_.data()[step.output_index];
}
if (map_it == map.end()) {
if (step.required) {
return false;
}
if (output) {
*output = nullptr;
}
continue;
}
// kExpectedCBORTypes is an array of bitmaps of acceptable types for each
// |Type|.
static constexpr uint8_t kExpectedCBORTypes[] = {
// kBytestring
CBORTypeToBitfield(cbor::Value::Type::BYTE_STRING),
// kString
CBORTypeToBitfield(cbor::Value::Type::STRING),
// kBoolean
CBORTypeToBitfield(cbor::Value::Type::SIMPLE_VALUE),
// kInt
CBORTypeToBitfield(cbor::Value::Type::NEGATIVE) |
CBORTypeToBitfield(cbor::Value::Type::UNSIGNED),
// kMap
CBORTypeToBitfield(cbor::Value::Type::MAP),
// kArray
CBORTypeToBitfield(cbor::Value::Type::ARRAY),
// kValue
0xff,
};
const cbor::Value& value = map_it->second;
const unsigned cbor_type_u = static_cast<unsigned>(value.type());
const unsigned value_type_u = static_cast<unsigned>(value_type);
DCHECK(value_type_u < base::size(kExpectedCBORTypes));
if (cbor_type_u >= 8 ||
(kExpectedCBORTypes[value_type_u] & (1u << cbor_type_u)) == 0) {
return false;
}
switch (value_type) {
case Type::kBytestring:
ASSERT_TYPE_IS(value.GetBytestring(), const std::vector<uint8_t>);
*output = &value.GetBytestring();
break;
case Type::kString:
ASSERT_TYPE_IS(value.GetString(), const std::string);
*output = &value.GetString();
break;
case Type::kBoolean:
switch (value.GetSimpleValue()) {
case cbor::Value::SimpleValue::TRUE_VALUE:
*output = &kTrue;
break;
case cbor::Value::SimpleValue::FALSE_VALUE:
*output = &kFalse;
break;
default:
return false;
}
break;
case Type::kInt:
ASSERT_TYPE_IS(value.GetInteger(), const int64_t);
*output = &value.GetInteger();
break;
case Type::kMap:
if (!ValuesFromMap(value.GetMap())) {
return false;
}
break;
case Type::kArray:
ASSERT_TYPE_IS(value.GetArray(), const std::vector<cbor::Value>);
*output = &value.GetArray();
break;
case Type::kValue:
*output = &value;
break;
case Type::kStop:
return false;
}
}
return true;
}
private:
base::span<const void*> outputs_;
base::span<const StepOrByte<void>> steps_;
size_t step_i_ = 0;
};
} // namespace
namespace internal {
bool Extract(base::span<const void*> outputs,
base::span<const StepOrByte<void>> steps,
const cbor::Value::MapValue& map) {
DCHECK(steps[steps.size() - 1].step.value_type ==
static_cast<uint8_t>(Type::kStop));
Extractor extractor(outputs, steps);
return extractor.ValuesFromMap(map);
}
} // namespace internal
bool ForEachPublicKeyEntry(
const cbor::Value::ArrayValue& array,
const cbor::Value& key,
base::RepeatingCallback<bool(const cbor::Value&)> callback) {
const cbor::Value type_key("type");
const std::string public_key("public-key");
for (const cbor::Value& value : array) {
if (!value.is_map()) {
return false;
}
const cbor::Value::MapValue& map = value.GetMap();
const auto type_it = map.find(type_key);
if (type_it == map.end() || !type_it->second.is_string()) {
return false;
}
if (type_it->second.GetString() != public_key) {
continue;
}
const auto value_it = map.find(key);
if (value_it == map.end() || !callback.Run(value_it->second)) {
return false;
}
}
return true;
}
} // namespace cbor_extract
} // namespace device

292
device/fido/cbor_extract.h Normal file

@ -0,0 +1,292 @@
// 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_CBOR_EXTRACT_H_
#define DEVICE_FIDO_CBOR_EXTRACT_H_
#include "base/callback_forward.h"
#include "base/component_export.h"
#include "base/containers/span.h"
#include "components/cbor/values.h"
namespace device {
namespace cbor_extract {
// cbor_extract implements a framework for pulling select members out of a
// cbor::Value and checking that they have the expected type. It is intended for
// use in contexts where code-size is important.
//
// The top-level cbor::Value must be a map. The extraction is driven by a series
// of commands specified by an array of StepOrByte. There are helper functions
// below for constructing the StepOrByte values and using them is strongly
// advised because they're constexprs, thus have no code-size cost, but they can
// statically check for some mistakes.
//
// As an example, consider a CBOR map {1: 2}. In order to extract the member
// with key '1', we can use:
//
// struct MyObj {
// const int64_t *value;
// };
//
// static constexpr StepOrByte<MyObj> kSteps[] = {
// ELEMENT(Is::kRequired, MyObj, value),
// IntKey<MyObj>(1),
// Stop<MyObj>(),
// };
//
// Note that a Stop() is required at the end of every map. If you have nested
// maps, they are deliminated by Stop()s.
//
// ELEMENT specifies an extraction output and whether it's required to have been
// found in the input. The target structure should only contain pointers and the
// CBOR type is taken from the type of the struct member. (See comments in
// |Type| for the list of C++ types.) A value with an incorrect CBOR type is an
// error, even if marked optional. Missing optional values result in |nullptr|.
//
// Only 16 pointers in the output structure can be addressed. Referencing a
// member past the 15th is a compile-time error.
//
// Output values are also pointers into the input cbor::Value, so that cannot
// be destroyed until processing is complete.
//
// Keys for the element are either specified by IntKey<S>(x), where x < 255, or
// StringKey<S>() followed by a NUL-terminated string:
//
// static constexpr StepOrByte<MyObj> kSteps[] = {
// ELEMENT(Is::kRequired, MyObj, value),
// StringKey<MyObj>(), 'k', 'e', 'y', '\0',
// Stop<MyObj>(),
// };
//
// Maps are recursed into and do not result in an output value. (If you want to
// extract a map itself, have an output with type |const cbor::Value *|.)
//
// static constexpr StepOrByte<MyObj> kSteps[] = {
// Map<MyObj>(),
// IntKey<MyObj>(2),
// ELEMENT(Is::kRequired, MyObj, value),
// StringKey<MyObj>(), 'k', 'e', 'y', '\0',
// Stop<MyObj>(),
// };
//
// A map cannot be optional at this time, although that can be fixed later.
//
// The target structure names gets repeated a lot. That's C++ templates for you.
//
// Because the StepOrByte helper functions are constexpr, the steps can be
// evaluated at compile time to produce a compact array of bytes. Each element
// takes a single byte.
enum class Is {
kRequired,
kOptional,
};
namespace internal {
// Type reflects the type of the struct members that can be given to ELEMENT().
enum class Type { // Output type
kBytestring = 0, // const std::vector<uint8_t>*
kString = 1, // const std::string*
kBoolean = 2, // const bool*
kInt = 3, // const int64_t*
kMap = 4, // <no output>
kArray = 5, // const std::vector<cbor::Value>*
kValue = 6, // const cbor::Value*
kStop = 7, // <no output>
};
// Step is an internal detail that needs to be in the header file in order to
// work.
struct Step {
Step() = default;
constexpr Step(uint8_t in_required,
uint8_t in_value_type,
uint8_t in_output_index)
: required(in_required),
value_type(in_value_type),
output_index(in_output_index) {}
struct {
bool required : 1;
uint8_t value_type : 3;
uint8_t output_index : 4;
};
};
} // namespace internal
// StepOrByte is an internal detail that needs to be in the header file in order
// to work.
template <typename S>
struct StepOrByte {
// STRING_KEY is the magic value of |u8| that indicates that this is not an
// integer key, but the a NUL-terminated string follows.
static constexpr uint8_t STRING_KEY = 255;
constexpr explicit StepOrByte(const internal::Step& in_step)
: step(in_step) {}
constexpr explicit StepOrByte(uint8_t b) : u8(b) {}
// This constructor is deliberately not |explicit| so that it's possible to
// write string keys in the steps array.
constexpr StepOrByte(char in_c) : c(in_c) {}
union {
char c;
uint8_t u8;
internal::Step step;
};
};
#define ELEMENT(required, clas, member) \
::device::cbor_extract::internal::Element(required, &clas::member, \
offsetof(clas, member))
template <typename S>
constexpr StepOrByte<S> IntKey(unsigned key) {
if (key >= 256 || key == StepOrByte<S>::STRING_KEY) {
// It's a compile-time error if __builtin_unreachable is reachable.
__builtin_unreachable();
}
return StepOrByte<S>(static_cast<char>(key));
}
template <typename S>
constexpr StepOrByte<S> StringKey() {
return StepOrByte<S>(static_cast<char>(StepOrByte<S>::STRING_KEY));
}
template <typename S>
constexpr StepOrByte<S> Map() {
return StepOrByte<S>(
internal::Step(true, static_cast<uint8_t>(internal::Type::kMap), -1));
}
template <typename S>
constexpr StepOrByte<S> Stop() {
return StepOrByte<S>(
internal::Step(false, static_cast<uint8_t>(internal::Type::kStop), 0));
}
namespace internal {
template <typename S, typename T>
constexpr StepOrByte<S> Element(const Is required,
T S::*member,
uintptr_t offset) {
// This generic version of |Element| causes a compile-time error if ELEMENT
// is used to reference a member with an invalid type.
__builtin_unreachable();
return StepOrByte<S>('\0');
}
// MemberNum translates an offset into a structure into an index if the
// structure is considered as an array of pointers.
constexpr uint8_t MemberNum(uintptr_t offset) {
if (offset % sizeof(void*)) {
__builtin_unreachable();
}
const uintptr_t index = offset / sizeof(void*);
if (index >= 16) {
__builtin_unreachable();
}
return static_cast<uint8_t>(index);
}
template <typename S>
constexpr StepOrByte<S> ElementImpl(const Is required,
uintptr_t offset,
internal::Type type) {
return StepOrByte<S>(internal::Step(required == Is::kRequired,
static_cast<uint8_t>(type),
MemberNum(offset)));
}
// These are specialisations of Element for each of the value output types.
template <typename S>
constexpr StepOrByte<S> Element(const Is required,
const std::vector<uint8_t>* S::*member,
uintptr_t offset) {
return ElementImpl<S>(required, offset, internal::Type::kBytestring);
}
template <typename S>
constexpr StepOrByte<S> Element(const Is required,
const std::string* S::*member,
uintptr_t offset) {
return ElementImpl<S>(required, offset, internal::Type::kString);
}
template <typename S>
constexpr StepOrByte<S> Element(const Is required,
const int64_t* S::*member,
uintptr_t offset) {
return ElementImpl<S>(required, offset, internal::Type::kInt);
}
template <typename S>
constexpr StepOrByte<S> Element(const Is required,
const std::vector<cbor::Value>* S::*member,
uintptr_t offset) {
return ElementImpl<S>(required, offset, internal::Type::kArray);
}
template <typename S>
constexpr StepOrByte<S> Element(const Is required,
const cbor::Value* S::*member,
uintptr_t offset) {
return ElementImpl<S>(required, offset, internal::Type::kValue);
}
template <typename S>
constexpr StepOrByte<S> Element(const Is required,
const bool* S::*member,
uintptr_t offset) {
return ElementImpl<S>(required, offset, internal::Type::kBoolean);
}
COMPONENT_EXPORT(DEVICE_FIDO)
bool Extract(base::span<const void*> outputs,
base::span<const StepOrByte<void>> steps,
const cbor::Value::MapValue& map);
} // namespace internal
template <typename S>
bool Extract(S* output,
base::span<const StepOrByte<S>> steps,
const cbor::Value::MapValue& map) {
// The compiler enforces that |output| points to the correct type and this
// code then erases those types for use in the non-templated internal code. We
// don't want to template the internal code because we don't want the compiler
// to generate a copy for every type.
static_assert(sizeof(S) % sizeof(void*) == 0, "struct contains non-pointers");
static_assert(sizeof(S) >= sizeof(void*),
"empty output structures are invalid, even if you just want to "
"check that maps exist, because the code unconditionally "
"indexes offset zero.");
base::span<const void*> outputs(reinterpret_cast<const void**>(output),
sizeof(S) / sizeof(void*));
base::span<const StepOrByte<void>> steps_void(
reinterpret_cast<const StepOrByte<void>*>(steps.data()), steps.size());
return internal::Extract(outputs, steps_void, map);
}
// ForEachPublicKeyEntry is a helper for dealing with CTAP2 structures. It takes
// an array and, for each value in the array, it expects the value to be a map
// and, in the map, it expects the key "type" to result in a string. If that
// string is not "public-key", it ignores the array element. Otherwise it looks
// up |key| in the map and passes it to |callback|.
COMPONENT_EXPORT(DEVICE_FIDO)
bool ForEachPublicKeyEntry(
const cbor::Value::ArrayValue& array,
const cbor::Value& key,
base::RepeatingCallback<bool(const cbor::Value&)> callback);
} // namespace cbor_extract
} // namespace device
#endif // DEVICE_FIDO_CBOR_EXTRACT_H_

@ -0,0 +1,185 @@
// 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 <algorithm>
#include "base/bind.h"
#include "base/callback.h"
#include "components/cbor/values.h"
#include "device/fido/cbor_extract.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace device {
namespace {
using cbor_extract::IntKey;
using cbor_extract::Is;
using cbor_extract::Map;
using cbor_extract::Stop;
using cbor_extract::StringKey;
template <typename T>
bool VectorSpanEqual(const std::vector<T>& v, base::span<const T> s) {
if (v.size() != s.size()) {
return false;
}
return std::equal(v.begin(), v.end(), s.begin());
}
struct MakeCredRequest {
const std::vector<uint8_t>* client_data_hash;
const std::string* rp_id;
const std::vector<uint8_t>* user_id;
const std::vector<cbor::Value>* cred_params;
const std::vector<cbor::Value>* excluded_credentials;
const bool* resident_key;
const bool* user_verification;
const bool* u8_test;
};
TEST(CBORExtract, Basic) {
cbor::Value::MapValue rp;
rp.emplace("id", "example.com");
rp.emplace("name", "Example");
static const uint8_t kUserId[] = {1, 2, 3, 4};
cbor::Value::MapValue user;
user.emplace("id", base::span<const uint8_t>(kUserId));
user.emplace("name", "Joe");
std::vector<cbor::Value> cred_params;
static const int64_t kAlgs[] = {-7, -257};
for (const int64_t alg : kAlgs) {
cbor::Value::MapValue cred_param;
cred_param.emplace("type", "public-key");
cred_param.emplace("alg", alg);
cred_params.emplace_back(std::move(cred_param));
}
std::vector<cbor::Value> excluded_creds;
for (int i = 0; i < 3; i++) {
cbor::Value::MapValue excluded_cred;
uint8_t id[1] = {static_cast<uint8_t>(i)};
excluded_cred.emplace("type", "public-key");
excluded_cred.emplace("id", base::span<const uint8_t>(id));
excluded_creds.emplace_back(std::move(excluded_cred));
}
cbor::Value::MapValue options;
options.emplace("rk", true);
static const uint8_t kClientDataHash[32] = {4, 3, 2, 1, 0};
cbor::Value::MapValue make_cred;
make_cred.emplace(1, base::span<const uint8_t>(kClientDataHash));
make_cred.emplace(2, std::move(rp));
make_cred.emplace(3, std::move(user));
make_cred.emplace(4, std::move(cred_params));
make_cred.emplace(5, std::move(excluded_creds));
make_cred.emplace(7, std::move(options));
make_cred.emplace(0xf0, false);
static constexpr cbor_extract::StepOrByte<MakeCredRequest>
kMakeCredParseSteps[] = {
// clang-format off
ELEMENT(Is::kRequired, MakeCredRequest, client_data_hash),
IntKey<MakeCredRequest>(1),
Map<MakeCredRequest>(),
IntKey<MakeCredRequest>(2),
ELEMENT(Is::kRequired, MakeCredRequest, rp_id),
StringKey<MakeCredRequest>(), 'i', 'd', '\0',
Stop<MakeCredRequest>(),
Map<MakeCredRequest>(),
IntKey<MakeCredRequest>(3),
ELEMENT(Is::kRequired, MakeCredRequest, user_id),
StringKey<MakeCredRequest>(), 'i', 'd', '\0',
Stop<MakeCredRequest>(),
ELEMENT(Is::kRequired, MakeCredRequest, cred_params),
IntKey<MakeCredRequest>(4),
ELEMENT(Is::kRequired, MakeCredRequest, excluded_credentials),
IntKey<MakeCredRequest>(5),
Map<MakeCredRequest>(),
IntKey<MakeCredRequest>(7),
ELEMENT(Is::kOptional, MakeCredRequest, resident_key),
StringKey<MakeCredRequest>(), 'r', 'k', '\0',
ELEMENT(Is::kOptional, MakeCredRequest, user_verification),
StringKey<MakeCredRequest>(), 'u', 'v', '\0',
Stop<MakeCredRequest>(),
ELEMENT(Is::kRequired, MakeCredRequest, u8_test),
IntKey<MakeCredRequest>(0xf0),
Stop<MakeCredRequest>(),
// clang-format on
};
MakeCredRequest make_cred_request;
ASSERT_TRUE(cbor_extract::Extract<MakeCredRequest>(
&make_cred_request, kMakeCredParseSteps, make_cred));
EXPECT_TRUE(VectorSpanEqual<uint8_t>(*make_cred_request.client_data_hash,
kClientDataHash));
EXPECT_EQ(*make_cred_request.rp_id, "example.com");
EXPECT_TRUE(VectorSpanEqual<uint8_t>(*make_cred_request.user_id, kUserId));
EXPECT_EQ(make_cred_request.cred_params->size(), 2u);
EXPECT_EQ(make_cred_request.excluded_credentials->size(), 3u);
EXPECT_TRUE(*make_cred_request.resident_key);
EXPECT_TRUE(make_cred_request.user_verification == nullptr);
EXPECT_FALSE(*make_cred_request.u8_test);
std::vector<int64_t> algs;
EXPECT_TRUE(cbor_extract::ForEachPublicKeyEntry(
*make_cred_request.cred_params, cbor::Value("alg"),
base::BindRepeating(
[](std::vector<int64_t>* out, const cbor::Value& value) -> bool {
if (!value.is_integer()) {
return false;
}
out->push_back(value.GetInteger());
return true;
},
base::Unretained(&algs))));
EXPECT_TRUE(VectorSpanEqual<int64_t>(algs, kAlgs));
}
TEST(CBORExtract, MissingRequired) {
struct Dummy {
const int64_t* value;
};
static constexpr cbor_extract::StepOrByte<Dummy> kSteps[] = {
ELEMENT(Is::kRequired, Dummy, value),
IntKey<Dummy>(1),
Stop<Dummy>(),
};
cbor::Value::MapValue map;
Dummy dummy;
EXPECT_FALSE(cbor_extract::Extract<Dummy>(&dummy, kSteps, map));
}
TEST(CBORExtract, WrongType) {
struct Dummy {
const int64_t* value;
};
static constexpr cbor_extract::StepOrByte<Dummy> kSteps[] = {
ELEMENT(Is::kRequired, Dummy, value),
IntKey<Dummy>(1),
Stop<Dummy>(),
};
cbor::Value::MapValue map;
map.emplace(1, "string");
Dummy dummy;
EXPECT_FALSE(cbor_extract::Extract<Dummy>(&dummy, kSteps, map));
}
} // namespace
} // namespace device