Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore cose_signatures configuration from ledger in Recovery #6709

Merged
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];
achamayou marked this conversation as resolved.
Show resolved Hide resolved

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
Loading