Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f980687
COSE-only ledgers
maxtropets Apr 9, 2026
462c454
Review
maxtropets Apr 10, 2026
8dbdcba
Review
maxtropets Apr 10, 2026
a235b84
Tidy, tidy, tidy...
maxtropets Apr 10, 2026
fac0afe
Comment
maxtropets Apr 10, 2026
e669ebe
Cose only as appl setting
maxtropets Apr 13, 2026
ef19eaf
More changes. Polishing
maxtropets Apr 13, 2026
1189551
Recovery test
maxtropets Apr 13, 2026
358cf02
Merge branch 'main' into f/COSE-only-sig
maxtropets Apr 13, 2026
bee192a
Recovery fix and test for seqnos
maxtropets Apr 13, 2026
8456759
Fix recovery
maxtropets Apr 13, 2026
fd5f58c
Changelog
maxtropets Apr 13, 2026
ccb2683
Changelog
maxtropets Apr 14, 2026
36602ce
Merge branch 'main' into f/COSE-only-sig
maxtropets Apr 14, 2026
e45c6ae
Update, more recovrey test
maxtropets Apr 14, 2026
cea8f12
Ok
maxtropets Apr 14, 2026
6bc1eb7
Fix recovery test
maxtropets Apr 14, 2026
5bed927
Merge branch 'main' into f/COSE-only-sig
achamayou Apr 15, 2026
3e31f1e
Merge branch 'main' into f/COSE-only-sig
maxtropets Apr 16, 2026
ff3c418
Unite configs. move out from research subfolder
maxtropets Apr 16, 2026
8f598e2
Apply suggestion from @achamayou
achamayou Apr 16, 2026
83e46a5
Apply suggestion from @achamayou
achamayou Apr 16, 2026
a12242d
Merge branch 'main' into f/COSE-only-sig
achamayou Apr 16, 2026
f02126f
Revert "Apply suggestion from @achamayou"
maxtropets Apr 16, 2026
a3b816e
Revert "Apply suggestion from @achamayou"
maxtropets Apr 16, 2026
05e450a
Revert public python api, force claims, build fix, schema manual fix
maxtropets Apr 16, 2026
81bf35f
Merge branch 'main' into f/COSE-only-sig
maxtropets Apr 16, 2026
e15cde5
Update error message
maxtropets Apr 16, 2026
e3e7e3f
Fixup two missed signature lookups
maxtropets Apr 16, 2026
cfa3a56
Merge branch 'main' into f/COSE-only-sig
achamayou Apr 16, 2026
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Added support for COSE-only ledger signatures. Networks can start in COSE-only mode or transition from dual signing (#7772).
- Added `/receipt/cose` endpoint returning a COSE Sign1 receipt with Merkle proof for a given transaction. Returns 404 if no COSE receipt is available (e.g. for signature transactions) (#7772).
Comment thread
maxtropets marked this conversation as resolved.
- 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
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,10 @@ if(BUILD_TESTS)
add_e2e_test(
NAME recovery_test
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/recovery.py
ADDITIONAL_ARGS ${ADDITIONAL_RECOVERY_ARGS}
ADDITIONAL_ARGS
${ADDITIONAL_RECOVERY_ARGS}
--constitution
${CMAKE_SOURCE_DIR}/samples/constitutions/virtual/virtual_attestation_actions.js
)

add_e2e_test(
Expand Down Expand Up @@ -1227,6 +1230,8 @@ if(BUILD_TESTS)
NAME schema_test
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/schema.py
ADDITIONAL_ARGS
--constitution
${CMAKE_SOURCE_DIR}/samples/constitutions/virtual/virtual_attestation_actions.js
--schema-dir
${CMAKE_SOURCE_DIR}/doc/schemas
--ledger-tutorial
Expand Down
19 changes: 19 additions & 0 deletions doc/operations/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,22 @@ 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"``.


Upgrading to COSE-Only Ledger Signatures
-----------------------------------------

By default, CCF nodes emit **dual** ledger signatures: a traditional node signature and a COSE Sign1 signature. Applications control this via the ``ccf::get_ledger_sign_mode()`` weak-symbol callback declared in ``ccf/node/ledger_sign_mode.h``, which returns one of three modes:

- ``Dual`` (default) — emit both signature types, accept all joiners.
- ``CoseAllowDualJoin`` — emit only COSE signatures, but still accept join requests from ``Dual``-mode nodes. Use during rolling upgrades.
- ``CoseOnly`` — emit only COSE signatures, reject ``Dual``-mode joiners. Final state after upgrade.

The mode is set at link time by providing a strong definition in the application binary. Joining nodes advertise their signing mode in the join request.
Comment thread
maxtropets marked this conversation as resolved.

A rolling upgrade from ``Dual`` to ``CoseOnly`` is a two-step process:

1. **CoseAllowDualJoin.** Deploy a binary that returns ``CoseAllowDualJoin``. Replace nodes one at a time. During this phase, new nodes running the old ``Dual`` binary can still join as replacements.

2. **CoseOnly.** Once all nodes are upgraded, deploy a binary that returns ``CoseOnly``. Replace nodes again. After this, ``Dual`` joiners are rejected.

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 @@ -1743,6 +1747,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. See https://datatracker.ietf.org/doc/draft-ietf-scitt-receipts-ccf-profile/ for a complete description.",
"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 @@ -1641,6 +1645,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. See https://datatracker.ietf.org/doc/draft-ietf-scitt-receipts-ccf-profile/ for a complete description.",
"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)
{
Comment thread
maxtropets marked this conversation as resolved.
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
40 changes: 40 additions & 0 deletions include/ccf/node/ledger_sign_mode.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once

#include "ccf/ds/json.h"

#include <cstdint>

namespace ccf
{
enum class LedgerSignMode : uint8_t
{
// Emit both traditional node signatures and COSE Sign1 signatures.
// Accept join requests from nodes in any signing mode.
Dual = 0,

// Emit only COSE Sign1 signatures, but accept join requests from
// nodes still running in Dual mode. Use during rolling upgrades.
CoseAllowDualJoin = 1,

// Emit only COSE Sign1 signatures and reject join requests from
// nodes running in Dual mode. Final state after a completed upgrade.
CoseOnly = 2
};

DECLARE_JSON_ENUM(
LedgerSignMode,
{{LedgerSignMode::Dual, "Dual"},
{LedgerSignMode::CoseAllowDualJoin, "CoseAllowDualJoin"},
{LedgerSignMode::CoseOnly, "CoseOnly"}});

/** Can be optionally implemented by the application to set the ledger
* signing mode.
*
* The default (weak) implementation returns LedgerSignMode::Dual.
*
* @return the desired ledger signing mode
*/
LedgerSignMode get_ledger_sign_mode();
}
1 change: 1 addition & 0 deletions python/src/ccf/cose.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ 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/

"""
key_pem = key.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo).decode(
"ascii"
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:
Comment thread
maxtropets marked this conversation as resolved.
signature_table = tables[SIGNATURE_TX_TABLE_NAME]

Expand Down
18 changes: 18 additions & 0 deletions samples/apps/logging/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,21 @@ if(NOT TARGET "ccf")
endif()

add_ccf_app(logging SRCS logging.cpp create_tx_claims_digest.cpp ../main.cpp)

add_ccf_app(
logging_cose_only
SRCS
logging.cpp
create_tx_claims_digest.cpp
get_ledger_sign_mode_cose.cpp
../main.cpp
)

add_ccf_app(
logging_cose_only_allow_join_dual
SRCS
logging.cpp
create_tx_claims_digest.cpp
get_ledger_sign_mode_cose_allow_join_dual.cpp
../main.cpp
)
12 changes: 12 additions & 0 deletions samples/apps/logging/get_ledger_sign_mode_cose.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.

#include "ccf/node/ledger_sign_mode.h"

namespace ccf
{
LedgerSignMode get_ledger_sign_mode()
{
return LedgerSignMode::CoseOnly;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.

#include "ccf/node/ledger_sign_mode.h"

namespace ccf
{
LedgerSignMode get_ledger_sign_mode()
{
return LedgerSignMode::CoseAllowDualJoin;
}
}
42 changes: 42 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,47 @@ 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. See "
"https://datatracker.ietf.org/doc/draft-ietf-scitt-receipts-ccf-"
"profile/ for a complete description.")
.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,
Comment thread
maxtropets marked this conversation as resolved.
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