Skip to content

Commit dc1a036

Browse files
COSE-only ledgers (#7772)
Co-authored-by: Amaury Chamayou <amchamay@microsoft.com> Co-authored-by: Amaury Chamayou <amaury@xargs.fr>
1 parent c9b4c18 commit dc1a036

32 files changed

Lines changed: 1490 additions & 237 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2626

2727
### Added
2828

29+
- Added support for COSE-only ledger signatures. Networks can start in COSE-only mode or transition from dual signing (#7772).
30+
- 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).
2931
- 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).
3032

3133
### Changed

CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,10 @@ if(BUILD_TESTS)
985985
add_e2e_test(
986986
NAME recovery_test
987987
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/recovery.py
988-
ADDITIONAL_ARGS ${ADDITIONAL_RECOVERY_ARGS}
988+
ADDITIONAL_ARGS
989+
${ADDITIONAL_RECOVERY_ARGS}
990+
--constitution
991+
${CMAKE_SOURCE_DIR}/samples/constitutions/virtual/virtual_attestation_actions.js
989992
)
990993

991994
add_e2e_test(
@@ -1227,6 +1230,8 @@ if(BUILD_TESTS)
12271230
NAME schema_test
12281231
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/schema.py
12291232
ADDITIONAL_ARGS
1233+
--constitution
1234+
${CMAKE_SOURCE_DIR}/samples/constitutions/virtual/virtual_attestation_actions.js
12301235
--schema-dir
12311236
${CMAKE_SOURCE_DIR}/doc/schemas
12321237
--ledger-tutorial

doc/operations/configuration.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,22 @@ Since these operations may require disk IO and produce large responses, these fe
4040
- 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).
4141

4242
- Time strings are expressed as the value suffixed with the duration (``us``, ``ms``, ``s``, ``min``, ``h``), e.g. ``"1000ms"``, ``"10s"`` or ``"30min"``.
43+
44+
45+
Upgrading to COSE-Only Ledger Signatures
46+
-----------------------------------------
47+
48+
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:
49+
50+
- ``Dual`` (default) — emit both signature types, accept all joiners.
51+
- ``CoseAllowDualJoin`` — emit only COSE signatures, but still accept join requests from ``Dual``-mode nodes. Use during rolling upgrades.
52+
- ``CoseOnly`` — emit only COSE signatures, reject ``Dual``-mode joiners. Final state after upgrade.
53+
54+
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.
55+
56+
A rolling upgrade from ``Dual`` to ``CoseOnly`` is a two-step process:
57+
58+
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.
59+
60+
2. **CoseOnly.** Once all nodes are upgraded, deploy a binary that returns ``CoseOnly``. Replace nodes again. After this, ``Dual`` joiners are rejected.
61+

doc/schemas/app_openapi.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
},
3232
"type": "object"
3333
},
34+
"Cose": {
35+
"format": "binary",
36+
"type": "string"
37+
},
3438
"GetCommit__Out": {
3539
"properties": {
3640
"transaction_id": {
@@ -1743,6 +1747,41 @@
17431747
}
17441748
}
17451749
},
1750+
"/app/receipt/cose": {
1751+
"get": {
1752+
"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.",
1753+
"operationId": "GetAppReceiptCose",
1754+
"parameters": [
1755+
{
1756+
"in": "query",
1757+
"name": "transaction_id",
1758+
"required": true,
1759+
"schema": {
1760+
"$ref": "#/components/schemas/TransactionId"
1761+
}
1762+
}
1763+
],
1764+
"responses": {
1765+
"200": {
1766+
"content": {
1767+
"application/cose": {
1768+
"schema": {
1769+
"$ref": "#/components/schemas/Cose"
1770+
}
1771+
}
1772+
},
1773+
"description": "Default response description"
1774+
},
1775+
"default": {
1776+
"$ref": "#/components/responses/default"
1777+
}
1778+
},
1779+
"summary": "COSE receipt for a transaction",
1780+
"x-ccf-forwarding": {
1781+
"$ref": "#/components/x-ccf-forwarding/sometimes"
1782+
}
1783+
}
1784+
},
17461785
"/app/tx": {
17471786
"get": {
17481787
"description": "Possible statuses returned are Unknown, Pending, Committed or Invalid.",

doc/schemas/node_openapi.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@
198198
],
199199
"type": "string"
200200
},
201+
"Cose": {
202+
"format": "binary",
203+
"type": "string"
204+
},
201205
"Endorsement": {
202206
"properties": {
203207
"authority": {
@@ -1641,6 +1645,41 @@
16411645
}
16421646
}
16431647
},
1648+
"/node/receipt/cose": {
1649+
"get": {
1650+
"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.",
1651+
"operationId": "GetNodeReceiptCose",
1652+
"parameters": [
1653+
{
1654+
"in": "query",
1655+
"name": "transaction_id",
1656+
"required": true,
1657+
"schema": {
1658+
"$ref": "#/components/schemas/TransactionId"
1659+
}
1660+
}
1661+
],
1662+
"responses": {
1663+
"200": {
1664+
"content": {
1665+
"application/cose": {
1666+
"schema": {
1667+
"$ref": "#/components/schemas/Cose"
1668+
}
1669+
}
1670+
},
1671+
"description": "Default response description"
1672+
},
1673+
"default": {
1674+
"$ref": "#/components/responses/default"
1675+
}
1676+
},
1677+
"summary": "COSE receipt for a transaction",
1678+
"x-ccf-forwarding": {
1679+
"$ref": "#/components/x-ccf-forwarding/sometimes"
1680+
}
1681+
}
1682+
},
16441683
"/node/self_signed_certificate": {
16451684
"get": {
16461685
"operationId": "GetNodeSelfSignedCertificate",

include/ccf/ds/openapi.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@
2020
* fill every _required_ field, and the resulting object can be further
2121
* modified by hand as required.
2222
*/
23+
namespace ccf::ds::openapi
24+
{
25+
/** Tag type representing a binary COSE body (application/cose). */
26+
struct Cose
27+
{};
28+
29+
inline void fill_json_schema(
30+
nlohmann::json& schema, [[maybe_unused]] const Cose* cose)
31+
{
32+
schema["type"] = "string";
33+
schema["format"] = "binary";
34+
}
35+
36+
inline std::string schema_name([[maybe_unused]] const Cose* cose)
37+
{
38+
return "Cose";
39+
}
40+
}
41+
2342
namespace ccf::ds::openapi
2443
{
2544
namespace access
@@ -393,6 +412,10 @@ namespace ccf::ds::openapi
393412
{
394413
return http::headervalues::contenttype::TEXT;
395414
}
415+
else if constexpr (std::is_same_v<T, Cose>)
416+
{
417+
return http::headervalues::contenttype::COSE;
418+
}
396419
else
397420
{
398421
return http::headervalues::contenttype::JSON;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the Apache 2.0 License.
3+
#pragma once
4+
5+
#include "ccf/ds/json.h"
6+
7+
#include <cstdint>
8+
9+
namespace ccf
10+
{
11+
enum class LedgerSignMode : uint8_t
12+
{
13+
// Emit both traditional node signatures and COSE Sign1 signatures.
14+
// Accept join requests from nodes in any signing mode.
15+
Dual = 0,
16+
17+
// Emit only COSE Sign1 signatures, but accept join requests from
18+
// nodes still running in Dual mode. Use during rolling upgrades.
19+
CoseAllowDualJoin = 1,
20+
21+
// Emit only COSE Sign1 signatures and reject join requests from
22+
// nodes running in Dual mode. Final state after a completed upgrade.
23+
CoseOnly = 2
24+
};
25+
26+
DECLARE_JSON_ENUM(
27+
LedgerSignMode,
28+
{{LedgerSignMode::Dual, "Dual"},
29+
{LedgerSignMode::CoseAllowDualJoin, "CoseAllowDualJoin"},
30+
{LedgerSignMode::CoseOnly, "CoseOnly"}});
31+
32+
/** Can be optionally implemented by the application to set the ledger
33+
* signing mode.
34+
*
35+
* The default (weak) implementation returns LedgerSignMode::Dual.
36+
*
37+
* @return the desired ledger signing mode
38+
*/
39+
LedgerSignMode get_ledger_sign_mode();
40+
}

python/src/ccf/cose.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def verify_receipt(
195195
"""
196196
Verify a COSE Sign1 receipt as defined in https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs/,
197197
using the CCF tree algorithm defined in https://datatracker.ietf.org/doc/draft-birkholz-cose-receipts-ccf-profile/
198+
198199
"""
199200
key_pem = key.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo).decode(
200201
"ascii"

python/src/ccf/ledger.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -724,10 +724,14 @@ def add_transaction(self, transaction):
724724
else:
725725
self.node_certificates[node_id] = endorsed_node_cert
726726

727-
# This is a merkle root/signature tx if the table exists
728-
if SIGNATURE_TX_TABLE_NAME in tables:
727+
# This is a merkle root/signature tx if either signature table exists
728+
is_signature_tx = (
729+
SIGNATURE_TX_TABLE_NAME in tables or COSE_SIGNATURE_TX_TABLE_NAME in tables
730+
)
731+
if is_signature_tx:
729732
self.signature_count += 1
730733

734+
if SIGNATURE_TX_TABLE_NAME in tables:
731735
if self.verification_level >= VerificationLevel.MERKLE:
732736
signature_table = tables[SIGNATURE_TX_TABLE_NAME]
733737

samples/apps/logging/CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,21 @@ if(NOT TARGET "ccf")
1212
endif()
1313

1414
add_ccf_app(logging SRCS logging.cpp create_tx_claims_digest.cpp ../main.cpp)
15+
16+
add_ccf_app(
17+
logging_cose_only
18+
SRCS
19+
logging.cpp
20+
create_tx_claims_digest.cpp
21+
get_ledger_sign_mode_cose.cpp
22+
../main.cpp
23+
)
24+
25+
add_ccf_app(
26+
logging_cose_only_allow_join_dual
27+
SRCS
28+
logging.cpp
29+
create_tx_claims_digest.cpp
30+
get_ledger_sign_mode_cose_allow_join_dual.cpp
31+
../main.cpp
32+
)

0 commit comments

Comments
 (0)