Skip to content

Commit 8213d42

Browse files
cjen1-msftCopilotachamayou
authored
Local sealing improvements (#7554)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Amaury Chamayou <amchamay@microsoft.com>
1 parent fea0caf commit 8213d42

33 files changed

Lines changed: 773 additions & 706 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
4848
### Changed
4949

5050
- Improved `ccf::historical::verify_self_issued_receipt` - now can verify receipts signed by the past service identities if they were back-endorsed (#7546).
51+
- Local sealing recovery now stores sealed secrets in the ledger instead of separately on disk. (#7554)
52+
- To configure this use `enable_local_sealing` (top-level) and `command.recover.previous_local_sealing_identity` in the configuration.
53+
- The configuration options `output_files.sealed_ledger_secret_location` and `command.recover.previous_sealed_ledger_secret_location` have been deprecated and are ignored.
54+
- Recovery keys are now stored in `public:ccf.gov.nodes.sealed_recovery_keys` and encrypted shares in `public:ccf.internal.sealed_shares`.
55+
- There is an update in the constitution to reseal whenever a node is added, this ensures that as soon as a node is trusted, it can recover from that point in the ledger.
5156

5257
### Removed
5358

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ add_ccf_static_library(
261261
${CCF_DIR}/src/node/historical_queries_adapter.cpp
262262
${CCF_DIR}/src/node/historical_queries_utils.cpp
263263
${CCF_DIR}/src/node/receipt.cpp
264+
${CCF_DIR}/src/node/local_sealing.cpp
264265
LINK_LIBS http_parser ccfcrypto ccf_kv
265266
)
266267

@@ -675,6 +676,7 @@ if(BUILD_TESTS)
675676
${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/historical_queries.cpp
676677
${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/receipt.cpp
677678
${CMAKE_CURRENT_SOURCE_DIR}/src/node/receipt.cpp
679+
${CMAKE_CURRENT_SOURCE_DIR}/src/node/local_sealing.cpp
678680
)
679681
target_link_libraries(
680682
historical_queries_test PRIVATE http_parser ccf_kv ccf_endpoints

doc/audit/builtin_maps.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,17 @@ The minimum trusted TCB version for new nodes allowed to join the network (:doc`
218218
* - ``00a00f11``
219219
- ``{"hexstring": "d315000000000004", "boot_loader": 4, "tee": 0, "snp": 21, "microcode": 211}``
220220

221+
``nodes.sealed_recovery_keys``
222+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
223+
224+
**Key** Node ID: SHA-256 digest of the node public key, represented as a hex-encoded string.
225+
226+
**Value** Sealed recovery key for the node. The private key is encrypted using a key derived from SNP's ``DERIVED_KEY``, allowing the node to unseal its recovery share during local sealing recovery.
227+
228+
.. doxygenstruct:: ccf::SealedRecoveryKey
229+
:project: CCF
230+
:members:
231+
221232
``service.info``
222233
~~~~~~~~~~~~~~~~
223234

@@ -559,6 +570,17 @@ While the contents themselves are encrypted, the table is public so as to be acc
559570

560571
**Value** Last signed Merkle root of previous service instance, represented as a hex-encoded string.
561572

573+
``sealed_shares``
574+
~~~~~~~~~~~~~~~~~
575+
576+
**Value** Per-node encrypted ledger secret wrapping keys, encrypted by the public keys recorded in ``nodes.sealed_recovery_keys``.
577+
578+
While the contents themselves are encrypted, the table is public so as to be accessible by a node starting a recovery service.
579+
580+
.. doxygenstruct:: ccf::SealedSharesInfo
581+
:project: CCF
582+
:members:
583+
562584
``last_recovery_type``
563585
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
564586
**Value** The mechanism by which the ledger secret was recovered.

doc/host_config_schema/cchost_config.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,9 @@
357357
"type": "string",
358358
"description": "Path to the previous service certificate (PEM) file"
359359
},
360-
"previous_sealed_ledger_secret_location": {
361-
"type": ["string"],
362-
"description": "Path to the sealed ledger secret folder, the ledger secrets for the recovered service will be unsealed from here instead of reconstructed from recovery shares."
360+
"previous_local_sealing_identity": {
361+
"type": ["string", "null"],
362+
"description": "The identity of the previous node which sealed the ledger secrets. Required if local sealing is enabled"
363363
},
364364
"self_healing_open": {
365365
"type": "object",
@@ -638,10 +638,6 @@
638638
"rpc_addresses_file": {
639639
"type": "string",
640640
"description": "Path to file in which all RPC addresses (hostnames and ports) will be written to on startup. This option is particularly useful when binding to port 0 and getting auto-assigned a port by the OS. No file is created if this entry is not specified"
641-
},
642-
"sealed_ledger_secret_location": {
643-
"type": "string",
644-
"description": "Path to the folder where the node will seal its ledger secrets."
645641
}
646642
},
647643
"description": "This section includes configuration for additional files output by the node",
@@ -713,6 +709,11 @@
713709
"type": "string",
714710
"default": "512MB",
715711
"description": "Historical queries cache soft limit (as size string)"
712+
},
713+
"enable_local_sealing": {
714+
"type": "boolean",
715+
"default": false,
716+
"description": "Enable sealing of ledger secrets using platform derived key capabilities (e.g. AMD SEV-SNP derived keys). This allows the node to unilaterally recover its ledger secrets on restart without needing to reconstruct them from recovery shares."
716717
}
717718
},
718719
"required": ["network", "command"],

doc/operations/recovery.rst

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -116,34 +116,106 @@ Once operators have established a recovered crash-fault tolerant public network,
116116
Local Sealing Recovery (Experimental)
117117
-------------------------------------
118118
119-
SNP provides the `DERIVED_KEY` guest message which derives a key from the CPU's VCEK (or VLEK), TCB version and the guest's measurement and host_data (policy), thus any change to the CPU, measurement or policy, or a rolled-back TCB version, will prevent the key from being reconstructed.
120-
If configured, the node will unseal the secrets it previously sealed instead of waiting for recovery shares from members after `transition_to_open` is triggered.
119+
SNP provides the ``DERIVED_KEY`` guest message which derives a key from the CPU's VCEK (or VLEK), TCB version and the guest's measurement and host_data (policy), thus any change to the CPU, measurement or policy, or a rolled-back TCB version, will prevent the key from being reconstructed.
120+
If configured, the node will unseal the secrets it previously sealed instead of waiting for recovery shares from members after ``transition_to_open`` is triggered.
121121
122-
If, in config.json, `output_files.sealed_ledger_secret_location` is set, the node will derive a key and seal versioned ledger secrets to that directory.
123-
This capability is noted in `public:ccf.gov.node.info[node].will_locally_seal_ledger_secrets`, to allow it to be audited.
122+
Overview
123+
~~~~~~~~
124124
125-
Then if `command.recover.previous_sealed_ledger_secret_location` is set in the config.json, when the node recovers and receives the `transition_to_open` transaction, the node will try to unseal the latest ledger secret and use that to recover the ledger.
126-
If this is unsuccessful, it will fall back to waiting for recovery shares.
127-
Which of these two paths is taken is noted in the `public:ccf.internal.last_recovery_type`.
125+
When local sealing is enabled, each node generates an RSA key pair (the "recovery key pair") during join. The private key is encrypted (sealed) using an AES-GCM key derived from the SNP ``DERIVED_KEY``, and the public key along with the sealed private key is stored in the ``public:ccf.gov.nodes.sealed_recovery_keys`` table.
128126
129-
.. code-block:: bash
127+
During normal operation, whenever the ledger secret changes, or a node joins the network, the system also shuffles "sealed shares".
128+
The primary generates a fresh ledger secret wrapping key, encrypts the ledger secret with that key, and stores a sealed copy of the wrapping key for each trusted node with a sealed recovery public key.
130129
131-
$ cat /path/to/config/file
132-
...
130+
During recovery, if the node was previously part of the network and has the same CPU, measurement, and policy, it can bypass the need for member recovery shares by re-deriving the sealing key, unsealing its recovery private key, decrypting the sealed wrapping key, and using that to unwrap the ledger secret.
131+
132+
The following diagram illustrates the key hierarchy and encryption relationships:
133+
134+
.. mermaid::
135+
136+
flowchart TB
137+
subgraph SNP["SNP PSP"]
138+
DK["DERIVED_KEY"]
139+
VCEK
140+
Measurement
141+
Policy["UserData (Policy)"]
142+
TCB
143+
144+
VCEK --> DK
145+
Measurement --> DK
146+
Policy --> DK
147+
TCB --> DK
148+
end
149+
150+
subgraph KG["Key Generation"]
151+
subgraph Sealing Key
152+
SK["Sealing Key<br/>(HKDF)"]
153+
Label["Label: <br/>CCF AMD Local Sealing Key"]
154+
DK -->|ikm| SK
155+
Label -->|info| SK
156+
end
157+
158+
RSA["Recovery Key<br/>(RSA Key Pair)"]
159+
PubKey["Public Key"]
160+
PrivKey["Private Key"]
161+
RSA --> PubKey
162+
RSA --> PrivKey
163+
164+
LS["Ledger secret"]
165+
LSWK["Ledger secret wrapping key"]
166+
end
167+
168+
subgraph Sealed["Store: nodes.sealed_recovery_keys"]
169+
SPK["Sealed Private Key<br/>(AES-GCM encrypted)"]
170+
SK -->|key| SPK
171+
PrivKey --> SPK
172+
173+
StoredPubKey["Public Key (plaintext)"]
174+
PubKey --> StoredPubKey
175+
end
176+
177+
178+
subgraph Shares["Store: internal.sealed_shares table"]
179+
WLS["Wrapped Ledger Secret<br/>(AES-GCM encrypted)"]
180+
LSWK -->|key| WLS
181+
LS --> WLS
182+
183+
EWK["Encrypted Wrapping Key<br/>(per-node, RSA-OAEP encrypted)"]
184+
StoredPubKey -->|key| EWK
185+
LSWK --> EWK
186+
end
187+
188+
subgraph Recovery["Recovery Process"]
189+
UPK["Unsealed Private Key"]
190+
SK -->|key| UPK
191+
SPK --> UPK
192+
193+
UWK["Unsealed Wrapping Key"]
194+
UPK -->|key| UWK
195+
EWK --> UWK
196+
197+
ULS["Unsealed Ledger Secret"]
198+
UWK -->|key| ULS
199+
WLS --> ULS
200+
end
201+
202+
Configuration
203+
~~~~~~~~~~~~~
204+
205+
To enable local sealing, set ``enable_local_sealing`` to ``true`` in the node configuration. During recovery, the node's previous identity (node ID) must be specified via ``command.recover.previous_local_sealing_identity`` so the node can look up its sealed share.
206+
In the future this will be a single shared identifier for both self-healing-open and local sealing recovery, but for now it is simply the previous node ID.
207+
208+
.. code-block:: json
209+
210+
{
211+
"enable_local_sealing": true,
133212
"command": {
134213
"type": "Recover",
135-
...
136214
"recover": {
137-
...
138-
"previous_sealed_ledger_secret_location": "/path/to/previous/secret"
215+
"previous_local_sealing_identity": "<previous-node-id>"
139216
}
140217
}
141-
"output_files": {
142-
...
143-
"sealed_ledger_secret_location": "/path/to/new/secret"
144-
}
145-
...
146-
$ /opt/ccf/bin/js_generic --config /path/to/config/file
218+
}
147219
148220
Self-Healing-Open recovery (Experimental)
149221
-----------------------------------------

include/ccf/node/startup_config.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "ccf/crypto/curve.h"
66
#include "ccf/ds/unit_strings.h"
7+
#include "ccf/entity_id.h"
78
#include "ccf/node/cose_signatures_config.h"
89
#include "ccf/pal/attestation_sev_snp_endorsements.h"
910
#include "ccf/service/consensus_config.h"
@@ -125,7 +126,7 @@ namespace ccf
125126
std::string service_subject_name = "CN=CCF Service";
126127
ccf::COSESignaturesConfig cose_signatures;
127128

128-
std::optional<std::string> sealed_ledger_secret_location;
129+
bool enable_local_sealing = false;
129130

130131
nlohmann::json service_data = nullptr;
131132

@@ -154,8 +155,7 @@ namespace ccf
154155
{
155156
std::optional<std::vector<uint8_t>> previous_service_identity =
156157
std::nullopt;
157-
std::optional<std::string> previous_sealed_ledger_secret_location =
158-
std::nullopt;
158+
std::optional<NodeId> previous_local_sealing_identity = std::nullopt;
159159
std::optional<SelfHealingOpenConfig> self_healing_open = std::nullopt;
160160
};
161161
Recover recover = {};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the Apache 2.0 License.
3+
#pragma once
4+
5+
#include "ccf/crypto/pem.h"
6+
#include "ccf/ds/json.h"
7+
#include "ccf/pal/attestation_sev_snp.h"
8+
9+
#include <cstdint>
10+
11+
namespace ccf
12+
{
13+
enum DerivedSealingKeyAlgorithm : uint8_t
14+
{
15+
SNP_v1 = 0
16+
};
17+
18+
DECLARE_JSON_ENUM(
19+
DerivedSealingKeyAlgorithm,
20+
{{DerivedSealingKeyAlgorithm::SNP_v1, "SNP_TCB_v1"}});
21+
22+
struct SealedRecoveryKey
23+
{
24+
DerivedSealingKeyAlgorithm version = DerivedSealingKeyAlgorithm::SNP_v1;
25+
std::vector<uint8_t> ciphertext;
26+
crypto::Pem pubkey;
27+
pal::snp::TcbVersionRaw tcb_version;
28+
29+
bool operator==(const SealedRecoveryKey&) const = default;
30+
};
31+
32+
DECLARE_JSON_TYPE(SealedRecoveryKey);
33+
DECLARE_JSON_REQUIRED_FIELDS(
34+
SealedRecoveryKey, version, ciphertext, pubkey, tcb_version);
35+
}

include/ccf/service/node_info_network.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ namespace ccf
150150
/// RPC interfaces
151151
RpcInterfaces rpc_interfaces;
152152

153-
// Denote whether this node will locally seal the ledger secret
154153
bool will_locally_seal_ledger_secrets = false;
155154
};
156155

samples/constitutions/default/actions.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,6 +1323,9 @@ const actions = new Map([
13231323
ccf.strToBuf(args.node_id),
13241324
ccf.jsonCompatibleToBuf(nodeInfo),
13251325
);
1326+
if (ccf.node.shuffleSealedShares !== undefined) {
1327+
ccf.node.shuffleSealedShares();
1328+
}
13261329

13271330
// Also generate and record service-endorsed node certificate from node CSR
13281331
if (nodeInfo.certificate_signing_request !== undefined) {

src/common/configuration.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,7 @@ namespace ccf
135135
DECLARE_JSON_REQUIRED_FIELDS(
136136
StartupConfig::Recover, previous_service_identity);
137137
DECLARE_JSON_OPTIONAL_FIELDS(
138-
StartupConfig::Recover,
139-
previous_sealed_ledger_secret_location,
140-
self_healing_open);
138+
StartupConfig::Recover, previous_local_sealing_identity, self_healing_open);
141139

142140
DECLARE_JSON_TYPE_WITH_BASE(StartupConfig, CCFConfig);
143141
DECLARE_JSON_REQUIRED_FIELDS(
@@ -152,5 +150,5 @@ namespace ccf
152150
start,
153151
join,
154152
recover,
155-
sealed_ledger_secret_location);
153+
enable_local_sealing);
156154
}

0 commit comments

Comments
 (0)