diff --git a/.daily_canary b/.daily_canary index aa139c2bd76d..8d267a9f80d7 100644 --- a/.daily_canary +++ b/.daily_canary @@ -1,4 +1,4 @@ -^- ___ ___ (- -) (= =) | Y & +--? ( V ) / . \ | +---=---' -/--x-m- /--n-n---xXx--/--yY------ +/--x-m- /--n-n---xXx--/--yY------>>>+++<<< diff --git a/CHANGELOG.md b/CHANGELOG.md index de3e89333e5c..a770e3e6b99b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [4.0.9] + +[4.0.9]: https://github.com/microsoft/CCF/releases/tag/ccf-4.0.9 + +- Secret sharing used for ledger recovery now relies on a much simpler implementation that requires no external dependencies. Note that while the code still accepts shares generated by the old code for now, it only generates shares with the new implementation. As a result, a DR attempt that would downgrade the code to a version that pre-dates this change, after having previously picked it up, would not succeed if a reshare had already taken place (#5655). + ## [4.0.8] [4.0.8]: https://github.com/microsoft/CCF/releases/tag/ccf-4.0.8 diff --git a/CMakeLists.txt b/CMakeLists.txt index bcbc7e60394c..aaae3591512b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -912,6 +912,13 @@ if(BUILD_TESTS) target_include_directories(crypto_test PRIVATE ${CCFCRYPTO_INC}) target_link_libraries(crypto_test PRIVATE ccfcrypto.host) + add_unit_test( + sharing_test + ${CMAKE_CURRENT_SOURCE_DIR}/src/crypto/test/secret_sharing.cpp + ) + target_include_directories(sharing_test PRIVATE ${CCFCRYPTO_INC}) + target_link_libraries(sharing_test PRIVATE ccfcrypto.host) + add_unit_test( key_exchange_test ${CMAKE_CURRENT_SOURCE_DIR}/src/crypto/test/key_exchange.cpp diff --git a/cmake/crypto.cmake b/cmake/crypto.cmake index 35f0f21b7541..55bc26b4471d 100644 --- a/cmake/crypto.cmake +++ b/cmake/crypto.cmake @@ -24,6 +24,7 @@ set(CCFCRYPTO_SRC ${CCF_DIR}/src/crypto/openssl/rsa_key_pair.cpp ${CCF_DIR}/src/crypto/openssl/verifier.cpp ${CCF_DIR}/src/crypto/openssl/cose_verifier.cpp + ${CCF_DIR}/src/crypto/sharing.cpp ) if(COMPILE_TARGET STREQUAL "sgx") diff --git a/doc/governance/adding_member.rst b/doc/governance/adding_member.rst index 0f1eacb82cf6..5dcba1edd701 100644 --- a/doc/governance/adding_member.rst +++ b/doc/governance/adding_member.rst @@ -3,8 +3,6 @@ Adding New Members It is possible for existing members to add new members to the consortium after a CCF network has been started. -.. note:: The maximum number of allowed active recovery members (i.e. those with a recovery share) at any given time is 255. - Generating Member Keys and Certificates --------------------------------------- diff --git a/doc/operations/recovery.rst b/doc/operations/recovery.rst index 035188a4d497..9e932b473265 100644 --- a/doc/operations/recovery.rst +++ b/doc/operations/recovery.rst @@ -7,6 +7,8 @@ The disaster recovery procedure is costly (e.g. the service identity certificate .. tip:: See :ccf_repo:`tests/infra/health_watcher.py` for an example of how a network can be monitored to detect a disaster recovery scenario. +.. note:: From 4.0.9/5.0.0-dev2 onwards secret sharing used for ledger recovery now relies on a much simpler implementation that requires no external dependencies. Note that while the code still accepts shares generated by the old code for now, it only generates shares with the new implementation. As a result, a DR attempt that would downgrade the code to a version that pre-dates this change, after having previously picked it up, would not succeed if a reshare had already taken place. + Overview -------- diff --git a/include/ccf/crypto/hkdf.h b/include/ccf/crypto/hkdf.h index 5188c3f10738..d2b2a438c86a 100644 --- a/include/ccf/crypto/hkdf.h +++ b/include/ccf/crypto/hkdf.h @@ -4,6 +4,7 @@ #include "ccf/crypto/md_type.h" +#include #include namespace crypto @@ -12,7 +13,7 @@ namespace crypto std::vector hkdf( MDType md_type, size_t length, - const std::vector& ikm, - const std::vector& salt = {}, - const std::vector& info = {}); + const std::span& ikm, + const std::span& salt = {}, + const std::span& info = {}); } \ No newline at end of file diff --git a/include/ccf/crypto/sha256.h b/include/ccf/crypto/sha256.h index beeb1b1c4e5a..97fac504517f 100644 --- a/include/ccf/crypto/sha256.h +++ b/include/ccf/crypto/sha256.h @@ -10,12 +10,16 @@ namespace crypto { /** Compute the SHA256 hash of @p data * @param data The data to compute the hash of + * + * @return hashed value */ - HashBytes sha256(const std::vector& data); + HashBytes sha256(const std::span& data); /** Compute the SHA256 hash of @p data * @param data The data to compute the hash of * @param len Length of the data + * + * @return hashed value */ HashBytes sha256(const uint8_t* data, size_t len); } diff --git a/src/crypto/hash.cpp b/src/crypto/hash.cpp index 44ffe11b1e2f..e193a44e79a7 100644 --- a/src/crypto/hash.cpp +++ b/src/crypto/hash.cpp @@ -21,6 +21,14 @@ namespace crypto return r; } + std::vector sha256(const std::span& data) + { + size_t hash_size = EVP_MD_size(OpenSSL::get_md_type(MDType::SHA256)); + std::vector r(hash_size); + openssl_sha256(data, r.data()); + return r; + } + std::vector sha256(const uint8_t* data, size_t len) { std::span buf(data, len); @@ -43,9 +51,9 @@ namespace crypto std::vector hkdf( MDType md_type, size_t length, - const std::vector& ikm, - const std::vector& salt, - const std::vector& info) + const std::span& ikm, + const std::span& salt, + const std::span& info) { return OpenSSL::hkdf(md_type, length, ikm, salt, info); } diff --git a/src/crypto/openssl/hash.cpp b/src/crypto/openssl/hash.cpp index e9d7fb13d1cd..290ef85c36fe 100644 --- a/src/crypto/openssl/hash.cpp +++ b/src/crypto/openssl/hash.cpp @@ -14,9 +14,9 @@ namespace crypto std::vector hkdf( MDType md_type, size_t length, - const std::vector& ikm, - const std::vector& salt, - const std::vector& info) + const std::span& ikm, + const std::span& salt, + const std::span& info) { auto md = get_md_type(md_type); EVP_PKEY_CTX* pctx; diff --git a/src/crypto/openssl/hash.h b/src/crypto/openssl/hash.h index 606c4fdde17e..d8117cbfb3a4 100644 --- a/src/crypto/openssl/hash.h +++ b/src/crypto/openssl/hash.h @@ -7,6 +7,7 @@ #include #include +#include #define FMT_HEADER_ONLY #include @@ -38,9 +39,9 @@ namespace crypto std::vector hkdf( MDType md_type, size_t length, - const std::vector& ikm, - const std::vector& salt = {}, - const std::vector& info = {}); + const std::span& ikm, + const std::span& salt = {}, + const std::span& info = {}); } // Hash Provider (OpenSSL) diff --git a/src/crypto/sharing.cpp b/src/crypto/sharing.cpp new file mode 100644 index 000000000000..ecf76fa27c76 --- /dev/null +++ b/src/crypto/sharing.cpp @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#include "sharing.h" + +#include "ccf/crypto/entropy.h" + +#include + +namespace crypto +{ + /* PRIME FIELD + + For simplicity, we use a finite field F[prime] where all operations + are defined in plain uint64_t arithmetic, and we reduce after every + operation. This is not meant to be efficient. Compared to e.g. GF(2^n), the + main drawback is that we need to hash the "raw secret" to obtain a + uniformly-distributed secret. + */ + + using element = uint64_t; + constexpr element prime = (1ul << 31) - 1ul; // a notorious Mersenne prime + + static element reduce(uint64_t x) + { + return (x % prime); + } + + static element mul(element x, element y) + { + return ((x * y) % prime); + } + + static element add(element x, element y) + { + return ((x + y) % prime); + } + + static element sub(element x, element y) + { + return ((prime + x - y)) % prime; + } + + // naive algorithm, used only to compute coefficients, not for use on secrets! + static element exp(element x, size_t n) + { + element y = 1; + while (n > 0) + { + if (n & 1) + y = mul(y, x); + x = mul(x, x); + n >>= 1; + } + return y; + } + + static element inv(element x) + { + if (x == 0) + { + throw std::invalid_argument("division by zero"); + } + return exp(x, prime - 2); + } + + // This function is specific to prime=2^31-1. + // We assume the lower 31 bits are uniformly distributed, + // and retry if they are all set to get uniformity in F[prime]. + + static element sample(const crypto::EntropyPtr& entropy) + { + uint64_t res = prime; + while (res == prime) + { + res = entropy->random64() & prime; + } + return res; + } + + /* POLYNOMIAL SHARING AND INTERPOLATION */ + + static void sample_polynomial( + element p[], size_t degree, const crypto::EntropyPtr& entropy) + { + for (size_t i = 0; i <= degree; i++) + { + p[i] = sample(entropy); + } + } + + static element eval(element p[], size_t degree, element x) + { + element y = 0, x_i = 1; + for (size_t i = 0; i <= degree; i++) + { + // x_i == x^i + y = add(y, mul(p[i], x_i)); + x_i = mul(x, x_i); + } + return y; + } + + void sample_secret_and_shares( + Share& raw_secret, const std::span& shares, size_t threshold) + { + if (shares.size() < 1) + { + throw std::invalid_argument("insufficient number of shares"); + } + + if (threshold < 1 || threshold > shares.size()) + { + throw std::invalid_argument("invalid threshold"); + } + + size_t degree = threshold - 1; + + raw_secret.x = 0; + for (size_t s = 0; s < shares.size(); s++) + { + shares[s].x = s + 1; + } + + auto entropy = crypto::create_entropy(); + + for (size_t limb = 0; limb < LIMBS; limb++) + { + element p[degree + 1]; /*SECRET*/ + sample_polynomial(p, degree, entropy); + raw_secret.y[limb] = p[0]; + for (size_t s = 0; s < shares.size(); s++) + { + shares[s].y[limb] = eval(p, degree, shares[s].x); + } + } + } + + void recover_unauthenticated_secret( + Share& raw_secret, const std::span& shares, size_t threshold) + { + if (shares.size() < threshold) + { + throw std::invalid_argument("insufficient input shares"); + } + // We systematically reduce the input shares instead of checking they are + // well-formed. + + size_t degree = threshold - 1; + + // Precomputes Lagrange coefficients for interpolating p(0). No secrets + // involved. + element lagrange[degree + 1]; + for (size_t i = 0; i <= degree; i++) + { + element numerator = 1, denominator = 1; + for (size_t j = 0; j <= degree; j++) + { + if (i != j) + { + numerator = mul(numerator, reduce(shares[j].x)); + denominator = + mul(denominator, sub(reduce(shares[j].x), reduce(shares[i].x))); + } + } + if (denominator == 0) + { + throw std::invalid_argument("duplicate input share"); + } + lagrange[i] = mul(numerator, inv(denominator)); + } + + // Interpolate every limb of the secret. Constant-time on y values. + raw_secret.x = 0; + for (size_t limb = 0; limb < LIMBS; limb++) + { + element y = 0; + for (size_t i = 0; i <= degree; i++) + { + y = add(y, mul(lagrange[i], reduce(shares[i].y[limb]))); + } + raw_secret.y[limb] = y; + } + } +} diff --git a/src/crypto/sharing.h b/src/crypto/sharing.h new file mode 100644 index 000000000000..6e60c1f8669c --- /dev/null +++ b/src/crypto/sharing.h @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include +#include +#include + +#define FMT_HEADER_ONLY +#include "ccf/crypto/hkdf.h" +#include "ccf/crypto/sha256.h" +#include "ds/serialized.h" +#include "openssl/crypto.h" + +#include + +namespace crypto +{ + static constexpr size_t LIMBS = 10; // = ((256+80)/31) + static constexpr const char* key_label = "CCF Wrapping Key v1"; + + struct Share + { + // Index in a re-share, 0 is a full key, and 1+ is a partial share + uint32_t x = 0; + uint32_t y[LIMBS]; + constexpr static size_t serialised_size = + sizeof(uint32_t) + sizeof(uint32_t) * LIMBS; + + Share() = default; + bool operator==(const Share& other) const = default; + + ~Share() + { + OPENSSL_cleanse(y, sizeof(y)); + }; + + HashBytes key(size_t key_size) const + { + if (x != 0) + { + throw std::invalid_argument("Cannot derive a key from a partial share"); + } + const std::span ikm( + reinterpret_cast(y), sizeof(y)); + const std::span label( + reinterpret_cast(y), sizeof(y)); + auto k = crypto::hkdf(crypto::MDType::SHA256, key_size, ikm, {}, label); + return k; + } + + std::vector serialise() const + { + auto size = serialised_size; + std::vector serialised(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& serialised) + { + if (serialised.size() != serialised_size) + { + throw std::invalid_argument("Invalid serialised share size"); + } + auto data = serialised.data(); + auto size = serialised.size(); + x = serialized::read(data, size); + for (size_t i = 0; i < LIMBS; ++i) + { + y[i] = serialized::read(data, size); + } + } + + std::string to_str() const + { + return fmt::format("x: {} y: {}", x, fmt::join(y, ", ")); + } + }; + + /** Sample a secret into @p raw_secret, and split it into @p output. + * Enforces 1 < @p threshold <= number of shares. + * + * @param[out] raw_secret sampled secret value + * @param[out] shares shares of raw_secret + * @param threshold number of shares necessary to recover the secret + * + * Note that is it not safe to use the secret as a key directly, + * and that a round of key derivation is necessary (Share::key()). + */ + void sample_secret_and_shares( + Share& raw_secret, const std::span& shares, size_t threshold); + + /** Using @p shares, recover @p secret, without authentication. + * + * @param[out] raw_secret recovered secret value + * @param[in] shares shares of raw_secret + * @param threshold number of shares necessary to recover the secret + * + * @throws std::invalid_argument if the number of shares is insufficient, + * or if two shares have the same x coordinate. + */ + void recover_unauthenticated_secret( + Share& raw_secret, const std::span& shares, size_t threshold); +} \ No newline at end of file diff --git a/src/crypto/test/bench.cpp b/src/crypto/test/bench.cpp index 98a8cc2e0fb0..75bf2ee37d04 100644 --- a/src/crypto/test/bench.cpp +++ b/src/crypto/test/bench.cpp @@ -12,6 +12,7 @@ #include "crypto/openssl/hash.h" #include "crypto/openssl/key_pair.h" #include "crypto/openssl/rsa_key_pair.h" +#include "crypto/sharing.h" #define PICOBENCH_IMPLEMENT_WITH_MAIN #include @@ -444,4 +445,77 @@ namespace HMAC_bench auto openssl_hmac_sha256_64 = benchmark_hmac; PICOBENCH(openssl_hmac_sha256_64).PICO_HASH_SUFFIX(); +} + +std::vector shares; + +PICOBENCH_SUITE("share"); +namespace SHARE_bench +{ + template + static void benchmark_share(picobench::state& s) + { + shares.resize(nshares); + + s.start_timer(); + for (auto _ : s) + { + (void)_; + crypto::Share secret; + crypto::sample_secret_and_shares(secret, shares, threshold); + do_not_optimize(secret); + clobber_memory(); + } + s.stop_timer(); + } + + auto share_10s_d1 = benchmark_share<10, 1>; + auto share_100s_d1 = benchmark_share<100, 1>; + auto share_1000s_d1 = benchmark_share<1000, 1>; + + PICOBENCH(share_10s_d1).PICO_SUFFIX(); + PICOBENCH(share_100s_d1).PICO_SUFFIX(); + PICOBENCH(share_1000s_d1).PICO_SUFFIX(); + + auto share_10s_d5 = benchmark_share<10, 5>; + auto share_100s_d5 = benchmark_share<100, 5>; + auto share_1000s_d5 = benchmark_share<1000, 5>; + + PICOBENCH(share_10s_d5).PICO_SUFFIX(); + PICOBENCH(share_100s_d5).PICO_SUFFIX(); + PICOBENCH(share_1000s_d5).PICO_SUFFIX(); + + template + static void benchmark_share_and_recover(picobench::state& s) + { + shares.resize(nshares); + + s.start_timer(); + for (auto _ : s) + { + (void)_; + crypto::Share secret; + crypto::sample_secret_and_shares(secret, shares, threshold); + crypto::recover_unauthenticated_secret(secret, shares, threshold); + do_not_optimize(secret); + clobber_memory(); + } + s.stop_timer(); + } + + auto share_n_recover_10s_d1 = benchmark_share_and_recover<10, 1>; + auto share_n_recover_100s_d1 = benchmark_share_and_recover<100, 1>; + auto share_n_recover_1000s_d1 = benchmark_share_and_recover<1000, 1>; + + PICOBENCH(share_n_recover_10s_d1).PICO_SUFFIX(); + PICOBENCH(share_n_recover_100s_d1).PICO_SUFFIX(); + PICOBENCH(share_n_recover_1000s_d1).PICO_SUFFIX(); + + auto share_n_recover_10s_d5 = benchmark_share_and_recover<10, 5>; + auto share_n_recover_100s_d5 = benchmark_share_and_recover<100, 5>; + auto share_n_recover_1000s_d5 = benchmark_share_and_recover<1000, 5>; + + PICOBENCH(share_n_recover_10s_d5).PICO_SUFFIX(); + PICOBENCH(share_n_recover_100s_d5).PICO_SUFFIX(); + PICOBENCH(share_n_recover_1000s_d5).PICO_SUFFIX(); } \ No newline at end of file diff --git a/src/crypto/test/secret_sharing.cpp b/src/crypto/test/secret_sharing.cpp new file mode 100644 index 000000000000..4a9f1a04fd15 --- /dev/null +++ b/src/crypto/test/secret_sharing.cpp @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include + +#define FMT_HEADER_ONLY +#include "crypto/sharing.h" + +#include + +using namespace crypto; + +void share_and_recover(size_t num_shares, size_t threshold, size_t recoveries) +{ + std::vector shares(num_shares); + + Share secret; + sample_secret_and_shares(secret, shares, threshold); + + std::mt19937 rng{std::random_device{}()}; + + for (size_t i = 0; i < recoveries; ++i) + { + std::vector recovered_shares; + std::sample( + shares.begin(), + shares.end(), + std::back_inserter(recovered_shares), + threshold, + rng); + { + Share recovered; + recover_unauthenticated_secret(recovered, recovered_shares, threshold); + INFO(fmt::format( + "Recovering secret with threshold {} from {} shares", + threshold, + recovered_shares.size())); + REQUIRE(secret == recovered); + } + + { + Share recovered; + recovered_shares.pop_back(); + INFO(fmt::format( + "Recovering secret with threshold {} from {} shares", + threshold - 1, + recovered_shares.size())); + REQUIRE_THROWS_AS( + recover_unauthenticated_secret(recovered, recovered_shares, threshold), + std::invalid_argument); + } + } +} + +TEST_CASE("The number of shares needs to allow recovery") +{ + std::vector shares(0); + + Share secret, recovered; + REQUIRE_THROWS_AS( + sample_secret_and_shares(secret, shares, 3), std::invalid_argument); + + shares.resize(3); + REQUIRE_THROWS_AS( + sample_secret_and_shares(secret, shares, 4), std::invalid_argument); +} + +TEST_CASE("Simple sharing and recovery") +{ + constexpr size_t num_shares = 10; + constexpr size_t threshold = 3; + + std::vector shares(num_shares); + + Share secret, recovered; + sample_secret_and_shares(secret, shares, threshold); + + recover_unauthenticated_secret(recovered, shares, threshold); + REQUIRE(secret == recovered); +} + +TEST_CASE("Simple sharing and recovery with duplicate shares") +{ + constexpr size_t num_shares = 10; + constexpr size_t threshold = 3; + + std::vector shares(num_shares); + + Share secret, recovered; + sample_secret_and_shares(secret, shares, threshold); + + std::vector shares_with_duplicates(threshold, shares[0]); + REQUIRE_THROWS_AS( + recover_unauthenticated_secret( + recovered, shares_with_duplicates, threshold), + std::invalid_argument); +} + +TEST_CASE("Cover a range of share and recover combinations") +{ + // Shares, Degree, Recoveries + share_and_recover(1, 1, 1); + share_and_recover(5, 1, 8); + share_and_recover(10, 3, 8); + share_and_recover(99, 6, 8); + share_and_recover(30000, 100, 8); + share_and_recover(200000, 400, 8); +} + +TEST_CASE("Serialisation") +{ + Share share; + share.x = 42; + share.y[0] = 34; + share.y[1] = 0; + share.y[2] = 1; + share.y[3] = 2; + share.y[4] = 3; + share.y[5] = 4; + share.y[6] = 5; + share.y[7] = 6; + share.y[8] = 7; + share.y[9] = 56; + Share new_share(share.serialise()); + + INFO(share.to_str()); + INFO(new_share.to_str()); + + REQUIRE(share == new_share); +} \ No newline at end of file diff --git a/src/node/channels.h b/src/node/channels.h index 6a857893dc33..06ca979d6c78 100644 --- a/src/node/channels.h +++ b/src/node/channels.h @@ -762,12 +762,14 @@ namespace ccf void update_send_key() { const std::string label_to = self.value() + peer_id.value(); + const std::span label( + reinterpret_cast(label_to.c_str()), label_to.size()); const auto key_bytes = crypto::hkdf( crypto::MDType::SHA256, shared_key_size, kex_ctx.get_shared_secret(), hkdf_salt, - {label_to.begin(), label_to.end()}); + label); send_key = crypto::make_key_aes_gcm(key_bytes); send_nonce = 1; @@ -776,12 +778,15 @@ namespace ccf void update_recv_key() { const std::string label_from = peer_id.value() + self.value(); + const std::span label( + reinterpret_cast(label_from.c_str()), + label_from.size()); const auto key_bytes = crypto::hkdf( crypto::MDType::SHA256, shared_key_size, kex_ctx.get_shared_secret(), hkdf_salt, - {label_from.begin(), label_from.end()}); + label); recv_key = crypto::make_key_aes_gcm(key_bytes); local_recv_nonce = 0; diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 72fef6c1359a..dec28b93033f 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -1149,8 +1149,8 @@ namespace ccf std::vector request_digest; if (cose_auth_id.has_value()) { - request_digest = crypto::sha256( - {cose_auth_id->signature.begin(), cose_auth_id->signature.end()}); + std::span sig = cose_auth_id->signature; + request_digest = crypto::sha256(sig); } ProposalId proposal_id; diff --git a/src/node/share_manager.h b/src/node/share_manager.h index a62ff5c8176a..16a160c7893a 100644 --- a/src/node/share_manager.h +++ b/src/node/share_manager.h @@ -4,8 +4,10 @@ #include "ccf/crypto/entropy.h" #include "ccf/crypto/rsa_key_pair.h" +#include "ccf/crypto/sha256.h" #include "ccf/crypto/symmetric_key.h" #include "ccf/ds/logger.h" +#include "crypto/sharing.h" #include "kv/encryptor.h" #include "ledger_secrets.h" #include "network_state.h" @@ -21,26 +23,69 @@ namespace ccf { private: static constexpr auto KZ_KEY_SIZE = crypto::GCM_DEFAULT_KEY_SIZE; - std::vector data; // Referred to as "kz" in TR bool has_wrapped = false; + size_t num_shares; + size_t recovery_threshold; + std::vector data; // Referred to as "kz" in TR + std::vector shares; public: - LedgerSecretWrappingKey() : - data(crypto::create_entropy()->random(KZ_KEY_SIZE)) - {} + LedgerSecretWrappingKey(size_t num_shares_, size_t recovery_threshold_) : + num_shares(num_shares_), + recovery_threshold(recovery_threshold_) + { + shares.resize(num_shares); + crypto::Share secret; + sample_secret_and_shares(secret, shares, recovery_threshold); + data = secret.key(KZ_KEY_SIZE); + } - template - LedgerSecretWrappingKey(T&& split_secret) : - data( - std::make_move_iterator(split_secret.begin()), - std::make_move_iterator(split_secret.begin() + split_secret.size())) - {} + LedgerSecretWrappingKey( + std::vector&& shares_, size_t recovery_threshold_) : + recovery_threshold(recovery_threshold_) + { + shares = shares_; + crypto::Share secret; + crypto::recover_unauthenticated_secret( + secret, shares, recovery_threshold); + data = secret.key(KZ_KEY_SIZE); + } + + LedgerSecretWrappingKey( + std::vector&& shares_, size_t recovery_threshold_) : + recovery_threshold(recovery_threshold_) + { + auto secret = SecretSharing::combine(shares_, shares_.size()); + data.resize(secret.size()); + std::copy_n(secret.begin(), secret.size(), data.begin()); + OPENSSL_cleanse(secret.data(), secret.size()); + } ~LedgerSecretWrappingKey() { OPENSSL_cleanse(data.data(), data.size()); } + size_t get_num_shares() const + { + return num_shares; + } + + size_t get_recovery_threshold() const + { + return recovery_threshold; + } + + std::vector> get_shares() const + { + std::vector> shares_; + for (const crypto::Share& share : shares) + { + shares_.emplace_back(share.serialise()); + } + return shares_; + } + template T get_raw_data() const { @@ -113,10 +158,29 @@ namespace ccf kv::Tx& tx, const LedgerSecretWrappingKey& ls_wrapping_key) { EncryptedSharesMap encrypted_shares; + auto shares = ls_wrapping_key.get_shares(); + + GenesisGenerator g(network, tx); + auto active_recovery_members_info = g.get_active_recovery_members(); + + size_t share_index = 0; + for (auto const& [member_id, enc_pub_key] : active_recovery_members_info) + { + auto member_enc_pubk = crypto::make_rsa_public_key(enc_pub_key); + auto raw_share = std::vector( + shares[share_index].begin(), shares[share_index].end()); + encrypted_shares[member_id] = member_enc_pubk->rsa_oaep_wrap(raw_share); + OPENSSL_cleanse(raw_share.data(), raw_share.size()); + OPENSSL_cleanse(shares[share_index].data(), shares[share_index].size()); + share_index++; + } - auto secret_to_split = - ls_wrapping_key.get_raw_data(); + return encrypted_shares; + } + void shuffle_recovery_shares( + kv::Tx& tx, const LedgerSecretPtr& latest_ledger_secret) + { GenesisGenerator g(network, tx); auto active_recovery_members_info = g.get_active_recovery_members(); size_t recovery_threshold = g.get_recovery_threshold(); @@ -144,32 +208,10 @@ namespace ccf active_recovery_members_info.size())); } - auto shares = SecretSharing::split( - secret_to_split, - active_recovery_members_info.size(), - recovery_threshold); + const auto num_shares = active_recovery_members_info.size(); + auto ls_wrapping_key = + LedgerSecretWrappingKey(num_shares, recovery_threshold); - size_t share_index = 0; - for (auto const& [member_id, enc_pub_key] : active_recovery_members_info) - { - auto member_enc_pubk = crypto::make_rsa_public_key(enc_pub_key); - auto raw_share = std::vector( - shares[share_index].begin(), shares[share_index].end()); - encrypted_shares[member_id] = member_enc_pubk->rsa_oaep_wrap(raw_share); - OPENSSL_cleanse(raw_share.data(), raw_share.size()); - OPENSSL_cleanse(shares[share_index].data(), shares[share_index].size()); - share_index++; - } - - OPENSSL_cleanse(secret_to_split.data(), secret_to_split.size()); - - return encrypted_shares; - } - - void shuffle_recovery_shares( - kv::Tx& tx, const LedgerSecretPtr& latest_ledger_secret) - { - auto ls_wrapping_key = LedgerSecretWrappingKey(); auto wrapped_latest_ls = ls_wrapping_key.wrap(latest_ledger_secret); auto recovery_shares = tx.rw(network.shares); recovery_shares->put( @@ -270,34 +312,73 @@ namespace ccf tx.rw(network.encrypted_submitted_shares); auto config = tx.rw(network.config); - std::vector shares = {}; + std::vector new_shares = {}; + std::vector old_shares = {}; + // Defensively allow shares in both formats for the time being, even if we + // get a mix, and so long as we have enough of one or the other, attempt + // to reassemble the secret. We only try with the most numerous kind of + // share, we won't try with the minority even if it meets the threshold + // too. encrypted_submitted_shares->foreach( - [&shares, &tx, this]( + [&new_shares, &old_shares, &tx, this]( const MemberId, const EncryptedSubmittedShare& encrypted_share) { - SecretSharing::Share share; auto decrypted_share = decrypt_submitted_share( encrypted_share, network.ledger_secrets->get_latest(tx).second); - std::copy_n( - decrypted_share.begin(), - SecretSharing::SHARE_LENGTH, - share.begin()); + switch (decrypted_share.size()) + { + case crypto::Share::serialised_size: + { + new_shares.emplace_back(decrypted_share); + break; + } + case SecretSharing::SHARE_LENGTH: + { + SecretSharing::Share share; + std::copy_n( + decrypted_share.begin(), + SecretSharing::SHARE_LENGTH, + share.begin()); + old_shares.emplace_back(std::move(share)); + break; + } + default: + { + OPENSSL_cleanse(decrypted_share.data(), decrypted_share.size()); + throw std::logic_error(fmt::format( + "Error combining recovery shares: decrypted share of {} bytes " + "is neither a new-style share of {} bytes nor an old-style " + "share of {} bytes", + decrypted_share.size(), + crypto::Share::serialised_size, + SecretSharing::SHARE_LENGTH)); + } + } OPENSSL_cleanse(decrypted_share.data(), decrypted_share.size()); - shares.emplace_back(share); return true; }); + auto num_shares = std::max(old_shares.size(), new_shares.size()); + auto recovery_threshold = config->get()->recovery_threshold; - if (recovery_threshold > shares.size()) + if (recovery_threshold > num_shares) { throw std::logic_error(fmt::format( "Error combining recovery shares: only {} recovery shares were " "submitted but recovery threshold is {}", - shares.size(), + num_shares, recovery_threshold)); } - return LedgerSecretWrappingKey( - SecretSharing::combine(shares, shares.size())); + if (new_shares.size() > old_shares.size()) + { + return LedgerSecretWrappingKey( + std::move(new_shares), recovery_threshold); + } + else + { + return LedgerSecretWrappingKey( + std::move(old_shares), recovery_threshold); + } } public: @@ -387,7 +468,6 @@ namespace ccf auto restored_ls = combine_from_encrypted_submitted_shares(tx).unwrap( recovery_shares_info->wrapped_latest_ledger_secret); - auto decryption_key = restored_ls->raw_key; LOG_DEBUG_FMT( "Recovering {} encrypted ledger secrets", diff --git a/tests/perf-system/submitter/CMakeLists.txt b/tests/perf-system/submitter/CMakeLists.txt index 1ee88e90babb..11cc8ada93be 100644 --- a/tests/perf-system/submitter/CMakeLists.txt +++ b/tests/perf-system/submitter/CMakeLists.txt @@ -25,6 +25,7 @@ set(CCFCRYPTO_SRC ${CCF_DIR}/src/crypto/openssl/rsa_public_key.cpp ${CCF_DIR}/src/crypto/openssl/rsa_key_pair.cpp ${CCF_DIR}/src/crypto/openssl/verifier.cpp + ${CCF_DIR}/src/crypto/sharing.cpp ) add_library(stdcxxccfcrypto.host STATIC "${CCFCRYPTO_SRC}")