Skip to content

Commit 1b30b24

Browse files
COSE signing API for raw payload (#6444)
Co-authored-by: Amaury Chamayou <[email protected]> Co-authored-by: Amaury Chamayou <[email protected]>
1 parent 56fd6b7 commit 1b30b24

File tree

5 files changed

+320
-2
lines changed

5 files changed

+320
-2
lines changed

cmake/crypto.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ set(CCFCRYPTO_SRC
2525
${CCF_DIR}/src/crypto/openssl/rsa_key_pair.cpp
2626
${CCF_DIR}/src/crypto/openssl/verifier.cpp
2727
${CCF_DIR}/src/crypto/openssl/cose_verifier.cpp
28+
${CCF_DIR}/src/crypto/openssl/cose_sign.cpp
2829
${CCF_DIR}/src/crypto/sharing.cpp
2930
)
3031

cmake/t_cose.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ set(T_COSE_DEFS -DT_COSE_USE_OPENSSL_CRYPTO=1
99
)
1010
set(T_COSE_SRCS
1111
"${T_COSE_SRC}/t_cose_parameters.c" "${T_COSE_SRC}/t_cose_sign1_verify.c"
12-
"${T_COSE_SRC}/t_cose_util.c"
12+
"${T_COSE_SRC}/t_cose_sign1_sign.c" "${T_COSE_SRC}/t_cose_util.c"
1313
"${T_COSE_DIR}/crypto_adapters/t_cose_openssl_crypto.c"
1414
)
1515
if(COMPILE_TARGET STREQUAL "snp")

src/crypto/openssl/cose_sign.cpp

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the Apache 2.0 License.
3+
4+
#include "crypto/openssl/cose_sign.h"
5+
6+
#include "ccf/ds/logger.h"
7+
8+
#include <openssl/evp.h>
9+
#include <t_cose/t_cose_sign1_sign.h>
10+
11+
namespace
12+
{
13+
constexpr int64_t COSE_HEADER_PARAM_ALG =
14+
1; // Duplicate of t_cose::COSE_HEADER_PARAM_ALG to keep it compatible.
15+
16+
size_t estimate_buffer_size(
17+
const ccf::crypto::COSEProtectedHeaders& protected_headers,
18+
std::span<const uint8_t> payload)
19+
{
20+
size_t result =
21+
300; // bytes for metadata even everything else is empty. This's the most
22+
// often used value in the t_cose examples, however no recommendation
23+
// is provided which one to use. We will consider this an affordable
24+
// starting point, as soon as we don't expect a shortage of memory on
25+
// the target platforms.
26+
27+
result = std::accumulate(
28+
protected_headers.begin(),
29+
protected_headers.end(),
30+
result,
31+
[](auto result, const auto& kv) {
32+
return result + sizeof(kv.first) + kv.second.size();
33+
});
34+
35+
return result + payload.size();
36+
}
37+
38+
void encode_protected_headers(
39+
t_cose_sign1_sign_ctx* ctx,
40+
QCBOREncodeContext* encode_ctx,
41+
const ccf::crypto::COSEProtectedHeaders& protected_headers)
42+
{
43+
QCBOREncode_BstrWrap(encode_ctx);
44+
QCBOREncode_OpenMap(encode_ctx);
45+
46+
// This's what the t_cose implementation of `encode_protected_parameters`
47+
// sets unconditionally.
48+
QCBOREncode_AddInt64ToMapN(
49+
encode_ctx, COSE_HEADER_PARAM_ALG, ctx->cose_algorithm_id);
50+
51+
// Caller-provided headers follow
52+
for (const auto& [label, value] : protected_headers)
53+
{
54+
QCBOREncode_AddSZStringToMapN(encode_ctx, label, value.c_str());
55+
}
56+
57+
QCBOREncode_CloseMap(encode_ctx);
58+
QCBOREncode_CloseBstrWrap2(encode_ctx, false, &ctx->protected_parameters);
59+
}
60+
61+
/* The original `t_cose_sign1_encode_parameters` can't accept a custom set of
62+
parameters to be encoded into headers. This version tags the context as
63+
COSE_SIGN1 and encodes the protected headers in the following order:
64+
- defaults
65+
- algorithm version
66+
- those provided by caller
67+
*/
68+
void encode_parameters_custom(
69+
struct t_cose_sign1_sign_ctx* me,
70+
QCBOREncodeContext* cbor_encode,
71+
const ccf::crypto::COSEProtectedHeaders& protected_headers)
72+
{
73+
QCBOREncode_AddTag(cbor_encode, CBOR_TAG_COSE_SIGN1);
74+
QCBOREncode_OpenArray(cbor_encode);
75+
76+
encode_protected_headers(me, cbor_encode, protected_headers);
77+
78+
QCBOREncode_OpenMap(cbor_encode);
79+
// Explicitly leave unprotected headers empty to be an empty map.
80+
QCBOREncode_CloseMap(cbor_encode);
81+
}
82+
}
83+
84+
namespace ccf::crypto
85+
{
86+
std::vector<uint8_t> cose_sign1(
87+
EVP_PKEY* key,
88+
const COSEProtectedHeaders& protected_headers,
89+
std::span<const uint8_t> payload)
90+
{
91+
const auto buf_size = estimate_buffer_size(protected_headers, payload);
92+
Q_USEFUL_BUF_MAKE_STACK_UB(signed_cose_buffer, buf_size);
93+
94+
QCBOREncodeContext cbor_encode;
95+
QCBOREncode_Init(&cbor_encode, signed_cose_buffer);
96+
97+
t_cose_sign1_sign_ctx sign_ctx;
98+
t_cose_sign1_sign_init(&sign_ctx, 0, T_COSE_ALGORITHM_ES256);
99+
100+
t_cose_key signing_key;
101+
signing_key.crypto_lib = T_COSE_CRYPTO_LIB_OPENSSL;
102+
signing_key.k.key_ptr = key;
103+
104+
t_cose_sign1_set_signing_key(&sign_ctx, signing_key, NULL_Q_USEFUL_BUF_C);
105+
106+
encode_parameters_custom(&sign_ctx, &cbor_encode, protected_headers);
107+
108+
// Mark empty payload manually.
109+
QCBOREncode_AddNULL(&cbor_encode);
110+
111+
// If payload is empty - we still want to sign. Putting NULL_Q_USEFUL_BUF_C,
112+
// however, makes t_cose think that the payload is included into the
113+
// context. Luckily, passing empty string instead works, so t_cose works
114+
// emplaces it for TBS (to be signed) as an empty byte sequence.
115+
q_useful_buf_c payload_to_encode = {"", 0};
116+
if (!payload.empty())
117+
{
118+
payload_to_encode.ptr = payload.data();
119+
payload_to_encode.len = payload.size();
120+
}
121+
auto err = t_cose_sign1_encode_signature_aad_internal(
122+
&sign_ctx, NULL_Q_USEFUL_BUF_C, payload_to_encode, &cbor_encode);
123+
if (err)
124+
{
125+
throw COSESignError(
126+
fmt::format("Can't encode signature with error code {}", err));
127+
}
128+
129+
struct q_useful_buf_c signed_cose;
130+
auto qerr = QCBOREncode_Finish(&cbor_encode, &signed_cose);
131+
if (qerr)
132+
{
133+
throw COSESignError(
134+
fmt::format("Can't finish QCBOR encoding with error code {}", err));
135+
}
136+
137+
return {
138+
static_cast<const uint8_t*>(signed_cose.ptr),
139+
static_cast<const uint8_t*>(signed_cose.ptr) + signed_cose.len};
140+
}
141+
}

src/crypto/openssl/cose_sign.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the Apache 2.0 License.
3+
#pragma once
4+
5+
#include <openssl/ossl_typ.h>
6+
#include <span>
7+
#include <string>
8+
#include <unordered_map>
9+
10+
namespace ccf::crypto
11+
{
12+
struct COSESignError : public std::runtime_error
13+
{
14+
COSESignError(const std::string& msg) : std::runtime_error(msg) {}
15+
};
16+
17+
using COSEProtectedHeaders = std::unordered_map<int64_t, std::string>;
18+
19+
/* Sign a cose_sign1 payload with custom protected headers as strings, where
20+
- key: integer label to be assigned in a COSE value
21+
- value: string behind the label.
22+
23+
Labels have to be unique. For standardised labels list check
24+
https://www.iana.org/assignments/cose/cose.xhtml#header-parameters.
25+
*/
26+
std::vector<uint8_t> cose_sign1(
27+
EVP_PKEY* key,
28+
const COSEProtectedHeaders& protected_headers,
29+
std::span<const uint8_t> payload);
30+
}

src/crypto/test/crypto.cpp

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include "ccf/crypto/verifier.h"
1515
#include "crypto/certs.h"
1616
#include "crypto/csr.h"
17+
#include "crypto/openssl/cose_sign.h"
18+
#include "crypto/openssl/cose_verifier.h"
1719
#include "crypto/openssl/key_pair.h"
1820
#include "crypto/openssl/rsa_key_pair.h"
1921
#include "crypto/openssl/symmetric_key.h"
@@ -26,7 +28,10 @@
2628
#include <ctime>
2729
#include <doctest/doctest.h>
2830
#include <optional>
31+
#include <qcbor/qcbor_spiffy_decode.h>
2932
#include <span>
33+
#include <t_cose/t_cose_sign1_sign.h>
34+
#include <t_cose/t_cose_sign1_verify.h>
3035

3136
using namespace std;
3237
using namespace ccf::crypto;
@@ -190,6 +195,107 @@ ccf::crypto::Pem generate_self_signed_cert(
190195
kp, name, {}, valid_from, certificate_validity_period_days);
191196
}
192197

198+
std::string qcbor_buf_to_string(const UsefulBufC& buf)
199+
{
200+
return std::string(reinterpret_cast<const char*>(buf.ptr), buf.len);
201+
}
202+
203+
t_cose_err_t verify_detached(
204+
EVP_PKEY* key, std::span<const uint8_t> buf, std::span<const uint8_t> payload)
205+
{
206+
t_cose_key cose_key;
207+
cose_key.crypto_lib = T_COSE_CRYPTO_LIB_OPENSSL;
208+
cose_key.k.key_ptr = key;
209+
210+
t_cose_sign1_verify_ctx verify_ctx;
211+
t_cose_sign1_verify_init(&verify_ctx, T_COSE_OPT_TAG_REQUIRED);
212+
t_cose_sign1_set_verification_key(&verify_ctx, cose_key);
213+
214+
q_useful_buf_c buf_;
215+
buf_.ptr = buf.data();
216+
buf_.len = buf.size();
217+
218+
q_useful_buf_c payload_;
219+
payload_.ptr = payload.data();
220+
payload_.len = payload.size();
221+
222+
t_cose_err_t error = t_cose_sign1_verify_detached(
223+
&verify_ctx, buf_, NULL_Q_USEFUL_BUF_C, payload_, nullptr);
224+
225+
return error;
226+
}
227+
228+
void require_match_headers(
229+
const std::unordered_map<int64_t, std::string>& headers,
230+
const std::vector<uint8_t>& cose_sign)
231+
{
232+
UsefulBufC msg{cose_sign.data(), cose_sign.size()};
233+
234+
// 0. Init and verify COSE tag
235+
QCBORDecodeContext ctx;
236+
QCBORDecode_Init(&ctx, msg, QCBOR_DECODE_MODE_NORMAL);
237+
QCBORDecode_EnterArray(&ctx, nullptr);
238+
REQUIRE_EQ(QCBORDecode_GetError(&ctx), QCBOR_SUCCESS);
239+
REQUIRE_EQ(QCBORDecode_GetNthTagOfLast(&ctx, 0), CBOR_TAG_COSE_SIGN1);
240+
241+
// 1. Protected headers
242+
struct q_useful_buf_c protected_parameters;
243+
QCBORDecode_EnterBstrWrapped(
244+
&ctx, QCBOR_TAG_REQUIREMENT_NOT_A_TAG, &protected_parameters);
245+
QCBORDecode_EnterMap(&ctx, NULL);
246+
247+
QCBORItem header_items[headers.size() + 2];
248+
size_t curr_id{0};
249+
for (const auto& kv : headers)
250+
{
251+
header_items[curr_id].label.int64 = kv.first;
252+
header_items[curr_id].uLabelType = QCBOR_TYPE_INT64;
253+
header_items[curr_id].uDataType = QCBOR_TYPE_TEXT_STRING;
254+
255+
curr_id++;
256+
}
257+
258+
// Verify 'alg' is default-encoded.
259+
header_items[curr_id].label.int64 = 1;
260+
header_items[curr_id].uLabelType = QCBOR_TYPE_INT64;
261+
header_items[curr_id].uDataType = QCBOR_TYPE_INT64;
262+
263+
header_items[++curr_id].uLabelType = QCBOR_TYPE_NONE;
264+
265+
QCBORDecode_GetItemsInMap(&ctx, header_items);
266+
REQUIRE_EQ(QCBORDecode_GetError(&ctx), QCBOR_SUCCESS);
267+
268+
curr_id = 0;
269+
for (const auto& kv : headers)
270+
{
271+
REQUIRE_NE(header_items[curr_id].uDataType, QCBOR_TYPE_NONE);
272+
REQUIRE_EQ(
273+
qcbor_buf_to_string(header_items[curr_id].val.string), kv.second);
274+
275+
curr_id++;
276+
}
277+
278+
// 'alg'
279+
REQUIRE_NE(header_items[curr_id].uDataType, QCBOR_TYPE_NONE);
280+
281+
QCBORDecode_ExitMap(&ctx);
282+
QCBORDecode_ExitBstrWrapped(&ctx);
283+
284+
// 2. Unprotected headers (skip).
285+
QCBORItem item;
286+
QCBORDecode_VGetNextConsume(&ctx, &item);
287+
288+
// 3. Skip payload (detached);
289+
QCBORDecode_GetNext(&ctx, &item);
290+
291+
// 4. skip signature (should be verified by cose verifier).
292+
QCBORDecode_GetNext(&ctx, &item);
293+
294+
// 5. Decode can be completed.
295+
QCBORDecode_ExitArray(&ctx);
296+
REQUIRE_EQ(QCBORDecode_Finish(&ctx), QCBOR_SUCCESS);
297+
}
298+
193299
TEST_CASE("Check verifier handles nested certs for both PEM and DER inputs")
194300
{
195301
auto cert_der = ccf::crypto::raw_from_b64(nested_cert);
@@ -1109,4 +1215,44 @@ TEST_CASE("Sign and verify with RSA key")
11091215
mdtype,
11101216
verify_salt_legth));
11111217
}
1112-
}
1218+
}
1219+
1220+
TEST_CASE("COSE sign & verify")
1221+
{
1222+
std::shared_ptr<KeyPair_OpenSSL> kp =
1223+
std::dynamic_pointer_cast<KeyPair_OpenSSL>(
1224+
ccf::crypto::make_key_pair(CurveID::SECP384R1));
1225+
1226+
std::vector<uint8_t> payload{1, 10, 42, 43, 44, 45, 100};
1227+
const std::unordered_map<int64_t, std::string> protected_headers = {
1228+
{36, "thirsty six"}, {47, "hungry seven"}};
1229+
auto cose_sign = cose_sign1(*kp, protected_headers, payload);
1230+
1231+
if constexpr (false) // enable to see the whole cose_sign as byte string
1232+
{
1233+
std::cout << "Public key: " << kp->public_key_pem().str() << std::endl;
1234+
std::cout << "Serialised cose: " << std::hex << std::uppercase
1235+
<< std::setw(2) << std::setfill('0');
1236+
for (uint8_t x : cose_sign)
1237+
std::cout << static_cast<int>(x) << ' ';
1238+
std::cout << std::endl;
1239+
std::cout << "Raw payload: ";
1240+
for (uint8_t x : payload)
1241+
std::cout << static_cast<int>(x) << ' ';
1242+
std::cout << std::endl;
1243+
}
1244+
1245+
require_match_headers(protected_headers, cose_sign);
1246+
1247+
REQUIRE_EQ(verify_detached(*kp, cose_sign, payload), T_COSE_SUCCESS);
1248+
1249+
// Wrong payload, must not pass verification.
1250+
REQUIRE_EQ(
1251+
verify_detached(*kp, cose_sign, std::vector<uint8_t>{1, 2, 3}),
1252+
T_COSE_ERR_SIG_VERIFY);
1253+
1254+
// Empty headers and payload handled correctly
1255+
cose_sign = cose_sign1(*kp, {}, {});
1256+
require_match_headers({}, cose_sign);
1257+
REQUIRE_EQ(verify_detached(*kp, cose_sign, {}), T_COSE_SUCCESS);
1258+
}

0 commit comments

Comments
 (0)