Skip to content
Open
Show file tree
Hide file tree
Changes from all 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.

39 changes: 39 additions & 0 deletions doc/schemas/app_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
},
"type": "object"
},
"Cose": {
"format": "binary",
"type": "string"
},
"GetCommit__Out": {
"properties": {
"transaction_id": {
Expand Down Expand Up @@ -1692,6 +1696,41 @@
}
}
},
"/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": {
"200": {
"content": {
"application/cose": {
"schema": {
"$ref": "#/components/schemas/Cose"
}
}
},
"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
39 changes: 39 additions & 0 deletions doc/schemas/node_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@
],
"type": "string"
},
"Cose": {
"format": "binary",
"type": "string"
},
"Endorsement": {
"properties": {
"authority": {
Expand Down Expand Up @@ -1683,6 +1687,41 @@
}
}
},
"/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": {
"200": {
"content": {
"application/cose": {
"schema": {
"$ref": "#/components/schemas/Cose"
}
}
},
"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
23 changes: 23 additions & 0 deletions include/ccf/ds/openapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@
* 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, [[maybe_unused]] const Cose* cose)
{
schema["type"] = "string";
schema["format"] = "binary";
}

inline std::string schema_name([[maybe_unused]] const Cose* cose)
{
return "Cose";
}
}

namespace ccf::ds::openapi
{
namespace access
Expand Down Expand Up @@ -393,6 +412,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
Loading