Skip to content

Commit e75668a

Browse files
authored
ONLY COSE receipts in snapshots (#7711)
1 parent 93db435 commit e75668a

7 files changed

Lines changed: 226 additions & 86 deletions

File tree

python/src/ccf/ledger.py

Lines changed: 86 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from cryptography.x509 import load_pem_x509_certificate
1616
from cryptography.hazmat.primitives import hashes, serialization
17+
from cryptography.hazmat.backends import default_backend
1718
from cryptography.exceptions import InvalidSignature
1819
from cryptography.hazmat.primitives.asymmetric import utils, ec
1920

@@ -44,6 +45,7 @@
4445
WELL_KNOWN_SINGLETON_TABLE_KEY = bytes(bytearray(8))
4546

4647
SHA256_DIGEST_SIZE = sha256().digest_size
48+
ENCODED_COSE_SIGN1_TAG = 0xD2
4749

4850

4951
class NodeStatus(Enum):
@@ -1020,34 +1022,8 @@ def __init__(self, filename: str):
10201022
if self.is_committed() and not self.is_snapshot_file_1_x():
10211023
receipt_pos = entry_start_pos + self._header.size
10221024
receipt_bytes = _peek_all(self._file, pos=receipt_pos)
1023-
1024-
try:
1025-
receipt = json.loads(receipt_bytes.decode("utf-8"))
1026-
except json.decoder.JSONDecodeError as e:
1027-
raise InvalidSnapshotException(
1028-
f"Cannot read receipt from snapshot {os.path.basename(self._filename)}: Receipt starts at {receipt_pos} (file is {self._file_size} bytes), and contains {receipt_bytes}"
1029-
) from e
1030-
1031-
# Receipts included in snapshots always contain leaf components,
1032-
# including a claims digest and commit evidence, from 2.0.0-rc0 onwards.
1033-
# This verification code deliberately does not support snapshots
1034-
# produced by 2.0.0-dev* releases.
1035-
assert "leaf_components" in receipt
1036-
write_set_digest = bytes.fromhex(
1037-
receipt["leaf_components"]["write_set_digest"]
1038-
)
1039-
claims_digest = bytes.fromhex(receipt["leaf_components"]["claims_digest"])
1040-
commit_evidence_digest = sha256(
1041-
receipt["leaf_components"]["commit_evidence"].encode()
1042-
).digest()
1043-
leaf = (
1044-
sha256(write_set_digest + commit_evidence_digest + claims_digest)
1045-
.digest()
1046-
.hex()
1047-
)
1048-
root = ccf.receipt.root(leaf, receipt["proof"])
1049-
node_cert = load_pem_x509_certificate(receipt["cert"].encode())
1050-
ccf.receipt.verify(root, receipt["signature"], node_cert)
1025+
snapshot_digest = sha256(_peek(self._file, receipt_pos, pos=0)).digest()
1026+
self._verify_snapshot_receipt(receipt_bytes, receipt_pos, snapshot_digest)
10511027

10521028
def is_committed(self):
10531029
return COMMITTED_FILE_SUFFIX in self._filename
@@ -1061,6 +1037,88 @@ def is_snapshot_file_1_x(self):
10611037
def get_len(self) -> int:
10621038
return self._file_size
10631039

1040+
def _verify_snapshot_receipt(
1041+
self, receipt_bytes: bytes, receipt_pos: int, snapshot_digest: bytes
1042+
):
1043+
if not receipt_bytes:
1044+
raise InvalidSnapshotException("Empty snapshot receipt")
1045+
1046+
first_byte = receipt_bytes[0]
1047+
if first_byte == ENCODED_COSE_SIGN1_TAG:
1048+
self._verify_cose_snapshot_receipt(receipt_bytes, snapshot_digest)
1049+
elif first_byte == ord("{"):
1050+
self._verify_json_snapshot_receipt(
1051+
receipt_bytes, receipt_pos, snapshot_digest
1052+
)
1053+
else:
1054+
raise InvalidSnapshotException(
1055+
f"Invalid snapshot receipt: unrecognised format (first byte: 0x{first_byte:02X})"
1056+
)
1057+
1058+
def _service_public_key(self):
1059+
service_info_table = (
1060+
self.get_public_domain().get_tables().get(SERVICE_INFO_TABLE_NAME)
1061+
)
1062+
if service_info_table is None:
1063+
raise InvalidSnapshotException(
1064+
"Snapshot is missing service info table for COSE receipt verification"
1065+
)
1066+
1067+
service_info = service_info_table.get(WELL_KNOWN_SINGLETON_TABLE_KEY)
1068+
if service_info is None:
1069+
raise InvalidSnapshotException(
1070+
"Snapshot is missing service info for COSE receipt verification"
1071+
)
1072+
1073+
service_info_json = json.loads(service_info)
1074+
cert = load_pem_x509_certificate(
1075+
service_info_json["cert"].encode("ascii"), default_backend()
1076+
)
1077+
return cert.public_key()
1078+
1079+
def _verify_cose_snapshot_receipt(
1080+
self, receipt_bytes: bytes, snapshot_digest: bytes
1081+
):
1082+
ccf.cose.verify_receipt(
1083+
receipt_bytes, self._service_public_key(), snapshot_digest
1084+
)
1085+
1086+
def _verify_json_snapshot_receipt(
1087+
self, receipt_bytes: bytes, receipt_pos: int, snapshot_digest: bytes
1088+
):
1089+
try:
1090+
receipt = json.loads(receipt_bytes.decode("utf-8"))
1091+
except json.decoder.JSONDecodeError as e:
1092+
raise InvalidSnapshotException(
1093+
f"Cannot read receipt from snapshot {os.path.basename(self._filename)}: Receipt starts at {receipt_pos} (file is {self._file_size} bytes), and contains {receipt_bytes!r}"
1094+
) from e
1095+
1096+
# Receipts included in snapshots always contain leaf components,
1097+
# including a claims digest and commit evidence, from 2.0.0-rc0 onwards.
1098+
# This verification code deliberately does not support snapshots
1099+
# produced by 2.0.0-dev* releases.
1100+
assert "leaf_components" in receipt
1101+
write_set_digest = bytes.fromhex(receipt["leaf_components"]["write_set_digest"])
1102+
claims_digest = bytes.fromhex(receipt["leaf_components"]["claims_digest"])
1103+
if snapshot_digest != claims_digest:
1104+
raise InvalidSnapshotException(
1105+
f"Snapshot digest ({snapshot_digest.hex()}) does not match receipt claim ({claims_digest.hex()})"
1106+
)
1107+
1108+
commit_evidence_digest = sha256(
1109+
receipt["leaf_components"]["commit_evidence"].encode()
1110+
).digest()
1111+
leaf = (
1112+
sha256(write_set_digest + commit_evidence_digest + claims_digest)
1113+
.digest()
1114+
.hex()
1115+
)
1116+
root = ccf.receipt.root(leaf, receipt["proof"])
1117+
node_cert = load_pem_x509_certificate(
1118+
receipt["cert"].encode(), default_backend()
1119+
)
1120+
ccf.receipt.verify(root, receipt["signature"], node_cert)
1121+
10641122

10651123
class TransactionIterator:
10661124
_positions: list[int]

src/node/cose_common.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ namespace ccf::cose
194194
{
195195
CcfCoseReceiptPhdr phdr;
196196
std::vector<uint8_t> merkle_root;
197+
std::vector<uint8_t> claims_digest;
197198
};
198199

199200
static std::vector<uint8_t> recompute_merkle_root(const MerkleProof& proof)
@@ -446,6 +447,7 @@ namespace ccf::cose
446447
}
447448

448449
receipt.merkle_root = recompute_merkle_root(proofs[0]);
450+
receipt.claims_digest = proofs[0].leaf.claims_digest;
449451
for (size_t i = 1; i < proofs.size(); ++i)
450452
{
451453
auto root = recompute_merkle_root(proofs[i]);
@@ -454,6 +456,13 @@ namespace ccf::cose
454456
throw COSEDecodeError(
455457
"Inconsistent Merkle roots computed from COSE receipt proofs");
456458
}
459+
if (proofs[i].leaf.claims_digest != receipt.claims_digest)
460+
{
461+
throw COSEDecodeError(fmt::format(
462+
"Claims from proofs don't match: {} != {}",
463+
ds::to_hex(receipt.claims_digest),
464+
ds::to_hex(proofs[i].leaf.claims_digest)));
465+
}
457466
}
458467
}
459468

src/node/node_state.h

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3038,14 +3038,13 @@ namespace ccf
30383038
// are always written by each signature transaction.
30393039

30403040
network.tables->set_map_hook(
3041-
network.signatures.get_name(),
3042-
Signatures::wrap_map_hook(
3041+
network.cose_signatures.get_name(),
3042+
CoseSignatures::wrap_map_hook(
30433043
[s = this->snapshotter](
30443044
ccf::kv::Version version,
3045-
const Signatures::Write& w) -> ccf::kv::ConsensusHookPtr {
3045+
const CoseSignatures::Write& w) -> ccf::kv::ConsensusHookPtr {
30463046
assert(w.has_value());
3047-
auto sig = w.value();
3048-
s->record_signature(version, sig.sig, sig.node, sig.cert);
3047+
s->record_cose_signature(version, w.value());
30493048
return {nullptr};
30503049
}));
30513050

src/node/snapshot_serdes.h

Lines changed: 111 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
// Licensed under the Apache 2.0 License.
33
#pragma once
44

5+
#include "ccf/crypto/cose.h"
6+
#include "ccf/crypto/cose_verifier.h"
57
#include "ccf/historical_queries_adapter.h"
68
#include "ccf/service/tables/nodes.h"
9+
#include "crypto/cose.h"
710
#include "ds/internal_logger.h"
811
#include "ds/serialized.h"
912
#include "kv/kv_types.h"
1013
#include "kv/serialised_entry_format.h"
14+
#include "node/cose_common.h"
1115
#include "node/history.h"
1216
#include "node/tx_receipt_impl.h"
1317

@@ -61,29 +65,47 @@ namespace ccf
6165
return SnapshotSegments{header_and_body, receipt};
6266
}
6367

64-
static void verify_snapshot(
68+
static void verify_cose_snapshot_receipt(
6569
const SnapshotSegments& segments,
66-
std::optional<std::vector<uint8_t>> prev_service_identity = std::nullopt)
70+
const std::optional<std::vector<uint8_t>>& prev_service_identity)
6771
{
68-
LOG_INFO_FMT(
69-
"Deserialising snapshot receipt (size: {}).", segments.receipt.size());
70-
constexpr size_t max_printed_size = 1024;
71-
if (segments.receipt.size() > max_printed_size)
72+
auto receipt = ccf::cose::decode_ccf_receipt(
73+
{segments.receipt.begin(), segments.receipt.end()},
74+
/* recompute_root */ true);
75+
76+
auto snapshot_digest = ccf::crypto::Sha256Hash(
77+
segments.header_and_body.data(), segments.header_and_body.size());
78+
if (
79+
receipt.claims_digest.size() != ccf::crypto::Sha256Hash::SIZE ||
80+
std::memcmp(
81+
snapshot_digest.h.data(),
82+
receipt.claims_digest.data(),
83+
ccf::crypto::Sha256Hash::SIZE) != 0)
7284
{
73-
LOG_INFO_FMT(
74-
"Receipt size ({}) exceeds max printed size ({}), only printing "
75-
"first {} bytes",
76-
segments.receipt.size(),
77-
max_printed_size,
78-
max_printed_size);
85+
throw std::logic_error(fmt::format(
86+
"Snapshot digest ({}) does not match receipt claim ({})",
87+
snapshot_digest,
88+
ds::to_hex(receipt.claims_digest)));
7989
}
80-
auto printed_size =
81-
std::min<size_t>(segments.receipt.size(), max_printed_size);
82-
LOG_INFO_FMT(
83-
"{}",
84-
ds::to_hex(
85-
segments.receipt.data(), segments.receipt.data() + printed_size));
8690

91+
if (prev_service_identity)
92+
{
93+
auto verifier =
94+
ccf::crypto::make_cose_verifier_from_cert(*prev_service_identity);
95+
if (!verifier->verify_detached(segments.receipt, receipt.merkle_root))
96+
{
97+
throw std::logic_error(
98+
"Previous service identity does not match the service identity that "
99+
"signed the snapshot");
100+
}
101+
LOG_DEBUG_FMT("Previous service identity matches snapshot signer");
102+
}
103+
}
104+
105+
static void verify_json_snapshot_receipt(
106+
const SnapshotSegments& segments,
107+
const std::optional<std::vector<uint8_t>>& prev_service_identity)
108+
{
87109
auto j =
88110
nlohmann::json::parse(segments.receipt.begin(), segments.receipt.end());
89111
auto receipt_p = j.get<ReceiptPtr>();
@@ -106,7 +128,6 @@ namespace ccf
106128
}
107129

108130
auto root = receipt->calculate_root();
109-
auto raw_sig = receipt->signature;
110131

111132
auto v = ccf::crypto::make_unique_verifier(receipt->cert);
112133
if (!v->verify_hash(
@@ -135,6 +156,54 @@ namespace ccf
135156
}
136157
}
137158

159+
static void verify_snapshot(
160+
const SnapshotSegments& segments,
161+
std::optional<std::vector<uint8_t>> prev_service_identity = std::nullopt)
162+
{
163+
LOG_INFO_FMT(
164+
"Deserialising snapshot receipt (size: {}).", segments.receipt.size());
165+
constexpr size_t max_printed_size = 1024;
166+
if (segments.receipt.size() > max_printed_size)
167+
{
168+
LOG_INFO_FMT(
169+
"Receipt size ({}) exceeds max printed size ({}), only printing "
170+
"first {} bytes",
171+
segments.receipt.size(),
172+
max_printed_size,
173+
max_printed_size);
174+
}
175+
auto printed_size =
176+
std::min<size_t>(segments.receipt.size(), max_printed_size);
177+
LOG_INFO_FMT(
178+
"{}",
179+
ds::to_hex(
180+
segments.receipt.data(), segments.receipt.data() + printed_size));
181+
182+
if (segments.receipt.empty())
183+
{
184+
throw std::logic_error("Empty snapshot receipt");
185+
}
186+
187+
auto first_byte = segments.receipt[0];
188+
constexpr uint8_t ENCODED_COSE_SIGN1_TAG = 0xD2;
189+
if (first_byte == ENCODED_COSE_SIGN1_TAG)
190+
{
191+
LOG_DEBUG_FMT("Snapshot with COSE receipt detected");
192+
verify_cose_snapshot_receipt(segments, prev_service_identity);
193+
}
194+
else if (first_byte == '{')
195+
{
196+
LOG_DEBUG_FMT("Snapshot with JSON receipt detected");
197+
verify_json_snapshot_receipt(segments, prev_service_identity);
198+
}
199+
else
200+
{
201+
throw std::logic_error(fmt::format(
202+
"Invalid snapshot receipt: unrecognised format (first byte: 0x{:02X})",
203+
first_byte));
204+
}
205+
}
206+
138207
static void deserialise_snapshot(
139208
const std::shared_ptr<ccf::kv::Store>& store,
140209
const SnapshotSegments& segments,
@@ -176,10 +245,8 @@ namespace ccf
176245
}
177246

178247
static std::vector<uint8_t> build_and_serialise_receipt(
179-
const std::vector<uint8_t>& sig,
248+
const std::vector<uint8_t>& cose_sig,
180249
const std::vector<uint8_t>& tree,
181-
const NodeId& node_id,
182-
const ccf::crypto::Pem& node_cert,
183250
ccf::kv::Version seqno,
184251
const ccf::crypto::Sha256Hash& write_set_digest,
185252
const std::string& commit_evidence,
@@ -191,18 +258,33 @@ namespace ccf
191258
// NOLINTNEXTLINE(performance-move-const-arg)
192259
cd.set(std::move(claims_digest));
193260
ccf::TxReceiptImpl tx_receipt(
194-
sig,
195-
std::nullopt, // cose
261+
{},
262+
cose_sig,
196263
proof.get_root(),
197264
proof.get_path(),
198-
node_id,
199-
node_cert,
265+
{},
266+
std::nullopt,
200267
write_set_digest,
201268
commit_evidence,
202269
cd);
203270

204-
auto receipt = ccf::describe_receipt_v1(tx_receipt);
205-
const auto receipt_str = receipt.dump();
206-
return {receipt_str.begin(), receipt_str.end()};
271+
// To be replaced with 'describe_cose_receipt' once 7700 is merged.
272+
auto cose_signature = ccf::describe_cose_signature_v1(tx_receipt);
273+
if (!cose_signature.has_value())
274+
{
275+
throw std::logic_error(
276+
"No COSE signature available for snapshot receipt");
277+
}
278+
auto merkle_proof = ccf::describe_merkle_proof_v1(tx_receipt);
279+
if (!merkle_proof.has_value())
280+
{
281+
return *cose_signature;
282+
}
283+
284+
ccf::cose::edit::desc::Value desc{
285+
ccf::cose::edit::pos::AtKey{ccf::cose::header::iana::INCLUSION_PROOFS},
286+
ccf::cose::header::iana::VDP,
287+
*merkle_proof};
288+
return ccf::cose::edit::set_unprotected_header(*cose_signature, desc);
207289
}
208290
}

0 commit comments

Comments
 (0)