COSE signing API for raw payload (#6444)

Co-authored-by: Amaury Chamayou <amchamay@microsoft.com>
Co-authored-by: Amaury Chamayou <amaury@xargs.fr>
This commit is contained in:
Max 2024-08-22 16:55:29 +01:00 коммит произвёл GitHub
Родитель 56fd6b79f3
Коммит 1b30b2472f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
5 изменённых файлов: 320 добавлений и 2 удалений

Просмотреть файл

@ -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
)

Просмотреть файл

@ -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")

Просмотреть файл

@ -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 <openssl/evp.h>
#include <t_cose/t_cose_sign1_sign.h>
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<const uint8_t> 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<uint8_t> cose_sign1(
EVP_PKEY* key,
const COSEProtectedHeaders& protected_headers,
std::span<const uint8_t> 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<const uint8_t*>(signed_cose.ptr),
static_cast<const uint8_t*>(signed_cose.ptr) + signed_cose.len};
}
}

Просмотреть файл

@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once
#include <openssl/ossl_typ.h>
#include <span>
#include <string>
#include <unordered_map>
namespace ccf::crypto
{
struct COSESignError : public std::runtime_error
{
COSESignError(const std::string& msg) : std::runtime_error(msg) {}
};
using COSEProtectedHeaders = std::unordered_map<int64_t, std::string>;
/* 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<uint8_t> cose_sign1(
EVP_PKEY* key,
const COSEProtectedHeaders& protected_headers,
std::span<const uint8_t> payload);
}

Просмотреть файл

@ -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 <ctime>
#include <doctest/doctest.h>
#include <optional>
#include <qcbor/qcbor_spiffy_decode.h>
#include <span>
#include <t_cose/t_cose_sign1_sign.h>
#include <t_cose/t_cose_sign1_verify.h>
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<const char*>(buf.ptr), buf.len);
}
t_cose_err_t verify_detached(
EVP_PKEY* key, std::span<const uint8_t> buf, std::span<const uint8_t> 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<int64_t, std::string>& headers,
const std::vector<uint8_t>& 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));
}
}
}
TEST_CASE("COSE sign & verify")
{
std::shared_ptr<KeyPair_OpenSSL> kp =
std::dynamic_pointer_cast<KeyPair_OpenSSL>(
ccf::crypto::make_key_pair(CurveID::SECP384R1));
std::vector<uint8_t> payload{1, 10, 42, 43, 44, 45, 100};
const std::unordered_map<int64_t, std::string> 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<int>(x) << ' ';
std::cout << std::endl;
std::cout << "Raw payload: ";
for (uint8_t x : payload)
std::cout << static_cast<int>(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<uint8_t>{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);
}