Skip to content

Commit

Permalink
Restore cose_signatures configuration from ledger in Recovery (#6709)
Browse files Browse the repository at this point in the history
  • Loading branch information
achamayou authored Jan 3, 2025
1 parent 44a669d commit 5f823c1
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ and this project adheres Fto [Semantic Versioning](http://semver.org/spec/v2.0.0

- `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.

### Fixed

- `cose_signatures` configuration (`issuer`/`subject`) is now correctly preserved across disaster recovery (#6709).

### Deprecated

- 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()`.
Expand Down
108 changes: 108 additions & 0 deletions src/node/cose_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

#pragma once

#include <crypto/openssl/cose_sign.h>
#include <qcbor/qcbor.h>
#include <qcbor/qcbor_spiffy_decode.h>
#include <stdexcept>
#include <string>
#include <t_cose/t_cose_common.h>
Expand Down Expand Up @@ -58,4 +60,110 @@ namespace ccf::cose
{}
};

static std::string tstring_to_string(QCBORItem& item)
{
return {
static_cast<const char*>(item.val.string.ptr),
static_cast<const char*>(item.val.string.ptr) + item.val.string.len};
}

static std::pair<std::string /* issuer */, std::string /* subject */>
extract_iss_sub_from_sig(const std::vector<uint8_t>& cose_sign1)
{
QCBORError qcbor_result;
QCBORDecodeContext ctx;
UsefulBufC buf{cose_sign1.data(), cose_sign1.size()};
QCBORDecode_Init(&ctx, buf, QCBOR_DECODE_MODE_NORMAL);

QCBORDecode_EnterArray(&ctx, nullptr);
qcbor_result = QCBORDecode_GetError(&ctx);
if (qcbor_result != QCBOR_SUCCESS)
{
throw COSEDecodeError("Failed to parse COSE_Sign1 outer array");
}

uint64_t tag = QCBORDecode_GetNthTagOfLast(&ctx, 0);
if (tag != CBOR_TAG_COSE_SIGN1)
{
throw COSEDecodeError("COSE_Sign1 is not tagged");
}

QCBORDecode_EnterBstrWrapped(&ctx, QCBOR_TAG_REQUIREMENT_NOT_A_TAG, NULL);
QCBORDecode_EnterMap(&ctx, NULL);

enum
{
CWT_CLAIMS_INDEX,
END_INDEX,
};
QCBORItem header_items[END_INDEX + 1];

header_items[CWT_CLAIMS_INDEX].label.int64 = crypto::COSE_PHEADER_KEY_CWT;
header_items[CWT_CLAIMS_INDEX].uLabelType = QCBOR_TYPE_INT64;
header_items[CWT_CLAIMS_INDEX].uDataType = QCBOR_TYPE_MAP;

header_items[END_INDEX].uLabelType = QCBOR_TYPE_NONE;

QCBORDecode_GetItemsInMap(&ctx, header_items);

qcbor_result = QCBORDecode_GetError(&ctx);
if (qcbor_result != QCBOR_SUCCESS)
{
throw COSEDecodeError(
fmt::format("Failed to decode protected header: {}", qcbor_result));
}

if (header_items[CWT_CLAIMS_INDEX].uDataType == QCBOR_TYPE_NONE)
{
throw COSEDecodeError("Missing CWT claims in COSE_Sign1");
}

QCBORDecode_EnterMapFromMapN(&ctx, crypto::COSE_PHEADER_KEY_CWT);
auto decode_error = QCBORDecode_GetError(&ctx);
if (decode_error != QCBOR_SUCCESS)
{
throw COSEDecodeError(
fmt::format("Failed to decode CWT claims: {}", decode_error));
}

enum
{
CWT_ISS_INDEX,
CWT_SUB_INDEX,
CWT_END_INDEX,
};
QCBORItem cwt_items[CWT_END_INDEX + 1];

cwt_items[CWT_ISS_INDEX].label.int64 = crypto::COSE_PHEADER_KEY_ISS;
cwt_items[CWT_ISS_INDEX].uLabelType = QCBOR_TYPE_INT64;
cwt_items[CWT_ISS_INDEX].uDataType = QCBOR_TYPE_TEXT_STRING;

cwt_items[CWT_SUB_INDEX].label.int64 = crypto::COSE_PHEADER_KEY_SUB;
cwt_items[CWT_SUB_INDEX].uLabelType = QCBOR_TYPE_INT64;
cwt_items[CWT_SUB_INDEX].uDataType = QCBOR_TYPE_TEXT_STRING;

cwt_items[CWT_END_INDEX].uLabelType = QCBOR_TYPE_NONE;

QCBORDecode_GetItemsInMap(&ctx, cwt_items);
decode_error = QCBORDecode_GetError(&ctx);
if (decode_error != QCBOR_SUCCESS)
{
throw COSEDecodeError(
fmt::format("Failed to decode CWT claim contents: {}", decode_error));
}

if (
cwt_items[CWT_ISS_INDEX].uDataType != QCBOR_TYPE_NONE &&
cwt_items[CWT_SUB_INDEX].uDataType != QCBOR_TYPE_NONE)
{
auto issuer = tstring_to_string(cwt_items[CWT_ISS_INDEX]);
auto subject = tstring_to_string(cwt_items[CWT_SUB_INDEX]);
return {issuer, subject};
}
else
{
throw COSEDecodeError(
"Missing issuer and subject values in CWT Claims in COSE_Sign1");
}
}
}
34 changes: 31 additions & 3 deletions src/node/node_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -544,9 +544,6 @@ namespace ccf
config.startup_host_time,
config.initial_service_certificate_validity_days);

history->set_service_signing_identity(
network.identity->get_key_pair(), config.cose_signatures);

LOG_INFO_FMT("Created recovery node {}", self);
return {self_signed_node_cert, network.identity->cert};
}
Expand Down Expand Up @@ -1049,6 +1046,37 @@ namespace ccf
index = s.seqno;
view = s.view;
}
else
{
throw std::logic_error("No signature found after recovery");
}

ccf::COSESignaturesConfig cs_cfg{};
auto lcs = tx.ro(network.cose_signatures)->get();
if (lcs.has_value())
{
CoseSignature cs = lcs.value();
LOG_INFO_FMT("COSE signature found after recovery");
try
{
auto [issuer, subject] = cose::extract_iss_sub_from_sig(cs);
LOG_INFO_FMT(
"COSE signature issuer: {}, subject: {}", issuer, subject);
cs_cfg = ccf::COSESignaturesConfig{issuer, subject};
}
catch (const cose::COSEDecodeError& e)
{
LOG_FAIL_FMT("COSE signature decode error: {}", e.what());
throw;
}
}
else
{
LOG_INFO_FMT("No COSE signature found after recovery");
}

history->set_service_signing_identity(
network.identity->get_key_pair(), cs_cfg);

auto h = dynamic_cast<MerkleTxHistory*>(history.get());
if (h)
Expand Down
1 change: 1 addition & 0 deletions src/service/network_tables.h
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ namespace ccf
// the same time so that the root of the tree in the signatures table
// matches the serialised Merkle tree.
const Signatures signatures = {Tables::SIGNATURES};
const CoseSignatures cose_signatures = {Tables::COSE_SIGNATURES};
const SerialisedMerkleTree serialise_tree = {
Tables::SERIALISED_MERKLE_TREE};

Expand Down
4 changes: 3 additions & 1 deletion tests/recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from infra.consortium import slurp_file
import infra.health_watcher
import time
from e2e_logging import verify_receipt
from e2e_logging import verify_receipt, test_cose_receipt_schema
import infra.service_load
import ccf.tx_id
import tempfile
Expand Down Expand Up @@ -934,6 +934,8 @@ def run(args):
ref_msg = get_and_verify_historical_receipt(network, ref_msg)

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

primary, _ = network.find_primary()
network.stop_all_nodes()
Expand Down

0 comments on commit 5f823c1

Please sign in to comment.