зеркало из https://github.com/microsoft/CCF.git
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:
Родитель
56fd6b79f3
Коммит
1b30b2472f
|
@ -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);
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче