Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Added `ledger_signatures.mode` configuration option to enable `Dual` or `COSE`-only signing (#7772).
- Added support for inline transaction receipt construction at commit time. Endpoint authors can use `build_receipt_for_committed_tx()` to construct a full `TxReceiptImpl` from the `CommittedTxInfo` passed to their `ConsensusCommittedEndpointFunction` callback. See the logging sample app (`/log/blocking/private/receipt`) for example usage (#7785).

### Changed
Expand Down
6 changes: 6 additions & 0 deletions doc/host_config_schema/cchost_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,12 @@
"type": "string",
"default": "1000ms",
"description": "Maximum duration after which a signature transaction is automatically generated"
},
"mode": {
"type": "string",
"enum": ["Dual", "COSE"],
"default": "Dual",
"description": "Ledger signature mode. Dual emits both regular and COSE signatures. COSE emits only COSE signatures."
}
},
"description": "This section includes configuration for the ledger signatures emitted by this node (note: should be the same for all other nodes in the service). Transaction commit latency in a CCF network is primarily a function of signature frequency. A network emitting signatures more frequently will be able to commit transactions faster, but will spend a larger proportion of its execution resources creating and verifying signatures. Setting signature frequency is a trade-off between transaction latency and throughput",
Expand Down
29 changes: 29 additions & 0 deletions doc/operations/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,32 @@ Since these operations may require disk IO and produce large responses, these fe
- Size strings are expressed as the value suffixed with the size in bytes (``B``, ``KB``, ``MB``, ``GB``, ``TB``, as factors of 1024), e.g. ``"20MB"``, ``"100KB"`` or ``"2048"`` (bytes).

- Time strings are expressed as the value suffixed with the duration (``us``, ``ms``, ``s``, ``min``, ``h``), e.g. ``"1000ms"``, ``"10s"`` or ``"30min"``.


COSE-Only Ledger Signatures
----------------------------

By default, CCF nodes emit **dual** ledger signatures: a traditional node signature (stored in ``ccf.internal.signatures``) and a COSE Sign1 signature (stored in ``ccf.internal.cose_signatures``). The ``ledger_signatures.mode`` configuration option allows switching to **COSE-only** mode, where only COSE signatures are emitted.

To enable COSE-only mode, set the ``mode`` field in your node configuration:

.. code-block:: json

{
"ledger_signatures": {
"mode": "COSE"
}
}

When ``ledger_signatures.mode`` is set to ``"COSE"``:

- The node signs ledger entries using only COSE Sign1 with the service key. Traditional node signatures (``ccf.internal.signatures``) are not emitted.
- The signature mode is preserved across service recovery. When recovering a COSE-only service, pass the same ``"COSE"`` mode in the recovery node configuration.
- All nodes in a service must use the same signature mode.

The default value is ``"Dual"``, which retains backward-compatible behaviour by emitting both signature types.

.. warning::

COSE-only mode is only supported from CCF 7.x onwards and is **not compatible with 6.x ledgers**. Operators upgrading from 6.x must first complete the migration to 7.x in ``"Dual"`` mode, and only then switch to ``"COSE"`` mode on a subsequent reconfiguration or recovery. Attempting to recover a 6.x ledger with ``"COSE"`` mode will fail.

28 changes: 28 additions & 0 deletions doc/schemas/app_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1692,6 +1692,34 @@
}
}
},
"/app/receipt/cose": {
"get": {
"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.",
"operationId": "GetAppReceiptCose",
"parameters": [
{
"in": "query",
"name": "transaction_id",
"required": true,
"schema": {
"$ref": "#/components/schemas/TransactionId"
}
}
],
"responses": {
"204": {
"description": "Default response description"
},
"default": {
"$ref": "#/components/responses/default"
}
},
"summary": "COSE receipt for a transaction",
"x-ccf-forwarding": {
"$ref": "#/components/x-ccf-forwarding/sometimes"
}
}
},
"/app/tx": {
"get": {
"description": "Possible statuses returned are Unknown, Pending, Committed or Invalid.",
Expand Down
28 changes: 28 additions & 0 deletions doc/schemas/node_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1683,6 +1683,34 @@
}
}
},
"/node/receipt/cose": {
"get": {
"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.",
"operationId": "GetNodeReceiptCose",
"parameters": [
{
"in": "query",
"name": "transaction_id",
"required": true,
"schema": {
"$ref": "#/components/schemas/TransactionId"
}
}
],
"responses": {
"204": {
"description": "Default response description"
},
"default": {
"$ref": "#/components/responses/default"
}
},
"summary": "COSE receipt for a transaction",
"x-ccf-forwarding": {
"$ref": "#/components/x-ccf-forwarding/sometimes"
}
}
},
"/node/self_signed_certificate": {
"get": {
"operationId": "GetNodeSelfSignedCertificate",
Expand Down
22 changes: 22 additions & 0 deletions include/ccf/ds/openapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@
* fill every _required_ field, and the resulting object can be further
* modified by hand as required.
*/
namespace ccf::ds::openapi
{
/** Tag type representing a binary COSE body (application/cose). */
struct Cose
{};

inline void fill_json_schema(nlohmann::json& schema, const Cose*)
{
schema["type"] = "string";
schema["format"] = "binary";
}

inline std::string schema_name(const Cose*)
{
return "Cose";
}
}

namespace ccf::ds::openapi
{
namespace access
Expand Down Expand Up @@ -393,6 +411,10 @@ namespace ccf::ds::openapi
{
return http::headervalues::contenttype::TEXT;
}
else if constexpr (std::is_same_v<T, Cose>)
{
return http::headervalues::contenttype::COSE;
}
else
{
return http::headervalues::contenttype::JSON;
Expand Down
7 changes: 7 additions & 0 deletions include/ccf/node/startup_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,17 @@ namespace ccf
};
Ledger ledger = {};

enum class LedgerSignMode : uint8_t
{
Dual = 0,
COSE = 1
};

struct LedgerSignatures
{
size_t tx_count = 5000;
ccf::ds::TimeString delay = {"1000ms"};
LedgerSignMode mode = LedgerSignMode::Dual;

bool operator==(const LedgerSignatures&) const = default;
};
Expand Down
4 changes: 3 additions & 1 deletion python/src/ccf/cose.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ def verify_receipt(
"""
Verify a COSE Sign1 receipt as defined in https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs/,
using the CCF tree algorithm defined in https://datatracker.ietf.org/doc/draft-birkholz-cose-receipts-ccf-profile/

If claim_digest is None, the claims digest in the leaf is not verified.
"""
key_pem = key.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo).decode(
"ascii"
Expand Down Expand Up @@ -227,7 +229,7 @@ def verify_receipt(
proof = cbor2.loads(inclusion_proof)
assert CCF_PROOF_LEAF_LABEL in proof, "Leaf must be present"
leaf = proof[CCF_PROOF_LEAF_LABEL]
if claim_digest != leaf[2]:
if claim_digest is not None and claim_digest != leaf[2]:
raise ValueError(f"Claim digest mismatch: {leaf[2]!r} != {claim_digest!r}")
accumulator = hashlib.sha256(
leaf[0] + hashlib.sha256(leaf[1].encode()).digest() + leaf[2]
Expand Down
8 changes: 6 additions & 2 deletions python/src/ccf/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,10 +724,14 @@ def add_transaction(self, transaction):
else:
self.node_certificates[node_id] = endorsed_node_cert

# This is a merkle root/signature tx if the table exists
if SIGNATURE_TX_TABLE_NAME in tables:
# This is a merkle root/signature tx if either signature table exists
is_signature_tx = (
SIGNATURE_TX_TABLE_NAME in tables or COSE_SIGNATURE_TX_TABLE_NAME in tables
)
if is_signature_tx:
self.signature_count += 1

if SIGNATURE_TX_TABLE_NAME in tables:
if self.verification_level >= VerificationLevel.MERKLE:
signature_table = tables[SIGNATURE_TX_TABLE_NAME]

Expand Down
3 changes: 2 additions & 1 deletion samples/config/join_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
},
"ledger_signatures": {
"tx_count": 5000,
"delay": "1s"
"delay": "1s",
"mode": "Dual"
},
"jwt": {
"key_refresh_interval": "30min"
Expand Down
3 changes: 2 additions & 1 deletion samples/config/recover_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
},
"ledger_signatures": {
"tx_count": 5000,
"delay": "1s"
"delay": "1s",
"mode": "Dual"
},
"jwt": {
"key_refresh_interval": "30min"
Expand Down
3 changes: 2 additions & 1 deletion samples/config/start_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@
},
"ledger_signatures": {
"tx_count": 5000,
"delay": "1s"
"delay": "1s",
"mode": "Dual"
},
"jwt": {
"key_refresh_interval": "30min"
Expand Down
8 changes: 7 additions & 1 deletion src/common/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,15 @@ namespace ccf
DECLARE_JSON_OPTIONAL_FIELDS(
CCFConfig::Ledger, directory, read_only_directories, chunk_size);

DECLARE_JSON_ENUM(
CCFConfig::LedgerSignMode,
{{CCFConfig::LedgerSignMode::Dual, "Dual"},
{CCFConfig::LedgerSignMode::COSE, "COSE"}});

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(CCFConfig::LedgerSignatures);
DECLARE_JSON_REQUIRED_FIELDS(CCFConfig::LedgerSignatures);
DECLARE_JSON_OPTIONAL_FIELDS(CCFConfig::LedgerSignatures, tx_count, delay);
DECLARE_JSON_OPTIONAL_FIELDS(
CCFConfig::LedgerSignatures, tx_count, delay, mode);

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(CCFConfig::JWT);
DECLARE_JSON_REQUIRED_FIELDS(CCFConfig::JWT);
Expand Down
40 changes: 40 additions & 0 deletions src/endpoints/common_endpoint_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<void, ds::openapi::Cose>()
.add_query_parameter<ccf::TxID>(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(
Expand Down
17 changes: 14 additions & 3 deletions src/endpoints/endpoint_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,24 @@ namespace ccf::endpoints
}
auto proof = tree.get_proof(info.tx_id.seqno);

std::optional<std::vector<uint8_t>> sig;
std::optional<ccf::crypto::Pem> cert;
NodeId node{};

if (cached_sig->sig)
{
sig = cached_sig->sig->sig;
cert = cached_sig->sig->cert;
node = cached_sig->sig->node;
}

return std::make_shared<TxReceiptImpl>(
cached_sig->sig.sig,
sig,
cached_sig->cose_signature,
proof.get_root(),
proof.get_path(),
cached_sig->sig.node,
cached_sig->sig.cert,
node,
cert,
info.write_set_digest,
info.commit_evidence,
info.claims_digest);
Expand Down
21 changes: 11 additions & 10 deletions src/kv/deserialise.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,23 +130,24 @@ namespace ccf::kv
}
auto success = ApplyResult::PASS;

auto search = changes.find(ccf::Tables::SIGNATURES);
if (search != changes.end())
const bool signature_in =
(changes.find(ccf::Tables::SIGNATURES) != changes.end());
const bool cose_signature_in =
(changes.find(ccf::Tables::COSE_SIGNATURES) != changes.end());

if (signature_in || cose_signature_in)
{
const bool merkle_tree_in =
changes.find(ccf::Tables::SERIALISED_MERKLE_TREE) != changes.end();
switch (changes.size())
{
case 2:
if (
changes.find(ccf::Tables::SERIALISED_MERKLE_TREE) !=
changes.end())
if (merkle_tree_in && (cose_signature_in != signature_in))
{
break;
}
case 3:
if (
changes.find(ccf::Tables::SERIALISED_MERKLE_TREE) !=
changes.end() &&
changes.find(ccf::Tables::COSE_SIGNATURES) != changes.end())
if (merkle_tree_in && cose_signature_in && signature_in)
{
break;
}
Expand All @@ -169,7 +170,7 @@ namespace ccf::kv
success = ApplyResult::PASS_SIGNATURE;
}

search = changes.find(ccf::Tables::ENCRYPTED_PAST_LEDGER_SECRET);
auto search = changes.find(ccf::Tables::ENCRYPTED_PAST_LEDGER_SECRET);
if (search != changes.end())
{
success = ApplyResult::PASS_ENCRYPTED_PAST_LEDGER_SECRET;
Expand Down
Loading
Loading