Skip to content

Commit

Permalink
Adding notion of a recovery owner for network recovery (#6705)
Browse files Browse the repository at this point in the history
Co-authored-by: Amaury Chamayou <[email protected]>
Co-authored-by: Amaury Chamayou <[email protected]>
  • Loading branch information
3 people authored Jan 31, 2025
1 parent c384f5a commit bd11afb
Show file tree
Hide file tree
Showing 23 changed files with 1,147 additions and 215 deletions.
5 changes: 5 additions & 0 deletions doc/host_config_schema/cchost_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@
"data_json_file": {
"type": ["string", "null"],
"description": "Path to member data file (JSON)"
},
"recovery_role": {
"type": "string",
"enum": ["NonParticipant", "Participant", "Owner"],
"description": "Whether the member acts as a recovery participant and gets assigned a share that can contribute towards a recovery threshold or as an owner and gets assigned a full recovery key"
}
},
"required": ["certificate_file"],
Expand Down
13 changes: 12 additions & 1 deletion doc/schemas/gov_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,9 @@
"member_data": {
"$ref": "#/components/schemas/json"
},
"recovery_role": {
"$ref": "#/components/schemas/MemberRecoveryRole"
},
"status": {
"$ref": "#/components/schemas/MemberStatus"
}
Expand Down Expand Up @@ -412,6 +415,14 @@
},
"type": "object"
},
"MemberRecoveryRole": {
"enum": [
"NonParticipant",
"Participant",
"Owner"
],
"type": "string"
},
"MemberStatus": {
"enum": [
"Accepted",
Expand Down Expand Up @@ -1337,7 +1348,7 @@
"info": {
"description": "This API is used to submit and query proposals which affect CCF's public governance tables.",
"title": "CCF Governance API",
"version": "4.5.1"
"version": "4.6.0"
},
"openapi": "3.0.0",
"paths": {
Expand Down
36 changes: 30 additions & 6 deletions include/ccf/service/tables/members.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ namespace ccf
DECLARE_JSON_ENUM(
MemberStatus,
{{MemberStatus::ACCEPTED, "Accepted"}, {MemberStatus::ACTIVE, "Active"}});

enum class MemberRecoveryRole
{
NonParticipant = 0,
Participant,

/** If set then the member is to receive a key allowing it
to single-handedly recover the network without requiring
any other recovery member to submit their shares. */
Owner
};
DECLARE_JSON_ENUM(
MemberRecoveryRole,
{{MemberRecoveryRole::NonParticipant, "NonParticipant"},
{MemberRecoveryRole::Participant, "Participant"},
{MemberRecoveryRole::Owner, "Owner"}});
}

namespace ccf
Expand All @@ -33,26 +49,31 @@ namespace ccf
std::optional<ccf::crypto::Pem> encryption_pub_key = std::nullopt;
nlohmann::json member_data = nullptr;

std::optional<MemberRecoveryRole> recovery_role = std::nullopt;

NewMember() {}

NewMember(
const ccf::crypto::Pem& cert_,
const std::optional<ccf::crypto::Pem>& encryption_pub_key_ = std::nullopt,
const nlohmann::json& member_data_ = nullptr) :
const nlohmann::json& member_data_ = nullptr,
const std::optional<MemberRecoveryRole>& recovery_role_ = std::nullopt) :
cert(cert_),
encryption_pub_key(encryption_pub_key_),
member_data(member_data_)
member_data(member_data_),
recovery_role(recovery_role_)
{}

bool operator==(const NewMember& rhs) const
{
return cert == rhs.cert && encryption_pub_key == rhs.encryption_pub_key &&
member_data == rhs.member_data;
member_data == rhs.member_data && recovery_role == rhs.recovery_role;
}
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(NewMember)
DECLARE_JSON_REQUIRED_FIELDS(NewMember, cert)
DECLARE_JSON_OPTIONAL_FIELDS(NewMember, encryption_pub_key, member_data)
DECLARE_JSON_OPTIONAL_FIELDS(
NewMember, encryption_pub_key, member_data, recovery_role)

struct MemberDetails
{
Expand All @@ -62,14 +83,17 @@ namespace ccf
members for example. */
nlohmann::json member_data = nullptr;

std::optional<MemberRecoveryRole> recovery_role = std::nullopt;

bool operator==(const MemberDetails& rhs) const
{
return status == rhs.status && member_data == rhs.member_data;
return status == rhs.status && member_data == rhs.member_data &&
recovery_role == rhs.recovery_role;
}
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(MemberDetails)
DECLARE_JSON_REQUIRED_FIELDS(MemberDetails, status)
DECLARE_JSON_OPTIONAL_FIELDS(MemberDetails, member_data)
DECLARE_JSON_OPTIONAL_FIELDS(MemberDetails, member_data, recovery_role)

using MemberInfo = ServiceMap<MemberId, MemberDetails>;

Expand Down
23 changes: 19 additions & 4 deletions samples/constitutions/default/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,11 +372,25 @@ const actions = new Map([
function (args) {
checkX509CertBundle(args.cert, "cert");
checkType(args.member_data, "object?", "member_data");
// Also check that public encryption key is well formed, if it exists
const recovery_role = args.recovery_role;
if (recovery_role !== undefined) {
checkEnum(
recovery_role,
["NonParticipant", "Participant", "Owner"],
"recovery_role",
);
}

// Check if member exists
// if not, check there is no enc pub key
// if it does, check it doesn't have an enc pub key in ledger
if (
args.encryption_pub_key == null &&
args.recovery_role !== null &&
args.recovery_role !== undefined
) {
throw new Error(
"Cannot specify a recovery_role value when encryption_pub_key is not specified",
);
}
// Also check that public encryption key is well formed, if it exists
},

function (args) {
Expand All @@ -401,6 +415,7 @@ const actions = new Map([

let member_info = {};
member_info.member_data = args.member_data;
member_info.recovery_role = args.recovery_role;
member_info.status = "Accepted";
ccf.kv["public:ccf.gov.members.info"].set(
rawMemberId,
Expand Down
9 changes: 6 additions & 3 deletions src/crypto/sharing.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,20 @@ namespace ccf::crypto
return k;
}

std::vector<uint8_t> serialise() const
void serialise(std::vector<uint8_t>& serialised) const
{
auto size = serialised_size;
std::vector<uint8_t> serialised(size);
if (serialised.size() != size)
{
throw std::invalid_argument("Invalid serialised share size");
}

auto data = serialised.data();
serialized::write(data, size, x);
for (size_t i = 0; i < LIMBS; ++i)
{
serialized::write(data, size, y[i]);
}
return serialised;
}

Share(const std::span<uint8_t const>& serialised)
Expand Down
4 changes: 3 additions & 1 deletion src/crypto/test/secret_sharing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@ TEST_CASE("Serialisation")
share.y[7] = 6;
share.y[8] = 7;
share.y[9] = 56;
Share new_share(share.serialise());
std::vector<uint8_t> serialised(share.serialised_size);
share.serialise(serialised);
Share new_share(serialised);

INFO(share.to_str());
INFO(new_share.to_str());
Expand Down
6 changes: 5 additions & 1 deletion src/host/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,18 @@ namespace host
std::string certificate_file;
std::optional<std::string> encryption_public_key_file = std::nullopt;
std::optional<std::string> data_json_file = std::nullopt;
std::optional<ccf::MemberRecoveryRole> recovery_role = std::nullopt;

bool operator==(const ParsedMemberInfo& other) const = default;
};

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(ParsedMemberInfo);
DECLARE_JSON_REQUIRED_FIELDS(ParsedMemberInfo, certificate_file);
DECLARE_JSON_OPTIONAL_FIELDS(
ParsedMemberInfo, encryption_public_key_file, data_json_file);
ParsedMemberInfo,
encryption_public_key_file,
data_json_file,
recovery_role);

struct CCHostConfig : public ccf::CCFConfig
{
Expand Down
80 changes: 62 additions & 18 deletions src/host/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,38 +221,76 @@ int main(int argc, char** argv)
"On start, ledger directory should not exist ({})",
config.ledger.directory));
}

// Count members with public encryption key as only these members will be
// handed a recovery share.
// Note that it is acceptable to start a network without any member having
// a recovery share. The service will check that at least one recovery
// member is added before the service can be opened.
size_t members_with_pubk_count = 0;
// Note that it is acceptable to start a network without any member
// having a recovery share. The service will check that at least one
// recovery member (participant or owner) is added before the
// service can be opened.
size_t recovery_participants_count = 0;
size_t recovery_owners_count = 0;
for (auto const& m : config.command.start.members)
{
if (m.encryption_public_key_file.has_value())
{
members_with_pubk_count++;
auto role =
m.recovery_role.value_or(ccf::MemberRecoveryRole::Participant);
if (role == ccf::MemberRecoveryRole::Participant)
{
recovery_participants_count++;
}
else if (role == ccf::MemberRecoveryRole::Owner)
{
recovery_owners_count++;
}
}
}

recovery_threshold =
config.command.start.service_configuration.recovery_threshold;
if (recovery_threshold == 0)
{
LOG_INFO_FMT(
"Recovery threshold unset. Defaulting to number of initial "
"consortium members with a public encryption key ({}).",
members_with_pubk_count);
recovery_threshold = members_with_pubk_count;
if (recovery_participants_count == 0 && recovery_owners_count != 0)
{
LOG_INFO_FMT(
"Recovery threshold unset. Defaulting to 1 as only consortium "
"members that are recovery owners ({}) are specified.",
recovery_owners_count);
recovery_threshold = 1;
}
else
{
LOG_INFO_FMT(
"Recovery threshold unset. Defaulting to number of initial "
"consortium members with a public encryption key ({}).",
recovery_participants_count);
recovery_threshold = recovery_participants_count;
}
}
else if (recovery_threshold > members_with_pubk_count)
else
{
throw std::logic_error(fmt::format(
"Recovery threshold ({}) cannot be greater than total number ({})"
"of initial consortium members with a public encryption "
"key (specified via --member-info options)",
recovery_threshold,
members_with_pubk_count));
if (recovery_participants_count == 0 && recovery_owners_count != 0)
{
if (recovery_threshold > 1)
{
throw std::logic_error(fmt::format(
"Recovery threshold ({}) cannot be greater than 1 when all "
"initial consortium members ({}) are of type recovery owner "
"(specified via --member-info options)",
recovery_threshold,
recovery_participants_count));
}
}
else if (recovery_threshold > recovery_participants_count)
{
throw std::logic_error(fmt::format(
"Recovery threshold ({}) cannot be greater than total number ({})"
"of initial consortium members with a public encryption "
"key (specified via --member-info options)",
recovery_threshold,
recovery_participants_count));
}
}
}
}
Expand Down Expand Up @@ -612,12 +650,17 @@ int main(int argc, char** argv)
for (auto const& m : config.command.start.members)
{
std::optional<ccf::crypto::Pem> public_encryption_key = std::nullopt;
std::optional<ccf::MemberRecoveryRole> recovery_role = std::nullopt;
if (
m.encryption_public_key_file.has_value() &&
!m.encryption_public_key_file.value().empty())
{
public_encryption_key = ccf::crypto::Pem(
files::slurp(m.encryption_public_key_file.value()));
if (m.recovery_role.has_value())
{
recovery_role = m.recovery_role.value();
}
}

nlohmann::json md = nullptr;
Expand All @@ -629,7 +672,8 @@ int main(int argc, char** argv)
startup_config.start.members.emplace_back(
ccf::crypto::Pem(files::slurp(m.certificate_file)),
public_encryption_key,
md);
md,
recovery_role);
}
startup_config.start.constitution = "";
for (const auto& constitution_path :
Expand Down
7 changes: 4 additions & 3 deletions src/node/gov/handlers/acks.h
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,12 @@ namespace ccf::gov::endpoints
return;
}

// If this is a newly-active recovery member in an open service,
// allocate them a recovery share immediately
// If this is a newly-active recovery participant/owner in an open
// service, allocate them a recovery share immediately
if (
newly_active &&
InternalTablesAccess::is_recovery_member(ctx.tx, member_id))
InternalTablesAccess::is_recovery_participant_or_owner(
ctx.tx, member_id))
{
auto service_status =
InternalTablesAccess::get_service_status(ctx.tx);
Expand Down
Loading

0 comments on commit bd11afb

Please sign in to comment.