Skip to content

Commit ee96ff2

Browse files
authored
ONLY COSE receipts in snapshots - backport (#7712)
1 parent de7b364 commit ee96ff2

23 files changed

Lines changed: 746 additions & 68 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [6.0.25]
9+
10+
[6.0.25]: https://github.com/microsoft/CCF/releases/tag/ccf-6.0.25
11+
12+
### Added
13+
14+
- Support for COSE-only receipts in snapshots to support #7711). #7712
15+
816
## [6.0.24]
917

1018
[6.0.24]: https://github.com/microsoft/CCF/releases/tag/ccf-6.0.24

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "ccf"
7-
version = "6.0.24"
7+
version = "6.0.25"
88
authors = [
99
{ name="CCF Team", email="CCF-Sec@microsoft.com" },
1010
]

src/crypto/cose_receipt.h

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the Apache 2.0 License.
3+
#pragma once
4+
5+
#include "ccf/crypto/sha256_hash.h"
6+
#include "ccf/crypto/verifier.h"
7+
#include "ccf/ds/logger.h"
8+
#include "ccf/receipt.h"
9+
10+
#include <qcbor/qcbor.h>
11+
#include <qcbor/qcbor_spiffy_decode.h>
12+
#include <span>
13+
#include <string>
14+
#include <vector>
15+
16+
namespace ccf::cose
17+
{
18+
// COSE header parameter keys
19+
static constexpr int64_t COSE_HEADER_KEY_ALG = 1;
20+
static constexpr int64_t COSE_HEADER_KEY_KID = 4;
21+
static constexpr int64_t COSE_HEADER_KEY_VDP = 396;
22+
static constexpr int64_t COSE_HEADER_KEY_INCLUSION_PROOFS = -1;
23+
24+
// Decoded Merkle proof from a COSE receipt unprotected header.
25+
struct MerkleProof
26+
{
27+
std::vector<uint8_t> write_set_digest;
28+
std::string commit_evidence;
29+
std::vector<uint8_t> claims_digest;
30+
// Each element: (direction, hash). direction != 0 means left sibling.
31+
std::vector<std::pair<int64_t, std::vector<uint8_t>>> path;
32+
};
33+
34+
// Result of parsing a COSE receipt's headers.
35+
struct ReceiptContents
36+
{
37+
std::string kid;
38+
std::vector<MerkleProof> proofs;
39+
};
40+
41+
// --- QCBOR helpers ---
42+
43+
static inline std::vector<uint8_t> qcbor_bstr_to_bytes(const QCBORItem& item)
44+
{
45+
return {
46+
static_cast<const uint8_t*>(item.val.string.ptr),
47+
static_cast<const uint8_t*>(item.val.string.ptr) + item.val.string.len};
48+
}
49+
50+
static inline std::string qcbor_tstr_to_string(const QCBORItem& item)
51+
{
52+
return {
53+
static_cast<const char*>(item.val.string.ptr),
54+
static_cast<const char*>(item.val.string.ptr) + item.val.string.len};
55+
}
56+
57+
// --- Proof decoding ---
58+
59+
// Decode the leaf components (write set digest, commit evidence, claims
60+
// digest) from a Merkle proof map that has already been entered.
61+
static MerkleProof decode_merkle_proof_leaf(QCBORDecodeContext& ctx)
62+
{
63+
QCBORDecode_EnterArrayFromMapN(
64+
&ctx, ccf::MerkleProofLabel::MERKLE_PROOF_LEAF_LABEL);
65+
if (QCBORDecode_GetError(&ctx) != QCBOR_SUCCESS)
66+
{
67+
throw std::logic_error("Failed to parse Merkle proof leaf array");
68+
}
69+
70+
QCBORItem item;
71+
MerkleProof proof;
72+
73+
QCBORDecode_GetNext(&ctx, &item);
74+
if (item.uDataType != QCBOR_TYPE_BYTE_STRING)
75+
{
76+
throw std::logic_error("Expected byte string for write_set_digest");
77+
}
78+
proof.write_set_digest = qcbor_bstr_to_bytes(item);
79+
80+
QCBORDecode_GetNext(&ctx, &item);
81+
if (item.uDataType != QCBOR_TYPE_TEXT_STRING)
82+
{
83+
throw std::logic_error("Expected text string for commit_evidence");
84+
}
85+
proof.commit_evidence = qcbor_tstr_to_string(item);
86+
87+
QCBORDecode_GetNext(&ctx, &item);
88+
if (item.uDataType != QCBOR_TYPE_BYTE_STRING)
89+
{
90+
throw std::logic_error("Expected byte string for claims_digest");
91+
}
92+
proof.claims_digest = qcbor_bstr_to_bytes(item);
93+
94+
QCBORDecode_ExitArray(&ctx);
95+
return proof;
96+
}
97+
98+
// Decode the path (list of [direction, hash] pairs) from a Merkle proof
99+
// map that has already been entered. Appends to proof.path.
100+
static void decode_merkle_proof_path(
101+
QCBORDecodeContext& ctx, MerkleProof& proof)
102+
{
103+
QCBORDecode_EnterArrayFromMapN(
104+
&ctx, ccf::MerkleProofLabel::MERKLE_PROOF_PATH_LABEL);
105+
if (QCBORDecode_GetError(&ctx) != QCBOR_SUCCESS)
106+
{
107+
throw std::logic_error("Failed to parse Merkle proof path array");
108+
}
109+
110+
for (;;)
111+
{
112+
QCBORItem item;
113+
QCBORDecode_EnterArray(&ctx, &item);
114+
if (QCBORDecode_GetError(&ctx) != QCBOR_SUCCESS)
115+
{
116+
break;
117+
}
118+
119+
std::pair<int64_t, std::vector<uint8_t>> path_item;
120+
121+
if (QCBORDecode_GetNext(&ctx, &item) != QCBOR_SUCCESS)
122+
{
123+
throw std::logic_error("Failed to parse path direction");
124+
}
125+
if (item.uDataType == CBOR_SIMPLEV_TRUE)
126+
{
127+
path_item.first = 1;
128+
}
129+
else if (item.uDataType == CBOR_SIMPLEV_FALSE)
130+
{
131+
path_item.first = 0;
132+
}
133+
else
134+
{
135+
throw std::logic_error("Invalid CBOR boolean in Merkle proof path");
136+
}
137+
138+
if (
139+
QCBORDecode_GetNext(&ctx, &item) != QCBOR_SUCCESS ||
140+
item.uDataType != QCBOR_TYPE_BYTE_STRING)
141+
{
142+
throw std::logic_error("Failed to parse path hash");
143+
}
144+
path_item.second = qcbor_bstr_to_bytes(item);
145+
146+
proof.path.push_back(path_item);
147+
QCBORDecode_ExitArray(&ctx);
148+
}
149+
}
150+
151+
// Decode a single bstr-wrapped Merkle proof (leaf + path).
152+
static MerkleProof decode_merkle_proof(const std::vector<uint8_t>& encoded)
153+
{
154+
q_useful_buf_c buf{encoded.data(), encoded.size()};
155+
QCBORDecodeContext ctx;
156+
QCBORDecode_Init(&ctx, buf, QCBOR_DECODE_MODE_NORMAL);
157+
QCBORDecode_EnterMap(&ctx, NULL);
158+
159+
auto proof = decode_merkle_proof_leaf(ctx);
160+
decode_merkle_proof_path(ctx, proof);
161+
162+
return proof;
163+
}
164+
165+
// --- Root recomputation ---
166+
167+
// Recompute the Merkle root from a decoded proof.
168+
static std::vector<uint8_t> recompute_root(const MerkleProof& proof)
169+
{
170+
auto ce_digest = ccf::crypto::Sha256Hash(proof.commit_evidence);
171+
172+
if (proof.write_set_digest.size() != ccf::crypto::Sha256Hash::SIZE)
173+
{
174+
throw std::logic_error(fmt::format(
175+
"Unsupported write set digest size: {}",
176+
proof.write_set_digest.size()));
177+
}
178+
if (proof.claims_digest.size() != ccf::crypto::Sha256Hash::SIZE)
179+
{
180+
throw std::logic_error(fmt::format(
181+
"Unsupported claims digest size: {}", proof.claims_digest.size()));
182+
}
183+
184+
std::span<const uint8_t, ccf::crypto::Sha256Hash::SIZE> wsd{
185+
proof.write_set_digest.data(), ccf::crypto::Sha256Hash::SIZE};
186+
std::span<const uint8_t, ccf::crypto::Sha256Hash::SIZE> cd{
187+
proof.claims_digest.data(), ccf::crypto::Sha256Hash::SIZE};
188+
auto leaf = ccf::crypto::Sha256Hash(
189+
ccf::crypto::Sha256Hash::from_span(wsd),
190+
ce_digest,
191+
ccf::crypto::Sha256Hash::from_span(cd));
192+
193+
for (const auto& element : proof.path)
194+
{
195+
std::span<const uint8_t, ccf::crypto::Sha256Hash::SIZE> sibling{
196+
element.second.data(), ccf::crypto::Sha256Hash::SIZE};
197+
if (element.first != 0)
198+
{
199+
leaf = ccf::crypto::Sha256Hash(
200+
ccf::crypto::Sha256Hash::from_span(sibling), leaf);
201+
}
202+
else
203+
{
204+
leaf = ccf::crypto::Sha256Hash(
205+
leaf, ccf::crypto::Sha256Hash::from_span(sibling));
206+
}
207+
}
208+
209+
return {leaf.h.begin(), leaf.h.end()};
210+
}
211+
212+
// --- COSE_Sign1 receipt parsing ---
213+
214+
// Extract the KID from the COSE_Sign1 protected header.
215+
// ctx must be positioned at the start of the COSE_Sign1 array elements
216+
// (i.e. after EnterArray). On return, ctx is positioned after the
217+
// protected header bstr.
218+
static std::string extract_kid_from_protected_header(QCBORDecodeContext& ctx)
219+
{
220+
QCBORDecode_EnterBstrWrapped(
221+
&ctx, QCBOR_TAG_REQUIREMENT_NOT_A_TAG, nullptr);
222+
if (QCBORDecode_GetError(&ctx) != QCBOR_SUCCESS)
223+
{
224+
throw std::logic_error("Failed to enter protected header bstr");
225+
}
226+
227+
QCBORDecode_EnterMap(&ctx, nullptr);
228+
if (QCBORDecode_GetError(&ctx) != QCBOR_SUCCESS)
229+
{
230+
throw std::logic_error("Failed to parse protected header map");
231+
}
232+
233+
QCBORItem item;
234+
QCBORDecode_GetItemInMapN(
235+
&ctx, COSE_HEADER_KEY_KID, QCBOR_TYPE_BYTE_STRING, &item);
236+
if (QCBORDecode_GetError(&ctx) != QCBOR_SUCCESS)
237+
{
238+
throw std::logic_error("Failed to find KID in protected header");
239+
}
240+
auto kid = qcbor_tstr_to_string(item);
241+
242+
QCBORDecode_ExitMap(&ctx);
243+
QCBORDecode_ExitBstrWrapped(&ctx);
244+
245+
return kid;
246+
}
247+
248+
// Extract inclusion proofs from the COSE_Sign1 unprotected header.
249+
// ctx must be positioned at the unprotected header (index 1).
250+
// On return, ctx is positioned after the unprotected header.
251+
static std::vector<MerkleProof> extract_inclusion_proofs(
252+
QCBORDecodeContext& ctx)
253+
{
254+
QCBORDecode_EnterMap(&ctx, nullptr);
255+
if (QCBORDecode_GetError(&ctx) != QCBOR_SUCCESS)
256+
{
257+
throw std::logic_error("Failed to parse unprotected header map");
258+
}
259+
260+
QCBORDecode_EnterMapFromMapN(&ctx, COSE_HEADER_KEY_VDP);
261+
if (QCBORDecode_GetError(&ctx) != QCBOR_SUCCESS)
262+
{
263+
throw std::logic_error("Failed to find VDP map in unprotected header");
264+
}
265+
266+
QCBORDecode_EnterArrayFromMapN(&ctx, COSE_HEADER_KEY_INCLUSION_PROOFS);
267+
if (QCBORDecode_GetError(&ctx) != QCBOR_SUCCESS)
268+
{
269+
throw std::logic_error("Failed to find inclusion proofs in VDP map");
270+
}
271+
272+
std::vector<MerkleProof> proofs;
273+
for (;;)
274+
{
275+
QCBORItem item;
276+
if (QCBORDecode_GetNext(&ctx, &item) != QCBOR_SUCCESS)
277+
{
278+
break;
279+
}
280+
if (item.uDataType != QCBOR_TYPE_BYTE_STRING)
281+
{
282+
throw std::logic_error(fmt::format(
283+
"Expected byte string for encoded proof, got QCBOR type {}",
284+
item.uDataType));
285+
}
286+
proofs.push_back(decode_merkle_proof(qcbor_bstr_to_bytes(item)));
287+
}
288+
289+
return proofs;
290+
}
291+
292+
// Parse a COSE_Sign1 receipt, extracting the KID and inclusion proofs.
293+
static ReceiptContents parse_cose_receipt(std::span<const uint8_t> receipt)
294+
{
295+
UsefulBufC cose_buf{receipt.data(), receipt.size()};
296+
QCBORDecodeContext ctx;
297+
QCBORDecode_Init(&ctx, cose_buf, QCBOR_DECODE_MODE_NORMAL);
298+
299+
QCBORDecode_EnterArray(&ctx, nullptr);
300+
if (QCBORDecode_GetError(&ctx) != QCBOR_SUCCESS)
301+
{
302+
throw std::logic_error("Failed to parse COSE_Sign1 outer array");
303+
}
304+
305+
uint64_t tag = QCBORDecode_GetNthTagOfLast(&ctx, 0);
306+
if (tag != CBOR_TAG_COSE_SIGN1)
307+
{
308+
throw std::logic_error("COSE receipt is not tagged as COSE_Sign1");
309+
}
310+
311+
auto kid = extract_kid_from_protected_header(ctx);
312+
auto proofs = extract_inclusion_proofs(ctx);
313+
314+
return {std::move(kid), std::move(proofs)};
315+
}
316+
317+
// Verify that there is a single proof in a CCF receipt, and return recomputed
318+
// root for that proof.
319+
static std::vector<uint8_t> verify_merkle_root(
320+
const std::vector<MerkleProof>& proofs)
321+
{
322+
if (proofs.empty())
323+
{
324+
throw std::logic_error("No Merkle proofs found in COSE receipt");
325+
}
326+
327+
if (proofs.size() != 1)
328+
{
329+
throw std::logic_error(fmt::format(
330+
"Expected exactly one inclusion proof, got {}", proofs.size()));
331+
}
332+
333+
return recompute_root(proofs[0]);
334+
}
335+
336+
// Verify that a KID matches the SHA-256 of a service identity
337+
// certificate's public key.
338+
static void verify_kid_matches_service_identity(
339+
const std::string& kid, const std::vector<uint8_t>& service_identity_pem)
340+
{
341+
ccf::crypto::Pem pem(service_identity_pem);
342+
LOG_DEBUG_FMT("Previous service identity PEM:\n{}", pem.str());
343+
344+
auto cert_der = ccf::crypto::cert_pem_to_der(pem);
345+
auto pubk_der = ccf::crypto::public_key_der_from_cert(cert_der);
346+
auto expected_kid = ccf::crypto::Sha256Hash(pubk_der).hex_str();
347+
348+
if (kid != expected_kid)
349+
{
350+
throw std::logic_error(fmt::format(
351+
"COSE receipt KID ({}) does not match SHA-256 of previous service "
352+
"identity public key ({})",
353+
kid,
354+
expected_kid));
355+
}
356+
LOG_DEBUG_FMT(
357+
"COSE receipt KID matches previous service identity public key");
358+
}
359+
}

0 commit comments

Comments
 (0)