зеркало из https://github.com/microsoft/CCF.git
One recovery with keyshare (#944)
This commit is contained in:
Родитель
e67b4f418e
Коммит
9e45c650d7
|
@ -355,7 +355,7 @@ if(BUILD_TESTS)
|
|||
)
|
||||
target_link_libraries(
|
||||
nodefrontend_test PRIVATE ${CMAKE_THREAD_LIBS_INIT} evercrypt.host lua.host
|
||||
secp256k1.host http_parser.host
|
||||
secp256k1.host http_parser.host sss.host
|
||||
)
|
||||
|
||||
if(NOT ENV{RUNTIME_CONFIG_DIR})
|
||||
|
|
|
@ -183,6 +183,7 @@ namespace ccf
|
|||
return active_nodes;
|
||||
}
|
||||
|
||||
// Service status should use a state machine, very much like NodeState.
|
||||
void create_service(
|
||||
const std::vector<uint8_t>& network_cert, kv::Version version = 1)
|
||||
{
|
||||
|
@ -207,7 +208,9 @@ namespace ccf
|
|||
return false;
|
||||
}
|
||||
|
||||
if (active_service->status != ServiceStatus::OPENING)
|
||||
if (
|
||||
active_service->status != ServiceStatus::OPENING &&
|
||||
active_service->status != ServiceStatus::WAITING_FOR_RECOVERY_SHARES)
|
||||
{
|
||||
LOG_FAIL_FMT("Could not open current service: status is not OPENING");
|
||||
return false;
|
||||
|
@ -219,6 +222,43 @@ namespace ccf
|
|||
return true;
|
||||
}
|
||||
|
||||
std::optional<ServiceStatus> get_service_status()
|
||||
{
|
||||
auto service_view = tx.get_view(tables.service);
|
||||
auto active_service = service_view->get(0);
|
||||
if (!active_service.has_value())
|
||||
{
|
||||
LOG_FAIL_FMT("Failed to get active service");
|
||||
return {};
|
||||
}
|
||||
|
||||
return active_service->status;
|
||||
}
|
||||
|
||||
bool service_wait_for_shares()
|
||||
{
|
||||
auto service_view = tx.get_view(tables.service);
|
||||
auto active_service = service_view->get(0);
|
||||
if (!active_service.has_value())
|
||||
{
|
||||
LOG_FAIL_FMT("Failed to get active service");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (active_service->status != ServiceStatus::OPENING)
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Could not wait for shares on current service: status is not "
|
||||
"OPENING");
|
||||
return false;
|
||||
}
|
||||
|
||||
active_service->status = ServiceStatus::WAITING_FOR_RECOVERY_SHARES;
|
||||
service_view->put(0, active_service.value());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void trust_node(NodeId node_id)
|
||||
{
|
||||
auto nodes_view = tx.get_view(tables.nodes);
|
||||
|
|
|
@ -22,22 +22,6 @@ namespace ccf
|
|||
const std::vector<uint8_t>& data) = 0;
|
||||
};
|
||||
|
||||
struct LedgerSecretWrappingKey
|
||||
{
|
||||
static constexpr auto KZ_KEY_SIZE = crypto::GCM_SIZE_KEY;
|
||||
std::vector<uint8_t> data; // Referred to as "kz" in TR
|
||||
|
||||
LedgerSecretWrappingKey() : data(tls::create_entropy()->random(KZ_KEY_SIZE))
|
||||
{}
|
||||
|
||||
template <typename T>
|
||||
LedgerSecretWrappingKey(const T& split_secret) :
|
||||
data(
|
||||
std::make_move_iterator(split_secret.begin()),
|
||||
std::make_move_iterator(split_secret.begin() + split_secret.size()))
|
||||
{}
|
||||
};
|
||||
|
||||
struct LedgerSecret
|
||||
{
|
||||
static constexpr auto MASTER_KEY_SIZE = crypto::GCM_SIZE_KEY;
|
||||
|
@ -57,9 +41,7 @@ namespace ccf
|
|||
}
|
||||
}
|
||||
|
||||
LedgerSecret(const std::vector<uint8_t>& ledger_master_) :
|
||||
master(ledger_master_)
|
||||
{}
|
||||
LedgerSecret(const std::vector<uint8_t>& master_) : master(master_) {}
|
||||
};
|
||||
|
||||
class LedgerSecrets
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
#include "rpc/serialization.h"
|
||||
#include "seal.h"
|
||||
#include "secretshare.h"
|
||||
#include "sharemanager.h"
|
||||
#include "timer.h"
|
||||
#include "tls/25519.h"
|
||||
#include "tls/client.h"
|
||||
|
@ -181,6 +182,7 @@ namespace ccf
|
|||
std::shared_ptr<kv::AbstractTxEncryptor> encryptor;
|
||||
|
||||
std::shared_ptr<Seal> seal;
|
||||
ShareManager share_manager;
|
||||
|
||||
//
|
||||
// join protocol
|
||||
|
@ -218,7 +220,8 @@ namespace ccf
|
|||
rpcsessions(rpcsessions),
|
||||
notifier(notifier),
|
||||
timers(timers),
|
||||
seal(std::make_shared<Seal>(writer_factory))
|
||||
seal(std::make_shared<Seal>(writer_factory)),
|
||||
share_manager(network)
|
||||
{
|
||||
::EverCrypt_AutoConfig2_init();
|
||||
}
|
||||
|
@ -790,11 +793,23 @@ namespace ccf
|
|||
}
|
||||
|
||||
bool finish_recovery(
|
||||
Store::Tx& tx, const nlohmann::json& sealed_secrets) override
|
||||
Store::Tx& tx,
|
||||
const nlohmann::json& sealed_secrets,
|
||||
bool with_shares) override
|
||||
{
|
||||
std::lock_guard<SpinLock> guard(lock);
|
||||
sm.expect(State::partOfPublicNetwork);
|
||||
|
||||
if (with_shares)
|
||||
{
|
||||
GenesisGenerator g(network, tx);
|
||||
if (!g.service_wait_for_shares())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_INFO_FMT("Initiating end of recovery (primary)");
|
||||
|
||||
// Unseal past network secrets
|
||||
|
@ -811,7 +826,8 @@ namespace ccf
|
|||
auto secret = network.ledger_secrets->get_secret(secret_idx);
|
||||
if (!secret.has_value())
|
||||
{
|
||||
LOG_FAIL_FMT("Ledger secrets have not been restored: {}", secret_idx);
|
||||
LOG_FAIL_FMT(
|
||||
"Ledger secrets have not been restored: {}", secret_idx);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -827,6 +843,36 @@ namespace ccf
|
|||
ledger_idx = 0;
|
||||
read_ledger_idx(++ledger_idx);
|
||||
|
||||
sm.advance(State::readingPrivateLedger);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool finish_recovery_with_shares(
|
||||
Store::Tx& tx, const LedgerSecret& ledger_secret)
|
||||
{
|
||||
std::lock_guard<SpinLock> guard(lock);
|
||||
sm.expect(State::partOfPublicNetwork);
|
||||
|
||||
// For now, this only supports one recovery
|
||||
|
||||
LOG_INFO_FMT("Initiating end of recovery with shares (primary)");
|
||||
|
||||
// Emit signature to certify transactions that happened on public
|
||||
// network
|
||||
history->emit_signature();
|
||||
|
||||
network.ledger_secrets->set_secret(0, ledger_secret.master);
|
||||
|
||||
broadcast_ledger_secret(tx, ledger_secret, 0, true);
|
||||
|
||||
// Setup new temporary store and record current version/root
|
||||
setup_private_recovery_store();
|
||||
|
||||
// Start reading private security domain of ledger
|
||||
ledger_idx = 0;
|
||||
read_ledger_idx(++ledger_idx);
|
||||
|
||||
sm.advance(State::readingPrivateLedger);
|
||||
return true;
|
||||
}
|
||||
|
@ -1011,59 +1057,36 @@ namespace ccf
|
|||
});
|
||||
};
|
||||
|
||||
void split_ledger_secrets(Store::Tx& tx) override
|
||||
bool split_ledger_secrets(Store::Tx& tx) override
|
||||
{
|
||||
auto share_wrapping_key_raw =
|
||||
tls::create_entropy()->random(crypto::GCM_SIZE_KEY);
|
||||
auto share_wrapping_key = crypto::KeyAesGcm(share_wrapping_key_raw);
|
||||
|
||||
// Once sealing is completely removed, this can be called from the
|
||||
// LedgerSecrets class directly
|
||||
crypto::GcmCipher encrypted_ls(LedgerSecret::MASTER_KEY_SIZE);
|
||||
share_wrapping_key.encrypt(
|
||||
encrypted_ls.hdr.get_iv(), // iv is always 0 here as the share wrapping
|
||||
// key is never re-used for encryption
|
||||
network.ledger_secrets->get_secret(1)->master,
|
||||
nullb,
|
||||
encrypted_ls.cipher.data(),
|
||||
encrypted_ls.hdr.tag);
|
||||
|
||||
GenesisGenerator g(network, tx);
|
||||
auto active_members = g.get_active_members_keyshare();
|
||||
|
||||
SecretSharing::SplitSecret secret_to_split = {};
|
||||
std::copy_n(
|
||||
share_wrapping_key_raw.begin(),
|
||||
share_wrapping_key_raw.size(),
|
||||
secret_to_split.begin());
|
||||
|
||||
// For now, the secret sharing threshold is set to the number of initial
|
||||
// members
|
||||
size_t threshold = active_members.size();
|
||||
auto shares =
|
||||
SecretSharing::split(secret_to_split, active_members.size(), threshold);
|
||||
|
||||
EncryptedSharesMap encrypted_shares;
|
||||
auto nonce = tls::create_entropy()->random(crypto::Box::NONCE_SIZE);
|
||||
|
||||
size_t share_index = 0;
|
||||
for (auto const& [member_id, enc_pub_key] : active_members)
|
||||
try
|
||||
{
|
||||
auto share_raw = std::vector<uint8_t>(
|
||||
shares[share_index].begin(), shares[share_index].end());
|
||||
|
||||
auto enc_pub_key_raw = tls::PublicX25519::parse(tls::Pem(enc_pub_key));
|
||||
auto encrypted_share = crypto::Box::create(
|
||||
share_raw,
|
||||
nonce,
|
||||
enc_pub_key_raw,
|
||||
network.encryption_key->private_raw);
|
||||
|
||||
encrypted_shares[member_id] = {nonce, encrypted_share};
|
||||
share_index++;
|
||||
share_manager.create(tx);
|
||||
}
|
||||
catch (const std::logic_error& e)
|
||||
{
|
||||
LOG_FAIL_FMT("Failed to create shares: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
g.add_key_share_info({encrypted_ls.serialise(), encrypted_shares});
|
||||
bool combine_recovery_shares(
|
||||
Store::Tx& tx, const std::vector<SecretSharing::Share>& shares) override
|
||||
{
|
||||
LedgerSecret restored_ledger_secret;
|
||||
try
|
||||
{
|
||||
restored_ledger_secret = share_manager.restore(tx, shares);
|
||||
finish_recovery_with_shares(tx, restored_ledger_secret);
|
||||
}
|
||||
catch (const std::logic_error& e)
|
||||
{
|
||||
LOG_FAIL_FMT("Failed to restore shares: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
NodeId get_node_id() const override
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
#include "forwarder.h"
|
||||
#include "node/clientsignatures.h"
|
||||
#include "node/nodes.h"
|
||||
#include "nodeinterface.h"
|
||||
#include "notifierinterface.h"
|
||||
#include "rpcexception.h"
|
||||
#include "tls/verifier.h"
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
#include "node/nodes.h"
|
||||
#include "node/quoteverification.h"
|
||||
#include "node/secretshare.h"
|
||||
#include "node/sharemanager.h"
|
||||
#include "nodeinterface.h"
|
||||
#include "tls/keypair.h"
|
||||
|
||||
#include <exception>
|
||||
|
@ -219,7 +221,31 @@ namespace ccf
|
|||
ObjectId proposal_id, Store::Tx& tx, const nlohmann::json& args) {
|
||||
if (node.is_part_of_public_network())
|
||||
{
|
||||
const auto recovery_successful = node.finish_recovery(tx, args);
|
||||
const auto recovery_successful =
|
||||
node.finish_recovery(tx, args, false);
|
||||
if (!recovery_successful)
|
||||
{
|
||||
LOG_FAIL_FMT("Proposal {}: Recovery failed", proposal_id);
|
||||
}
|
||||
return recovery_successful;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Proposal {}: Node is not part of public network", proposal_id);
|
||||
return false;
|
||||
}
|
||||
}},
|
||||
// For now, members can propose to accept a recovery with shares. In
|
||||
// that case, members will have to submit their shares after this
|
||||
// proposal is accepted.
|
||||
{"accept_recovery_with_shares",
|
||||
[this](
|
||||
ObjectId proposal_id, Store::Tx& tx, const nlohmann::json& args) {
|
||||
if (node.is_part_of_public_network())
|
||||
{
|
||||
const auto recovery_successful =
|
||||
node.finish_recovery(tx, nullptr, true);
|
||||
if (!recovery_successful)
|
||||
{
|
||||
LOG_FAIL_FMT("Proposal {}: Recovery failed", proposal_id);
|
||||
|
@ -707,6 +733,12 @@ namespace ccf
|
|||
std::optional<EncryptedShare> enc_s;
|
||||
auto current_keyshare =
|
||||
args.tx.get_view(this->network.shares)->get(0);
|
||||
if (!current_keyshare.has_value())
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
"Failed to retrieve current key share info");
|
||||
}
|
||||
for (auto const& s : current_keyshare->encrypted_shares)
|
||||
{
|
||||
if (s.first == args.caller_id)
|
||||
|
@ -730,14 +762,24 @@ namespace ccf
|
|||
json_adapter(get_encrypted_recovery_share),
|
||||
Read);
|
||||
|
||||
auto submit_recovery_share =
|
||||
[this](RequestArgs& args, const nlohmann::json& params) {
|
||||
auto submit_recovery_share = [this](
|
||||
RequestArgs& args,
|
||||
const nlohmann::json& params) {
|
||||
// Only active members can submit their shares for recovery
|
||||
if (!check_member_active(args.tx, args.caller_id))
|
||||
{
|
||||
return make_error(HTTP_STATUS_FORBIDDEN, "Member is not active");
|
||||
}
|
||||
|
||||
GenesisGenerator g(this->network, args.tx);
|
||||
if (
|
||||
g.get_service_status() != ServiceStatus::WAITING_FOR_RECOVERY_SHARES)
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_FORBIDDEN,
|
||||
"Service is not waiting for recovery shares");
|
||||
}
|
||||
|
||||
const auto in = params.get<SubmitRecoveryShare>();
|
||||
|
||||
SecretSharing::Share share;
|
||||
|
@ -745,8 +787,6 @@ namespace ccf
|
|||
in.share.begin(), SecretSharing::SHARE_LENGTH, share.begin());
|
||||
|
||||
pending_shares.emplace_back(share);
|
||||
|
||||
GenesisGenerator g(this->network, args.tx);
|
||||
if (pending_shares.size() < g.get_active_members_count())
|
||||
{
|
||||
// The number of shares required to re-assemble the secret has not
|
||||
|
@ -757,40 +797,12 @@ namespace ccf
|
|||
LOG_DEBUG_FMT(
|
||||
"Reached secret sharing threshold {}", pending_shares.size());
|
||||
|
||||
auto share_wrapping_key = LedgerSecretWrappingKey(
|
||||
SecretSharing::combine(pending_shares, pending_shares.size()));
|
||||
|
||||
auto shares_view = args.tx.get_view(this->network.shares);
|
||||
auto key_share_info = shares_view->get(0);
|
||||
if (!key_share_info.has_value())
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR, "No key share info available");
|
||||
}
|
||||
std::vector<uint8_t> decrypted_ls(LedgerSecret::MASTER_KEY_SIZE);
|
||||
crypto::GcmCipher encrypted_ls;
|
||||
try
|
||||
{
|
||||
encrypted_ls.deserialise(key_share_info->encrypted_ledger_secret);
|
||||
}
|
||||
catch (const std::logic_error& e)
|
||||
if (!node.combine_recovery_shares(args.tx, pending_shares))
|
||||
{
|
||||
pending_shares.clear();
|
||||
return make_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
"Failed to deserialise ledger secrets");
|
||||
}
|
||||
if (!crypto::KeyAesGcm(share_wrapping_key.data)
|
||||
.decrypt(
|
||||
encrypted_ls.hdr.get_iv(),
|
||||
encrypted_ls.hdr.tag,
|
||||
encrypted_ls.cipher,
|
||||
nullb,
|
||||
decrypted_ls.data()))
|
||||
{
|
||||
LOG_FAIL_FMT("Decryption of ledger secrets failed");
|
||||
return make_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
"Decryption of ledger secrets failed");
|
||||
"Failed to combine recovery shares");
|
||||
}
|
||||
|
||||
pending_shares.clear();
|
||||
|
@ -823,7 +835,13 @@ namespace ccf
|
|||
|
||||
g.add_consensus(in.consensus_type);
|
||||
|
||||
node.split_ledger_secrets(tx);
|
||||
if (!node.split_ledger_secrets(tx))
|
||||
{
|
||||
LOG_FAIL_FMT("Error splitting ledger secrets");
|
||||
return make_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
"Error splitting ledger secrets");
|
||||
}
|
||||
|
||||
size_t self = g.add_node({in.node_info_network,
|
||||
in.node_cert,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "node/entities.h"
|
||||
#include "node/networkstate.h"
|
||||
#include "node/quoteverification.h"
|
||||
#include "nodeinterface.h"
|
||||
|
||||
namespace ccf
|
||||
{
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "node/entities.h"
|
||||
#include "node/secretshare.h"
|
||||
#include "nodecalltypes.h"
|
||||
|
||||
namespace ccf
|
||||
|
@ -11,7 +12,8 @@ namespace ccf
|
|||
{
|
||||
public:
|
||||
virtual ~AbstractNodeState() {}
|
||||
virtual bool finish_recovery(Store::Tx& tx, const nlohmann::json& args) = 0;
|
||||
virtual bool finish_recovery(
|
||||
Store::Tx& tx, const nlohmann::json& args, bool with_shares) = 0;
|
||||
virtual bool open_network(Store::Tx& tx) = 0;
|
||||
virtual bool rekey_ledger(Store::Tx& tx) = 0;
|
||||
virtual bool is_part_of_public_network() const = 0;
|
||||
|
@ -23,14 +25,10 @@ namespace ccf
|
|||
Store::Tx& tx,
|
||||
GetQuotes::Out& result,
|
||||
const std::optional<std::set<NodeId>>& filter = std::nullopt) = 0;
|
||||
virtual void split_ledger_secrets(Store::Tx& tx) = 0;
|
||||
virtual NodeId get_node_id() const = 0;
|
||||
};
|
||||
|
||||
class AbstractNotifier
|
||||
{
|
||||
public:
|
||||
virtual ~AbstractNotifier() {}
|
||||
virtual void notify(const std::vector<uint8_t>& data) = 0;
|
||||
virtual bool split_ledger_secrets(Store::Tx& tx) = 0;
|
||||
virtual bool combine_recovery_shares(
|
||||
Store::Tx& tx, const std::vector<SecretSharing::Share>& shares) = 0;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
|
||||
namespace ccf
|
||||
{
|
||||
class AbstractNotifier
|
||||
{
|
||||
public:
|
||||
virtual ~AbstractNotifier() {}
|
||||
virtual void notify(const std::vector<uint8_t>& data) = 0;
|
||||
};
|
||||
}
|
|
@ -1553,6 +1553,94 @@ DOCTEST_TEST_CASE("User data")
|
|||
}
|
||||
}
|
||||
|
||||
DOCTEST_TEST_CASE("Submit recovery shares")
|
||||
{
|
||||
// Setup original state
|
||||
NetworkTables network;
|
||||
auto node = StubNodeState(std::make_shared<NetworkTables>(network));
|
||||
MemberRpcFrontend frontend(network, node);
|
||||
std::map<size_t, ccf::Cert> members;
|
||||
size_t member_count = 4;
|
||||
std::map<size_t, EncryptedShare> retrieved_shares;
|
||||
|
||||
DOCTEST_INFO("Setup state");
|
||||
{
|
||||
Store::Tx gen_tx;
|
||||
|
||||
GenesisGenerator gen(network, gen_tx);
|
||||
gen.init_values();
|
||||
gen.create_service({});
|
||||
|
||||
for (size_t i = 0; i < member_count; i++)
|
||||
{
|
||||
auto cert = get_cert_data(i, kp);
|
||||
members[gen.add_member(cert, {}, MemberStatus::ACTIVE)] = cert;
|
||||
}
|
||||
DOCTEST_REQUIRE(node.split_ledger_secrets(gen_tx));
|
||||
gen.finalize();
|
||||
|
||||
frontend.open();
|
||||
}
|
||||
|
||||
DOCTEST_INFO("Retrieve recovery shares");
|
||||
{
|
||||
const auto get_recovery_shares =
|
||||
create_request(nullptr, "getEncryptedRecoveryShare");
|
||||
|
||||
for (auto const& m : members)
|
||||
{
|
||||
retrieved_shares[m.first] = parse_response_body<EncryptedShare>(
|
||||
frontend_process(frontend, get_recovery_shares, m.second));
|
||||
}
|
||||
}
|
||||
|
||||
DOCTEST_INFO("Submit share before the service is in correct state");
|
||||
{
|
||||
MemberId member_id = 0;
|
||||
const auto submit_recovery_share = create_request(
|
||||
SubmitRecoveryShare({retrieved_shares[member_id].encrypted_share}),
|
||||
"submitRecoveryShare");
|
||||
|
||||
check_error(
|
||||
frontend_process(frontend, submit_recovery_share, members[member_id]),
|
||||
HTTP_STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
DOCTEST_INFO("Change service state to waiting for recovery shares");
|
||||
{
|
||||
Store::Tx tx;
|
||||
GenesisGenerator g(network, tx);
|
||||
|
||||
DOCTEST_REQUIRE(g.service_wait_for_shares());
|
||||
|
||||
g.finalize();
|
||||
}
|
||||
|
||||
DOCTEST_INFO("Submit recovery shares");
|
||||
{
|
||||
for (auto const& m : members)
|
||||
{
|
||||
const auto submit_recovery_share = create_request(
|
||||
SubmitRecoveryShare({retrieved_shares[m.first].encrypted_share}),
|
||||
"submitRecoveryShare");
|
||||
|
||||
auto ret = parse_response_body<bool>(
|
||||
frontend_process(frontend, submit_recovery_share, m.second));
|
||||
|
||||
// Share submission should only complete when last member submits their
|
||||
// share
|
||||
if (m.first != (member_count - 1))
|
||||
{
|
||||
DOCTEST_REQUIRE(!ret);
|
||||
}
|
||||
else
|
||||
{
|
||||
DOCTEST_REQUIRE(ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need an explicit main to initialize kremlib and EverCrypt
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "node/rpc/nodeinterface.h"
|
||||
#include "node/secretshare.h"
|
||||
|
||||
namespace ccf
|
||||
{
|
||||
|
@ -10,9 +11,15 @@ namespace ccf
|
|||
{
|
||||
private:
|
||||
bool is_public = false;
|
||||
std::shared_ptr<NetworkTables> network;
|
||||
|
||||
public:
|
||||
bool finish_recovery(Store::Tx& tx, const nlohmann::json& args) override
|
||||
StubNodeState(std::shared_ptr<NetworkTables> network_ = nullptr) :
|
||||
network(network_)
|
||||
{}
|
||||
|
||||
bool finish_recovery(
|
||||
Store::Tx& tx, const nlohmann::json& args, bool with_shares) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
@ -58,7 +65,40 @@ namespace ccf
|
|||
const std::optional<std::set<NodeId>>& filter) override
|
||||
{}
|
||||
|
||||
void split_ledger_secrets(Store::Tx& tx) override {}
|
||||
bool split_ledger_secrets(Store::Tx& tx) override
|
||||
{
|
||||
auto [members_view, shares_view] =
|
||||
tx.get_view(network->members, network->shares);
|
||||
SecretSharing::SplitSecret secret_to_split = {};
|
||||
|
||||
GenesisGenerator g(*network.get(), tx);
|
||||
auto active_member_count = g.get_active_members_count();
|
||||
|
||||
// All member shares are required to construct secrets
|
||||
size_t threshold = active_member_count;
|
||||
|
||||
auto shares =
|
||||
SecretSharing::split(secret_to_split, active_member_count, threshold);
|
||||
|
||||
// Here, shares are not encrypted and record in the ledger in plain text
|
||||
EncryptedSharesMap recorded_shares;
|
||||
MemberId member_id = 0;
|
||||
for (auto const& s : shares)
|
||||
{
|
||||
auto share_raw = std::vector<uint8_t>(s.begin(), s.end());
|
||||
recorded_shares[member_id] = {{}, share_raw};
|
||||
member_id++;
|
||||
}
|
||||
g.add_key_share_info({{}, recorded_shares});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool combine_recovery_shares(
|
||||
Store::Tx& tx, const std::vector<SecretSharing::Share>& shares) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
NodeId get_node_id() const override
|
||||
{
|
||||
|
|
|
@ -17,13 +17,16 @@ namespace ccf
|
|||
{
|
||||
OPENING = 1,
|
||||
OPEN = 2,
|
||||
CLOSED = 3 // For now, unused
|
||||
WAITING_FOR_RECOVERY_SHARES = 3,
|
||||
CLOSED = 4 // For now, unused
|
||||
};
|
||||
|
||||
DECLARE_JSON_ENUM(
|
||||
ServiceStatus,
|
||||
{{ServiceStatus::OPENING, "OPENING"},
|
||||
{ServiceStatus::OPEN, "OPEN"},
|
||||
{ServiceStatus::WAITING_FOR_RECOVERY_SHARES,
|
||||
"WAITING_FOR_RECOVERY_SHARES"},
|
||||
{ServiceStatus::CLOSED, "CLOSED"}});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
#pragma once
|
||||
|
||||
#include "crypto/cryptobox.h"
|
||||
#include "crypto/symmkey.h"
|
||||
#include "ds/logger.h"
|
||||
#include "genesisgen.h"
|
||||
#include "ledgersecrets.h"
|
||||
#include "networkstate.h"
|
||||
#include "secretshare.h"
|
||||
#include "tls/25519.h"
|
||||
#include "tls/entropy.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace ccf
|
||||
{
|
||||
struct LedgerSecretWrappingKey
|
||||
{
|
||||
private:
|
||||
static constexpr auto KZ_KEY_SIZE = crypto::GCM_SIZE_KEY;
|
||||
|
||||
public:
|
||||
std::vector<uint8_t> data; // Referred to as "kz" in TR
|
||||
|
||||
LedgerSecretWrappingKey() : data(tls::create_entropy()->random(KZ_KEY_SIZE))
|
||||
{}
|
||||
|
||||
template <typename T>
|
||||
LedgerSecretWrappingKey(const T& split_secret) :
|
||||
data(
|
||||
std::make_move_iterator(split_secret.begin()),
|
||||
std::make_move_iterator(split_secret.begin() + split_secret.size()))
|
||||
{}
|
||||
};
|
||||
|
||||
class ShareManager
|
||||
{
|
||||
private:
|
||||
NetworkState& network;
|
||||
|
||||
public:
|
||||
ShareManager(NetworkState& network_) : network(network_) {}
|
||||
|
||||
void create(Store::Tx& tx)
|
||||
{
|
||||
// First, generated a fresh ledger secrets wrapping key and encrypt the
|
||||
// current ledger secrets with it
|
||||
auto ls_wrapping_key = LedgerSecretWrappingKey();
|
||||
|
||||
crypto::GcmCipher encrypted_ls(LedgerSecret::MASTER_KEY_SIZE);
|
||||
crypto::KeyAesGcm(ls_wrapping_key.data)
|
||||
.encrypt(
|
||||
encrypted_ls.hdr
|
||||
.get_iv(), // iv is always 0 here as the share wrapping
|
||||
// key is never re-used for encryption
|
||||
network.ledger_secrets->get_secret(1)->master,
|
||||
nullb,
|
||||
encrypted_ls.cipher.data(),
|
||||
encrypted_ls.hdr.tag);
|
||||
|
||||
// Then, split the ledger secrets wrapping key, allocating a share to each
|
||||
// active member
|
||||
SecretSharing::SplitSecret secret_to_split = {};
|
||||
std::copy_n(
|
||||
ls_wrapping_key.data.begin(),
|
||||
ls_wrapping_key.data.size(),
|
||||
secret_to_split.begin());
|
||||
|
||||
GenesisGenerator g(network, tx);
|
||||
auto active_members_info = g.get_active_members_keyshare();
|
||||
|
||||
// For now, the secret sharing threshold is set to the number of initial
|
||||
// members
|
||||
size_t threshold = active_members_info.size();
|
||||
auto shares = SecretSharing::split(
|
||||
secret_to_split, active_members_info.size(), threshold);
|
||||
|
||||
// Finally, encrypt each share with the public key of each member, using a
|
||||
// random nonce, and record in the KV
|
||||
EncryptedSharesMap encrypted_shares;
|
||||
auto nonce = tls::create_entropy()->random(crypto::Box::NONCE_SIZE);
|
||||
|
||||
size_t share_index = 0;
|
||||
for (auto const& [member_id, enc_pub_key] : active_members_info)
|
||||
{
|
||||
auto share_raw = std::vector<uint8_t>(
|
||||
shares[share_index].begin(), shares[share_index].end());
|
||||
|
||||
auto enc_pub_key_raw = tls::PublicX25519::parse(tls::Pem(enc_pub_key));
|
||||
auto encrypted_share = crypto::Box::create(
|
||||
share_raw,
|
||||
nonce,
|
||||
enc_pub_key_raw,
|
||||
network.encryption_key->private_raw);
|
||||
|
||||
encrypted_shares[member_id] = {nonce, encrypted_share};
|
||||
share_index++;
|
||||
}
|
||||
|
||||
g.add_key_share_info({encrypted_ls.serialise(), encrypted_shares});
|
||||
}
|
||||
|
||||
// For now, the shares are passed directly to this function. Shares should
|
||||
// be retrieved from the KV instead.
|
||||
LedgerSecret restore(
|
||||
Store::Tx& tx, const std::vector<SecretSharing::Share>& shares)
|
||||
{
|
||||
// First, re-assemble the ledger secrets wrapping key from the given
|
||||
// shares
|
||||
auto ls_wrapping_key =
|
||||
LedgerSecretWrappingKey(SecretSharing::combine(shares, shares.size()));
|
||||
|
||||
// Then, decrypt the ledger secrets
|
||||
auto shares_view = tx.get_view(network.shares);
|
||||
auto key_share_info = shares_view->get(0);
|
||||
if (!key_share_info.has_value())
|
||||
{
|
||||
throw std::logic_error("Failed to retrieve current key share info");
|
||||
}
|
||||
|
||||
std::vector<uint8_t> decrypted_ls(LedgerSecret::MASTER_KEY_SIZE);
|
||||
crypto::GcmCipher encrypted_ls;
|
||||
encrypted_ls.deserialise(key_share_info->encrypted_ledger_secret);
|
||||
|
||||
if (!crypto::KeyAesGcm(ls_wrapping_key.data)
|
||||
.decrypt(
|
||||
encrypted_ls.hdr.get_iv(), // iv is 0
|
||||
encrypted_ls.hdr.tag,
|
||||
encrypted_ls.cipher,
|
||||
nullb,
|
||||
decrypted_ls.data()))
|
||||
{
|
||||
throw std::logic_error("Decryption of ledger secrets failed");
|
||||
}
|
||||
|
||||
return LedgerSecret(std::move(decrypted_ls));
|
||||
}
|
||||
};
|
||||
}
|
|
@ -293,6 +293,14 @@ class Consortium:
|
|||
response = self.propose(member_id, remote_node, script, sealed_secrets)
|
||||
self.vote_using_majority(remote_node, response.result["proposal_id"])
|
||||
|
||||
def accept_recovery_with_shares(self, member_id, remote_node):
|
||||
script = """
|
||||
tables = ...
|
||||
return Calls:call("accept_recovery_with_shares")
|
||||
"""
|
||||
response = self.propose(member_id, remote_node, script)
|
||||
self.vote_using_majority(remote_node, response.result["proposal_id"])
|
||||
|
||||
def store_current_network_encryption_key(self):
|
||||
cmd = [
|
||||
"cp",
|
||||
|
@ -301,7 +309,7 @@ class Consortium:
|
|||
]
|
||||
infra.proc.ccall(*cmd).check_returncode()
|
||||
|
||||
def get_and_decrypt_shares(self, remote_node):
|
||||
def get_decrypt_and_submit_shares(self, remote_node):
|
||||
for m in self.members:
|
||||
with remote_node.member_client(member_id=m) as mc:
|
||||
r = mc.rpc("getEncryptedRecoveryShare", params={})
|
||||
|
@ -319,6 +327,7 @@ class Consortium:
|
|||
r = mc.rpc(
|
||||
"submitRecoveryShare", params={"share": list(decrypted_share)}
|
||||
)
|
||||
assert r.error is None, f"Error submitting recovery share: {r.error}"
|
||||
if m == 2:
|
||||
assert (
|
||||
r.result == True
|
||||
|
|
|
@ -43,8 +43,12 @@ def test(network, args, use_shares=False):
|
|||
recovered_network.wait_for_all_nodes_to_be_trusted()
|
||||
|
||||
if use_shares:
|
||||
recovered_network.consortium.get_and_decrypt_shares(remote_node=primary)
|
||||
|
||||
LOG.warning("Retrieve and submit recovery shares")
|
||||
recovered_network.consortium.accept_recovery_with_shares(
|
||||
member_id=1, remote_node=primary
|
||||
)
|
||||
recovered_network.consortium.get_decrypt_and_submit_shares(remote_node=primary)
|
||||
else:
|
||||
LOG.info("Members vote to complete the recovery")
|
||||
recovered_network.consortium.accept_recovery(
|
||||
member_id=1, remote_node=primary, sealed_secrets=sealed_secrets
|
||||
|
|
Загрузка…
Ссылка в новой задаче