diff --git a/cmake/crypto.cmake b/cmake/crypto.cmake index fa8a7324b14..14608984878 100644 --- a/cmake/crypto.cmake +++ b/cmake/crypto.cmake @@ -26,6 +26,7 @@ set(CCFCRYPTO_SRC ${CCF_DIR}/src/crypto/openssl/cose_verifier.cpp ${CCF_DIR}/src/crypto/openssl/cose_sign.cpp ${CCF_DIR}/src/crypto/sharing.cpp + ${CCF_DIR}/src/crypto/cbor.cpp ) find_library(CRYPTO_LIBRARY crypto) @@ -41,6 +42,7 @@ target_link_libraries(ccfcrypto PUBLIC qcbor) target_link_libraries(ccfcrypto PUBLIC t_cose) target_link_libraries(ccfcrypto PUBLIC crypto) target_link_libraries(ccfcrypto PUBLIC ssl) +target_link_libraries(ccfcrypto PRIVATE evercbor) set_property(TARGET ccfcrypto PROPERTY POSITION_INDEPENDENT_CODE ON) install( diff --git a/src/crypto/cbor.cpp b/src/crypto/cbor.cpp new file mode 100644 index 00000000000..840074e9a48 --- /dev/null +++ b/src/crypto/cbor.cpp @@ -0,0 +1,481 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +#include "crypto/cbor.h" + +#include +#include +#include + +#define FMT_HEADER_ONLY +#include + +extern "C" +{ +#include "evercbor/CBORNondet.h" +} + +using namespace ccf::cbor; + +namespace +{ + Value consume(cbor_nondet_t cbor); + + // Helper to wrap operations with context-aware error messages + template + decltype(auto) with_context( + std::string_view context, std::string_view operation, const F& func) + { + try + { + return func(); + } + catch (const CBORDecodeError& e) + { + if (!context.empty()) + { + throw CBORDecodeError( + fmt::format("Failed to {} {}: {}", operation, context, e.what())); + } + throw; + } + } + + void print_indent(std::ostringstream& os, size_t indent) + { + for (size_t i = 0; i < indent; ++i) + { + os << " "; + } + } + + Value consume_unsigned(cbor_nondet_t cbor) + { + Unsigned value{0}; + if (!cbor_nondet_read_uint64(cbor, &value)) + { + throw CBORDecodeError("Failed to consume unsigned value"); + } + return std::make_unique(value); + } + + Value consume_signed(cbor_nondet_t cbor) + { + Signed value{0}; + if (!cbor_nondet_read_int64(cbor, &value)) + { + throw CBORDecodeError("Failed to decode signed value"); + } + return std::make_unique(value); + } + + Value consume_byte_string(cbor_nondet_t cbor) + { + uint8_t* data = nullptr; + uint64_t length = 0; + if (!cbor_nondet_get_byte_string(cbor, &data, &length)) + { + throw CBORDecodeError("Failed to decode byte string"); + } + Bytes value{data, static_cast(length)}; + return std::make_unique(value); + } + + Value consume_text_string(cbor_nondet_t cbor) + { + uint8_t* data = nullptr; + uint64_t length = 0; + if (!cbor_nondet_get_text_string(cbor, &data, &length)) + { + throw CBORDecodeError("Failed to decode text string"); + } + String value{ + reinterpret_cast(data), static_cast(length)}; + return std::make_unique(value); + } + + Value consume_array(cbor_nondet_t cbor) + { + cbor_nondet_array_iterator_t iter; + if (!cbor_nondet_array_iterator_start(cbor, &iter)) + { + throw CBORDecodeError("Failed to start array iterator"); + } + + Array array; + while (!cbor_nondet_array_iterator_is_empty(iter)) + { + cbor_nondet_t item; + if (!cbor_nondet_array_iterator_next(&iter, &item)) + { + throw CBORDecodeError("Failed to get next array item"); + } + array.items.push_back(consume(item)); + } + return std::make_unique(std::move(array)); + } + + Value consume_map(cbor_nondet_t cbor) + { + cbor_map_iterator iter; + if (!cbor_nondet_map_iterator_start(cbor, &iter)) + { + throw CBORDecodeError("Failed to start map iterator"); + } + + Map map; + while (!cbor_nondet_map_iterator_is_empty(iter)) + { + cbor_raw key_raw; + cbor_raw value_raw; + if (!cbor_nondet_map_iterator_next(&iter, &key_raw, &value_raw)) + { + throw CBORDecodeError("Failed to get next map entry"); + } + map.items.emplace_back(consume(key_raw), consume(value_raw)); + } + return std::make_unique(std::move(map)); + } + + Value consume_tagged(cbor_nondet_t cbor) + { + uint64_t tag = 0; + cbor_nondet_t payload; + if (!cbor_nondet_get_tagged(cbor, &payload, &tag)) + { + throw CBORDecodeError("Failed to decode tagged value"); + } + + Tagged tagged; + tagged.tag = tag; + tagged.item = consume(payload); + return std::make_unique(std::move(tagged)); + } + + Value consume_simple(cbor_nondet_t cbor) + { + // Return all as one bytes, leave detailed parsing to the user. EverCBOR + // does not support more granular parsing, as well as floating point numbers + // with extra payload yet. + Simple value{0}; + if (!cbor_nondet_read_simple_value(cbor, &value)) + { + throw CBORDecodeError("Failed to decode simple value"); + } + return std::make_unique(value); + } + + Value consume(cbor_nondet_t cbor) + { + const auto mt = cbor_nondet_major_type(cbor); + switch (mt) + { + case CBOR_MAJOR_TYPE_UINT64: + return consume_unsigned(cbor); + case CBOR_MAJOR_TYPE_NEG_INT64: + return consume_signed(cbor); + case CBOR_MAJOR_TYPE_BYTE_STRING: + return consume_byte_string(cbor); + case CBOR_MAJOR_TYPE_TEXT_STRING: + return consume_text_string(cbor); + case CBOR_MAJOR_TYPE_ARRAY: + return consume_array(cbor); + case CBOR_MAJOR_TYPE_MAP: + return consume_map(cbor); + case CBOR_MAJOR_TYPE_TAGGED: + return consume_tagged(cbor); + case CBOR_MAJOR_TYPE_SIMPLE_VALUE: + return consume_simple(cbor); + default: + throw CBORDecodeError("Unknown CBOR major type"); + } + } + + void print_value_impl( + std::ostringstream& os, const Value& value, size_t indent) + { + if (!value) + { + print_indent(os, indent); + os << "" << std::endl; + return; + } + + std::visit( + [&os, indent](const auto& v) { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + print_indent(os, indent); + os << "Unsigned: " << v << std::endl; + } + else if constexpr (std::is_same_v) + { + print_indent(os, indent); + os << "Signed: " << v << std::endl; + } + else if constexpr (std::is_same_v) + { + print_indent(os, indent); + os << "Bytes[" << v.size() << "]: "; + for (size_t i = 0; i < std::min(v.size(), size_t(16)); ++i) + { + os << std::hex << std::setw(2) << std::setfill('0') + << static_cast(v[i]); + } + if (v.size() > 16) + { + os << "..."; + } + os << std::dec << std::endl; + } + else if constexpr (std::is_same_v) + { + print_indent(os, indent); + os << "String: \"" << v << "\"" << std::endl; + } + else if constexpr (std::is_same_v) + { + print_indent(os, indent); + os << "Array[" << v.items.size() << "]:" << std::endl; + for (const auto& item : v.items) + { + print_value_impl(os, item, indent + 1); + } + } + else if constexpr (std::is_same_v) + { + print_indent(os, indent); + os << "Map[" << v.items.size() << "]:" << std::endl; + for (const auto& [key, val] : v.items) + { + print_indent(os, indent + 1); + os << "Key:" << std::endl; + print_value_impl(os, key, indent + 2); + print_indent(os, indent + 1); + os << "Value:" << std::endl; + print_value_impl(os, val, indent + 2); + } + } + else if constexpr (std::is_same_v) + { + print_indent(os, indent); + os << "Tagged[" << v.tag << "]:" << std::endl; + print_value_impl(os, v.item, indent + 1); + } + }, + value->value); + } + +} // namespace + +namespace ccf::cbor +{ + Value make_unsigned(uint64_t value) + { + return std::make_unique(value); + } + Value make_signed(int64_t value) + { + return std::make_unique(value); + } + Value make_string(std::string_view data) + { + return std::make_unique(data); + } + + Value parse_value(std::span raw, std::string_view context) + { + return with_context(context, "parse", [&] { + cbor_nondet_t cbor; + const bool check_map_key_bound = false; + const size_t map_key_bound = 0; + auto* cbor_parse_input = const_cast(raw.data()); + size_t cbor_parse_size = raw.size(); + if (!cbor_nondet_parse( + check_map_key_bound, + map_key_bound, + &cbor_parse_input, + &cbor_parse_size, + &cbor)) + { + throw CBORDecodeError("Failed to parse top-level cbor"); + } + + return consume(cbor); + }); + } + + std::string print_value(const Value& value, size_t indent) + { + std::ostringstream os; + print_value_impl(os, value, indent); + return os.str(); + } + + const Value& ValueImpl::array_at(size_t index, std::string_view context) const + { + return with_context(context, "access array element", [&]() -> const Value& { + if (!std::holds_alternative(value)) + { + throw CBORDecodeError("Not an array"); + } + + const auto& arr = std::get(value); + if (index >= arr.items.size()) + { + throw CBORDecodeError("Array index out of bounds"); + } + + return arr.items[index]; + }); + } + + const Value& ValueImpl::map_at( + const Value& key, std::string_view context) const + { + return with_context(context, "access map key", [&]() -> const Value& { + if (!std::holds_alternative(value)) + { + throw CBORDecodeError("Not a map"); + } + + // Fail fast: Array, Map, Tagged are not supported as map keys in this + // version, and probably shouldn't be in the future. + std::visit( + [](const auto& k) { + using T = std::decay_t; + if constexpr ( + std::is_same_v || std::is_same_v || + std::is_same_v) + { + throw CBORDecodeError( + "Array, Map, and Tagged values cannot be used as map keys"); + } + }, + key->value); + + const auto& map = std::get(value); + for (const auto& [k, v] : map.items) + { + const bool match = std::visit( + [](const auto& a, const auto& b) -> bool { + using TA = std::decay_t; + using TB = std::decay_t; + + if constexpr (!std::is_same_v) + { + return false; + } + else if constexpr ( + std::is_same_v || std::is_same_v) + { + return a == b; + } + else if constexpr ( + std::is_same_v || std::is_same_v) + { + return std::equal(a.begin(), a.end(), b.begin(), b.end()); + } + else + { + return false; + } + }, + key->value, + k->value); + + if (match) + { + return v; + } + } + + throw CBORDecodeError("Key not found in map"); + }); + } + + size_t ValueImpl::size() const + { + if (std::holds_alternative(value)) + { + const auto& arr = std::get(value); + return arr.items.size(); + } + if (std::holds_alternative(value)) + { + const auto& map = std::get(value); + return map.items.size(); + } + throw CBORDecodeError("Not a collection"); + } + + const Value& ValueImpl::tag_at(uint64_t tag, std::string_view context) const + { + return with_context(context, "extract tag", [&]() -> const Value& { + if (!std::holds_alternative(value)) + { + throw CBORDecodeError("Not a tagged value"); + } + + const auto& tagged = std::get(value); + if (tagged.tag != tag) + { + throw CBORDecodeError("Tag does not match"); + } + + return tagged.item; + }); + } + + Unsigned ValueImpl::as_unsigned(std::string_view context) const + { + return with_context(context, "convert to unsigned", [&] { + if (!std::holds_alternative(value)) + { + throw CBORDecodeError("Not an unsigned value"); + } + return std::get(value); + }); + } + Signed ValueImpl::as_signed(std::string_view context) const + { + return with_context(context, "convert to signed", [&] { + if (!std::holds_alternative(value)) + { + throw CBORDecodeError("Not a signed value"); + } + return std::get(value); + }); + } + Bytes ValueImpl::as_bytes(std::string_view context) const + { + return with_context(context, "convert to bytes", [&] { + if (!std::holds_alternative(value)) + { + throw CBORDecodeError("Not a bytes value"); + } + return std::get(value); + }); + } + String ValueImpl::as_string(std::string_view context) const + { + return with_context(context, "convert to string", [&] { + if (!std::holds_alternative(value)) + { + throw CBORDecodeError("Not a string value"); + } + return std::get(value); + }); + } + Simple ValueImpl::as_simple(std::string_view context) const + { + return with_context(context, "convert to simple", [&] { + if (!std::holds_alternative(value)) + { + throw CBORDecodeError("Not a simple value"); + } + return std::get(value); + }); + } +} // namespace ccf::cbor \ No newline at end of file diff --git a/src/crypto/cbor.h b/src/crypto/cbor.h new file mode 100644 index 00000000000..84212dc5992 --- /dev/null +++ b/src/crypto/cbor.h @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace ccf::cbor +{ + struct ValueImpl; + using Value = std::unique_ptr; + + using Unsigned = uint64_t; + using Signed = int64_t; + using Bytes = std::span; + using String = std::string_view; + using Simple = uint8_t; + + struct Array + { + std::vector items; + }; + + struct Map + { + std::vector> items; + }; + + struct Tagged + { + uint64_t tag{0}; + Value item{nullptr}; + }; + + using Type = + std::variant; + + using CBORDecodeError = std::runtime_error; + + struct ValueImpl + { + ValueImpl(Type value_) : value(std::move(value_)) {} + Type value; + + [[nodiscard]] const Value& array_at( + size_t index, std::string_view context = {}) const; + [[nodiscard]] const Value& map_at( + const Value& key, std::string_view context = {}) const; + [[nodiscard]] const Value& tag_at( + uint64_t tag, std::string_view context = {}) const; + [[nodiscard]] Unsigned as_unsigned(std::string_view context = {}) const; + [[nodiscard]] Signed as_signed(std::string_view context = {}) const; + [[nodiscard]] Bytes as_bytes(std::string_view context = {}) const; + [[nodiscard]] String as_string(std::string_view context = {}) const; + [[nodiscard]] Simple as_simple(std::string_view context = {}) const; + [[nodiscard]] size_t size() const; + }; + + Value make_unsigned(uint64_t value); + Value make_signed(int64_t value); + Value make_string(std::string_view data); + + Value parse_value( + std::span raw, std::string_view context = {}); + std::string print_value(const Value& value, size_t indent = 0); +} // namespace ccf::cbor \ No newline at end of file diff --git a/src/node/uvm_endorsements.cpp b/src/node/uvm_endorsements.cpp index 965c90a3069..c419d4e0ba5 100644 --- a/src/node/uvm_endorsements.cpp +++ b/src/node/uvm_endorsements.cpp @@ -3,13 +3,9 @@ #include "node/uvm_endorsements.h" +#include "crypto/cbor.h" #include "ds/internal_logger.h" -extern "C" -{ -#include "evercbor/CBORNondet.h" -} - namespace ccf { bool matches_uvm_roots_of_trust( @@ -51,449 +47,155 @@ namespace ccf { namespace { - // Header parameter names. We define their type explicitly as char[] - // to enable sizeof() usage later on. - constexpr char HEADER_PARAM_ISSUER[] = "iss"; - constexpr char HEADER_PARAM_FEED[] = "feed"; - - std::vector> decode_x5chain(cbor_nondet_t x5chain) + std::vector> parse_x5chain( + const ccf::cbor::Value& x5chain_value) { - std::vector> parsed; - - cbor_nondet_array_iterator_t array; - if (cbor_nondet_array_iterator_start(x5chain, &array)) + std::vector> chain; + // x5chain can be either an array of byte strings or a single byte + // string + try { - cbor_nondet_t item; - while (cbor_nondet_array_iterator_next(&array, &item)) + for (size_t i = 0; i < x5chain_value->size(); ++i) { - uint8_t* payload = nullptr; - uint64_t len = 0; - if (cbor_nondet_get_byte_string(item, &payload, &len)) - { - parsed.emplace_back(payload, payload + len); // This is a copy - } - else - { - throw COSEDecodeError( - "Next item in x5chain was not of type byte string"); - } - } - if (parsed.empty()) - { - throw COSEDecodeError("x5chain array length was 0 in COSE header"); + const auto x5chain_ctx = "x5chain[" + std::to_string(i) + "]"; + const auto& bytes = + x5chain_value->array_at(i, x5chain_ctx)->as_bytes(x5chain_ctx); + chain.emplace_back(bytes.begin(), bytes.end()); } } - else + catch (const ccf::cbor::CBORDecodeError&) { - uint8_t* payload = nullptr; - uint64_t len = 0; - if (cbor_nondet_get_byte_string(x5chain, &payload, &len)) - { - parsed.emplace_back(payload, payload + len); // This is a copy - } - else - { - throw COSEDecodeError(fmt::format( - "Value type {} of x5chain in COSE header is not array or byte " - "string", - cbor_nondet_major_type(x5chain))); - } + auto bytes = x5chain_value->as_bytes("x5chain"); + chain.emplace_back(bytes.begin(), bytes.end()); } - - return parsed; + return chain; } UvmEndorsementsProtectedHeader decode_protected_header( const std::vector& uvm_endorsements_raw) { - cbor_nondet_t cbor; - auto* cbor_parse_input = - const_cast(uvm_endorsements_raw.data()); - size_t cbor_parse_size = uvm_endorsements_raw.size(); - - if (!cbor_nondet_parse( - true, 0, &cbor_parse_input, &cbor_parse_size, &cbor)) - { - throw COSEDecodeError( - "Failed to validate COSE_Sign1 as a definite-length CBOR object " - "without floating-points and with no maps in map keys"); - } - - uint64_t tag = 0; - cbor_nondet_t tagged_payload; - if (!cbor_nondet_get_tagged(cbor, &tagged_payload, &tag)) - { - throw COSEDecodeError("Failed to parse COSE_Sign1 tag"); - } - - if (tag != CBOR_TAG_COSE_SIGN1) - { - throw COSEDecodeError("Failed to parse COSE_Sign1 tag"); - } - - cbor_nondet_array_iterator_t outer_array; - if (!cbor_nondet_array_iterator_start(tagged_payload, &outer_array)) - { - throw COSEDecodeError("Failed to parse COSE_Sign1 outer array"); - } - - cbor_nondet_t protected_parameters_as_bstr; - if (!cbor_nondet_array_iterator_next( - &outer_array, &protected_parameters_as_bstr)) - { - throw COSEDecodeError( - "Failed to decode COSE_Sign1 protected parameters"); - } - - uint8_t* protected_parameters_input = nullptr; - uint64_t protected_parameters_len64 = 0; - if (!cbor_nondet_get_byte_string( - protected_parameters_as_bstr, - &protected_parameters_input, - &protected_parameters_len64)) - { - throw COSEDecodeError( - "Failed to decode COSE_Sign1 protected parameters"); - } - - size_t protected_parameters_len = protected_parameters_len64; - cbor_nondet_t protected_parameters; - if (!cbor_nondet_parse( - true, - 0, - &protected_parameters_input, - &protected_parameters_len, - &protected_parameters)) - { - throw COSEDecodeError( - "Failed to decode COSE_Sign1 protected parameters"); - } - - enum HeaderIndex : uint8_t - { - ALG_INDEX, - CONTENT_TYPE_INDEX, - X5_CHAIN_INDEX, - ISS_INDEX, - FEED_INDEX, - END_INDEX - }; - cbor_nondet_map_get_multiple_entry_t header_items[END_INDEX]; - - header_items[ALG_INDEX].key = cbor_nondet_mk_int64(headers::PARAM_ALG); - header_items[CONTENT_TYPE_INDEX].key = - cbor_nondet_mk_int64(headers::PARAM_CONTENT_TYPE); - header_items[X5_CHAIN_INDEX].key = - cbor_nondet_mk_int64(headers::PARAM_X5CHAIN); - if (!cbor_nondet_mk_text_string( - const_cast( - reinterpret_cast(HEADER_PARAM_ISSUER)), - sizeof(HEADER_PARAM_ISSUER) - 1, - &header_items[ISS_INDEX] - .key)) // sizeof() - 1 to strip the null terminator from the - // C-style string - { - throw COSEDecodeError("Failed to encode HEADER_PARAM_ISSUER"); - } - if (!cbor_nondet_mk_text_string( - const_cast( - reinterpret_cast(HEADER_PARAM_FEED)), - sizeof(HEADER_PARAM_FEED) - 1, - &header_items[FEED_INDEX] - .key)) // sizeof() - 1 to strip the null terminator from the - // C-style string - { - throw COSEDecodeError("Failed to encode HEADER_PARAM_FEED"); - } - - if (!cbor_nondet_map_get_multiple( - protected_parameters, header_items, END_INDEX)) - { - throw COSEDecodeError("Failed to decode protected header"); - } - - UvmEndorsementsProtectedHeader phdr = {}; - - if (header_items[ALG_INDEX].found) - { - if (!cbor_nondet_read_int64(header_items[ALG_INDEX].value, &phdr.alg)) - { - throw COSEDecodeError("Failed to decode protected header"); - } - } - - if (header_items[CONTENT_TYPE_INDEX].found) - { - uint8_t* payload = nullptr; - uint64_t len = 0; - if (!cbor_nondet_get_text_string( - header_items[CONTENT_TYPE_INDEX].value, &payload, &len)) - { - throw COSEDecodeError("Failed to decode protected header"); - } - phdr.content_type = std::string( - reinterpret_cast(payload), - len); // This is a copy. We don't need to reinstate - // a null terminator because C++ strings are - // not null-terminated. The extra len argument - // to the constructor is crucial to this end. - } - - if (header_items[X5_CHAIN_INDEX].found) - { - phdr.x5_chain = decode_x5chain(header_items[X5_CHAIN_INDEX].value); - } - - if (header_items[ISS_INDEX].found) - { - uint8_t* payload = nullptr; - uint64_t len = 0; - if (!cbor_nondet_get_text_string( - header_items[ISS_INDEX].value, &payload, &len)) - { - throw COSEDecodeError("Failed to decode protected header"); - } - phdr.iss = std::string( - reinterpret_cast(payload), - len); // This is a copy. We don't need to reinstate - // a null terminator because C++ strings are - // not null-terminated. The extra len argument - // to the constructor is crucial to this end. - } - - if (header_items[FEED_INDEX].found) - { - uint8_t* payload = nullptr; - uint64_t len = 0; - if (!cbor_nondet_get_text_string( - header_items[FEED_INDEX].value, &payload, &len)) - { - throw COSEDecodeError("Failed to decode protected header"); - } - phdr.feed = std::string( - reinterpret_cast(payload), - len); // This is a copy. We don't need to reinstate - // a null terminator because C++ strings are - // not null-terminated. The extra len argument - // to the constructor is crucial to this end. - } - - return phdr; - } - } - - std::pair - decode_protected_header_with_cwt( - const std::vector& uvm_endorsements_raw) - { - cbor_nondet_t cbor; - auto* cbor_parse_input = - const_cast(uvm_endorsements_raw.data()); - size_t cbor_parse_size = uvm_endorsements_raw.size(); - - if (!cbor_nondet_parse( - true, 0, &cbor_parse_input, &cbor_parse_size, &cbor)) - { - throw COSEDecodeError( - "Failed to validate COSE_Sign1 as a definite-length CBOR object " - "without floating-points and with no maps in map keys"); - } - - uint64_t tag = 0; - cbor_nondet_t tagged_payload; - if (!cbor_nondet_get_tagged(cbor, &tagged_payload, &tag)) - { - throw COSEDecodeError("Failed to parse COSE_Sign1 tag"); - } - - if (tag != CBOR_TAG_COSE_SIGN1) - { - throw COSEDecodeError("Failed to parse COSE_Sign1 tag"); - } - - cbor_nondet_array_iterator_t outer_array; - if (!cbor_nondet_array_iterator_start(tagged_payload, &outer_array)) - { - throw COSEDecodeError("Failed to parse COSE_Sign1 outer array"); - } - - cbor_nondet_t protected_parameters_as_bstr; - if (!cbor_nondet_array_iterator_next( - &outer_array, &protected_parameters_as_bstr)) - { - throw COSEDecodeError( - "Failed to decode COSE_Sign1 protected parameters"); - } - - uint8_t* protected_parameters_input = nullptr; - uint64_t protected_parameters_len64 = 0; - if (!cbor_nondet_get_byte_string( - protected_parameters_as_bstr, - &protected_parameters_input, - &protected_parameters_len64)) - { - throw COSEDecodeError( - "Failed to decode COSE_Sign1 protected parameters"); - } - - size_t protected_parameters_len = protected_parameters_len64; - cbor_nondet_t protected_parameters; - if (!cbor_nondet_parse( - true, - 0, - &protected_parameters_input, - &protected_parameters_len, - &protected_parameters)) - { - throw COSEDecodeError( - "Failed to decode COSE_Sign1 protected parameters"); - } - - enum HeaderIndex : uint8_t - { - ALG_INDEX, - CONTENT_TYPE_INDEX, - X5_CHAIN_INDEX, - CWT_CLAIMS_INDEX, - END_INDEX - }; - cbor_nondet_map_get_multiple_entry_t header_items[END_INDEX]; - - header_items[ALG_INDEX].key = cbor_nondet_mk_int64(headers::PARAM_ALG); - header_items[CONTENT_TYPE_INDEX].key = cbor_nondet_mk_int64(259); - header_items[X5_CHAIN_INDEX].key = - cbor_nondet_mk_int64(headers::PARAM_X5CHAIN); - - header_items[CWT_CLAIMS_INDEX].key = cbor_nondet_mk_int64(15); - - if (!cbor_nondet_map_get_multiple( - protected_parameters, header_items, END_INDEX)) - { - throw COSEDecodeError("Failed to decode protected header"); - } - - UvmEndorsementsProtectedHeader phdr = {}; - - if (header_items[ALG_INDEX].found) - { - if (!cbor_nondet_read_int64(header_items[ALG_INDEX].value, &phdr.alg)) - { - throw COSEDecodeError("Failed to decode protected header"); - } - } - - if (header_items[CONTENT_TYPE_INDEX].found) - { - uint8_t* payload = nullptr; - uint64_t len = 0; - if (!cbor_nondet_get_text_string( - header_items[CONTENT_TYPE_INDEX].value, &payload, &len)) - { - throw COSEDecodeError("Failed to decode protected header"); - } - phdr.content_type = std::string( - reinterpret_cast(payload), - len); // This is a copy. We don't need to reinstate a null terminator - // because C++ strings are not null-terminated. The extra len - // argument to the constructor is crucial to this end. - } - - if (header_items[X5_CHAIN_INDEX].found) - { - phdr.x5_chain = decode_x5chain(header_items[X5_CHAIN_INDEX].value); - } - - enum CwtIndex : std::uint8_t - { - CWT_ISS_INDEX, - CWT_SUB_INDEX, - CWT_SVN_INDEX, - CWT_END_INDEX, - }; - cbor_nondet_map_get_multiple_entry_t cwt_items[CWT_END_INDEX]; - - cwt_items[CWT_ISS_INDEX].key = cbor_nondet_mk_int64(1); - cwt_items[CWT_SUB_INDEX].key = cbor_nondet_mk_int64(2); - - const char svn_label[] = "svn"; - if (!cbor_nondet_mk_text_string( - const_cast(reinterpret_cast(svn_label)), - sizeof(svn_label) - 1, - &cwt_items[CWT_SVN_INDEX] - .key)) // sizeof() - 1 to strip the null terminator from the - // C-style string - { - throw COSEDecodeError("Failed to encode svn_label"); - } - - if (!header_items[CWT_CLAIMS_INDEX].found) - { - throw COSEDecodeError("CWT claims not found in protected header"); - } - - if (!cbor_nondet_map_get_multiple( - header_items[CWT_CLAIMS_INDEX].value, cwt_items, CWT_END_INDEX)) - { - throw COSEDecodeError("Failed to decode CWT claim contents"); + std::span as_span( + uvm_endorsements_raw.data(), uvm_endorsements_raw.size()); + + auto parsed = ccf::cbor::parse_value(as_span, "COSE envelope"); + const auto& cose_array = + parsed->tag_at(CBOR_TAG_COSE_SIGN1, "COSE_Sign1 tag"); + constexpr std::string_view phdr_context{"COSE_Sign1[0]"}; + const auto& phdr_bytes = cose_array->array_at(0, phdr_context); + auto phdr_bytes_span = phdr_bytes->as_bytes(phdr_context); + auto parsed_phdr = ccf::cbor::parse_value(phdr_bytes_span, "phdr CBOR"); + + UvmEndorsementsProtectedHeader result; + + const auto alg_context = "phdr: " + std::to_string(headers::PARAM_ALG); + const auto& alg = parsed_phdr->map_at( + ccf::cbor::make_unsigned(headers::PARAM_ALG), alg_context); + result.alg = alg->as_signed(alg_context); + + const auto ct_context = + "phdr: " + std::to_string(headers::PARAM_CONTENT_TYPE); + const auto& content_type = parsed_phdr->map_at( + ccf::cbor::make_unsigned(headers::PARAM_CONTENT_TYPE), ct_context); + result.content_type = std::string(content_type->as_string(ct_context)); + + const auto x5chain_context = + "phdr: " + std::to_string(headers::PARAM_X5CHAIN); + result.x5_chain = parse_x5chain(parsed_phdr->map_at( + ccf::cbor::make_unsigned(headers::PARAM_X5CHAIN), x5chain_context)); + + constexpr std::string_view iss_context{"phdr: iss"}; + const auto& iss = + parsed_phdr->map_at(ccf::cbor::make_string("iss"), iss_context); + result.iss = iss->as_string(iss_context); + + constexpr std::string_view feed_context{"phdr: feed"}; + const auto& feed = + parsed_phdr->map_at(ccf::cbor::make_string("feed"), feed_context); + result.feed = std::string(feed->as_string(feed_context)); + + return result; } - if (cwt_items[CWT_ISS_INDEX].found) + std::pair + decode_protected_header_with_cwt( + const std::vector& uvm_endorsements_raw) { - uint8_t* payload = nullptr; - uint64_t len = 0; - if (!cbor_nondet_get_text_string( - cwt_items[CWT_ISS_INDEX].value, &payload, &len)) - { - throw COSEDecodeError("Failed to decode protected header"); - } - phdr.iss = std::string(reinterpret_cast(payload), len); + std::span as_span( + uvm_endorsements_raw.data(), uvm_endorsements_raw.size()); + + auto parsed = ccf::cbor::parse_value(as_span, "COSE envelope"); + const auto& cose_array = + parsed->tag_at(CBOR_TAG_COSE_SIGN1, "COSE_Sign1 tag"); + + constexpr std::string_view phdr_context{"COSE_Sign1[0]"}; + const auto& phdr_bytes = cose_array->array_at(0, phdr_context); + auto phdr_bytes_span = phdr_bytes->as_bytes(phdr_context); + + auto parsed_phdr = ccf::cbor::parse_value(phdr_bytes_span, "phdr CBOR"); + + UvmEndorsementsProtectedHeader result; + + const auto alg_context = "phdr: " + std::to_string(headers::PARAM_ALG); + const auto& alg = parsed_phdr->map_at( + ccf::cbor::make_unsigned(headers::PARAM_ALG), alg_context); + result.alg = alg->as_signed(alg_context); + + const auto ct_context = "phdr: " + std::to_string(259); + const auto& content_type = + parsed_phdr->map_at(ccf::cbor::make_unsigned(259), ct_context); + result.content_type = std::string(content_type->as_string(ct_context)); + + const auto x5chain_context = + "phdr: " + std::to_string(headers::PARAM_X5CHAIN); + result.x5_chain = parse_x5chain(parsed_phdr->map_at( + ccf::cbor::make_unsigned(headers::PARAM_X5CHAIN), x5chain_context)); + + const auto cwt_context = + "phdr: " + std::to_string(ccf::crypto::COSE_PHEADER_KEY_CWT); + const auto& cwt_claims = parsed_phdr->map_at( + ccf::cbor::make_unsigned(ccf::crypto::COSE_PHEADER_KEY_CWT), + cwt_context); + + const auto iss_context = + "cwt: " + std::to_string(ccf::crypto::COSE_PHEADER_KEY_ISS); + const auto& iss = cwt_claims->map_at( + ccf::cbor::make_unsigned(ccf::crypto::COSE_PHEADER_KEY_ISS), + iss_context); + result.iss = std::string(iss->as_string(iss_context)); + + const auto feed_context = + "cwt: " + std::to_string(ccf::crypto::COSE_PHEADER_KEY_SUB); + const auto& feed = cwt_claims->map_at( + ccf::cbor::make_unsigned(ccf::crypto::COSE_PHEADER_KEY_SUB), + feed_context); + result.feed = std::string(feed->as_string(feed_context)); + + constexpr std::string_view svn_context{"cwt: svn"}; + const auto& svn_value = + cwt_claims->map_at(ccf::cbor::make_string("svn"), svn_context); + auto svn = svn_value->as_unsigned(svn_context); + + return {result, std::to_string(svn)}; } - if (cwt_items[CWT_SUB_INDEX].found) + std::span verify_uvm_endorsements_signature( + const ccf::crypto::Pem& leaf_cert_pub_key, + const std::vector& uvm_endorsements_raw) { - uint8_t* payload = nullptr; - uint64_t len = 0; - if (!cbor_nondet_get_text_string( - cwt_items[CWT_SUB_INDEX].value, &payload, &len)) - { - throw COSEDecodeError("Failed to decode protected header"); - } - phdr.feed = std::string(reinterpret_cast(payload), len); - } + auto verifier = + ccf::crypto::make_cose_verifier_from_key(leaf_cert_pub_key); - size_t svn{0}; - if (cwt_items[CWT_SVN_INDEX].found) - { - uint64_t svn64 = 0; - if (!cbor_nondet_read_uint64(cwt_items[CWT_SVN_INDEX].value, &svn64)) + std::span payload; + if (!verifier->verify(uvm_endorsements_raw, payload)) { - throw COSEDecodeError("Failed to decode protected header"); + throw cose::COSESignatureValidationError( + "Signature verification failed"); } - svn = static_cast(svn64); - } - return {phdr, std::to_string(svn)}; - } - - std::span verify_uvm_endorsements_signature( - const ccf::crypto::Pem& leaf_cert_pub_key, - const std::vector& uvm_endorsements_raw) - { - auto verifier = - ccf::crypto::make_cose_verifier_from_key(leaf_cert_pub_key); - - std::span payload; - if (!verifier->verify(uvm_endorsements_raw, payload)) - { - throw cose::COSESignatureValidationError( - "Signature verification failed"); + return payload; } - - return payload; } - } - pal::UVMEndorsements verify_uvm_endorsements( const std::vector& uvm_endorsements_raw, const pal::PlatformAttestationMeasurement& uvm_measurement, @@ -510,7 +212,7 @@ namespace ccf } // Since ContainerPlat 0.2.10, UVM endorsements carry SVN in CWT claims, // alongside ISS and SUB(feed), so on decoding failure fallback to legacy. - catch (const cose::COSEDecodeError&) + catch (const ccf::cbor::CBORDecodeError&) { phdr = cose::decode_protected_header(uvm_endorsements_raw); } diff --git a/src/node/uvm_endorsements.h b/src/node/uvm_endorsements.h index 0edde4f0a9f..1bd4eff555d 100644 --- a/src/node/uvm_endorsements.h +++ b/src/node/uvm_endorsements.h @@ -18,7 +18,7 @@ namespace ccf { struct UvmEndorsementsProtectedHeader { - int64_t alg; + int64_t alg{}; std::string content_type; std::vector> x5_chain; std::string iss;