From 1676517a50081ff23fb5a2c7c0eae3cb984a8a05 Mon Sep 17 00:00:00 2001 From: Max Tropets Date: Mon, 30 Mar 2026 13:31:57 +0000 Subject: [PATCH] WIP COSE sig only --- doc/host_config_schema/cchost_config.json | 5 ++ include/ccf/node/cose_signatures_config.h | 4 +- samples/config/start_config.json | 7 +- src/endpoints/common_endpoint_registry.cpp | 40 ++++++++++ src/kv/deserialise.h | 4 +- src/kv/test/kv_test.cpp | 14 ++++ src/node/gov/handlers/acks.h | 14 ++-- src/node/historical_queries.h | 68 +++++++++++++--- src/node/historical_queries_adapter.cpp | 37 ++++++--- src/node/history.h | 64 ++++++++------- src/node/node_state.h | 90 ++++++++++++---------- src/node/test/history.cpp | 64 +++++++++++---- src/node/test/snapshot.cpp | 2 +- src/node/tx_receipt_impl.h | 8 +- src/service/internal_tables_access.h | 17 ++-- tests/config.jinja | 3 +- tests/infra/remote.py | 2 + tests/recovery.py | 30 ++++++++ 18 files changed, 343 insertions(+), 130 deletions(-) diff --git a/doc/host_config_schema/cchost_config.json b/doc/host_config_schema/cchost_config.json index a0e13d951497..9af8ac47850c 100644 --- a/doc/host_config_schema/cchost_config.json +++ b/doc/host_config_schema/cchost_config.json @@ -209,6 +209,11 @@ "subject": { "type": "string", "description": "Subject, set in CWT_Claims of COSE ledger signatures. Can only be set once on service start." + }, + "cose_only_ledger": { + "type": "boolean", + "default": false, + "description": "Only COSE-sign Merkle tree root" } } }, diff --git a/include/ccf/node/cose_signatures_config.h b/include/ccf/node/cose_signatures_config.h index f4e19300359a..e76cb430f786 100644 --- a/include/ccf/node/cose_signatures_config.h +++ b/include/ccf/node/cose_signatures_config.h @@ -12,10 +12,12 @@ namespace ccf { std::string issuer; std::string subject; + bool cose_only_ledger = false; bool operator==(const COSESignaturesConfig& other) const = default; }; DECLARE_JSON_TYPE(COSESignaturesConfig); - DECLARE_JSON_REQUIRED_FIELDS(COSESignaturesConfig, issuer, subject); + DECLARE_JSON_REQUIRED_FIELDS( + COSESignaturesConfig, issuer, subject, cose_only_ledger); } diff --git a/samples/config/start_config.json b/samples/config/start_config.json index fd7b9ab15152..79b233c531ba 100644 --- a/samples/config/start_config.json +++ b/samples/config/start_config.json @@ -1,6 +1,8 @@ { "network": { - "node_to_node_interface": { "bind_address": "127.0.0.1:8081" }, + "node_to_node_interface": { + "bind_address": "127.0.0.1:8081" + }, "rpc_interfaces": { "primary_rpc_interface": { "bind_address": "127.0.0.1:8080", @@ -49,7 +51,8 @@ "service_subject_name": "CN=A Sample CCF Service", "cose_signatures": { "issuer": "service.example.com", - "subject": "ledger.signature" + "subject": "ledger.signature", + "cose_only_ledger": true } } }, diff --git a/src/endpoints/common_endpoint_registry.cpp b/src/endpoints/common_endpoint_registry.cpp index 9b9f39434e6d..1e039fa1c249 100644 --- a/src/endpoints/common_endpoint_registry.cpp +++ b/src/endpoints/common_endpoint_registry.cpp @@ -10,6 +10,7 @@ #include "ccf/http_query.h" #include "ccf/json_handler.h" #include "ccf/node_context.h" +#include "ccf/receipt.h" #include "ccf/service/tables/code_id.h" #include "ccf/service/tables/host_data.h" #include "ccf/service/tables/snp_measurements.h" @@ -285,6 +286,45 @@ namespace ccf "A signed statement from the service over a transaction entry in the " "ledger") .install(); + + auto get_cose_receipt = + []( + auto& ctx, + ccf::historical::StatePtr + historical_state) { // NOLINT(performance-unnecessary-value-param) + assert(historical_state->receipt); + auto cose_receipt = + ccf::describe_cose_receipt_v1(*historical_state->receipt); + if (!cose_receipt.has_value()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "No COSE receipt available for this transaction."); + return; + } + + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_header( + ccf::http::headers::CONTENT_TYPE, + ccf::http::headervalues::contenttype::COSE); + ctx.rpc_ctx->set_response_body(*cose_receipt); + }; + + make_read_only_endpoint( + "/receipt/cose", + HTTP_GET, + ccf::historical::read_only_adapter_v4( + get_cose_receipt, context, is_tx_committed, txid_from_query_string), + no_auth_required) + .set_auto_schema() + .add_query_parameter(tx_id_param_key) + .set_openapi_summary("COSE receipt for a transaction") + .set_openapi_description( + "A COSE Sign1 envelope containing a signed statement from the " + "service over a transaction entry in the ledger, with a Merkle " + "proof in the unprotected header.") + .install(); } void CommonEndpointRegistry::api_endpoint( diff --git a/src/kv/deserialise.h b/src/kv/deserialise.h index dffff3e4c000..605e13afa7bd 100644 --- a/src/kv/deserialise.h +++ b/src/kv/deserialise.h @@ -130,7 +130,7 @@ namespace ccf::kv } auto success = ApplyResult::PASS; - auto search = changes.find(ccf::Tables::SIGNATURES); + auto search = changes.find(ccf::Tables::COSE_SIGNATURES); if (search != changes.end()) { switch (changes.size()) @@ -146,7 +146,7 @@ namespace ccf::kv if ( changes.find(ccf::Tables::SERIALISED_MERKLE_TREE) != changes.end() && - changes.find(ccf::Tables::COSE_SIGNATURES) != changes.end()) + changes.find(ccf::Tables::SIGNATURES) != changes.end()) { break; } diff --git a/src/kv/test/kv_test.cpp b/src/kv/test/kv_test.cpp index 27d16debc7f2..947679347a54 100644 --- a/src/kv/test/kv_test.cpp +++ b/src/kv/test/kv_test.cpp @@ -2263,6 +2263,7 @@ TEST_CASE("Deserialise return status") store.set_encryptor(encryptor); ccf::Signatures signatures(ccf::Tables::SIGNATURES); + ccf::CoseSignatures cose_signatures(ccf::Tables::COSE_SIGNATURES); ccf::SerialisedMerkleTree serialised_tree( ccf::Tables::SERIALISED_MERKLE_TREE); @@ -2292,9 +2293,12 @@ TEST_CASE("Deserialise return status") { auto tx = store.create_reserved_tx(store.next_txid()); auto sig_handle = tx.rw(signatures); + auto cose_sig_handle = tx.rw(cose_signatures); auto tree_handle = tx.rw(serialised_tree); ccf::PrimarySignature sigv(ccf::kv::test::PrimaryNodeId, 2); sig_handle->put(sigv); + ccf::CoseSignature cose_sigv; + cose_sig_handle->put(cose_sigv); tree_handle->put({}); auto [success_, data_, claims_digest, commit_evidence_digest, hooks] = tx.commit_reserved(); @@ -2310,9 +2314,12 @@ TEST_CASE("Deserialise return status") { auto tx = store.create_reserved_tx(store.next_txid()); auto sig_handle = tx.rw(signatures); + auto cose_sig_handle = tx.rw(cose_signatures); auto data_handle = tx.rw(data); ccf::PrimarySignature sigv(ccf::kv::test::PrimaryNodeId, 2); sig_handle->put(sigv); + ccf::CoseSignature cose_sigv; + cose_sig_handle->put(cose_sigv); data_handle->put(43, 43); auto [success_, data_, claims_digest, commit_evidence_digest, hooks] = tx.commit_reserved(); @@ -3363,6 +3370,7 @@ TEST_CASE("Ledger entry chunk request") store.set_consensus(consensus); ccf::Signatures signatures(ccf::Tables::SIGNATURES); + ccf::CoseSignatures cose_signatures(ccf::Tables::COSE_SIGNATURES); ccf::SerialisedMerkleTree serialised_tree( ccf::Tables::SERIALISED_MERKLE_TREE); @@ -3442,9 +3450,12 @@ TEST_CASE("Ledger entry chunk request") auto txid = store.next_txid(); auto tx = store.create_reserved_tx(txid); auto sig_handle = tx.rw(signatures); + auto cose_sig_handle = tx.rw(cose_signatures); auto tree_handle = tx.rw(serialised_tree); ccf::PrimarySignature sigv(ccf::kv::test::PrimaryNodeId, txid.seqno); sig_handle->put(sigv); + ccf::CoseSignature cose_sigv; + cose_sig_handle->put(cose_sigv); tree_handle->put({}); auto [success_, data_, claims_digest, commit_evidence_digest, hooks] = tx.commit_reserved(); @@ -3518,9 +3529,12 @@ TEST_CASE("Ledger entry chunk request") // Add the signature auto sig_handle = tx.rw(signatures); + auto cose_sig_handle = tx.rw(cose_signatures); auto tree_handle = tx.rw(serialised_tree); ccf::PrimarySignature sigv(ccf::kv::test::PrimaryNodeId, txid.seqno); sig_handle->put(sigv); + ccf::CoseSignature cose_sigv; + cose_sig_handle->put(cose_sigv); tree_handle->put({}); auto [success_, data_, claims_digest, commit_evidence_digest, hooks] = tx.commit_reserved(); diff --git a/src/node/gov/handlers/acks.h b/src/node/gov/handlers/acks.h index a4ed1ce81242..4614d8990357 100644 --- a/src/node/gov/handlers/acks.h +++ b/src/node/gov/handlers/acks.h @@ -5,6 +5,7 @@ #include "ccf/base_endpoint_registry.h" #include "node/gov/api_version.h" #include "node/gov/handlers/helpers.h" +#include "node/history.h" #include "node/share_manager.h" #include "service/internal_tables_access.h" @@ -124,11 +125,11 @@ namespace ccf::gov::endpoints ack = ack_opt.value(); } - // Get signature, containing merkle root state digest - auto sigs_handle = - ctx.tx.template ro(Tables::SIGNATURES); - auto sig = sigs_handle->get(); - if (!sig.has_value()) + // Get merkle root state digest from serialised merkle tree + auto tree_handle = ctx.tx.template ro( + Tables::SERIALISED_MERKLE_TREE); + auto tree = tree_handle->get(); + if (!tree.has_value()) { detail::set_gov_error( ctx.rpc_ctx, @@ -137,9 +138,10 @@ namespace ccf::gov::endpoints "Service has no signatures to ack yet - try again soon."); return; } + ccf::MerkleTreeHistory history(tree.value()); // Write ack back to the KV - ack.state_digest = sig->root.hex_str(); + ack.state_digest = history.get_root().hex_str(); acks_handle->put(member_id, ack); auto body = nlohmann::json::object(); diff --git a/src/node/historical_queries.h b/src/node/historical_queries.h index bdb917cb2a45..be20b691cda1 100644 --- a/src/node/historical_queries.h +++ b/src/node/historical_queries.h @@ -7,6 +7,7 @@ #include "consensus/ledger_enclave_types.h" #include "ds/ccf_assert.h" #include "kv/store.h" +#include "node/cose_common.h" #include "node/encryptor.h" #include "node/history.h" #include "node/ledger_secrets.h" @@ -505,12 +506,11 @@ namespace ccf::historical { // Iterate through earlier indices. If this signature covers them // then create a receipt for them - const auto sig = get_signature(sig_details->store); - if (!sig.has_value()) + const auto cose_sig = get_cose_signature(sig_details->store); + if (!cose_sig.has_value()) { return false; } - const auto cose_sig = get_cose_signature(sig_details->store); const auto serialised_tree = get_tree(sig_details->store); if (!serialised_tree.has_value()) { @@ -535,15 +535,36 @@ namespace ccf::historical auto details = search_rit->second; if (details != nullptr && details->store != nullptr) { + auto sig = get_signature(sig_details->store); + std::optional> sig_bytes{std::nullopt}; + std::optional sig_cert{std::nullopt}; + ccf::NodeId sig_node{}; + if (sig.has_value()) + { + sig_bytes = sig->sig; + sig_node = sig->node; + sig_cert = sig->cert; + } + auto proof = tree.get_proof(seqno); - details->transaction_id = {sig->view, seqno}; + auto cose_receipt = + ccf::cose::decode_ccf_receipt(cose_sig.value(), false); + auto parsed_txid = + ccf::TxID::from_str(cose_receipt.phdr.ccf.txid); + if (!parsed_txid.has_value()) + { + throw std::logic_error(fmt::format( + "Cannot parse CCF TxID: {}", cose_receipt.phdr.ccf.txid)); + } + + details->transaction_id = {parsed_txid->view, seqno}; details->receipt = std::make_shared( - sig->sig, + sig_bytes, cose_sig, proof.get_root(), proof.get_path(), - sig->node, - sig->cert, + sig_node, + sig_cert, details->entry_digest, details->get_commit_evidence(), details->claims_digest); @@ -821,13 +842,36 @@ namespace ccf::historical // the state to do so already, and it's simpler than constructing // the receipt _later_ for an already-fetched signature // transaction. - const auto sig = get_signature(details->store); const auto cose_sig = get_cose_signature(details->store); - if (sig.has_value()) + if (cose_sig.has_value()) { - details->transaction_id = {sig->view, sig->seqno}; - details->receipt = std::make_shared( - sig->sig, cose_sig, sig->root.h, nullptr, sig->node, sig->cert); + auto receipt = ccf::cose::decode_ccf_receipt(cose_sig.value(), false); + const auto& txid = receipt.phdr.ccf.txid; + auto parsed_txid = ccf::TxID::from_str(txid); + + if (!parsed_txid.has_value()) + { + throw std::logic_error( + fmt::format("Cannot parse CCF TxID: {}", txid)); + } + details->transaction_id = parsed_txid.value(); + + const auto sig = get_signature(details->store); + if (sig.has_value()) + { + details->receipt = std::make_shared( + sig->sig, cose_sig, sig->root.h, nullptr, sig->node, sig->cert); + } + else + { + details->receipt = std::make_shared( + std::nullopt, + cose_sig, + std::nullopt, + nullptr, + ccf::NodeId{}, + std::nullopt); + } } } diff --git a/src/node/historical_queries_adapter.cpp b/src/node/historical_queries_adapter.cpp index 3058e80ad51c..8ad2ff2e9b90 100644 --- a/src/node/historical_queries_adapter.cpp +++ b/src/node/historical_queries_adapter.cpp @@ -68,7 +68,12 @@ namespace ccf // Legacy JSON format, retained for compatibility nlohmann::json out = nlohmann::json::object(); - out["signature"] = ccf::crypto::b64_from_raw(receipt.signature); + if (!receipt.signature.has_value()) + { + throw std::logic_error( + "Non-COSE receipt requires a non-COSE signature TX"); + } + out["signature"] = ccf::crypto::b64_from_raw(receipt.signature.value()); auto proof = nlohmann::json::array(); if (receipt.path != nullptr) @@ -99,7 +104,12 @@ namespace ccf if (receipt.path == nullptr) { // Signature transaction - out["leaf"] = receipt.root.to_string(); + if (!receipt.root.has_value()) + { + throw std::logic_error( + "Non-COSE receipt on a signature requires a root"); + } + out["leaf"] = receipt.root.value().to_string(); } else if (!receipt.commit_evidence.has_value()) { @@ -181,17 +191,22 @@ namespace ccf else { // Signature transaction - auto sig_receipt = std::make_shared(); - sig_receipt->signed_root = ccf::crypto::Sha256Hash::from_span( - std::span( - in.root.bytes, sizeof(in.root.bytes))); - - receipt = sig_receipt; + if (in.root.has_value()) + { + auto sig_receipt = std::make_shared(); + sig_receipt->signed_root = ccf::crypto::Sha256Hash::from_span( + std::span( + in.root.value().bytes, sizeof(in.root.value().bytes))); + receipt = sig_receipt; + } } auto& out = *receipt; - out.signature = in.signature; + if (in.signature.has_value()) + { + out.signature = in.signature.value(); + } out.node_id = in.node_id; @@ -264,12 +279,10 @@ namespace ccf { return std::nullopt; } - auto proof = describe_merkle_proof_v1(receipt); if (!proof.has_value()) { - // Signature TX: return COSE signature as-is, with empty UHDR - return signature; + return std::nullopt; } auto inclusion_proof = diff --git a/src/node/history.h b/src/node/history.h index 64d28ba6dedd..9c27e7d483dd 100644 --- a/src/node/history.h +++ b/src/node/history.h @@ -337,28 +337,32 @@ namespace ccf ccf::kv::PendingTxInfo call() override { auto sig = store.create_reserved_tx(txid); - auto* signatures = - sig.template wo(ccf::Tables::SIGNATURES); - auto* cose_signatures = - sig.template wo(ccf::Tables::COSE_SIGNATURES); - auto* serialised_tree = sig.template wo( - ccf::Tables::SERIALISED_MERKLE_TREE); ccf::crypto::Sha256Hash root = history.get_replicated_state_root(); - std::vector primary_sig; - std::vector root_hash{ root.h.data(), root.h.data() + root.h.size()}; - primary_sig = node_kp.sign_hash(root_hash.data(), root_hash.size()); - PrimarySignature sig_value( - id, - txid.seqno, - txid.view, - root, - {}, // Nonce is currently empty - primary_sig, - endorsed_cert); + if (cose_signatures_config.cose_only_ledger == false) + { + auto* signatures = + sig.template wo(ccf::Tables::SIGNATURES); + auto primary_sig = + node_kp.sign_hash(root_hash.data(), root_hash.size()); + + PrimarySignature sig_value( + id, + txid.seqno, + txid.view, + root, + {}, // Nonce is currently empty + primary_sig, + endorsed_cert); + + signatures->put(sig_value); + } + + auto* cose_signatures = + sig.template wo(ccf::Tables::COSE_SIGNATURES); auto kid = ccf::crypto::kid_from_key(service_kp.public_key_der()); const auto tx_id = txid.to_str(); @@ -410,9 +414,12 @@ namespace ccf } std::vector cose_sign(cose_buf.to_vector()); - signatures->put(sig_value); cose_signatures->put(cose_sign); + + auto* serialised_tree = sig.template wo( + ccf::Tables::SERIALISED_MERKLE_TREE); serialised_tree->put(history.serialise_tree(txid.seqno - 1)); + return sig.commit_reserved(); } }; @@ -752,20 +759,18 @@ namespace ccf { auto tx = store.create_read_only_tx(); + auto root = get_replicated_state_root(); + log_hash(root, VERIFY); + auto* signatures = tx.template ro(ccf::Tables::SIGNATURES); auto sig = signatures->get(); - if (!sig.has_value()) - { - LOG_FAIL_FMT("No signature found in signatures map"); - return false; - } - - auto root = get_replicated_state_root(); - log_hash(root, VERIFY); - if (!verify_node_signature(tx, sig->node, sig->sig, root)) + if (sig.has_value()) { - return false; + if (!verify_node_signature(tx, sig->node, sig->sig, root)) + { + return false; + } } auto* cose_signatures = @@ -774,7 +779,8 @@ namespace ccf if (!cose_sig.has_value()) { - return true; + LOG_FAIL_FMT("No COSE signature found in COSE signatures map"); + return false; } // Since COSE signatures have not always been emitted, it is possible in a diff --git a/src/node/node_state.h b/src/node/node_state.h index 7188569f0bbe..7508af8abb1f 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1449,17 +1449,35 @@ namespace ccf // If the ledger entry is a signature, it is safe to compact the store network.tables->compact(last_recovered_idx); auto tx = network.tables->create_read_only_tx(); - auto last_sig = tx.ro(network.signatures)->get(); - if (!last_sig.has_value()) + // Get the view from the COSE signature (always present), falling + // back to the legacy signatures table + ccf::kv::Term sig_view = 0; + auto lcs = tx.ro(network.cose_signatures)->get(); + if (lcs.has_value()) { - throw std::logic_error("Signature missing"); + auto receipt = cose::decode_ccf_receipt(lcs.value(), false); + auto tx_id_opt = ccf::TxID::from_str(receipt.phdr.ccf.txid); + if (!tx_id_opt.has_value()) + { + throw std::logic_error(fmt::format( + "Failed to parse TxID from COSE signature: {}", + receipt.phdr.ccf.txid)); + } + sig_view = tx_id_opt->view; + } + else + { + auto last_sig = tx.ro(network.signatures)->get(); + if (!last_sig.has_value()) + { + throw std::logic_error("Signature missing"); + } + sig_view = last_sig->view; } LOG_DEBUG_FMT( - "Read signature at {} for view {}", - last_recovered_idx, - last_sig->view); + "Read signature at {} for view {}", last_recovered_idx, sig_view); // Initial transactions, before the first signature, must have // happened in the first signature's view (eg - if the first // signature is at seqno 20 in view 4, then transactions 1->19 must @@ -1469,12 +1487,8 @@ namespace ccf // valid signature. const auto view_start_idx = view_history.empty() ? 1 : last_recovered_signed_idx + 1; - CCF_ASSERT_FMT( - last_sig->view >= 0, - "last_sig->view is invalid, {}", - last_sig->view); - for (auto i = view_history.size(); - i < static_cast(last_sig->view); + CCF_ASSERT_FMT(sig_view >= 0, "sig_view is invalid, {}", sig_view); + for (auto i = view_history.size(); i < static_cast(sig_view); ++i) { view_history.push_back(view_start_idx); @@ -1538,43 +1552,39 @@ namespace ccf ccf::kv::Version index = 0; ccf::kv::Term view = 0; - auto ls = tx.ro(network.signatures)->get(); - if (ls.has_value()) - { - auto s = ls.value(); - index = s.seqno; - view = s.view; - } - else + auto lcs = tx.ro(network.cose_signatures)->get(); + if (!lcs.has_value()) { - throw std::logic_error("No signature found after recovery"); + throw std::logic_error("No COSE signature found after recovery"); } + CoseSignature cs = lcs.value(); ccf::COSESignaturesConfig cs_cfg{}; - auto lcs = tx.ro(network.cose_signatures)->get(); - if (lcs.has_value()) + try { - CoseSignature cs = lcs.value(); - LOG_INFO_FMT("COSE signature found after recovery"); - try - { - auto receipt = - cose::decode_ccf_receipt(cs, /* recompute_root */ false); - auto issuer = receipt.phdr.cwt.iss; - auto subject = receipt.phdr.cwt.sub; - LOG_INFO_FMT( - "COSE signature issuer: {}, subject: {}", issuer, subject); - cs_cfg = ccf::COSESignaturesConfig{issuer, subject}; - } - catch (const cose::COSEDecodeError& e) + auto receipt = cose::decode_ccf_receipt(cs, /* recompute_root */ false); + auto issuer = receipt.phdr.cwt.iss; + auto subject = receipt.phdr.cwt.sub; + LOG_INFO_FMT("COSE signature issuer: {}, subject: {}", issuer, subject); + + auto tx_id_opt = ccf::TxID::from_str(receipt.phdr.ccf.txid); + if (!tx_id_opt.has_value()) { - LOG_FAIL_FMT("COSE signature decode error: {}", e.what()); - throw; + throw std::logic_error(fmt::format( + "Failed to parse TxID from COSE signature: {}", + receipt.phdr.ccf.txid)); } + index = tx_id_opt->seqno; + view = tx_id_opt->view; + + bool cose_only = !tx.ro(network.signatures)->get().has_value(); + + cs_cfg = ccf::COSESignaturesConfig{issuer, subject, cose_only}; } - else + catch (const cose::COSEDecodeError& e) { - LOG_INFO_FMT("No COSE signature found after recovery"); + LOG_FAIL_FMT("COSE signature decode error: {}", e.what()); + throw; } history->set_service_signing_identity( diff --git a/src/node/test/history.cpp b/src/node/test/history.cpp index 39c955488238..b77983d0e492 100644 --- a/src/node/test/history.cpp +++ b/src/node/test/history.cpp @@ -74,6 +74,16 @@ class DummyConsensus : public ccf::kv::test::StubConsensus TEST_CASE("Check signature verification") { + bool cose_only = false; + SUBCASE("normal") {} + SUBCASE("cose_only") + { + cose_only = true; + } + + ccf::COSESignaturesConfig cose_config; + cose_config.cose_only_ledger = cose_only; + auto encryptor = std::make_shared(); auto node_kp = ccf::crypto::make_ec_key_pair(); @@ -89,8 +99,7 @@ TEST_CASE("Check signature verification") std::make_shared( primary_store, ccf::kv::test::PrimaryNodeId, *node_kp); primary_history->set_endorsed_certificate(self_signed); - primary_history->set_service_signing_identity( - service_kp, ccf::COSESignaturesConfig{}); + primary_history->set_service_signing_identity(service_kp, cose_config); primary_store.set_history(primary_history); primary_store.initialise_term(store_term); @@ -100,14 +109,14 @@ TEST_CASE("Check signature verification") std::make_shared( backup_store, ccf::kv::test::FirstBackupNodeId, *node_kp); backup_history->set_endorsed_certificate(self_signed); - backup_history->set_service_signing_identity( - service_kp, ccf::COSESignaturesConfig{}); + backup_history->set_service_signing_identity(service_kp, cose_config); backup_store.set_history(backup_history); backup_store.initialise_term(store_term); ccf::Nodes nodes(ccf::Tables::NODES); ccf::Service service(ccf::Tables::SERVICE); - ccf::Signatures signatures(ccf::Tables::SIGNATURES); + ccf::Signatures signatures_table(ccf::Tables::SIGNATURES); + ccf::CoseSignatures cose_signatures(ccf::Tables::COSE_SIGNATURES); std::shared_ptr consensus = std::make_shared(&backup_store); @@ -139,19 +148,50 @@ TEST_CASE("Check signature verification") REQUIRE(backup_store.current_version() == 2); } + INFO("COSE signature table should be populated"); + { + auto tx = primary_store.create_read_only_tx(); + auto cose_sigs = tx.ro(cose_signatures); + REQUIRE(cose_sigs->get().has_value()); + } + + if (cose_only) + { + INFO("In COSE-only mode, node signature table should not be populated"); + auto tx = primary_store.create_read_only_tx(); + auto sigs = tx.ro(signatures_table); + REQUIRE_FALSE(sigs->get().has_value()); + } + else + { + INFO("In normal mode, node signature table should be populated"); + auto tx = primary_store.create_read_only_tx(); + auto sigs = tx.ro(signatures_table); + REQUIRE(sigs->get().has_value()); + } + INFO("Issue a bogus signature, rejected by verification on the backup"); { auto txs = primary_store.create_tx(); - auto sigs = txs.rw(signatures); - ccf::PrimarySignature bogus(ccf::kv::test::PrimaryNodeId, 0); - bogus.sig = std::vector(256, 1); - sigs->put(bogus); + auto cose_sigs = txs.rw(cose_signatures); + ccf::CoseSignature bogus{}; + cose_sigs->put(bogus); REQUIRE(txs.commit() == ccf::kv::CommitResult::FAIL_NO_REPLICATE); } } TEST_CASE("Check signing works across rollback") { + bool cose_only = false; + SUBCASE("normal") {} + SUBCASE("cose_only") + { + cose_only = true; + } + + ccf::COSESignaturesConfig cose_config; + cose_config.cose_only_ledger = cose_only; + auto encryptor = std::make_shared(); auto node_kp = ccf::crypto::make_ec_key_pair(); @@ -167,8 +207,7 @@ TEST_CASE("Check signing works across rollback") std::make_shared( primary_store, ccf::kv::test::PrimaryNodeId, *node_kp); primary_history->set_endorsed_certificate(self_signed); - primary_history->set_service_signing_identity( - service_kp, ccf::COSESignaturesConfig{}); + primary_history->set_service_signing_identity(service_kp, cose_config); primary_store.set_history(primary_history); primary_store.initialise_term(store_term); @@ -177,8 +216,7 @@ TEST_CASE("Check signing works across rollback") std::make_shared( backup_store, ccf::kv::test::FirstBackupNodeId, *node_kp); backup_history->set_endorsed_certificate(self_signed); - backup_history->set_service_signing_identity( - service_kp, ccf::COSESignaturesConfig{}); + backup_history->set_service_signing_identity(service_kp, cose_config); backup_store.set_history(backup_history); backup_store.set_encryptor(encryptor); backup_store.initialise_term(store_term); diff --git a/src/node/test/snapshot.cpp b/src/node/test/snapshot.cpp index fb68fb886e70..b0546fb4aedb 100644 --- a/src/node/test/snapshot.cpp +++ b/src/node/test/snapshot.cpp @@ -67,7 +67,7 @@ TEST_CASE("Snapshot with merkle tree" * doctest::test_suite("snapshot")) // No snapshot here, only verify that a fresh tree can be started from the // mini-tree in a signature and the hash of the signature auto tx = source_store.create_read_only_tx(); - auto signatures = tx.ro(ccf::Tables::SIGNATURES); + auto signatures = tx.ro(ccf::Tables::COSE_SIGNATURES); REQUIRE(signatures->has()); auto sig = signatures->get().value(); auto serialised_tree = diff --git a/src/node/tx_receipt_impl.h b/src/node/tx_receipt_impl.h index 079bd3ad0a6f..2dd465b925c3 100644 --- a/src/node/tx_receipt_impl.h +++ b/src/node/tx_receipt_impl.h @@ -12,9 +12,9 @@ namespace ccf // public interface by ccf::Receipt struct TxReceiptImpl { - std::vector signature; + std::optional> signature; std::optional> cose_signature = std::nullopt; - HistoryTree::Hash root; + std::optional root; std::shared_ptr path; ccf::NodeId node_id; std::optional node_cert = std::nullopt; @@ -26,9 +26,9 @@ namespace ccf std::optional cose_endorsements = std::nullopt; TxReceiptImpl( - const std::vector& signature_, + const std::optional>& signature_, const std::optional>& cose_signature, - const HistoryTree::Hash& root_, + const std::optional& root_, std::shared_ptr path_, NodeId node_id_, const std::optional& node_cert_, diff --git a/src/service/internal_tables_access.h b/src/service/internal_tables_access.h index 9457f0bffefd..15491f4dfad1 100644 --- a/src/service/internal_tables_access.h +++ b/src/service/internal_tables_access.h @@ -17,6 +17,7 @@ #include "ccf/tx.h" #include "consensus/aft/raft_types.h" #include "cose/cose_rs_ffi.h" +#include "node/history.h" #include "node/ledger_secrets.h" #include "node/uvm_endorsements.h" #include "service/tables/governance_history.h" @@ -499,19 +500,21 @@ namespace ccf auto* last_signed_root = tx.wo( ccf::Tables::PREVIOUS_SERVICE_LAST_SIGNED_ROOT); - auto* sigs = tx.ro(ccf::Tables::SIGNATURES); - if (!sigs->has()) + auto* tree_handle = + tx.ro(ccf::Tables::SERIALISED_MERKLE_TREE); + if (!tree_handle->has()) { throw std::logic_error( - "Previous service doesn't have any signed transactions"); + "Previous service doesn't have a serialised merkle tree"); } - auto sig_opt = sigs->get(); - if (!sig_opt.has_value()) + auto tree_opt = tree_handle->get(); + if (!tree_opt.has_value()) { throw std::logic_error( - "Previous service doesn't have signature value"); + "Previous service doesn't have serialised merkle tree value"); } - last_signed_root->put(sig_opt->root); + ccf::MerkleTreeHistory tree(tree_opt.value()); + last_signed_root->put(tree.get_root()); // Record number of recoveries for service. If the value does // not exist in the table (i.e. pre 2.x ledger), assume it is the diff --git a/tests/config.jinja b/tests/config.jinja index 447807d32557..7ab9f0f62d4f 100644 --- a/tests/config.jinja +++ b/tests/config.jinja @@ -37,7 +37,8 @@ "cose_signatures": { "issuer": {{ cose_signatures_issuer|tojson }}, - "subject": {{ cose_signatures_subject|tojson }} + "subject": {{ cose_signatures_subject|tojson }}, + "cose_only_ledger": {{ cose_only_ledger|tojson }} } }, "join": diff --git a/tests/infra/remote.py b/tests/infra/remote.py index b8a7acaa5faa..2b822b1141ff 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -334,6 +334,7 @@ def __init__( historical_cache_soft_limit=None, cose_signatures_issuer="service.example.com", cose_signatures_subject="ledger.signature", + cose_only_ledger=False, sealing_recovery_location=None, recovery_decision_protocol_expected_locations=None, backup_snapshot_fetch_enabled=False, @@ -534,6 +535,7 @@ def __init__( historical_cache_soft_limit=historical_cache_soft_limit, cose_signatures_issuer=cose_signatures_issuer, cose_signatures_subject=cose_signatures_subject, + cose_only_ledger=cose_only_ledger, sealing_recovery_location=sealing_recovery_location, recovery_decision_protocol_expected_locations=recovery_decision_protocol_expected_locations, backup_snapshot_fetch_enabled=backup_snapshot_fetch_enabled, diff --git a/tests/recovery.py b/tests/recovery.py index 694576243738..a622d8465b83 100644 --- a/tests/recovery.py +++ b/tests/recovery.py @@ -1415,6 +1415,27 @@ def run_recover_via_added_recovery_owner(args): return network +def run_recovery_cose_only(args): + """ + Recover a service that was started with cose_only_ledger=True. + In this mode, only COSE signatures are emitted (no node signatures). + """ + txs = app.LoggingTxs("user0") + with infra.network.network( + args.nodes, + args.binary_dir, + args.debug_nodes, + pdb=args.pdb, + txs=txs, + ) as network: + network.start_and_open(args, cose_only_ledger=True) + # network.txs.issue(network, number_txs=5) + # network = test_recover_service(network, args, from_snapshot=True) + network.txs.issue(network, number_txs=5) + network = test_recover_service(network, args, from_snapshot=False) + return network + + if __name__ == "__main__": def add(parser): @@ -1537,4 +1558,13 @@ def add(parser): snapshot_tx_interval=10000, ) + cr.add( + "recovery_cose_only", + run_recovery_cose_only, + package="samples/apps/logging/logging", + nodes=infra.e2e_args.min_nodes(cr.args, f=1), + ledger_chunk_bytes="50KB", + snapshot_tx_interval=30, + ) + cr.run()