Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding notion of a recovery owner for network recovery #6705

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
07b152f
Update
gaurav137 Dec 11, 2024
7cd6011
Update
gaurav137 Dec 11, 2024
580670c
Update
gaurav137 Dec 11, 2024
4df08bd
Update
gaurav137 Dec 11, 2024
491aed2
Update
gaurav137 Dec 12, 2024
41e16fd
Update
gaurav137 Dec 12, 2024
90cbdfb
Update
gaurav137 Dec 12, 2024
1c1bda9
Update
gaurav137 Dec 12, 2024
2f7ff7f
Update
gaurav137 Dec 13, 2024
7d0fe02
working
gaurav137 Dec 13, 2024
607b8a4
Update
gaurav137 Dec 13, 2024
986138a
Update
gaurav137 Dec 13, 2024
0ae745c
Update
gaurav137 Dec 13, 2024
33b736b
Update
gaurav137 Dec 13, 2024
96838b7
Update
gaurav137 Dec 13, 2024
7104982
Update
gaurav137 Dec 13, 2024
7639bf9
Update
gaurav137 Dec 13, 2024
8bd2d95
Update
gaurav137 Dec 13, 2024
e5021f1
Update
gaurav137 Dec 13, 2024
0dcc258
Test failure fixes
gaurav137 Dec 13, 2024
e0bc7a7
schema test fix
gaurav137 Dec 13, 2024
b28b452
Update samples/constitutions/default/actions.js
gaurav137 Dec 13, 2024
8dc3f06
picking main
gaurav137 Dec 13, 2024
01df49a
Update
gaurav137 Dec 13, 2024
7546e41
Update
gaurav137 Dec 15, 2024
754adc3
formatting fixes
gaurav137 Dec 16, 2024
123387b
Update
gaurav137 Dec 16, 2024
1228c1d
Update
gaurav137 Dec 18, 2024
12d965d
Merge branch 'main' into dev/gsinha/recovery-owner
achamayou Jan 13, 2025
32aa899
Compilation fixes post picking latest changes from main
gaurav137 Jan 14, 2025
8c931b4
Merge branch 'main' into dev/gsinha/recovery-owner
gaurav137 Jan 14, 2025
ff6ba59
Refactoring LedgerSecretWrappingKey
gaurav137 Jan 17, 2025
476636f
Merge branch 'dev/gsinha/recovery-owner' of https://github.com/gaurav…
gaurav137 Jan 17, 2025
a2f79a9
Merge branch 'main' into dev/gsinha/recovery-owner
gaurav137 Jan 17, 2025
cab53eb
clang-format fixes
gaurav137 Jan 17, 2025
d5ffb66
Renaming methods to remove ambiguity
gaurav137 Jan 20, 2025
5f56142
Merge branch 'main' into dev/gsinha/recovery-owner
gaurav137 Jan 22, 2025
02e7df9
Taking comments
gaurav137 Jan 22, 2025
8416064
Merge branch 'dev/gsinha/recovery-owner' of https://github.com/gaurav…
gaurav137 Jan 22, 2025
f78144c
Updates
gaurav137 Jan 23, 2025
ae6f19a
Update
gaurav137 Jan 23, 2025
4691ac8
Merge branch 'main' into dev/gsinha/recovery-owner
gaurav137 Jan 23, 2025
9b18e9f
Updates
gaurav137 Jan 23, 2025
2b90ece
Update
gaurav137 Jan 23, 2025
b5c3c6a
Test code updates
gaurav137 Jan 23, 2025
d9f1308
Update
gaurav137 Jan 23, 2025
9f48285
Taking comments
gaurav137 Jan 24, 2025
969aaae
Merge branch 'main' into dev/gsinha/recovery-owner
gaurav137 Jan 24, 2025
fd8ee6a
Merge branch 'main' into dev/gsinha/recovery-owner
achamayou Jan 24, 2025
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
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 @@ -1331,7 +1342,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.0"
"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
12 changes: 12 additions & 0 deletions samples/constitutions/default/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,17 @@ const actions = new Map([
function (args) {
checkX509CertBundle(args.cert, "cert");
checkType(args.member_data, "object?", "member_data");
checkType(args.recovery_role, "string?", "recovery_role");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also a checkEnum() that will be slightly more precise (and prevent a much-later error, if C++ code tries to deserialise an unknown/badly-cased string). It doesn't have the ? optional syntax, so I think it would be something like:

Suggested change
checkType(args.recovery_role, "string?", "recovery_role");
const recovery_role = args.recovery_role;
if (recovery_role !== undefined) {
checkEnum(
recovery_role,
["NonParticipant", "Participant", "Owner"],
"recovery_role"
);
}

(I'm not completely sure of the semantics here - should recovery_role be null or "NonParticipant"?)


if (
args.encryption_pub_key == null &&
gaurav137 marked this conversation as resolved.
Show resolved Hide resolved
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

// Check if member exists
Expand Down Expand Up @@ -401,6 +412,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
13 changes: 12 additions & 1 deletion src/crypto/sharing.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,24 @@ namespace ccf::crypto
{
auto size = serialised_size;
std::vector<uint8_t> serialised(size);
serialise(serialised);
return serialised;
}

void serialise(std::vector<uint8_t>& serialised) const
{
auto size = 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
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 @@ -216,38 +216,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
gaurav137 marked this conversation as resolved.
Show resolved Hide resolved
// 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 @@ -602,12 +640,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 @@ -619,7 +662,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
11 changes: 10 additions & 1 deletion src/node/gov/handlers/recovery.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,14 @@ namespace ccf::gov::endpoints
params["share"].template get<std::string>());

size_t submitted_shares_count = 0;
bool full_key_submitted = false;
try
{
submitted_shares_count = share_manager.submit_recovery_share(
ctx.tx, member_id, raw_recovery_share);

full_key_submitted = ShareManager::is_full_key(raw_recovery_share);

OPENSSL_cleanse(
raw_recovery_share.data(), raw_recovery_share.size());
}
Expand Down Expand Up @@ -164,8 +167,13 @@ namespace ccf::gov::endpoints
submitted_shares_count,
threshold);

if (submitted_shares_count >= threshold)
if (submitted_shares_count >= threshold || full_key_submitted)
{
if (full_key_submitted)
{
message += "\nFull recovery key successfully submitted";
}
Comment on lines +172 to +175
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't add as suggestion because it affects the untouched lines above, but suggest that this is a replacement for the "x/n" message above when full_key_submitted.

Something like:

std::string message;
if (full_key_submitted)
{
  message = "Full Owner recovery key successfuly submitted";
}
else
{
  // Same format of message, whether this is sufficient to trigger
  // recovery or not
  message = fmt::format(
    "{}/{} recovery shares successfully submitted",
    submitted_shares_count,
    threshold);
}

if (submitted_shares_count >= threshold || full_key_submitted)
{
  message += "\nEnd of recovery procedure initiated";
  ...


message += "\nEnd of recovery procedure initiated";
GOV_INFO_FMT("{} - initiating recovery", message);

Expand Down Expand Up @@ -196,6 +204,7 @@ namespace ccf::gov::endpoints
response_body["message"] = message;
response_body["submittedCount"] = submitted_shares_count;
response_body["recoveryThreshold"] = threshold;
response_body["fullKeySubmitted"] = full_key_submitted;

ctx.rpc_ctx->set_response_json(response_body, HTTP_STATUS_OK);
return;
Expand Down
13 changes: 13 additions & 0 deletions src/node/gov/handlers/service_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ namespace ccf::gov::endpoints
member["publicEncryptionKey"] = enc_key.value().str();
}

ccf::MemberRecoveryRole recovery_role =
ccf::MemberRecoveryRole::NonParticipant;
if (member_details.recovery_role.has_value())
{
recovery_role = member_details.recovery_role.value();
}
else if (enc_key.has_value())
{
recovery_role = ccf::MemberRecoveryRole::Participant;
}

member["recoveryRole"] = recovery_role;

return member;
}

Expand Down
Loading
Loading