Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

[7.0.19]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.19

### Added

- The `transition_service_to_open_with_signing_keys` proposal action opens a service with `previous_service_signing_keys` and `next_service_signing_keys`, maps of identity types to PEM public keys, instead of certificates. The public header `ccf/service_signing_keys.h` declares `ccf::ServiceSigningKeys` and `ccf::SigningKeyType`. See [accepting recovery](https://microsoft.github.io/CCF/main/governance/accept_recovery.html) (#8477).

### Deprecated

- The `previous_service_identity` argument of the `transition_service_to_open` proposal and the `command.recover.previous_service_identity_file` configuration option are deprecated in favour of the `transition_service_to_open_with_signing_keys` proposal and `command.recover.previous_service_signing_key_files` respectively. In C++, `ccf::CCFConfig::Command::Recover::previous_service_identity_file` is now `std::optional<std::string>` (#8477).

### Fixed

- Paused RPC reads now resume when another interface releases the inbound budget, even if older libuv versions coalesce the notification. Previously, reads could remain paused until an unrelated event triggered a recheck (#8498).
Expand Down
28 changes: 26 additions & 2 deletions doc/governance/accept_recovery.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,33 @@ A member proposes to recover the network and other members can vote on the propo

Once the proposal to recover the network has passed under the rules of the :term:`Constitution`, the recovered service is ready for members to submit their recovery shares.

Note that the ``transition_service_to_open`` proposal takes two parameters: the previous and the next :term:`Service Identity` (X.509 certificates in PEM format). The previous identity must match the identity supplied to the recovery node at startup, while the next identity must match the recovered service's newly generated identity. Snapshot validation is performed earlier at node startup using the configured previous service identity. Since both identities are recorded on the ledger with the proposal, it is always clear at which point the identity changed.
Members can open the recovered service with either of two proposals. ``transition_service_to_open`` takes ``previous_service_identity`` and ``next_service_identity``, PEM certificates. ``transition_service_to_open_with_signing_keys`` takes ``previous_service_signing_keys`` and ``next_service_signing_keys``, JSON objects mapping identity types to PEM public keys, and does not accept certificates. The previous and next values must match the previous service identity recorded in the ledger and the recovered service, respectively. Each key map must contain a ``CLASSICAL`` key, and only ``CLASSICAL`` keys are compared.

.. note:: The ``previous_service_identity`` argument to the ``transition_service_to_open`` proposal is required for recovery, but must not be provided when opening a new service as there is no previous identity.
The ``previous_service_identity`` argument is deprecated, so recovery proposals should use ``transition_service_to_open_with_signing_keys``. These identities are recorded on the ledger with the proposal.

Each service writes its signing keys to the files configured by ``command.service_signing_key_files``, by default ``service_signing_key_classical.pem``. A ``transition_service_to_open_with_signing_keys`` proposal uses the previous service's file and the recovered service's file:

.. code-block:: json

{
"actions": [
{
"name": "transition_service_to_open_with_signing_keys",
"args": {
"previous_service_signing_keys": {
"CLASSICAL": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
},
"next_service_signing_keys": {
"CLASSICAL": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
}
}
]
}

Snapshot validation happens earlier at node startup. Operators must supply ``command.recover.previous_service_signing_key_files`` or the deprecated ``command.recover.previous_service_identity_file``. When key files are supplied, they are used for verification even if a certificate is also supplied, with no fallback to the certificate. The recovered service certificate inherits the subject of the previous certificate when ``command.recover.previous_service_identity_file`` is supplied. Otherwise, ``command.recover.service_cert_subject_name`` is required and sets the subject. See :doc:`/operations/configuration`.

.. note:: Recovery proposals require previous signing keys or a previous service certificate. Neither previous-identity argument is needed when opening a new service.

Submitting Recovery Shares
--------------------------
Expand Down
48 changes: 45 additions & 3 deletions doc/host_config_schema/host_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@
"type": "string",
"default": "service_cert.pem",
"description": "For ``Start`` and ``Recover`` nodes, path to which service certificate will be written to on startup. For ``Join`` nodes, path to the certificate of the existing service to join"
},
"service_signing_key_files": {
"type": "object",
"default": { "CLASSICAL": "service_signing_key_classical.pem" },
"properties": {
"CLASSICAL": {
"type": "string",
"minLength": 1,
"description": "Output path for the classical service signing public key (PEM)"
}
},
"required": ["CLASSICAL"],
"additionalProperties": false,
"description": "For ``Start`` and ``Recover`` nodes, paths to which service signing public keys will be written on startup"
}
},
"allOf": [
Expand Down Expand Up @@ -364,15 +378,43 @@
"description": "Initial validity period (days) for service certificate",
"minimum": 1
},
"service_cert_subject_name": {
"type": "string",
"minLength": 1,
"description": "Subject name for the recovered service certificate when ``previous_service_identity_file`` is not set; otherwise the previous certificate's subject is inherited"
},
"previous_service_identity_file": {
"type": "string",
"description": "Path to the previous service certificate (PEM) file"
"minLength": 1,
"description": "Deprecated. Path to the previous service certificate (PEM) file. Use ``previous_service_signing_key_files`` instead"
},
"previous_service_signing_key_files": {
"type": "object",
"properties": {
"CLASSICAL": {
"type": "string",
"minLength": 1,
"description": "Path to the previous classical service signing public key (PEM)"
}
},
"required": ["CLASSICAL"],
"additionalProperties": false,
"description": "Paths to the previous service signing public keys"
}
},
"required": ["previous_service_identity_file"],
"anyOf": [
{ "required": ["previous_service_identity_file"] },
{
"required": [
"previous_service_signing_key_files",
"service_cert_subject_name"
]
}
],
"additionalProperties": false
}
}
},
"required": ["recover"]
}
}
],
Expand Down
21 changes: 18 additions & 3 deletions include/ccf/node/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
#include "ccf/service/tables/host_data.h"
#include "ccf/service/tables/members.h"
#include "ccf/service/tables/self_healing_open.h"
#include "ccf/service_signing_keys.h"

#include <map>
#include <optional>
#include <string>
#include <vector>
Expand Down Expand Up @@ -225,6 +227,8 @@ namespace ccf
{
StartType type = StartType::Start;
std::string service_certificate_file = "service_cert.pem";
std::map<std::string, std::string> service_signing_key_files = {
{SigningKeyType::CLASSICAL, "service_signing_key_classical.pem"}};

struct Start
{
Expand Down Expand Up @@ -258,7 +262,11 @@ namespace ccf
struct Recover
{
size_t initial_service_certificate_validity_days = 1;
std::string previous_service_identity_file;
std::optional<std::string> service_cert_subject_name = std::nullopt;
std::optional<std::string> previous_service_identity_file =
std::nullopt;
std::optional<std::map<std::string, std::string>>
previous_service_signing_key_files = std::nullopt;
bool operator==(const Recover&) const = default;
};
Recover recover = {};
Expand Down Expand Up @@ -412,12 +420,19 @@ namespace ccf
DECLARE_JSON_OPTIONAL_FIELDS(
CCFConfig::Command::Recover,
initial_service_certificate_validity_days,
previous_service_identity_file);
service_cert_subject_name,
previous_service_identity_file,
previous_service_signing_key_files);

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(CCFConfig::Command);
DECLARE_JSON_REQUIRED_FIELDS(CCFConfig::Command, type);
DECLARE_JSON_OPTIONAL_FIELDS(
CCFConfig::Command, service_certificate_file, start, join, recover);
CCFConfig::Command,
service_certificate_file,
service_signing_key_files,
start,
join,
recover);

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(CCFConfig);
DECLARE_JSON_REQUIRED_FIELDS(CCFConfig, network, command);
Expand Down
18 changes: 18 additions & 0 deletions include/ccf/service_signing_keys.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once

#include "ccf/crypto/pem.h"

#include <map>
#include <string>

namespace ccf
{
struct SigningKeyType
{
static constexpr auto CLASSICAL = "CLASSICAL";
};

using ServiceSigningKeys = std::map<std::string, ccf::crypto::Pem>;
}
9 changes: 8 additions & 1 deletion samples/config/recover_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@
"command": {
"type": "Recover",
"service_certificate_file": "service_cert.pem",
"service_signing_key_files": {
"CLASSICAL": "service_signing_key_classical.pem"
},
"recover": {
"initial_service_certificate_validity_days": 1,
"previous_service_identity_file": "previous_service_cert.pem"
"service_cert_subject_name": "CN=A Sample CCF Service",
"previous_service_identity_file": "previous_service_cert.pem",
"previous_service_signing_key_files": {
"CLASSICAL": "previous_service_signing_key_classical.pem"
}
}
},
"ledger": {
Expand Down
3 changes: 3 additions & 0 deletions samples/config/start_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"command": {
"type": "Start",
"service_certificate_file": "service_cert.pem",
"service_signing_key_files": {
"CLASSICAL": "service_signing_key_classical.pem"
},
"start": {
"constitution_files": [
"validate.js",
Expand Down
68 changes: 68 additions & 0 deletions samples/constitutions/default/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,17 @@ function checkX509CertBundle(value, field) {
}
}

function checkServiceSigningKeys(value, field) {
if (value === null || Array.isArray(value)) {
throw new Error(`${field} must be an object`);
}
checkType(value, "object", field);
checkType(value.CLASSICAL, "string", `${field}.CLASSICAL (PEM public key)`);
for (const [identityType, key] of Object.entries(value)) {
checkType(key, "string", `${field}.${identityType} (PEM public key)`);
}
}

function invalidateOtherOpenProposals(proposalIdToRetain) {
const proposalsMap = ccf.kv["public:ccf.gov.proposals_info"];
proposalsMap.forEach((v, k) => {
Expand Down Expand Up @@ -935,6 +946,63 @@ const actions = new Map([
},
),
],
[
"transition_service_to_open_with_signing_keys",
new Action(
function (args) {
if (
args.previous_service_identity !== undefined ||
args.next_service_identity !== undefined
) {
throw new Error(
"Service certificates are not accepted, use transition_service_to_open instead",
);
}
checkServiceSigningKeys(
args.next_service_signing_keys,
"next_service_signing_keys",
);
if (args.previous_service_signing_keys !== undefined) {
checkServiceSigningKeys(
args.previous_service_signing_keys,
"previous_service_signing_keys",
);
}
},

function (args) {
const service_info = "public:ccf.gov.service.info";
const rawService = ccf.kv[service_info].get(getSingletonKvKey());
if (rawService === undefined) {
throw new Error("Service information could not be found");
}

const service = ccf.bufToJsonCompatible(rawService);

if (
service.status === "Recovering" &&
(args.previous_service_signing_keys === undefined ||
args.next_service_signing_keys === undefined)
) {
throw new Error(
`Opening a recovering network requires both, the previous and the next service signing keys`,
);
}

const previous_keys =
args.previous_service_signing_keys !== undefined
? ccf.jsonCompatibleToBuf(args.previous_service_signing_keys)
: undefined;
const next_keys = ccf.jsonCompatibleToBuf(
args.next_service_signing_keys,
);
ccf.node.transitionServiceToOpenWithSigningKeys(
previous_keys,
next_keys,
);
},
),
],
[
"set_js_app",
new Action(
Expand Down
68 changes: 68 additions & 0 deletions samples/minimal_ccf/app/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,17 @@ function checkX509CertBundle(value, field) {
}
}

function checkServiceSigningKeys(value, field) {
if (value === null || Array.isArray(value)) {
throw new Error(`${field} must be an object`);
}
checkType(value, "object", field);
checkType(value.CLASSICAL, "string", `${field}.CLASSICAL (PEM public key)`);
for (const [identityType, key] of Object.entries(value)) {
checkType(key, "string", `${field}.${identityType} (PEM public key)`);
}
}

function invalidateOtherOpenProposals(proposalIdToRetain) {
const proposalsMap = ccf.kv["public:ccf.gov.proposals_info"];
proposalsMap.forEach((v, k) => {
Expand Down Expand Up @@ -914,6 +925,63 @@ const actions = new Map([
},
),
],
[
"transition_service_to_open_with_signing_keys",
new Action(
function (args) {
if (
args.previous_service_identity !== undefined ||
args.next_service_identity !== undefined
) {
throw new Error(
"Service certificates are not accepted, use transition_service_to_open instead",
);
}
checkServiceSigningKeys(
args.next_service_signing_keys,
"next_service_signing_keys",
);
if (args.previous_service_signing_keys !== undefined) {
checkServiceSigningKeys(
args.previous_service_signing_keys,
"previous_service_signing_keys",
);
}
},

function (args) {
const service_info = "public:ccf.gov.service.info";
const rawService = ccf.kv[service_info].get(getSingletonKvKey());
if (rawService === undefined) {
throw new Error("Service information could not be found");
}

const service = ccf.bufToJsonCompatible(rawService);

if (
service.status === "Recovering" &&
(args.previous_service_signing_keys === undefined ||
args.next_service_signing_keys === undefined)
) {
throw new Error(
`Opening a recovering network requires both, the previous and the next service signing keys`,
);
}

const previous_keys =
args.previous_service_signing_keys !== undefined
? ccf.jsonCompatibleToBuf(args.previous_service_signing_keys)
: undefined;
const next_keys = ccf.jsonCompatibleToBuf(
args.next_service_signing_keys,
);
ccf.node.transitionServiceToOpenWithSigningKeys(
previous_keys,
next_keys,
);
},
),
],
[
"set_js_app",
new Action(
Expand Down
Loading
Loading