Skip to content

Commit 02521e1

Browse files
maxtropetsachamayoueddyashtonCopilot
authored
Self-transparent code update (#7681)
Co-authored-by: Amaury Chamayou <amaury@xargs.fr> Co-authored-by: Eddy Ashton <ashton.eddy@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Amaury Chamayou <amchamay@microsoft.com>
1 parent 4e02b8b commit 02521e1

37 files changed

Lines changed: 1912 additions & 83 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1414
- Added `ccf::IdentityHistoryNotFetched` exception type to distinguish identity-history-fetching errors from other logic errors in the network identity subsystem (#7708).
1515
- 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).
1616
- `NetworkIdentitySubsystemInterface` now exposes `get_trusted_keys()`, returning all trusted network identity keys as a `TrustedKeys` map (#7690).
17+
- Added support for self-transparent code update policies (#7681).
1718

1819
### Changed
1920

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,13 @@ if(BUILD_TESTS)
665665
js_test PRIVATE ccf_js ccf_kv ccf_endpoints ccfcrypto http_parser
666666
)
667667

668+
add_unit_test(
669+
js_policy_test ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/js_policy.cpp
670+
)
671+
target_link_libraries(
672+
js_policy_test PRIVATE ccf_js ccf_kv ccf_endpoints ccfcrypto
673+
)
674+
668675
add_unit_test(
669676
endorsements_test
670677
${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/endorsements.cpp

doc/host_config_schema/cchost_config.json

Lines changed: 5 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"],

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
@@ -1489,6 +1489,38 @@
14891489
}
14901490
}
14911491
},
1492+
"/app/log/signed_statement": {
1493+
"post": {
1494+
"operationId": "PostAppLogSignedStatement",
1495+
"responses": {
1496+
"204": {
1497+
"description": "Default response description"
1498+
},
1499+
"default": {
1500+
"$ref": "#/components/responses/default"
1501+
}
1502+
},
1503+
"x-ccf-forwarding": {
1504+
"$ref": "#/components/x-ccf-forwarding/always"
1505+
}
1506+
}
1507+
},
1508+
"/app/log/transparent_statement": {
1509+
"get": {
1510+
"operationId": "GetAppLogTransparentStatement",
1511+
"responses": {
1512+
"204": {
1513+
"description": "Default response description"
1514+
},
1515+
"default": {
1516+
"$ref": "#/components/responses/default"
1517+
}
1518+
},
1519+
"x-ccf-forwarding": {
1520+
"$ref": "#/components/x-ccf-forwarding/never"
1521+
}
1522+
}
1523+
},
14921524
"/app/multi_auth": {
14931525
"post": {
14941526
"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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ namespace ccf
159159
size_t fetch_snapshot_max_attempts{};
160160
ccf::ds::TimeString fetch_snapshot_retry_interval;
161161
ccf::ds::SizeString fetch_snapshot_max_size;
162+
std::optional<std::string> host_data_transparent_statement_path =
163+
std::nullopt;
162164
};
163165
Join join = {};
164166

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)