Skip to content

Commit 5f823c1

Browse files
authored
Restore cose_signatures configuration from ledger in Recovery (#6709)
1 parent 44a669d commit 5f823c1

File tree

5 files changed

+147
-4
lines changed

5 files changed

+147
-4
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ and this project adheres Fto [Semantic Versioning](http://semver.org/spec/v2.0.0
1313

1414
- `GET /gov/service/javascript-app` now takes an optional `?case=original` query argument. When passed, the response will contain the raw original `snake_case` field names, for direct comparison, rather than the API-standard `camelCase` projections.
1515

16+
### Fixed
17+
18+
- `cose_signatures` configuration (`issuer`/`subject`) is now correctly preserved across disaster recovery (#6709).
19+
1620
### Deprecated
1721

1822
- The function `ccf::get_js_plugins()` and associated FFI plugin system for JS is deprecated. Similar functionality should now be implemented through a `js::Extension` returned from `DynamicJSEndpointRegistry::get_extensions()`.

src/node/cose_common.h

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
#pragma once
55

6+
#include <crypto/openssl/cose_sign.h>
67
#include <qcbor/qcbor.h>
8+
#include <qcbor/qcbor_spiffy_decode.h>
79
#include <stdexcept>
810
#include <string>
911
#include <t_cose/t_cose_common.h>
@@ -58,4 +60,110 @@ namespace ccf::cose
5860
{}
5961
};
6062

63+
static std::string tstring_to_string(QCBORItem& item)
64+
{
65+
return {
66+
static_cast<const char*>(item.val.string.ptr),
67+
static_cast<const char*>(item.val.string.ptr) + item.val.string.len};
68+
}
69+
70+
static std::pair<std::string /* issuer */, std::string /* subject */>
71+
extract_iss_sub_from_sig(const std::vector<uint8_t>& cose_sign1)
72+
{
73+
QCBORError qcbor_result;
74+
QCBORDecodeContext ctx;
75+
UsefulBufC buf{cose_sign1.data(), cose_sign1.size()};
76+
QCBORDecode_Init(&ctx, buf, QCBOR_DECODE_MODE_NORMAL);
77+
78+
QCBORDecode_EnterArray(&ctx, nullptr);
79+
qcbor_result = QCBORDecode_GetError(&ctx);
80+
if (qcbor_result != QCBOR_SUCCESS)
81+
{
82+
throw COSEDecodeError("Failed to parse COSE_Sign1 outer array");
83+
}
84+
85+
uint64_t tag = QCBORDecode_GetNthTagOfLast(&ctx, 0);
86+
if (tag != CBOR_TAG_COSE_SIGN1)
87+
{
88+
throw COSEDecodeError("COSE_Sign1 is not tagged");
89+
}
90+
91+
QCBORDecode_EnterBstrWrapped(&ctx, QCBOR_TAG_REQUIREMENT_NOT_A_TAG, NULL);
92+
QCBORDecode_EnterMap(&ctx, NULL);
93+
94+
enum
95+
{
96+
CWT_CLAIMS_INDEX,
97+
END_INDEX,
98+
};
99+
QCBORItem header_items[END_INDEX + 1];
100+
101+
header_items[CWT_CLAIMS_INDEX].label.int64 = crypto::COSE_PHEADER_KEY_CWT;
102+
header_items[CWT_CLAIMS_INDEX].uLabelType = QCBOR_TYPE_INT64;
103+
header_items[CWT_CLAIMS_INDEX].uDataType = QCBOR_TYPE_MAP;
104+
105+
header_items[END_INDEX].uLabelType = QCBOR_TYPE_NONE;
106+
107+
QCBORDecode_GetItemsInMap(&ctx, header_items);
108+
109+
qcbor_result = QCBORDecode_GetError(&ctx);
110+
if (qcbor_result != QCBOR_SUCCESS)
111+
{
112+
throw COSEDecodeError(
113+
fmt::format("Failed to decode protected header: {}", qcbor_result));
114+
}
115+
116+
if (header_items[CWT_CLAIMS_INDEX].uDataType == QCBOR_TYPE_NONE)
117+
{
118+
throw COSEDecodeError("Missing CWT claims in COSE_Sign1");
119+
}
120+
121+
QCBORDecode_EnterMapFromMapN(&ctx, crypto::COSE_PHEADER_KEY_CWT);
122+
auto decode_error = QCBORDecode_GetError(&ctx);
123+
if (decode_error != QCBOR_SUCCESS)
124+
{
125+
throw COSEDecodeError(
126+
fmt::format("Failed to decode CWT claims: {}", decode_error));
127+
}
128+
129+
enum
130+
{
131+
CWT_ISS_INDEX,
132+
CWT_SUB_INDEX,
133+
CWT_END_INDEX,
134+
};
135+
QCBORItem cwt_items[CWT_END_INDEX + 1];
136+
137+
cwt_items[CWT_ISS_INDEX].label.int64 = crypto::COSE_PHEADER_KEY_ISS;
138+
cwt_items[CWT_ISS_INDEX].uLabelType = QCBOR_TYPE_INT64;
139+
cwt_items[CWT_ISS_INDEX].uDataType = QCBOR_TYPE_TEXT_STRING;
140+
141+
cwt_items[CWT_SUB_INDEX].label.int64 = crypto::COSE_PHEADER_KEY_SUB;
142+
cwt_items[CWT_SUB_INDEX].uLabelType = QCBOR_TYPE_INT64;
143+
cwt_items[CWT_SUB_INDEX].uDataType = QCBOR_TYPE_TEXT_STRING;
144+
145+
cwt_items[CWT_END_INDEX].uLabelType = QCBOR_TYPE_NONE;
146+
147+
QCBORDecode_GetItemsInMap(&ctx, cwt_items);
148+
decode_error = QCBORDecode_GetError(&ctx);
149+
if (decode_error != QCBOR_SUCCESS)
150+
{
151+
throw COSEDecodeError(
152+
fmt::format("Failed to decode CWT claim contents: {}", decode_error));
153+
}
154+
155+
if (
156+
cwt_items[CWT_ISS_INDEX].uDataType != QCBOR_TYPE_NONE &&
157+
cwt_items[CWT_SUB_INDEX].uDataType != QCBOR_TYPE_NONE)
158+
{
159+
auto issuer = tstring_to_string(cwt_items[CWT_ISS_INDEX]);
160+
auto subject = tstring_to_string(cwt_items[CWT_SUB_INDEX]);
161+
return {issuer, subject};
162+
}
163+
else
164+
{
165+
throw COSEDecodeError(
166+
"Missing issuer and subject values in CWT Claims in COSE_Sign1");
167+
}
168+
}
61169
}

src/node/node_state.h

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -544,9 +544,6 @@ namespace ccf
544544
config.startup_host_time,
545545
config.initial_service_certificate_validity_days);
546546

547-
history->set_service_signing_identity(
548-
network.identity->get_key_pair(), config.cose_signatures);
549-
550547
LOG_INFO_FMT("Created recovery node {}", self);
551548
return {self_signed_node_cert, network.identity->cert};
552549
}
@@ -1049,6 +1046,37 @@ namespace ccf
10491046
index = s.seqno;
10501047
view = s.view;
10511048
}
1049+
else
1050+
{
1051+
throw std::logic_error("No signature found after recovery");
1052+
}
1053+
1054+
ccf::COSESignaturesConfig cs_cfg{};
1055+
auto lcs = tx.ro(network.cose_signatures)->get();
1056+
if (lcs.has_value())
1057+
{
1058+
CoseSignature cs = lcs.value();
1059+
LOG_INFO_FMT("COSE signature found after recovery");
1060+
try
1061+
{
1062+
auto [issuer, subject] = cose::extract_iss_sub_from_sig(cs);
1063+
LOG_INFO_FMT(
1064+
"COSE signature issuer: {}, subject: {}", issuer, subject);
1065+
cs_cfg = ccf::COSESignaturesConfig{issuer, subject};
1066+
}
1067+
catch (const cose::COSEDecodeError& e)
1068+
{
1069+
LOG_FAIL_FMT("COSE signature decode error: {}", e.what());
1070+
throw;
1071+
}
1072+
}
1073+
else
1074+
{
1075+
LOG_INFO_FMT("No COSE signature found after recovery");
1076+
}
1077+
1078+
history->set_service_signing_identity(
1079+
network.identity->get_key_pair(), cs_cfg);
10521080

10531081
auto h = dynamic_cast<MerkleTxHistory*>(history.get());
10541082
if (h)

src/service/network_tables.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ namespace ccf
221221
// the same time so that the root of the tree in the signatures table
222222
// matches the serialised Merkle tree.
223223
const Signatures signatures = {Tables::SIGNATURES};
224+
const CoseSignatures cose_signatures = {Tables::COSE_SIGNATURES};
224225
const SerialisedMerkleTree serialise_tree = {
225226
Tables::SERIALISED_MERKLE_TREE};
226227

tests/recovery.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from infra.consortium import slurp_file
1717
import infra.health_watcher
1818
import time
19-
from e2e_logging import verify_receipt
19+
from e2e_logging import verify_receipt, test_cose_receipt_schema
2020
import infra.service_load
2121
import ccf.tx_id
2222
import tempfile
@@ -934,6 +934,8 @@ def run(args):
934934
ref_msg = get_and_verify_historical_receipt(network, ref_msg)
935935

936936
LOG.success("Recovery complete on all nodes")
937+
# Verify COSE receipt schema and issuer/subject have remained the same
938+
test_cose_receipt_schema(network, args)
937939

938940
primary, _ = network.find_primary()
939941
network.stop_all_nodes()

0 commit comments

Comments
 (0)