Skip to content

Commit bab29d5

Browse files
committed
Merge branch 'main' of https://github.com/microsoft/CCF into respond_at_commit
2 parents b8cefa9 + e75668a commit bab29d5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3482
-289
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1111

1212
### Added
1313

14+
- Backup nodes can now be configured to automatically fetch snapshots from the primary when snapshot evidence is detected. This is controlled by the `snapshots.backup_fetch` configuration section, with `enabled`, `max_attempts`, `retry_interval`, `max_size` and `target_rpc_interface` options. Note that the target RPC interface selected must have the `SnapshotRead` operator feature enabled.
1415
- Added `ccf::IdentityHistoryNotFetched` exception type to distinguish identity-history-fetching errors from other logic errors in the network identity subsystem (#7708).
1516
- Added `ccf::describe_cose_receipt_v1(receipt)` to obtain COSE receipts with Merkle proof in unprotected header for non-signature TXs, and empty unprotected header for signature TXs (#7700).
1617
- `NetworkIdentitySubsystemInterface` now exposes `get_trusted_keys()`, returning all trusted network identity keys as a `TrustedKeys` map (#7690).
18+
- Added support for self-transparent code update policies (#7681).
1719

1820
### Changed
1921

22+
- On recovery, the UVM descriptor SVN is now set to the minimum of the previously stored value in the KV and the value found in the new node's startup endorsements. On start, the behaviour is unchanged (#7716).
2023
- Refactored the user facing surface of self-healing-open and local sealing. The whole feature is now `sealing-recovery` with `self-healing-open` now referred to as the `recovery-decision-protocol`. (#7679)
2124
- Local sealing is enabled by setting the `sealing-recovery` config field (for both the sealing node, and the unsealing recovery node)
2225
- The local sealing identity is under `sealing-recovery.location.name`

CMakeLists.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ include(GNUInstallDirs)
4444
# Use fixed name instead of absolute path for reproducible builds
4545
add_compile_options("-ffile-prefix-map=${CCF_DIR}=CCF")
4646

47+
# In Debug builds, export symbols to the dynamic symbol table so that
48+
# backtrace_symbols can resolve function names in stacktraces, and preserve
49+
# frame pointers so that backtrace() can walk the full call stack.
50+
add_link_options($<$<CONFIG:Debug>:-rdynamic>)
51+
add_compile_options($<$<CONFIG:Debug>:-fno-omit-frame-pointer>)
52+
4753
set(CMAKE_MODULE_PATH "${CCF_DIR}/cmake;${CMAKE_MODULE_PATH}")
4854

4955
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@@ -275,7 +281,15 @@ add_ccf_static_library(
275281
${CCF_DIR}/src/tasks/ordered_tasks.cpp
276282
${CCF_DIR}/src/tasks/fan_in_tasks.cpp
277283
${CCF_DIR}/src/tasks/thread_manager.cpp
284+
${CCF_DIR}/src/tasks/worker.cpp
278285
)
286+
target_link_libraries(ccf_tasks PRIVATE ${CMAKE_DL_LIBS})
287+
if(USE_LIBCXX)
288+
# worker.cpp uses backtrace() which, under libc++, resolves through libunwind.
289+
# On systems where libunwind is built with LZMA support (e.g. Azure Linux),
290+
# this creates a transitive dependency on liblzma.
291+
target_link_libraries(ccf_tasks PRIVATE lzma)
292+
endif()
279293

280294
# Common test args for Python scripts starting up CCF networks
281295
set(WORKER_THREADS
@@ -657,6 +671,13 @@ if(BUILD_TESTS)
657671
js_test PRIVATE ccf_js ccf_kv ccf_endpoints ccfcrypto http_parser
658672
)
659673

674+
add_unit_test(
675+
js_policy_test ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/js_policy.cpp
676+
)
677+
target_link_libraries(
678+
js_policy_test PRIVATE ccf_js ccf_kv ccf_endpoints ccfcrypto
679+
)
680+
660681
add_unit_test(
661682
endorsements_test
662683
${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/endorsements.cpp
@@ -804,6 +825,13 @@ if(BUILD_TESTS)
804825
ccf_tasks
805826
)
806827

828+
add_unit_test(
829+
internal_tables_access_test
830+
${CMAKE_CURRENT_SOURCE_DIR}/src/node/rpc/test/internal_tables_access_test.cpp
831+
${CCF_DIR}/src/node/uvm_endorsements.cpp
832+
)
833+
target_link_libraries(internal_tables_access_test PRIVATE ccfcrypto ccf_kv)
834+
807835
add_unit_test(
808836
merkle_test ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/merkle_test.cpp
809837
)

doc/host_config_schema/cchost_config.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,11 @@
325325
"type": "string",
326326
"default": "10GB",
327327
"description": "Maximum size of snapshot this node is willing to fetch"
328+
},
329+
"host_data_transparent_statement_path": {
330+
"type": ["string", "null"],
331+
"default": null,
332+
"description": "Path to a SCITT Transparent Statement over the attested host_data of the node"
328333
}
329334
},
330335
"required": ["target_rpc_address"],
@@ -493,6 +498,39 @@
493498
"read_only_directory": {
494499
"type": ["string", "null"],
495500
"description": "Path to read-only snapshots directory"
501+
},
502+
"backup_fetch": {
503+
"type": "object",
504+
"properties": {
505+
"enabled": {
506+
"type": "boolean",
507+
"default": false,
508+
"description": "If true, backup nodes will automatically fetch snapshots from the primary when snapshot evidence is detected"
509+
},
510+
"max_attempts": {
511+
"type": "integer",
512+
"default": 3,
513+
"description": "Maximum number of fetch attempts before giving up",
514+
"minimum": 1
515+
},
516+
"retry_interval": {
517+
"type": "string",
518+
"default": "1000ms",
519+
"description": "Delay between retry attempts"
520+
},
521+
"target_rpc_interface": {
522+
"type": "string",
523+
"default": "primary_rpc_interface",
524+
"description": "Name of the RPC interface on the primary node to use for downloading snapshots. Must have the SnapshotRead feature enabled."
525+
},
526+
"max_size": {
527+
"type": "string",
528+
"default": "200MB",
529+
"description": "Maximum size of snapshot this node is willing to fetch"
530+
}
531+
},
532+
"description": "Configuration for automatic snapshot fetching by backup nodes",
533+
"additionalProperties": false
496534
}
497535
},
498536
"description": "This section includes configuration for the snapshot directories and files",

doc/operations/code_upgrade.rst

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,134 @@ Notes
184184

185185
- The :http:GET:`/node/version` endpoint can be used by operators to check which version of CCF a specific node is running.
186186
- A code upgrade procedure provides very little service downtime compared to a disaster recovery. The service is only unavailable to process write transactions while the primary-ship changes (typically a few seconds) but can still process read-only transactions throughout the whole procedure. Note that this is true during any primary-ship change, and not just during the code upgrade procedure.
187+
188+
Code Update Policy
189+
------------------
190+
191+
Instead of explicitly trusting host data values, members can set a **code update policy** — a JavaScript function that evaluates transparent statements presented by joining nodes. A transparent statement is a COSE_Sign1 envelope carrying a signed statement about the node's code, countersigned with a CCF receipt that proves the statement was registered on a ledger.
192+
193+
.. note::
194+
195+
CCF currently only supports **self-issued** transparent statements: the service itself acts as the transparency service, issuing receipts over signed statements registered on its own ledger.
196+
197+
The policy receives an array of transparent statements and must return ``true`` to accept or a string describing the rejection reason. Any other return value is treated as an error. Structural validation (non-empty fields, receipt signature verification, claims digest binding) is performed by CCF before the policy runs; the policy only needs to compare values.
198+
199+
Policy Input Schema
200+
~~~~~~~~~~~~~~~~~~~
201+
202+
The ``apply(transparent_statements)`` function receives an array of transparent statement objects. Each element has the following shape:
203+
204+
.. code-block:: javascript
205+
206+
[
207+
{
208+
phdr: { // COSE_Sign1 protected header
209+
alg: <int>, // REQUIRED - COSE algorithm (e.g. -7 for ES256)
210+
cty: <int|string|undefined>, // OPTIONAL - content type
211+
x5chain: [<string>, ...], // REQUIRED - certificate chain (PEM)
212+
cwt: { // CWT claims
213+
iss: <string>, // REQUIRED - issuer DID (did:x509:...)
214+
sub: <string>, // REQUIRED - subject / feed
215+
iat: <int|undefined>, // OPTIONAL - issued-at (Unix timestamp)
216+
svn: <int|undefined>, // OPTIONAL - security version number
217+
},
218+
},
219+
receipts: [ // at least one CCF receipt
220+
{
221+
alg: <int>, // REQUIRED - signature algorithm
222+
vds: <int>, // REQUIRED - verifiable data structure (1 = CCF_LEDGER_SHA256)
223+
kid: <string|undefined>, // OPTIONAL - key identifier
224+
cwt: { // receipt CWT claims
225+
iss: <string>, // REQUIRED - receipt issuer (e.g. "service.example.com")
226+
sub: <string>, // REQUIRED - receipt subject
227+
iat: <int|undefined>, // OPTIONAL - receipt issued-at
228+
},
229+
ccf: { // CCF-specific claims
230+
txid: <string|undefined>, // OPTIONAL - transaction ID (e.g. "2.42")
231+
},
232+
leaves: [ // at least one Merkle tree leaf
233+
{
234+
claims_digest: <string>, // hex-encoded SHA-256
235+
commit_evidence: <string>, // commit evidence string
236+
write_set_digest: <string>, // hex-encoded SHA-256
237+
},
238+
...
239+
],
240+
},
241+
...
242+
],
243+
},
244+
...
245+
]
246+
247+
Example Policy
248+
~~~~~~~~~~~~~~
249+
250+
.. code-block:: javascript
251+
252+
export function apply(transparent_statements) {
253+
for (const ts of transparent_statements) {
254+
if (ts.phdr.alg !== -7) {
255+
return "Unexpected algorithm: " + ts.phdr.alg;
256+
}
257+
if (ts.phdr.cwt.iss !== "did:x509:abc::eku:1.2.3") {
258+
return "Invalid issuer: " + ts.phdr.cwt.iss;
259+
}
260+
if (ts.phdr.cwt.sub !== "my-application") {
261+
return "Invalid subject: " + ts.phdr.cwt.sub;
262+
}
263+
if (ts.phdr.cwt.svn < 100) {
264+
return "SVN too low: " + ts.phdr.cwt.svn;
265+
}
266+
267+
for (const r of ts.receipts) {
268+
if (r.alg !== -7) {
269+
return "Unexpected receipt algorithm: " + r.alg;
270+
}
271+
if (r.vds !== 1) {
272+
return "Unexpected VDS: " + r.vds;
273+
}
274+
if (r.cwt.iss !== "service.example.com") {
275+
return "Invalid receipt issuer: " + r.cwt.iss;
276+
}
277+
if (r.cwt.sub !== "ledger.signature") {
278+
return "Invalid receipt subject: " + r.cwt.sub;
279+
}
280+
281+
for (const leaf of r.leaves) {
282+
if (leaf.claims_digest !== "abcdef...") {
283+
return "Unexpected claims_digest: " + leaf.claims_digest;
284+
}
285+
if (leaf.commit_evidence !== "ce:2.42:deadbeef") {
286+
return "Unexpected commit_evidence: " + leaf.commit_evidence;
287+
}
288+
if (leaf.write_set_digest !== "012345...") {
289+
return "Unexpected write_set_digest: " + leaf.write_set_digest;
290+
}
291+
}
292+
}
293+
}
294+
return true;
295+
}
296+
297+
Setting the Policy
298+
~~~~~~~~~~~~~~~~~~
299+
300+
Use the ``set_node_join_policy`` governance action to register the policy and ``remove_node_join_policy`` to remove it.
301+
302+
Joining with a Transparent Statement
303+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
304+
305+
When a node joins the network, it can present a transparent statement by setting the ``host_data_transparent_statement_path`` field in the ``join`` section of its configuration file. This must point to a COSE Sign1 file (the transparent statement) that attests to the node's host data:
306+
307+
.. code-block:: json
308+
309+
{
310+
"command": {
311+
"join": {
312+
"host_data_transparent_statement_path": "/path/to/transparent_statement.cose"
313+
}
314+
}
315+
}
316+
317+
If the joining node's host data is not in the trusted list (i.e. not registered via ``add_snp_host_data``), CCF falls back to evaluating the transparent statement against the code update policy. If no transparent statement is provided, or the policy rejects it, the node will not be allowed to join. If the host data is already explicitly trusted, the node joins without evaluating the policy, regardless of whether a transparent statement is provided.

doc/schemas/app_openapi.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,6 +1569,38 @@
15691569
}
15701570
}
15711571
},
1572+
"/app/log/signed_statement": {
1573+
"post": {
1574+
"operationId": "PostAppLogSignedStatement",
1575+
"responses": {
1576+
"204": {
1577+
"description": "Default response description"
1578+
},
1579+
"default": {
1580+
"$ref": "#/components/responses/default"
1581+
}
1582+
},
1583+
"x-ccf-forwarding": {
1584+
"$ref": "#/components/x-ccf-forwarding/always"
1585+
}
1586+
}
1587+
},
1588+
"/app/log/transparent_statement": {
1589+
"get": {
1590+
"operationId": "GetAppLogTransparentStatement",
1591+
"responses": {
1592+
"204": {
1593+
"description": "Default response description"
1594+
},
1595+
"default": {
1596+
"$ref": "#/components/responses/default"
1597+
}
1598+
},
1599+
"x-ccf-forwarding": {
1600+
"$ref": "#/components/x-ccf-forwarding/never"
1601+
}
1602+
}
1603+
},
15721604
"/app/multi_auth": {
15731605
"post": {
15741606
"operationId": "PostAppMultiAuth",

include/ccf/network_identity_interface.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "ccf/crypto/ec_public_key.h"
66
#include "ccf/node_subsystem_interface.h"
7+
#include "ccf/tx_id.h"
78

89
#include <exception>
910
#include <map>

include/ccf/node/quote.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
#pragma once
44

55
#include "ccf/ds/quote_info.h"
6+
#include "ccf/network_identity_interface.h"
67
#include "ccf/pal/attestation_sev_snp.h"
78
#include "ccf/pal/measurement.h"
89
#include "ccf/service/tables/host_data.h"
910
#include "ccf/tx.h"
1011

12+
#include <memory>
1113
#include <optional>
1214
#include <vector>
1315

@@ -44,7 +46,10 @@ namespace ccf
4446
ccf::kv::ReadOnlyTx& tx,
4547
const QuoteInfo& quote_info,
4648
const std::vector<uint8_t>& expected_node_public_key_der,
47-
pal::PlatformAttestationMeasurement& measurement);
49+
pal::PlatformAttestationMeasurement& measurement,
50+
const std::optional<std::vector<uint8_t>>& code_transparent_statement,
51+
std::shared_ptr<NetworkIdentitySubsystemInterface>
52+
network_identity_subsystem = nullptr);
4853
};
4954
QuoteVerificationResult verify_tcb_version_against_store(
5055
ccf::kv::ReadOnlyTx& tx, const QuoteInfo& quote_info);

include/ccf/node/startup_config.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ namespace ccf
9999
size_t tx_count = 10'000;
100100
std::optional<std::string> read_only_directory = std::nullopt;
101101

102+
struct BackupFetch
103+
{
104+
bool enabled = false;
105+
size_t max_attempts = 3;
106+
ccf::ds::TimeString retry_interval = {"1000ms"};
107+
std::string target_rpc_interface = ccf::PRIMARY_RPC_INTERFACE;
108+
ccf::ds::SizeString max_size = {"200MB"};
109+
110+
bool operator==(const BackupFetch&) const = default;
111+
};
112+
BackupFetch backup_fetch = {};
113+
102114
bool operator==(const Snapshots&) const = default;
103115
};
104116
Snapshots snapshots = {};
@@ -159,6 +171,8 @@ namespace ccf
159171
size_t fetch_snapshot_max_attempts{};
160172
ccf::ds::TimeString fetch_snapshot_retry_interval;
161173
ccf::ds::SizeString fetch_snapshot_max_size;
174+
std::optional<std::string> host_data_transparent_statement_path =
175+
std::nullopt;
162176
};
163177
Join join = {};
164178

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the Apache 2.0 License.
3+
#pragma once
4+
5+
#include "ccf/kv/value.h"
6+
7+
namespace ccf
8+
{
9+
using CodeUpdatePolicy = ccf::kv::RawCopySerialisedValue<std::string>;
10+
11+
namespace Tables
12+
{
13+
static constexpr auto NODE_JOIN_POLICY =
14+
"public:ccf.gov.nodes.node_join_policy";
15+
}
16+
}

0 commit comments

Comments
 (0)