diff --git a/CMakeLists.txt b/CMakeLists.txt index 187b73303c..7050cd5e88 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/src/node/genesisgen.h b/src/node/genesisgen.h index 8fcf666c0a..f733f0acb3 100644 --- a/src/node/genesisgen.h +++ b/src/node/genesisgen.h @@ -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& 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 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); diff --git a/src/node/ledgersecrets.h b/src/node/ledgersecrets.h index b41967f727..b54d0a7619 100644 --- a/src/node/ledgersecrets.h +++ b/src/node/ledgersecrets.h @@ -22,22 +22,6 @@ namespace ccf const std::vector& data) = 0; }; - struct LedgerSecretWrappingKey - { - static constexpr auto KZ_KEY_SIZE = crypto::GCM_SIZE_KEY; - std::vector data; // Referred to as "kz" in TR - - LedgerSecretWrappingKey() : data(tls::create_entropy()->random(KZ_KEY_SIZE)) - {} - - template - 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& ledger_master_) : - master(ledger_master_) - {} + LedgerSecret(const std::vector& master_) : master(master_) {} }; class LedgerSecrets diff --git a/src/node/nodestate.h b/src/node/nodestate.h index 240bb2e72d..b055a9f18d 100644 --- a/src/node/nodestate.h +++ b/src/node/nodestate.h @@ -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 encryptor; std::shared_ptr seal; + ShareManager share_manager; // // join protocol @@ -218,7 +220,8 @@ namespace ccf rpcsessions(rpcsessions), notifier(notifier), timers(timers), - seal(std::make_shared(writer_factory)) + seal(std::make_shared(writer_factory)), + share_manager(network) { ::EverCrypt_AutoConfig2_init(); } @@ -790,35 +793,78 @@ 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 guard(lock); sm.expect(State::partOfPublicNetwork); - LOG_INFO_FMT("Initiating end of recovery (primary)"); + 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 - auto past_secrets_idx = network.ledger_secrets->restore(sealed_secrets); + // Unseal past network secrets + auto past_secrets_idx = network.ledger_secrets->restore(sealed_secrets); + + // Emit signature to certify transactions that happened on public + // network + history->emit_signature(); + + // For all nodes in the new network, write all past network secrets to + // the secrets table, encrypted with the respective public keys + for (auto const& secret_idx : past_secrets_idx) + { + auto secret = network.ledger_secrets->get_secret(secret_idx); + if (!secret.has_value()) + { + LOG_FAIL_FMT( + "Ledger secrets have not been restored: {}", secret_idx); + return false; + } + + // Do not broadcast the ledger secrets to self since they were already + // restored from sealed file + broadcast_ledger_secret(tx, secret.value(), secret_idx, 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; + } + + bool finish_recovery_with_shares( + Store::Tx& tx, const LedgerSecret& ledger_secret) + { + std::lock_guard 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(); - // For all nodes in the new network, write all past network secrets to - // the secrets table, encrypted with the respective public keys - for (auto const& secret_idx : past_secrets_idx) - { - auto secret = network.ledger_secrets->get_secret(secret_idx); - if (!secret.has_value()) - { - LOG_FAIL_FMT("Ledger secrets have not been restored: {}", secret_idx); - return false; - } + network.ledger_secrets->set_secret(0, ledger_secret.master); - // Do not broadcast the ledger secrets to self since they were already - // restored from sealed file - broadcast_ledger_secret(tx, secret.value(), secret_idx, true); - } + broadcast_ledger_secret(tx, ledger_secret, 0, true); // Setup new temporary store and record current version/root setup_private_recovery_store(); @@ -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( - shares[share_index].begin(), shares[share_index].end()); + share_manager.create(tx); + } + catch (const std::logic_error& e) + { + LOG_FAIL_FMT("Failed to create shares: {}", e.what()); + return false; + } + return true; + } - 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++; + bool combine_recovery_shares( + Store::Tx& tx, const std::vector& 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; } - g.add_key_share_info({encrypted_ls.serialise(), encrypted_shares}); + return true; } NodeId get_node_id() const override diff --git a/src/node/rpc/frontend.h b/src/node/rpc/frontend.h index 87c75ea836..85da5d7b04 100644 --- a/src/node/rpc/frontend.h +++ b/src/node/rpc/frontend.h @@ -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" diff --git a/src/node/rpc/memberfrontend.h b/src/node/rpc/memberfrontend.h index 878ae66317..38c3ca6905 100644 --- a/src/node/rpc/memberfrontend.h +++ b/src/node/rpc/memberfrontend.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 @@ -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 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,72 +762,52 @@ namespace ccf json_adapter(get_encrypted_recovery_share), Read); - 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"); - } + 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"); + } - const auto in = params.get(); + 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"); + } - SecretSharing::Share share; - std::copy_n( - in.share.begin(), SecretSharing::SHARE_LENGTH, share.begin()); + const auto in = params.get(); - pending_shares.emplace_back(share); + SecretSharing::Share share; + std::copy_n( + in.share.begin(), SecretSharing::SHARE_LENGTH, share.begin()); - 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 - // yet been reached - return make_success(false); - } + pending_shares.emplace_back(share); + if (pending_shares.size() < g.get_active_members_count()) + { + // The number of shares required to re-assemble the secret has not + // yet been reached + return make_success(false); + } - 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 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) - { - 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"); - } + LOG_DEBUG_FMT( + "Reached secret sharing threshold {}", pending_shares.size()); + if (!node.combine_recovery_shares(args.tx, pending_shares)) + { pending_shares.clear(); - return make_success(true); - }; + return make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + "Failed to combine recovery shares"); + } + + pending_shares.clear(); + return make_success(true); + }; install_with_auto_schema( MemberProcs::SUBMIT_RECOVERY_SHARE, json_adapter(submit_recovery_share), @@ -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, diff --git a/src/node/rpc/nodefrontend.h b/src/node/rpc/nodefrontend.h index 446c7605d9..e027cda8ba 100644 --- a/src/node/rpc/nodefrontend.h +++ b/src/node/rpc/nodefrontend.h @@ -7,6 +7,7 @@ #include "node/entities.h" #include "node/networkstate.h" #include "node/quoteverification.h" +#include "nodeinterface.h" namespace ccf { diff --git a/src/node/rpc/nodeinterface.h b/src/node/rpc/nodeinterface.h index 87332bd689..fdb6d74a62 100644 --- a/src/node/rpc/nodeinterface.h +++ b/src/node/rpc/nodeinterface.h @@ -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>& 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& data) = 0; + virtual bool split_ledger_secrets(Store::Tx& tx) = 0; + virtual bool combine_recovery_shares( + Store::Tx& tx, const std::vector& shares) = 0; }; } diff --git a/src/node/rpc/notifierinterface.h b/src/node/rpc/notifierinterface.h new file mode 100644 index 0000000000..05c0ab6d4c --- /dev/null +++ b/src/node/rpc/notifierinterface.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include +#include + +namespace ccf +{ + class AbstractNotifier + { + public: + virtual ~AbstractNotifier() {} + virtual void notify(const std::vector& data) = 0; + }; +} \ No newline at end of file diff --git a/src/node/rpc/test/membervoting_test.cpp b/src/node/rpc/test/membervoting_test.cpp index ef2b1f270b..5334d25259 100644 --- a/src/node/rpc/test/membervoting_test.cpp +++ b/src/node/rpc/test/membervoting_test.cpp @@ -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(network)); + MemberRpcFrontend frontend(network, node); + std::map members; + size_t member_count = 4; + std::map 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( + 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( + 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) { diff --git a/src/node/rpc/test/node_stub.h b/src/node/rpc/test/node_stub.h index 1001e15556..b3ff3d9159 100644 --- a/src/node/rpc/test/node_stub.h +++ b/src/node/rpc/test/node_stub.h @@ -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 network; public: - bool finish_recovery(Store::Tx& tx, const nlohmann::json& args) override + StubNodeState(std::shared_ptr 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>& 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(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& shares) override + { + return true; + } NodeId get_node_id() const override { diff --git a/src/node/service.h b/src/node/service.h index 34af5d99e2..cd215733df 100644 --- a/src/node/service.h +++ b/src/node/service.h @@ -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"}}); } diff --git a/src/node/sharemanager.h b/src/node/sharemanager.h new file mode 100644 index 0000000000..bc8250b620 --- /dev/null +++ b/src/node/sharemanager.h @@ -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 + +namespace ccf +{ + struct LedgerSecretWrappingKey + { + private: + static constexpr auto KZ_KEY_SIZE = crypto::GCM_SIZE_KEY; + + public: + std::vector data; // Referred to as "kz" in TR + + LedgerSecretWrappingKey() : data(tls::create_entropy()->random(KZ_KEY_SIZE)) + {} + + template + 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( + 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& 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 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)); + } + }; +} \ No newline at end of file diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 952212a511..1d4daf0d2e 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -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 diff --git a/tests/recovery.py b/tests/recovery.py index 10d873d7b6..0978541985 100644 --- a/tests/recovery.py +++ b/tests/recovery.py @@ -43,12 +43,16 @@ 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.info("Members vote to complete the recovery") - recovered_network.consortium.accept_recovery( - member_id=1, remote_node=primary, sealed_secrets=sealed_secrets - ) + 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 + ) for node in recovered_network.nodes: recovered_network.wait_for_state(node, "partOfNetwork")