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:
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
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
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_
|
185
device/fido/cbor_extract_unittest.cc
Normal file
185
device/fido/cbor_extract_unittest.cc
Normal file
@ -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
|
Reference in New Issue
Block a user