From 1b30b2472f2eb6ddb2b8c61d5dbd1188479d55b2 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 22 Aug 2024 16:55:29 +0100 Subject: [PATCH] COSE signing API for raw payload (#6444) Co-authored-by: Amaury Chamayou Co-authored-by: Amaury Chamayou --- cmake/crypto.cmake | 1 + cmake/t_cose.cmake | 2 +- src/crypto/openssl/cose_sign.cpp | 141 +++++++++++++++++++++++++++++ src/crypto/openssl/cose_sign.h | 30 +++++++ src/crypto/test/crypto.cpp | 148 ++++++++++++++++++++++++++++++- 5 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 src/crypto/openssl/cose_sign.cpp create mode 100644 src/crypto/openssl/cose_sign.h diff --git a/cmake/crypto.cmake b/cmake/crypto.cmake index 8dfb7e1b0..c5de9c2fc 100644 --- a/cmake/crypto.cmake +++ b/cmake/crypto.cmake @@ -25,6 +25,7 @@ set(CCFCRYPTO_SRC ${CCF_DIR}/src/crypto/openssl/rsa_key_pair.cpp ${CCF_DIR}/src/crypto/openssl/verifier.cpp ${CCF_DIR}/src/crypto/openssl/cose_verifier.cpp + ${CCF_DIR}/src/crypto/openssl/cose_sign.cpp ${CCF_DIR}/src/crypto/sharing.cpp ) diff --git a/cmake/t_cose.cmake b/cmake/t_cose.cmake index 67a8011da..f1374512f 100644 --- a/cmake/t_cose.cmake +++ b/cmake/t_cose.cmake @@ -9,7 +9,7 @@ set(T_COSE_DEFS -DT_COSE_USE_OPENSSL_CRYPTO=1 ) set(T_COSE_SRCS "${T_COSE_SRC}/t_cose_parameters.c" "${T_COSE_SRC}/t_cose_sign1_verify.c" - "${T_COSE_SRC}/t_cose_util.c" + "${T_COSE_SRC}/t_cose_sign1_sign.c" "${T_COSE_SRC}/t_cose_util.c" "${T_COSE_DIR}/crypto_adapters/t_cose_openssl_crypto.c" ) if(COMPILE_TARGET STREQUAL "snp") diff --git a/src/crypto/openssl/cose_sign.cpp b/src/crypto/openssl/cose_sign.cpp new file mode 100644 index 000000000..6ff3d49cf --- /dev/null +++ b/src/crypto/openssl/cose_sign.cpp @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +#include "crypto/openssl/cose_sign.h" + +#include "ccf/ds/logger.h" + +#include +#include + +namespace +{ + constexpr int64_t COSE_HEADER_PARAM_ALG = + 1; // Duplicate of t_cose::COSE_HEADER_PARAM_ALG to keep it compatible. + + size_t estimate_buffer_size( + const ccf::crypto::COSEProtectedHeaders& protected_headers, + std::span payload) + { + size_t result = + 300; // bytes for metadata even everything else is empty. This's the most + // often used value in the t_cose examples, however no recommendation + // is provided which one to use. We will consider this an affordable + // starting point, as soon as we don't expect a shortage of memory on + // the target platforms. + + result = std::accumulate( + protected_headers.begin(), + protected_headers.end(), + result, + [](auto result, const auto& kv) { + return result + sizeof(kv.first) + kv.second.size(); + }); + + return result + payload.size(); + } + + void encode_protected_headers( + t_cose_sign1_sign_ctx* ctx, + QCBOREncodeContext* encode_ctx, + const ccf::crypto::COSEProtectedHeaders& protected_headers) + { + QCBOREncode_BstrWrap(encode_ctx); + QCBOREncode_OpenMap(encode_ctx); + + // This's what the t_cose implementation of `encode_protected_parameters` + // sets unconditionally. + QCBOREncode_AddInt64ToMapN( + encode_ctx, COSE_HEADER_PARAM_ALG, ctx->cose_algorithm_id); + + // Caller-provided headers follow + for (const auto& [label, value] : protected_headers) + { + QCBOREncode_AddSZStringToMapN(encode_ctx, label, value.c_str()); + } + + QCBOREncode_CloseMap(encode_ctx); + QCBOREncode_CloseBstrWrap2(encode_ctx, false, &ctx->protected_parameters); + } + + /* The original `t_cose_sign1_encode_parameters` can't accept a custom set of + parameters to be encoded into headers. This version tags the context as + COSE_SIGN1 and encodes the protected headers in the following order: + - defaults + - algorithm version + - those provided by caller + */ + void encode_parameters_custom( + struct t_cose_sign1_sign_ctx* me, + QCBOREncodeContext* cbor_encode, + const ccf::crypto::COSEProtectedHeaders& protected_headers) + { + QCBOREncode_AddTag(cbor_encode, CBOR_TAG_COSE_SIGN1); + QCBOREncode_OpenArray(cbor_encode); + + encode_protected_headers(me, cbor_encode, protected_headers); + + QCBOREncode_OpenMap(cbor_encode); + // Explicitly leave unprotected headers empty to be an empty map. + QCBOREncode_CloseMap(cbor_encode); + } +} + +namespace ccf::crypto +{ + std::vector cose_sign1( + EVP_PKEY* key, + const COSEProtectedHeaders& protected_headers, + std::span payload) + { + const auto buf_size = estimate_buffer_size(protected_headers, payload); + Q_USEFUL_BUF_MAKE_STACK_UB(signed_cose_buffer, buf_size); + + QCBOREncodeContext cbor_encode; + QCBOREncode_Init(&cbor_encode, signed_cose_buffer); + + t_cose_sign1_sign_ctx sign_ctx; + t_cose_sign1_sign_init(&sign_ctx, 0, T_COSE_ALGORITHM_ES256); + + t_cose_key signing_key; + signing_key.crypto_lib = T_COSE_CRYPTO_LIB_OPENSSL; + signing_key.k.key_ptr = key; + + t_cose_sign1_set_signing_key(&sign_ctx, signing_key, NULL_Q_USEFUL_BUF_C); + + encode_parameters_custom(&sign_ctx, &cbor_encode, protected_headers); + + // Mark empty payload manually. + QCBOREncode_AddNULL(&cbor_encode); + + // If payload is empty - we still want to sign. Putting NULL_Q_USEFUL_BUF_C, + // however, makes t_cose think that the payload is included into the + // context. Luckily, passing empty string instead works, so t_cose works + // emplaces it for TBS (to be signed) as an empty byte sequence. + q_useful_buf_c payload_to_encode = {"", 0}; + if (!payload.empty()) + { + payload_to_encode.ptr = payload.data(); + payload_to_encode.len = payload.size(); + } + auto err = t_cose_sign1_encode_signature_aad_internal( + &sign_ctx, NULL_Q_USEFUL_BUF_C, payload_to_encode, &cbor_encode); + if (err) + { + throw COSESignError( + fmt::format("Can't encode signature with error code {}", err)); + } + + struct q_useful_buf_c signed_cose; + auto qerr = QCBOREncode_Finish(&cbor_encode, &signed_cose); + if (qerr) + { + throw COSESignError( + fmt::format("Can't finish QCBOR encoding with error code {}", err)); + } + + return { + static_cast(signed_cose.ptr), + static_cast(signed_cose.ptr) + signed_cose.len}; + } +} diff --git a/src/crypto/openssl/cose_sign.h b/src/crypto/openssl/cose_sign.h new file mode 100644 index 000000000..f41dc3388 --- /dev/null +++ b/src/crypto/openssl/cose_sign.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include +#include +#include +#include + +namespace ccf::crypto +{ + struct COSESignError : public std::runtime_error + { + COSESignError(const std::string& msg) : std::runtime_error(msg) {} + }; + + using COSEProtectedHeaders = std::unordered_map; + + /* Sign a cose_sign1 payload with custom protected headers as strings, where + - key: integer label to be assigned in a COSE value + - value: string behind the label. + + Labels have to be unique. For standardised labels list check + https://www.iana.org/assignments/cose/cose.xhtml#header-parameters. + */ + std::vector cose_sign1( + EVP_PKEY* key, + const COSEProtectedHeaders& protected_headers, + std::span payload); +} diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index 35bce3efa..e1f31d573 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -14,6 +14,8 @@ #include "ccf/crypto/verifier.h" #include "crypto/certs.h" #include "crypto/csr.h" +#include "crypto/openssl/cose_sign.h" +#include "crypto/openssl/cose_verifier.h" #include "crypto/openssl/key_pair.h" #include "crypto/openssl/rsa_key_pair.h" #include "crypto/openssl/symmetric_key.h" @@ -26,7 +28,10 @@ #include #include #include +#include #include +#include +#include using namespace std; using namespace ccf::crypto; @@ -190,6 +195,107 @@ ccf::crypto::Pem generate_self_signed_cert( kp, name, {}, valid_from, certificate_validity_period_days); } +std::string qcbor_buf_to_string(const UsefulBufC& buf) +{ + return std::string(reinterpret_cast(buf.ptr), buf.len); +} + +t_cose_err_t verify_detached( + EVP_PKEY* key, std::span buf, std::span payload) +{ + t_cose_key cose_key; + cose_key.crypto_lib = T_COSE_CRYPTO_LIB_OPENSSL; + cose_key.k.key_ptr = key; + + t_cose_sign1_verify_ctx verify_ctx; + t_cose_sign1_verify_init(&verify_ctx, T_COSE_OPT_TAG_REQUIRED); + t_cose_sign1_set_verification_key(&verify_ctx, cose_key); + + q_useful_buf_c buf_; + buf_.ptr = buf.data(); + buf_.len = buf.size(); + + q_useful_buf_c payload_; + payload_.ptr = payload.data(); + payload_.len = payload.size(); + + t_cose_err_t error = t_cose_sign1_verify_detached( + &verify_ctx, buf_, NULL_Q_USEFUL_BUF_C, payload_, nullptr); + + return error; +} + +void require_match_headers( + const std::unordered_map& headers, + const std::vector& cose_sign) +{ + UsefulBufC msg{cose_sign.data(), cose_sign.size()}; + + // 0. Init and verify COSE tag + QCBORDecodeContext ctx; + QCBORDecode_Init(&ctx, msg, QCBOR_DECODE_MODE_NORMAL); + QCBORDecode_EnterArray(&ctx, nullptr); + REQUIRE_EQ(QCBORDecode_GetError(&ctx), QCBOR_SUCCESS); + REQUIRE_EQ(QCBORDecode_GetNthTagOfLast(&ctx, 0), CBOR_TAG_COSE_SIGN1); + + // 1. Protected headers + struct q_useful_buf_c protected_parameters; + QCBORDecode_EnterBstrWrapped( + &ctx, QCBOR_TAG_REQUIREMENT_NOT_A_TAG, &protected_parameters); + QCBORDecode_EnterMap(&ctx, NULL); + + QCBORItem header_items[headers.size() + 2]; + size_t curr_id{0}; + for (const auto& kv : headers) + { + header_items[curr_id].label.int64 = kv.first; + header_items[curr_id].uLabelType = QCBOR_TYPE_INT64; + header_items[curr_id].uDataType = QCBOR_TYPE_TEXT_STRING; + + curr_id++; + } + + // Verify 'alg' is default-encoded. + header_items[curr_id].label.int64 = 1; + header_items[curr_id].uLabelType = QCBOR_TYPE_INT64; + header_items[curr_id].uDataType = QCBOR_TYPE_INT64; + + header_items[++curr_id].uLabelType = QCBOR_TYPE_NONE; + + QCBORDecode_GetItemsInMap(&ctx, header_items); + REQUIRE_EQ(QCBORDecode_GetError(&ctx), QCBOR_SUCCESS); + + curr_id = 0; + for (const auto& kv : headers) + { + REQUIRE_NE(header_items[curr_id].uDataType, QCBOR_TYPE_NONE); + REQUIRE_EQ( + qcbor_buf_to_string(header_items[curr_id].val.string), kv.second); + + curr_id++; + } + + // 'alg' + REQUIRE_NE(header_items[curr_id].uDataType, QCBOR_TYPE_NONE); + + QCBORDecode_ExitMap(&ctx); + QCBORDecode_ExitBstrWrapped(&ctx); + + // 2. Unprotected headers (skip). + QCBORItem item; + QCBORDecode_VGetNextConsume(&ctx, &item); + + // 3. Skip payload (detached); + QCBORDecode_GetNext(&ctx, &item); + + // 4. skip signature (should be verified by cose verifier). + QCBORDecode_GetNext(&ctx, &item); + + // 5. Decode can be completed. + QCBORDecode_ExitArray(&ctx); + REQUIRE_EQ(QCBORDecode_Finish(&ctx), QCBOR_SUCCESS); +} + TEST_CASE("Check verifier handles nested certs for both PEM and DER inputs") { auto cert_der = ccf::crypto::raw_from_b64(nested_cert); @@ -1109,4 +1215,44 @@ TEST_CASE("Sign and verify with RSA key") mdtype, verify_salt_legth)); } -} \ No newline at end of file +} + +TEST_CASE("COSE sign & verify") +{ + std::shared_ptr kp = + std::dynamic_pointer_cast( + ccf::crypto::make_key_pair(CurveID::SECP384R1)); + + std::vector payload{1, 10, 42, 43, 44, 45, 100}; + const std::unordered_map protected_headers = { + {36, "thirsty six"}, {47, "hungry seven"}}; + auto cose_sign = cose_sign1(*kp, protected_headers, payload); + + if constexpr (false) // enable to see the whole cose_sign as byte string + { + std::cout << "Public key: " << kp->public_key_pem().str() << std::endl; + std::cout << "Serialised cose: " << std::hex << std::uppercase + << std::setw(2) << std::setfill('0'); + for (uint8_t x : cose_sign) + std::cout << static_cast(x) << ' '; + std::cout << std::endl; + std::cout << "Raw payload: "; + for (uint8_t x : payload) + std::cout << static_cast(x) << ' '; + std::cout << std::endl; + } + + require_match_headers(protected_headers, cose_sign); + + REQUIRE_EQ(verify_detached(*kp, cose_sign, payload), T_COSE_SUCCESS); + + // Wrong payload, must not pass verification. + REQUIRE_EQ( + verify_detached(*kp, cose_sign, std::vector{1, 2, 3}), + T_COSE_ERR_SIG_VERIFY); + + // Empty headers and payload handled correctly + cose_sign = cose_sign1(*kp, {}, {}); + require_match_headers({}, cose_sign); + REQUIRE_EQ(verify_detached(*kp, cose_sign, {}), T_COSE_SUCCESS); +}