From 7512f539909226b8f3ea0827ac7a3cee6eb638a6 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 9 Jul 2021 15:10:36 +0100 Subject: [PATCH 001/105] Add new table for endorsed node cert --- src/node/entities.h | 2 ++ src/node/network_tables.h | 2 ++ src/node/nodes.h | 9 +++++++++ 3 files changed, 13 insertions(+) diff --git a/src/node/entities.h b/src/node/entities.h index 991b5314aa6..ff630f8610e 100644 --- a/src/node/entities.h +++ b/src/node/entities.h @@ -68,6 +68,8 @@ namespace ccf static constexpr auto NODE_CODE_IDS = "public:ccf.gov.nodes.code_ids"; static constexpr auto NETWORK_CONFIGURATIONS = "public:ccf.gov.network.configurations"; + static constexpr auto NODE_ENDORSED_CERTIFICATES = + "public:ccf.gov.nodes.endorsed_certificates"; // Service information static constexpr auto SERVICE = "public:ccf.gov.service.info"; diff --git a/src/node/network_tables.h b/src/node/network_tables.h index e59d6a9d643..682315d6163 100644 --- a/src/node/network_tables.h +++ b/src/node/network_tables.h @@ -81,6 +81,7 @@ namespace ccf // Nodes nodes; NetworkConfigurations network_configurations; + NodeEndorsedCertificates node_endorsed_certificates; // // Internal CCF tables @@ -130,6 +131,7 @@ namespace ccf user_info(Tables::USER_INFO), nodes(Tables::NODES), network_configurations(Tables::NETWORK_CONFIGURATIONS), + node_endorsed_certificates(Tables::NODE_ENDORSED_CERTIFICATES), service(Tables::SERVICE), values(Tables::VALUES), secrets(Tables::ENCRYPTED_LEDGER_SECRETS), diff --git a/src/node/nodes.h b/src/node/nodes.h index b407bb0a768..8bdb5facbec 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -35,6 +35,7 @@ namespace ccf { struct NodeInfo : NodeInfoNetwork { + // TODO: Replace with node's public key in PEM format /// Node certificate crypto::Pem cert; /// Node enclave quote @@ -57,6 +58,14 @@ namespace ccf DECLARE_JSON_OPTIONAL_FIELDS(NodeInfo, ledger_secret_seqno, code_digest); using Nodes = ServiceMap; + + struct NodeEndorsedCertificate + { + crypto::Pem endorsed_certificate; + }; + DECLARE_JSON_TYPE(NodeEndorsedCertificate); + DECLARE_JSON_REQUIRED_FIELDS(NodeEndorsedCertificate, endorsed_certificate); + using NodeEndorsedCertificates = ServiceMap; } FMT_BEGIN_NAMESPACE From 706c294d626c3e05cb04d729cb48cf0a19421757 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 9 Jul 2021 15:46:37 +0100 Subject: [PATCH 002/105] Refactor san/cn into unique struct --- src/crypto/mbedtls/public_key.h | 1 - src/crypto/san.h | 12 ++++++++++++ src/enclave/interface.h | 6 ++---- src/host/main.cpp | 13 ++++++++----- src/node/node_state.h | 8 +++++--- src/node/rpc/node_call_types.h | 2 ++ src/node/rpc/serialization.h | 3 ++- src/tls/test/cert.cpp | 13 +++++++------ 8 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/crypto/mbedtls/public_key.h b/src/crypto/mbedtls/public_key.h index e5eef5a24c0..84027ef2352 100644 --- a/src/crypto/mbedtls/public_key.h +++ b/src/crypto/mbedtls/public_key.h @@ -3,7 +3,6 @@ #pragma once #include "../public_key.h" - #include "../san.h" #include "mbedtls_wrappers.h" diff --git a/src/crypto/san.h b/src/crypto/san.h index a3c63bd4375..ee2616e73ed 100644 --- a/src/crypto/san.h +++ b/src/crypto/san.h @@ -6,6 +6,7 @@ #include +// TODO: Rename file? namespace crypto { struct SubjectAltName @@ -15,4 +16,15 @@ namespace crypto }; DECLARE_JSON_TYPE(SubjectAltName); DECLARE_JSON_REQUIRED_FIELDS(SubjectAltName, san, is_ip); + + struct CertificateSubjectIdentity + { + std::vector sans; + std::string name; + + CertificateSubjectIdentity() = default; + CertificateSubjectIdentity(const std::string& name) : name(name) {} + }; + DECLARE_JSON_TYPE(CertificateSubjectIdentity); + DECLARE_JSON_REQUIRED_FIELDS(CertificateSubjectIdentity, sans, name); } diff --git a/src/enclave/interface.h b/src/enclave/interface.h index 53f661c2ff8..4d46a0df2af 100644 --- a/src/enclave/interface.h +++ b/src/enclave/interface.h @@ -78,8 +78,7 @@ struct CCFConfig }; Joining joining = {}; - std::string subject_name; - std::vector subject_alternative_names; + crypto::CertificateSubjectIdentity node_certificate_subject_identity; size_t jwt_key_refresh_interval_s; @@ -112,8 +111,7 @@ DECLARE_JSON_REQUIRED_FIELDS( signature_intervals, genesis, joining, - subject_name, - subject_alternative_names, + node_certificate_subject_identity, jwt_key_refresh_interval_s, curve_id); diff --git a/src/host/main.cpp b/src/host/main.cpp index b1c2c0567fd..ea65c0feaaa 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -340,16 +340,19 @@ int main(int argc, char** argv) app.add_option( "--domain", domain, "DNS to use for TLS certificate validation"); - std::string subject_name("CN=CCF Node"); + crypto::CertificateSubjectIdentity node_certificate_subject_identity( + "CN=CCF Node"); app .add_option( - "--sn", subject_name, "Subject Name in node certificate, eg. CN=CCF Node") + "--sn", + node_certificate_subject_identity.name, + "Subject Name in node certificate, eg. CN=CCF Node") ->capture_default_str(); std::vector subject_alternative_names; cli::add_subject_alternative_name_option( app, - subject_alternative_names, + node_certificate_subject_identity.sans, "--san", "Subject Alternative Name in node certificate. Can be either " "iPAddress:xxx.xxx.xxx.xxx, or dNSName:sub.domain.tld"); @@ -725,8 +728,8 @@ int main(int argc, char** argv) ccf_config.max_open_sessions_soft = max_open_sessions; ccf_config.max_open_sessions_hard = max_open_sessions_hard; - ccf_config.subject_name = subject_name; - ccf_config.subject_alternative_names = subject_alternative_names; + ccf_config.node_certificate_subject_identity = + node_certificate_subject_identity; ccf_config.jwt_key_refresh_interval_s = jwt_key_refresh_interval_s; diff --git a/src/node/node_state.h b/src/node/node_state.h index 3670030fc23..112c0528e01 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1582,7 +1582,7 @@ namespace ccf std::vector get_subject_alternative_names() { std::vector sans = - config.subject_alternative_names; + config.node_certificate_subject_identity.sans; sans.push_back(get_subject_alt_name()); return sans; } @@ -1590,13 +1590,15 @@ namespace ccf Pem create_self_signed_node_cert() { auto sans = get_subject_alternative_names(); - return node_sign_kp->self_sign(config.subject_name, sans); + return node_sign_kp->self_sign( + config.node_certificate_subject_identity.name, sans); } Pem create_endorsed_node_cert() { auto nw = crypto::make_key_pair(network.identity->priv_key); - auto csr = node_sign_kp->create_csr(config.subject_name); + auto csr = + node_sign_kp->create_csr(config.node_certificate_subject_identity.name); auto sans = get_subject_alternative_names(); return nw->sign_csr(network.identity->cert, csr, sans); } diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 300978287bb..0c08fcd39be 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -81,6 +81,8 @@ namespace ccf crypto::Pem public_encryption_key; ConsensusType consensus_type = ConsensusType::CFT; std::optional startup_seqno = std::nullopt; + std::optional + certificate_subject_identity = std::nullopt; }; struct Out diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index 17dd728386d..f85c87df431 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -35,7 +35,8 @@ namespace ccf quote_info, public_encryption_key, consensus_type, - startup_seqno) + startup_seqno, + certificate_subject_identity) DECLARE_JSON_TYPE(NetworkIdentity) DECLARE_JSON_REQUIRED_FIELDS(NetworkIdentity, cert, priv_key) diff --git a/src/tls/test/cert.cpp b/src/tls/test/cert.cpp index f51bd4fa1ed..fa3f01c1ec8 100644 --- a/src/tls/test/cert.cpp +++ b/src/tls/test/cert.cpp @@ -8,16 +8,17 @@ int main(int argc, char** argv) { CLI::App app{"Cert creation"}; - std::string subject_name; + crypto::CertificateSubjectIdentity cert_subject_identity; app .add_option( - "--sn", subject_name, "Subject Name in node certificate, eg. CN=CCF Node") + "--sn", + cert_subject_identity.name, + "Subject Name in node certificate, eg. CN=CCF Node") ->capture_default_str(); - std::vector subject_alternative_names; cli::add_subject_alternative_name_option( app, - subject_alternative_names, + cert_subject_identity.sans, "--san", "Subject Alternative Name in node certificate. Can be either " "iPAddress:xxx.xxx.xxx.xxx, or dNSName:sub.domain.tld"); @@ -25,8 +26,8 @@ int main(int argc, char** argv) auto kp = crypto::make_key_pair(); auto icrt = kp->self_sign("CN=issuer"); - auto csr = kp->create_csr(subject_name); - auto cert = kp->sign_csr(icrt, csr, subject_alternative_names); + auto csr = kp->create_csr(cert_subject_identity.name); + auto cert = kp->sign_csr(icrt, csr, cert_subject_identity.sans); std::cout << cert.str() << std::endl; return 0; From 753fec18a5e58dcb3b6dde3cb4d317d763196b5f Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 9 Jul 2021 17:09:27 +0100 Subject: [PATCH 003/105] WIP something is wrong with ledger --- src/crypto/san.h | 22 +++++++++++++++++++++- src/node/nodes.h | 11 +++++++++-- src/node/rpc/member_frontend.h | 3 ++- src/node/rpc/node_frontend.h | 3 ++- src/node/rpc/serialization.h | 7 ++++--- 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/crypto/san.h b/src/crypto/san.h index ee2616e73ed..2c0f3e1bb4e 100644 --- a/src/crypto/san.h +++ b/src/crypto/san.h @@ -13,17 +13,37 @@ namespace crypto { std::string san; bool is_ip; + + bool operator==(const SubjectAltName& other) const + { + return san == other.san && is_ip == other.is_ip; + } + + bool operator!=(const SubjectAltName& other) const + { + return !(*this == other); + } }; DECLARE_JSON_TYPE(SubjectAltName); DECLARE_JSON_REQUIRED_FIELDS(SubjectAltName, san, is_ip); struct CertificateSubjectIdentity { - std::vector sans; + std::vector sans = {}; std::string name; CertificateSubjectIdentity() = default; CertificateSubjectIdentity(const std::string& name) : name(name) {} + + bool operator==(const CertificateSubjectIdentity& other) const + { + return sans == other.sans && name == other.name; + } + + bool operator!=(const CertificateSubjectIdentity& other) const + { + return !(*this == other); + } }; DECLARE_JSON_TYPE(CertificateSubjectIdentity); DECLARE_JSON_REQUIRED_FIELDS(CertificateSubjectIdentity, sans, name); diff --git a/src/node/nodes.h b/src/node/nodes.h index 8bdb5facbec..7d9971f6107 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -3,6 +3,7 @@ #pragma once #include "ccf/entity_id.h" +#include "crypto/san.h" #include "entities.h" #include "kv/map.h" #include "node_info_network.h" @@ -50,12 +51,18 @@ namespace ccf std::optional ledger_secret_seqno = std::nullopt; /** Code identity for the node **/ - std::optional code_digest; + std::optional code_digest = std::nullopt; + + // TODO: Move elsewhere? + /** Node certificate subject identity */ + std::optional + certificate_subject_identity = std::nullopt; }; DECLARE_JSON_TYPE_WITH_BASE_AND_OPTIONAL_FIELDS(NodeInfo, NodeInfoNetwork); DECLARE_JSON_REQUIRED_FIELDS( NodeInfo, cert, quote_info, encryption_pub_key, status); - DECLARE_JSON_OPTIONAL_FIELDS(NodeInfo, ledger_secret_seqno, code_digest); + DECLARE_JSON_OPTIONAL_FIELDS( + NodeInfo, ledger_secret_seqno, code_digest, certificate_subject_identity); using Nodes = ServiceMap; diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index ace0093d5fb..8d690a817da 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -842,7 +842,8 @@ namespace ccf in.public_encryption_key, NodeStatus::TRUSTED, std::nullopt, - ds::to_hex(in.code_digest.data)}); + ds::to_hex(in.code_digest.data), + std::nullopt}); #ifdef GET_QUOTE g.trust_node_code_id(in.code_digest); diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 84829567634..ca6bea8ea24 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -190,7 +190,8 @@ namespace ccf in.public_encryption_key, node_status, ledger_secret_seqno, - ds::to_hex(code_digest.data)}); + ds::to_hex(code_digest.data), + in.certificate_subject_identity}); kv::NetworkConfiguration nc = get_latest_network_configuration(network, tx); diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index f85c87df431..03f755d8339 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -28,15 +28,16 @@ namespace ccf DECLARE_JSON_TYPE(GetVersion::Out) DECLARE_JSON_REQUIRED_FIELDS(GetVersion::Out, ccf_version, quickjs_version) - DECLARE_JSON_TYPE(JoinNetworkNodeToNode::In) + DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JoinNetworkNodeToNode::In) DECLARE_JSON_REQUIRED_FIELDS( JoinNetworkNodeToNode::In, node_info_network, quote_info, public_encryption_key, consensus_type, - startup_seqno, - certificate_subject_identity) + startup_seqno) + DECLARE_JSON_OPTIONAL_FIELDS( + JoinNetworkNodeToNode::In, certificate_subject_identity) DECLARE_JSON_TYPE(NetworkIdentity) DECLARE_JSON_REQUIRED_FIELDS(NetworkIdentity, cert, priv_key) From 6d3d0adb8c4d59559876b86b8850570512320939 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 13 Jul 2021 11:39:50 +0100 Subject: [PATCH 004/105] Test read_ledger.py from e2e test infra --- python/ccf/read_ledger.py | 27 ++++++++++++++++++--------- tests/governance_history.py | 13 ++++++++++++- tests/suite/test_suite.py | 9 +++++++-- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/python/ccf/read_ledger.py b/python/ccf/read_ledger.py index 9db8abad61f..63e6b55aa12 100644 --- a/python/ccf/read_ledger.py +++ b/python/ccf/read_ledger.py @@ -73,14 +73,7 @@ def dump_entry(entry, table_filter): print_key(key_indent, key, is_removed=True) -if __name__ == "__main__": - - LOG.remove() - LOG.add( - sys.stdout, - format="{message}", - ) - +def main(args): parser = argparse.ArgumentParser(description="Read CCF ledger or snapshot") parser.add_argument( "paths", help="Path to ledger directories or snapshot file", nargs="+" @@ -101,7 +94,8 @@ def dump_entry(entry, table_filter): parser.add_argument( "--uncommitted", help="Also parse uncommitted ledger files", action="store_true" ) - args = parser.parse_args() + args = parser.parse_args(args) + table_filter = re.compile(args.tables) if args.snapshot: @@ -127,9 +121,24 @@ def dump_entry(entry, table_filter): dump_entry(transaction, table_filter) except Exception as e: LOG.exception(f"Error parsing ledger: {e}") + has_error = True else: LOG.success("Ledger verification complete") + has_error = False finally: LOG.info( f"Found {ledger.signature_count()} signatures, and verified until {ledger.last_verified_txid()}" ) + return not has_error + + +if __name__ == "__main__": + + LOG.remove() + LOG.add( + sys.stdout, + format="{message}", + ) + + if not main(sys.argv[1:]): + sys.exit(1) diff --git a/tests/governance_history.py b/tests/governance_history.py index 7563c3c8d2b..d16e9ba8bc5 100644 --- a/tests/governance_history.py +++ b/tests/governance_history.py @@ -13,6 +13,7 @@ import json from loguru import logger as LOG import suite.test_requirements as reqs +import ccf.read_ledger def check_operations(ledger, operations): @@ -86,7 +87,7 @@ def test_tables_doc(network, args): return network -@reqs.description("Test that all node's ledgers can be read") +@reqs.description("Test that all nodes' ledgers can be read") def test_ledger_is_readable(network, args): primary, backups = network.find_nodes() for node in (primary, *backups): @@ -99,6 +100,15 @@ def test_ledger_is_readable(network, args): return network +@reqs.description("Test that all nodes' ledgers can be read using read_ledger.py") +def test_read_ledger_utility(network, args): + primary, backups = network.find_nodes() + for node in (primary, *backups): + ledger_dirs = node.remote.ledger_paths() + assert ccf.read_ledger.main(ledger_dirs) + return network + + def run(args): # Keep track of governance operations that happened in the test governance_operations = set() @@ -164,6 +174,7 @@ def run(args): check_operations(ledger, governance_operations) test_ledger_is_readable(network, args) + test_read_ledger_utility(network, args) test_tables_doc(network, args) diff --git a/tests/suite/test_suite.py b/tests/suite/test_suite.py index d14d68abba7..e2b183e7e1d 100644 --- a/tests/suite/test_suite.py +++ b/tests/suite/test_suite.py @@ -107,13 +107,18 @@ # code update: code_update.test_verify_quotes, code_update.test_add_node_with_bad_code, - governance_history.test_ledger_is_readable, - governance_history.test_tables_doc, # curve migration: reconfiguration.test_change_curve, recovery.test, # jwt jwt_test.test_refresh_jwt_issuer, + # + # + # + # Below tests should always stay last + governance_history.test_ledger_is_readable, + governance_history.test_tables_doc, + governance_history.test_read_ledger_utility, ] suites["all"] = all_tests_suite From 5db7548a028591e492d0b2995c1f6874fbdf0150 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 13 Jul 2021 11:40:14 +0100 Subject: [PATCH 005/105] WIP --- src/crypto/verifier.cpp | 6 ++- src/js/wrap.cpp | 66 ++++++++++++++++++++++++++- src/node/node_state.h | 1 + src/node/nodes.h | 16 +++++-- src/node/rpc/member_frontend.h | 1 + src/node/rpc/node_frontend.h | 3 +- src/node/rpc/node_interface.h | 1 + src/runtime_config/default/actions.js | 13 ++++++ 8 files changed, 98 insertions(+), 9 deletions(-) diff --git a/src/crypto/verifier.cpp b/src/crypto/verifier.cpp index 02f70f2ff12..635ca68467b 100644 --- a/src/crypto/verifier.cpp +++ b/src/crypto/verifier.cpp @@ -2,7 +2,6 @@ // Licensed under the Apache 2.0 License. #include "crypto/mbedtls/verifier.h" - #include "crypto/openssl/verifier.h" #include "verifier.h" @@ -54,6 +53,11 @@ namespace crypto return make_unique_verifier(der)->public_key_der(); } + crypto::Pem public_key_pem_from_cert(const std::vector& der) + { + return make_unique_verifier(der)->public_key_pem(); + } + void check_is_cert(const CBuffer& der) { make_unique_verifier((std::vector)der); // throws on error diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index 07c8f63d5c2..1b647bf873f 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -1,7 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. -#include "js/wrap.h" - #include "ccf/tx_id.h" #include "ccf/version.h" #include "ds/logger.h" @@ -9,6 +7,7 @@ #include "js/conv.cpp" #include "js/crypto.cpp" #include "js/oe.cpp" +#include "js/wrap.h" #include "kv/untyped_map.h" #include "node/jwt.h" #include "node/rpc/call_types.h" @@ -489,6 +488,60 @@ namespace js return JS_UNDEFINED; } + JSValue js_network_generate_endorsed_certificate( + JSContext* ctx, + JSValueConst this_val, + int argc, + [[maybe_unused]] JSValueConst* argv) + { + if (argc != 2) + { + return JS_ThrowTypeError(ctx, "Passed %d arguments but expected 2", argc); + } + + auto node = static_cast( + JS_GetOpaque(this_val, node_class_id)); + + if (node == nullptr) + { + return JS_ThrowInternalError(ctx, "Node state is not set"); + } + + ////////// + + // TODO: + // 1. Parse arguments and verify type + // 2. Call into node state + auto public_key = argv[0]; + if (!JS_IsArray(ctx, args)) + { + return JS_ThrowTypeError(ctx, "First argument must be an array"); + } + + auto + + /////////// + + auto global_obj = JS_GetGlobalObject(ctx); + auto ccf = JS_GetPropertyStr(ctx, global_obj, "ccf"); + auto kv = JS_GetPropertyStr(ctx, ccf, "kv"); + + auto tx_ctx_ptr = static_cast(JS_GetOpaque(kv, kv_class_id)); + + if (tx_ctx_ptr->tx == nullptr) + { + return JS_ThrowInternalError( + ctx, "No transaction available to fetch latest ledger secret seqno"); + } + + JS_FreeValue(ctx, kv); + JS_FreeValue(ctx, ccf); + JS_FreeValue(ctx, global_obj); + + return JS_NewInt64( + ctx, network->ledger_secrets->get_latest(*tx_ctx_ptr->tx).first); + } + JSValue js_network_latest_ledger_secret_seqno( JSContext* ctx, JSValueConst this_val, @@ -1357,6 +1410,15 @@ namespace js js_node_trigger_recovery_shares_refresh, "triggerRecoverySharesRefresh", 0)); + JS_SetPropertyStr( + ctx, + node, + "generateEndorsedCertificate", + JS_NewCFunction( + ctx, + js_node_generate_endorsed_certificate, + "generateEndorsedCertificate", + 0)); } if (host_node_state != nullptr) diff --git a/src/node/node_state.h b/src/node/node_state.h index f3288a186f3..7e2113bf54c 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -990,6 +990,7 @@ namespace ccf } #endif + // TODO: Record SAN and public key as well! g.add_node( self, {node_info_network, diff --git a/src/node/nodes.h b/src/node/nodes.h index 7d9971f6107..9e3cde96f6f 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -36,8 +36,8 @@ namespace ccf { struct NodeInfo : NodeInfoNetwork { - // TODO: Replace with node's public key in PEM format - /// Node certificate + /** Node certificate. Only set for 1.x releases. Further releases record + * node identity in `public_key` field */ crypto::Pem cert; /// Node enclave quote QuoteInfo quote_info; @@ -53,16 +53,22 @@ namespace ccf /** Code identity for the node **/ std::optional code_digest = std::nullopt; - // TODO: Move elsewhere? - /** Node certificate subject identity */ + /// Node certificate subject identity std::optional certificate_subject_identity = std::nullopt; + + /** Public key. Only set from 2.x releases onwards. */ + std::optional public_key = std::nullopt; }; DECLARE_JSON_TYPE_WITH_BASE_AND_OPTIONAL_FIELDS(NodeInfo, NodeInfoNetwork); DECLARE_JSON_REQUIRED_FIELDS( NodeInfo, cert, quote_info, encryption_pub_key, status); DECLARE_JSON_OPTIONAL_FIELDS( - NodeInfo, ledger_secret_seqno, code_digest, certificate_subject_identity); + NodeInfo, + ledger_secret_seqno, + code_digest, + certificate_subject_identity, + public_key); using Nodes = ServiceMap; diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 2c8b8a6239d..3432142d2f9 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -839,6 +839,7 @@ namespace ccf // recovery member is added before the service is opened. g.init_configuration(in.configuration); + // TODO: Record SAN and public key as well g.add_node( in.node_id, {in.node_info_network, diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 02938e181d8..45dedf6ba8f 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -191,7 +191,8 @@ namespace ccf node_status, ledger_secret_seqno, ds::to_hex(code_digest.data), - in.certificate_subject_identity}); + in.certificate_subject_identity, + crypto::public_key_pem_from_cert(node_der)}); kv::NetworkConfiguration nc = get_latest_network_configuration(network, tx); diff --git a/src/node/rpc/node_interface.h b/src/node/rpc/node_interface.h index 031f1c956fb..186062a675c 100644 --- a/src/node/rpc/node_interface.h +++ b/src/node/rpc/node_interface.h @@ -56,5 +56,6 @@ namespace ccf CodeDigest& code_digest) = 0; virtual std::optional get_startup_snapshot_seqno() = 0; virtual SessionMetrics get_session_metrics() = 0; + // virtual void generate_endorsed_certificate() }; } \ No newline at end of file diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 20352454a53..e39f63d6cc9 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -776,6 +776,19 @@ const actions = new Map([ ccf.strToBuf(args.node_id), ccf.jsonCompatibleToBuf(nodeInfo) ); + + // Also endorse and record node certificate + // TODO: For now, assume that node public key is always present, which isn't true for 1.x! + let node_cert = {}; + node_cert.endorsed_certificate = + ccf.network.generateEndorsedCertificate( + nodeInfo.public_key, + nodeInfo.certificate_subject_identity + ); + ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( + ccf.strToBuf(args.node_id), + ccf.jsonCompatibleToBuf(node_cert) + ); } } ), From c57c59eb68ca0a8f5d68f392619af68c0bf3504b Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 13 Jul 2021 15:23:28 +0100 Subject: [PATCH 006/105] Store CSR of joiner in KV --- src/crypto/verifier.h | 3 + src/js/wrap.cpp | 125 +++++++++++++------------- src/node/node_state.h | 15 ++++ src/node/nodes.h | 12 ++- src/node/rpc/node_call_types.h | 1 + src/node/rpc/node_frontend.h | 1 + src/node/rpc/node_interface.h | 5 +- src/node/rpc/serialization.h | 4 +- src/runtime_config/default/actions.js | 20 ++--- 9 files changed, 110 insertions(+), 76 deletions(-) diff --git a/src/crypto/verifier.h b/src/crypto/verifier.h index c9156378f51..720b8c3b12a 100644 --- a/src/crypto/verifier.h +++ b/src/crypto/verifier.h @@ -229,5 +229,8 @@ namespace crypto std::vector public_key_der_from_cert( const std::vector& der); + crypto::Pem public_key_pem_from_cert( + const std::vector& der); + void check_is_cert(const CBuffer& der); } diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index 1b647bf873f..172aa8f8417 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -488,59 +488,60 @@ namespace js return JS_UNDEFINED; } - JSValue js_network_generate_endorsed_certificate( - JSContext* ctx, - JSValueConst this_val, - int argc, - [[maybe_unused]] JSValueConst* argv) - { - if (argc != 2) - { - return JS_ThrowTypeError(ctx, "Passed %d arguments but expected 2", argc); - } - - auto node = static_cast( - JS_GetOpaque(this_val, node_class_id)); - - if (node == nullptr) - { - return JS_ThrowInternalError(ctx, "Node state is not set"); - } - - ////////// - - // TODO: - // 1. Parse arguments and verify type - // 2. Call into node state - auto public_key = argv[0]; - if (!JS_IsArray(ctx, args)) - { - return JS_ThrowTypeError(ctx, "First argument must be an array"); - } - - auto - - /////////// - - auto global_obj = JS_GetGlobalObject(ctx); - auto ccf = JS_GetPropertyStr(ctx, global_obj, "ccf"); - auto kv = JS_GetPropertyStr(ctx, ccf, "kv"); - - auto tx_ctx_ptr = static_cast(JS_GetOpaque(kv, kv_class_id)); - - if (tx_ctx_ptr->tx == nullptr) - { - return JS_ThrowInternalError( - ctx, "No transaction available to fetch latest ledger secret seqno"); - } - - JS_FreeValue(ctx, kv); - JS_FreeValue(ctx, ccf); - JS_FreeValue(ctx, global_obj); - - return JS_NewInt64( - ctx, network->ledger_secrets->get_latest(*tx_ctx_ptr->tx).first); - } + // JSValue js_node_generate_endorsed_certificate( + // JSContext* ctx, + // JSValueConst this_val, + // int argc, + // [[maybe_unused]] JSValueConst* argv) + // { + // if (argc != 2) + // { + // return JS_ThrowTypeError(ctx, "Passed %d arguments but expected 2", + // argc); + // } + + // auto node = static_cast( + // JS_GetOpaque(this_val, node_class_id)); + + // if (node == nullptr) + // { + // return JS_ThrowInternalError(ctx, "Node state is not set"); + // } + + // ////////// + + // // TODO: + // // 1. Parse arguments and verify type + // // 2. Call into node state + // auto public_key = argv[0]; + // if (!JS_IsArray(ctx, args)) + // { + // return JS_ThrowTypeError(ctx, "First argument must be an array"); + // } + + // auto + + // /////////// + + // auto global_obj = JS_GetGlobalObject(ctx); + // auto ccf = JS_GetPropertyStr(ctx, global_obj, "ccf"); + // auto kv = JS_GetPropertyStr(ctx, ccf, "kv"); + + // auto tx_ctx_ptr = static_cast(JS_GetOpaque(kv, kv_class_id)); + + // if (tx_ctx_ptr->tx == nullptr) + // { + // return JS_ThrowInternalError( + // ctx, "No transaction available to fetch latest ledger secret seqno"); + // } + + // JS_FreeValue(ctx, kv); + // JS_FreeValue(ctx, ccf); + // JS_FreeValue(ctx, global_obj); + + // return JS_NewInt64( + // ctx, network->ledger_secrets->get_latest(*tx_ctx_ptr->tx).first); + // } JSValue js_network_latest_ledger_secret_seqno( JSContext* ctx, @@ -1410,15 +1411,15 @@ namespace js js_node_trigger_recovery_shares_refresh, "triggerRecoverySharesRefresh", 0)); - JS_SetPropertyStr( - ctx, - node, - "generateEndorsedCertificate", - JS_NewCFunction( - ctx, - js_node_generate_endorsed_certificate, - "generateEndorsedCertificate", - 0)); + // JS_SetPropertyStr( + // ctx, + // node, + // "generateEndorsedCertificate", + // JS_NewCFunction( + // ctx, + // js_node_generate_endorsed_certificate, + // "generateEndorsedCertificate", + // 0)); } if (host_node_state != nullptr) diff --git a/src/node/node_state.h b/src/node/node_state.h index 7e2113bf54c..ed44b60b200 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -667,6 +667,10 @@ namespace ccf join_params.quote_info = quote_info; join_params.consensus_type = network.consensus_type; join_params.startup_seqno = startup_seqno; + join_params.certificate_subject_identity = + config.node_certificate_subject_identity; + join_params.certificate_signing_request = + node_sign_kp->create_csr(config.node_certificate_subject_identity.name); LOG_DEBUG_FMT( "Sending join request to {}:{}", @@ -1606,6 +1610,17 @@ namespace ccf return nw->sign_csr(network.identity->cert, csr, sans); } + // crypto::Pem generate_endorsed_certificate( + // const crypto::Pem& subject_public_key, + // const crypto::CertificateSubjectIdentity& subject_identity, + // const crypto::Pem& endorser_private_key, + // const crypto::Pem& endorser_cert) override + // { + // auto endorser_privk = crypto::make_key_pair(endorser_private_key); + // auto csr = node_sign_kp->create_csr(subject_identity.name); + // return endorser_privk->sign_csr(endorser_cert, csr, sans); + // } + void accept_node_tls_connections() { // Accept TLS connections, presenting self-signed (i.e. non-endorsed) diff --git a/src/node/nodes.h b/src/node/nodes.h index 9e3cde96f6f..7d8a837b98a 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -50,14 +50,21 @@ namespace ccf trusted */ std::optional ledger_secret_seqno = std::nullopt; - /** Code identity for the node **/ + /// Code identity for the node std::optional code_digest = std::nullopt; + /** + * Fields below are added in 2.x + */ + /// Node certificate subject identity std::optional certificate_subject_identity = std::nullopt; - /** Public key. Only set from 2.x releases onwards. */ + /// Node original certificate signing request + std::optional certificate_signing_request = std::nullopt; + + /// Public key std::optional public_key = std::nullopt; }; DECLARE_JSON_TYPE_WITH_BASE_AND_OPTIONAL_FIELDS(NodeInfo, NodeInfoNetwork); @@ -68,6 +75,7 @@ namespace ccf ledger_secret_seqno, code_digest, certificate_subject_identity, + certificate_signing_request, public_key); using Nodes = ServiceMap; diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 0c08fcd39be..2b457071bf6 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -83,6 +83,7 @@ namespace ccf std::optional startup_seqno = std::nullopt; std::optional certificate_subject_identity = std::nullopt; + std::optional certificate_signing_request = std::nullopt; }; struct Out diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 45dedf6ba8f..3f2f314b269 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -192,6 +192,7 @@ namespace ccf ledger_secret_seqno, ds::to_hex(code_digest.data), in.certificate_subject_identity, + in.certificate_signing_request, crypto::public_key_pem_from_cert(node_der)}); kv::NetworkConfiguration nc = diff --git a/src/node/rpc/node_interface.h b/src/node/rpc/node_interface.h index 186062a675c..44df299bda8 100644 --- a/src/node/rpc/node_interface.h +++ b/src/node/rpc/node_interface.h @@ -56,6 +56,9 @@ namespace ccf CodeDigest& code_digest) = 0; virtual std::optional get_startup_snapshot_seqno() = 0; virtual SessionMetrics get_session_metrics() = 0; - // virtual void generate_endorsed_certificate() + // virtual void generate_endorsed_certificate( + // const crypto::Pem& subject_public_key, + // const crypto::CertificateSubjectIdentity& subject_identity, + // const crypto::Pem& endorser_private_key) = 0; }; } \ No newline at end of file diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index 03f755d8339..f7ed35999ad 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -37,7 +37,9 @@ namespace ccf consensus_type, startup_seqno) DECLARE_JSON_OPTIONAL_FIELDS( - JoinNetworkNodeToNode::In, certificate_subject_identity) + JoinNetworkNodeToNode::In, + certificate_subject_identity, + certificate_signing_request) DECLARE_JSON_TYPE(NetworkIdentity) DECLARE_JSON_REQUIRED_FIELDS(NetworkIdentity, cert, priv_key) diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index e39f63d6cc9..d172d337cad 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -779,16 +779,16 @@ const actions = new Map([ // Also endorse and record node certificate // TODO: For now, assume that node public key is always present, which isn't true for 1.x! - let node_cert = {}; - node_cert.endorsed_certificate = - ccf.network.generateEndorsedCertificate( - nodeInfo.public_key, - nodeInfo.certificate_subject_identity - ); - ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( - ccf.strToBuf(args.node_id), - ccf.jsonCompatibleToBuf(node_cert) - ); + // let node_cert = {}; + // node_cert.endorsed_certificate = + // ccf.network.generateEndorsedCertificate( + // nodeInfo.public_key, + // nodeInfo.certificate_subject_identity + // ); + // ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( + // ccf.strToBuf(args.node_id), + // ccf.jsonCompatibleToBuf(node_cert) + // ); } } ), From 79eff2d8e74ca1badaf12ee05283ae70ad16c82d Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 14 Jul 2021 11:57:30 +0100 Subject: [PATCH 007/105] First version works --- python/ccf/read_ledger.py | 18 +-- src/host/main.cpp | 1 - src/js/wrap.cpp | 151 +++++++++++++++----------- src/node/node_state.h | 24 ++-- src/node/rpc/node_frontend.h | 4 + src/node/rpc/node_interface.h | 9 +- src/node/rpc/test/node_stub.h | 9 ++ src/runtime_config/default/actions.js | 20 ++-- 8 files changed, 141 insertions(+), 95 deletions(-) diff --git a/python/ccf/read_ledger.py b/python/ccf/read_ledger.py index 63e6b55aa12..4de48f20c9c 100644 --- a/python/ccf/read_ledger.py +++ b/python/ccf/read_ledger.py @@ -15,13 +15,17 @@ def indent(n): def stringify_bytes(bs): - s = bs.decode() - if s.isprintable(): - return s - if len(bs) > 0 and len(bs) <= 8: - n = int.from_bytes(bs, byteorder="little") - return f"" - return bs + try: + s = bs.decode() + if s.isprintable(): + return s + except: + pass + finally: + if len(bs) > 0 and len(bs) <= 8: + n = int.from_bytes(bs, byteorder="little") + return f"" + return bs def print_key(indent_s, k, is_removed=False): diff --git a/src/host/main.cpp b/src/host/main.cpp index ea65c0feaaa..aeb43df15e0 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -349,7 +349,6 @@ int main(int argc, char** argv) "Subject Name in node certificate, eg. CN=CCF Node") ->capture_default_str(); - std::vector subject_alternative_names; cli::add_subject_alternative_name_option( app, node_certificate_subject_identity.sans, diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index 172aa8f8417..4c7232460cb 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -488,60 +488,81 @@ namespace js return JS_UNDEFINED; } - // JSValue js_node_generate_endorsed_certificate( - // JSContext* ctx, - // JSValueConst this_val, - // int argc, - // [[maybe_unused]] JSValueConst* argv) - // { - // if (argc != 2) - // { - // return JS_ThrowTypeError(ctx, "Passed %d arguments but expected 2", - // argc); - // } - - // auto node = static_cast( - // JS_GetOpaque(this_val, node_class_id)); - - // if (node == nullptr) - // { - // return JS_ThrowInternalError(ctx, "Node state is not set"); - // } - - // ////////// - - // // TODO: - // // 1. Parse arguments and verify type - // // 2. Call into node state - // auto public_key = argv[0]; - // if (!JS_IsArray(ctx, args)) - // { - // return JS_ThrowTypeError(ctx, "First argument must be an array"); - // } - - // auto - - // /////////// - - // auto global_obj = JS_GetGlobalObject(ctx); - // auto ccf = JS_GetPropertyStr(ctx, global_obj, "ccf"); - // auto kv = JS_GetPropertyStr(ctx, ccf, "kv"); - - // auto tx_ctx_ptr = static_cast(JS_GetOpaque(kv, kv_class_id)); - - // if (tx_ctx_ptr->tx == nullptr) - // { - // return JS_ThrowInternalError( - // ctx, "No transaction available to fetch latest ledger secret seqno"); - // } - - // JS_FreeValue(ctx, kv); - // JS_FreeValue(ctx, ccf); - // JS_FreeValue(ctx, global_obj); - - // return JS_NewInt64( - // ctx, network->ledger_secrets->get_latest(*tx_ctx_ptr->tx).first); - // } + JSValue js_network_generate_endorsed_certificate( + JSContext* ctx, + JSValueConst this_val, + int argc, + [[maybe_unused]] JSValueConst* argv) + { + if (argc != 2) + { + return JS_ThrowTypeError(ctx, "Passed %d arguments but expected 2", argc); + } + + auto network = + static_cast(JS_GetOpaque(this_val, network_class_id)); + if (network == nullptr) + { + return JS_ThrowInternalError(ctx, "Network state is not set"); + } + + auto global_obj = JS_GetGlobalObject(ctx); + auto ccf = JS_GetPropertyStr(ctx, global_obj, "ccf"); + auto node_ = JS_GetPropertyStr(ctx, ccf, "node"); + + auto node = + static_cast(JS_GetOpaque(node_, node_class_id)); + + if (node == nullptr) + { + return JS_ThrowInternalError(ctx, "Node state is not set"); + } + + JS_FreeValue(ctx, node_); + JS_FreeValue(ctx, ccf); + JS_FreeValue(ctx, global_obj); + + auto csr_cstr = JS_ToCString(ctx, argv[0]); + if (csr_cstr == nullptr) + { + throw JS_ThrowTypeError(ctx, "csr argument is not a string"); + } + auto csr = crypto::Pem(csr_cstr); + + JSValue certificate_subject_identity_val = + JS_JSONStringify(ctx, argv[1], JS_NULL, JS_NULL); + if (JS_IsException(certificate_subject_identity_val)) + { + return JS_ThrowTypeError( + ctx, "certificate subject identity argument is not a JSON object"); + } + auto certificate_subject_identity_cstr = + JS_ToCString(ctx, certificate_subject_identity_val); + std::string certificate_subject_identity_json( + certificate_subject_identity_cstr); + JS_FreeCString(ctx, certificate_subject_identity_cstr); + JS_FreeValue(ctx, certificate_subject_identity_val); + + crypto::CertificateSubjectIdentity certificate_subject_identity; + try + { + certificate_subject_identity = + nlohmann::json::parse(certificate_subject_identity_json) + .get(); + } + catch (std::exception& exc) + { + return JS_ThrowInternalError(ctx, "Error: %s", exc.what()); + } + + auto endorsed_cert = node->generate_endorsed_certificate( + csr, + certificate_subject_identity, + network->identity->priv_key, + network->identity->cert); + + return JS_NewString(ctx, endorsed_cert.str().c_str()); + } JSValue js_network_latest_ledger_secret_seqno( JSContext* ctx, @@ -1411,15 +1432,6 @@ namespace js js_node_trigger_recovery_shares_refresh, "triggerRecoverySharesRefresh", 0)); - // JS_SetPropertyStr( - // ctx, - // node, - // "generateEndorsedCertificate", - // JS_NewCFunction( - // ctx, - // js_node_generate_endorsed_certificate, - // "generateEndorsedCertificate", - // 0)); } if (host_node_state != nullptr) @@ -1455,6 +1467,19 @@ namespace js js_network_latest_ledger_secret_seqno, "getLatestLedgerSecretSeqno", 0)); + + if (node_state != nullptr) + { + JS_SetPropertyStr( + ctx, + network, + "generateEndorsedCertificate", + JS_NewCFunction( + ctx, + js_network_generate_endorsed_certificate, + "generateEndorsedCertificate", + 0)); + } } if (rpc_ctx != nullptr) diff --git a/src/node/node_state.h b/src/node/node_state.h index ed44b60b200..fcaaf6616e8 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -328,7 +328,10 @@ namespace ccf std::lock_guard guard(lock); sm.expect(State::initialized); + // TODO: To remove once Eddy's PR is merged config = std::move(config_); + config.node_certificate_subject_identity.sans = + get_subject_alternative_names(); js::register_class_ids(); open_frontend(ActorsType::nodes); @@ -1610,16 +1613,17 @@ namespace ccf return nw->sign_csr(network.identity->cert, csr, sans); } - // crypto::Pem generate_endorsed_certificate( - // const crypto::Pem& subject_public_key, - // const crypto::CertificateSubjectIdentity& subject_identity, - // const crypto::Pem& endorser_private_key, - // const crypto::Pem& endorser_cert) override - // { - // auto endorser_privk = crypto::make_key_pair(endorser_private_key); - // auto csr = node_sign_kp->create_csr(subject_identity.name); - // return endorser_privk->sign_csr(endorser_cert, csr, sans); - // } + // TODO: Rename + crypto::Pem generate_endorsed_certificate( + const crypto::Pem& subject_csr, + const crypto::CertificateSubjectIdentity& subject_identity, + const crypto::Pem& endorser_private_key, + const crypto::Pem& endorser_cert) override + { + LOG_FAIL_FMT("SAN count: {}", subject_identity.sans.size()); + return crypto::make_key_pair(endorser_private_key) + ->sign_csr(endorser_cert, subject_csr, subject_identity.sans); + } void accept_node_tls_connections() { diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 3f2f314b269..2d57bda1981 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -182,6 +182,10 @@ namespace ccf this->network.ledger_secrets->get_latest(tx).first; } + // TODO: Check that the public key in the CSR matches the TLS identity + + // TODO: Add node id to [CN] + nodes->put( joining_node_id, {in.node_info_network, diff --git a/src/node/rpc/node_interface.h b/src/node/rpc/node_interface.h index 44df299bda8..9676a876963 100644 --- a/src/node/rpc/node_interface.h +++ b/src/node/rpc/node_interface.h @@ -56,9 +56,10 @@ namespace ccf CodeDigest& code_digest) = 0; virtual std::optional get_startup_snapshot_seqno() = 0; virtual SessionMetrics get_session_metrics() = 0; - // virtual void generate_endorsed_certificate( - // const crypto::Pem& subject_public_key, - // const crypto::CertificateSubjectIdentity& subject_identity, - // const crypto::Pem& endorser_private_key) = 0; + virtual crypto::Pem generate_endorsed_certificate( + const crypto::Pem& subject_csr, + const crypto::CertificateSubjectIdentity& subject_identity, + const crypto::Pem& endorser_private_key, + const crypto::Pem& endorser_cert) = 0; }; } \ No newline at end of file diff --git a/src/node/rpc/test/node_stub.h b/src/node/rpc/test/node_stub.h index 869e0d4113a..8cf0d178667 100644 --- a/src/node/rpc/test/node_stub.h +++ b/src/node/rpc/test/node_stub.h @@ -116,6 +116,15 @@ namespace ccf { return {}; } + + crypto::Pem generate_endorsed_certificate( + const crypto::Pem& subject_csr, + const crypto::CertificateSubjectIdentity& subject_identity, + const crypto::Pem& endorser_private_key, + const crypto::Pem& endorser_cert) override + { + throw std::logic_error("Unimplemented"); + } }; class StubNodeStateCache : public historical::AbstractStateCache diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index d172d337cad..f8dd69bf2a0 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -779,16 +779,16 @@ const actions = new Map([ // Also endorse and record node certificate // TODO: For now, assume that node public key is always present, which isn't true for 1.x! - // let node_cert = {}; - // node_cert.endorsed_certificate = - // ccf.network.generateEndorsedCertificate( - // nodeInfo.public_key, - // nodeInfo.certificate_subject_identity - // ); - // ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( - // ccf.strToBuf(args.node_id), - // ccf.jsonCompatibleToBuf(node_cert) - // ); + let node_cert = {}; + node_cert.endorsed_certificate = + ccf.network.generateEndorsedCertificate( + nodeInfo.certificate_signing_request, + nodeInfo.certificate_subject_identity + ); + ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( + ccf.strToBuf(args.node_id), + ccf.jsonCompatibleToBuf(node_cert) + ); } } ), From c146a436a17d2edcc2cc4a8d0ebbb15f934ca557 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 14 Jul 2021 15:03:07 +0100 Subject: [PATCH 008/105] Remove deprecated --domain option to cchost --- doc/operations/recovery.rst | 1 - doc/operations/start_network.rst | 1 - src/ds/net.h | 5 +++++ src/enclave/interface.h | 2 -- src/host/main.cpp | 13 ------------- src/node/node_state.h | 24 +++++++++++------------- tests/infra/e2e_args.py | 4 ---- tests/infra/network.py | 1 - tests/infra/remote.py | 4 ---- 9 files changed, 16 insertions(+), 39 deletions(-) diff --git a/doc/operations/recovery.rst b/doc/operations/recovery.rst index 8dc8586d484..ec9ffab20ab 100644 --- a/doc/operations/recovery.rst +++ b/doc/operations/recovery.rst @@ -25,7 +25,6 @@ To initiate the first phase of the recovery procedure, one or several nodes shou --node-address node_ip:node_port --rpc-address --public-rpc-address - [--domain domain] --ledger-dir /path/to/ledger/dir/to/recover --node-cert-file /path/to/node_certificate recover diff --git a/doc/operations/start_network.rst b/doc/operations/start_network.rst index d1be7029c96..62a5f14ae25 100644 --- a/doc/operations/start_network.rst +++ b/doc/operations/start_network.rst @@ -18,7 +18,6 @@ To create a new CCF network, the first node of the network should be started wit --rpc-address --node-address --public-rpc-address - [--domain domain] --ledger-dir /path/to/ledger/dir --node-cert-file /path/to/node_certificate start diff --git a/src/ds/net.h b/src/ds/net.h index 9658c4f57da..cd364ee5fe0 100644 --- a/src/ds/net.h +++ b/src/ds/net.h @@ -38,4 +38,9 @@ namespace ds { return ip_to_binary(hostname).has_value(); } + + inline bool is_valid_ip(const std::string& hostname) + { + return is_valid_ip(hostname.c_str()); + } } \ No newline at end of file diff --git a/src/enclave/interface.h b/src/enclave/interface.h index 8eac7ec3193..2481c15b36d 100644 --- a/src/enclave/interface.h +++ b/src/enclave/interface.h @@ -45,7 +45,6 @@ struct CCFConfig { consensus::Configuration consensus_config = {}; ccf::NodeInfoNetwork node_info_network = {}; - std::string domain; size_t snapshot_tx_interval; size_t max_open_sessions_soft; size_t max_open_sessions_hard; @@ -103,7 +102,6 @@ DECLARE_JSON_REQUIRED_FIELDS( CCFConfig, consensus_config, node_info_network, - domain, snapshot_tx_interval, max_open_sessions_soft, max_open_sessions_hard, diff --git a/src/host/main.cpp b/src/host/main.cpp index b1c2c0567fd..ca735fddefd 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -336,10 +336,6 @@ int main(int argc, char** argv) "latency at a cost to throughput") ->capture_default_str(); - std::string domain; - app.add_option( - "--domain", domain, "DNS to use for TLS certificate validation"); - std::string subject_name("CN=CCF Node"); app .add_option( @@ -496,14 +492,6 @@ int main(int argc, char** argv) uint32_t oe_flags = 0; try { - if (domain.empty() && !ds::is_valid_ip(rpc_address.hostname.c_str())) - { - throw std::logic_error(fmt::format( - "--rpc-address ({}) does not appear to specify valid IP address. " - "Please specify a domain name via the --domain option", - rpc_address.hostname)); - } - if (*start && files::exists(ledger_dir)) { throw std::logic_error(fmt::format( @@ -720,7 +708,6 @@ int main(int argc, char** argv) node_address.port, rpc_address.port, public_rpc_address.port}; - ccf_config.domain = domain; ccf_config.snapshot_tx_interval = snapshot_tx_interval; ccf_config.max_open_sessions_soft = max_open_sessions; ccf_config.max_open_sessions_hard = max_open_sessions_hard; diff --git a/src/node/node_state.h b/src/node/node_state.h index 9804c5b5eaa..22da710872d 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -10,6 +10,7 @@ #include "crypto/symmetric_key.h" #include "crypto/verifier.h" #include "ds/logger.h" +#include "ds/net.h" #include "ds/state_machine.h" #include "enclave/rpc_sessions.h" #include "encryptor.h" @@ -1572,21 +1573,18 @@ namespace ccf } private: - crypto::SubjectAltName get_subject_alt_name() - { - // If a domain is passed at node creation, record domain in SAN for node - // hostname authentication over TLS. Otherwise, record IP in SAN. - bool san_is_ip = config.domain.empty(); - return {san_is_ip ? config.node_info_network.rpchost : config.domain, - san_is_ip}; - } - std::vector get_subject_alternative_names() { - std::vector sans = - config.subject_alternative_names; - sans.push_back(get_subject_alt_name()); - return sans; + // If no Subject Alternative Names are passed in at node creation, default + // to using node's RPC address as single SAN. Otherwise, use specified + // SANs. + if (!config.subject_alternative_names.empty()) + { + return config.subject_alternative_names; + } + + return {{config.node_info_network.rpchost, + ds::is_valid_ip(config.node_info_network.rpchost)}}; } Pem create_self_signed_node_cert() diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index 6faffd1a3fa..26cd07325d7 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -188,10 +188,6 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): action="store_true", default=False, ) - parser.add_argument( - "--domain", - help="Domain name used for node certificate verification, eg. example.com", - ) parser.add_argument( "--sn", help="Subject Name in node certificate, eg. CN=CCF Node", diff --git a/tests/infra/network.py b/tests/infra/network.py index f6111da3849..5bf05e9215d 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -92,7 +92,6 @@ class Network: "join_timer", "worker_threads", "ledger_chunk_bytes", - "domain", "san", "snapshot_tx_interval", "max_open_sessions", diff --git a/tests/infra/remote.py b/tests/infra/remote.py index 0baa3dc54b4..ccccfc61998 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -575,7 +575,6 @@ def __init__( log_format_json=None, binary_dir=".", ledger_chunk_bytes=(5 * 1000 * 1000), - domain=None, san=None, snapshot_tx_interval=None, max_open_sessions=None, @@ -671,9 +670,6 @@ def __init__( if ledger_chunk_bytes: cmd += [f"--ledger-chunk-bytes={ledger_chunk_bytes}"] - if domain: - cmd += [f"--domain={domain}"] - if san: cmd += [f"--san={s}" for s in san] From 2c73f74a4e52a1df38f73528b8662cdeb539be18 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 14 Jul 2021 15:27:09 +0100 Subject: [PATCH 009/105] Changelog --- CHANGELOG.md | 4 ++++ src/node/node_state.h | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce47f2eb569..4557dcbd77c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Upgrade OpenEnclave from 0.17.0 to 0.17.1. +### Removed + +- Remove long-deprecated `--domain` argument from `cchost`. Node certificate Subject Alternative Names should be passed in via existing `--san` argument. + ## [2.0.0-dev2] ### Changed diff --git a/src/node/node_state.h b/src/node/node_state.h index 22da710872d..3fdd8009dd0 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1575,9 +1575,9 @@ namespace ccf private: std::vector get_subject_alternative_names() { - // If no Subject Alternative Names are passed in at node creation, default - // to using node's RPC address as single SAN. Otherwise, use specified - // SANs. + // If no Subject Alternative Name (SAN) is passed in at node creation, + // default to using node's RPC address as single SAN. Otherwise, use + // specified SANs. if (!config.subject_alternative_names.empty()) { return config.subject_alternative_names; From 6d7e4fa13e34b7abc3b91c1c9c1eb5543eb81c69 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 14 Jul 2021 17:22:03 +0100 Subject: [PATCH 010/105] Return endorsed certificate in response --- src/node/rpc/node_call_types.h | 7 +++-- src/node/rpc/node_frontend.h | 49 ++++++++++++++++++++++++---------- src/node/rpc/serialization.h | 5 +++- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 2b457071bf6..1e862d54384 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -100,7 +100,9 @@ namespace ccf LedgerSecretsMap ledger_secrets; NetworkIdentity identity; - ServiceStatus service_status; + ServiceStatus service_status; // TODO: This isn't serialised! + + std::optional endorsed_certificate = std::nullopt; bool operator==(const NetworkInfo& other) const { @@ -109,7 +111,8 @@ namespace ccf consensus_type == other.consensus_type && ledger_secrets == other.ledger_secrets && service_status == other.service_status && - identity == other.identity; + identity == other.identity && + endorsed_certificate == other.endorsed_certificate; } bool operator!=(const NetworkInfo& other) const diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 2d57bda1981..403da8251db 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -70,25 +70,45 @@ namespace ccf private: NetworkState& network; - using ExistingNodeInfo = std::pair>; + struct ExistingNodeInfo + { + NodeId node_id; + std::optional ledger_secret_seqno = std::nullopt; + crypto::Pem endorsed_certificate; + }; std::optional check_node_exists( kv::Tx& tx, const std::vector& node_der, std::optional node_status = std::nullopt) { - auto nodes = tx.rw(network.nodes); + auto nodes = tx.ro(network.nodes); + auto endorsed_node_certificates = + tx.ro(network.node_endorsed_certificates); auto node_pem = crypto::cert_der_to_pem(node_der); std::optional existing_node_info = std::nullopt; - nodes->foreach([&existing_node_info, &node_pem, &node_status]( + nodes->foreach([&existing_node_info, + &node_pem, + &node_status, + &endorsed_node_certificates]( const NodeId& nid, const NodeInfo& ni) { if ( ni.cert == node_pem && (!node_status.has_value() || ni.status == node_status.value())) { - existing_node_info = std::make_pair(nid, ni.ledger_secret_seqno); + auto endorsed_node_certificate = endorsed_node_certificates->get(nid); + if (!endorsed_node_certificate.has_value()) + { + // TODO: Compatibility issue? + throw std::logic_error(fmt::format( + "Did not find endorsed certificate for node {}", nid)); + } + existing_node_info = { + nid, + ni.ledger_secret_seqno, + endorsed_node_certificate->endorsed_certificate}; return false; } return true; @@ -126,6 +146,7 @@ namespace ccf NodeStatus node_status, ServiceStatus service_status) { + LOG_FAIL_FMT("Service status: {}", service_status); auto nodes = tx.rw(network.nodes); auto conflicting_node_id = @@ -184,8 +205,6 @@ namespace ccf // TODO: Check that the public key in the CSR matches the TLS identity - // TODO: Add node id to [CN] - nodes->put( joining_node_id, {in.node_info_network, @@ -235,7 +254,7 @@ namespace ccf openapi_info.description = "This API provides public, uncredentialed access to service and node " "state."; - openapi_info.document_version = "1.4.0"; + openapi_info.document_version = "1.4.0"; // TODO: Bump up } void init_handlers() override @@ -310,15 +329,16 @@ namespace ccf { JoinNetworkNodeToNode::Out rep; rep.node_status = joining_node_status; - rep.node_id = existing_node_info->first; + rep.node_id = existing_node_info->node_id; rep.network_info = { context.get_node_state().is_part_of_public_network(), context.get_node_state().get_last_recovered_signed_idx(), this->network.consensus_type, this->network.ledger_secrets->get( - args.tx, existing_node_info->second), + args.tx, existing_node_info->ledger_secret_seqno), *this->network.identity.get(), - active_service->status}; + active_service->status, + existing_node_info->endorsed_certificate}; return make_success(rep); } @@ -369,11 +389,11 @@ namespace ccf if (existing_node_info.has_value()) { JoinNetworkNodeToNode::Out rep; - rep.node_id = existing_node_info->first; + rep.node_id = existing_node_info->node_id; // If the node already exists, return network secrets if is already // trusted. Otherwise, only return its status - auto node_status = nodes->get(existing_node_info->first)->status; + auto node_status = nodes->get(existing_node_info->node_id)->status; rep.node_status = node_status; if ( node_status == NodeStatus::TRUSTED || @@ -384,9 +404,10 @@ namespace ccf context.get_node_state().get_last_recovered_signed_idx(), this->network.consensus_type, this->network.ledger_secrets->get( - args.tx, existing_node_info->second), + args.tx, existing_node_info->ledger_secret_seqno), *this->network.identity.get(), - active_service->status}; + active_service->status, + existing_node_info->endorsed_certificate}; return make_success(rep); } else if (node_status == NodeStatus::PENDING) diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index f7ed35999ad..bd1e1421d12 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -44,7 +44,8 @@ namespace ccf DECLARE_JSON_TYPE(NetworkIdentity) DECLARE_JSON_REQUIRED_FIELDS(NetworkIdentity, cert, priv_key) - DECLARE_JSON_TYPE(JoinNetworkNodeToNode::Out::NetworkInfo) + DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS( + JoinNetworkNodeToNode::Out::NetworkInfo) DECLARE_JSON_REQUIRED_FIELDS( JoinNetworkNodeToNode::Out::NetworkInfo, public_only, @@ -52,6 +53,8 @@ namespace ccf consensus_type, ledger_secrets, identity) + DECLARE_JSON_OPTIONAL_FIELDS( + JoinNetworkNodeToNode::Out::NetworkInfo, endorsed_certificate) DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JoinNetworkNodeToNode::Out) DECLARE_JSON_REQUIRED_FIELDS(JoinNetworkNodeToNode::Out, node_status, node_id) DECLARE_JSON_OPTIONAL_FIELDS(JoinNetworkNodeToNode::Out, network_info) From 50711253be26fea14f2431ca598d55135167ac15 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 15 Jul 2021 16:18:55 +0100 Subject: [PATCH 011/105] Endorsement works fine on the backup + compatibility --- src/node/node_state.h | 96 ++++++++++++++++-------- src/node/rpc/node_call_types.h | 3 +- src/node/rpc/node_frontend.h | 36 ++++++--- src/node/rpc/test/frontend_test_infra.h | 1 - src/node/rpc/test/node_frontend_test.cpp | 28 +++++-- 5 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/node/node_state.h b/src/node/node_state.h index 54c88bb9933..b4d655a18ee 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -539,7 +539,18 @@ namespace ccf network.identity = std::make_unique(resp.network_info.identity); - node_cert = create_endorsed_node_cert(); + // Endorsed node certificate is included in join response from 2.x. + // When joining an existing 1.x service, self-sign own certificate + // and use it to endorse TLS connections + if (resp.network_info.endorsed_certificate.has_value()) + { + node_cert = resp.network_info.endorsed_certificate.value(); + } + else + { + node_cert = create_endorsed_node_cert(); + accept_network_tls_connections(); + } network.ledger_secrets->init_from_map( std::move(resp.network_info.ledger_secrets)); @@ -628,10 +639,9 @@ namespace ccf sig->view); } + // TODO: Move later? open_frontend(ActorsType::members); - accept_network_tls_connections(); - if (resp.network_info.public_only) { sm.advance(State::partOfPublicNetwork); @@ -648,8 +658,7 @@ namespace ccf self, (resp.network_info.public_only ? "public only" : "all domains")); - // The network identity is now known, the user frontend can be - // opened once the KV state catches up + // TODO: Move later? open_user_frontend(); } else if (resp.node_status == NodeStatus::PENDING) @@ -1060,6 +1069,8 @@ namespace ccf // Sets itself as trusted g.trust_node(self, network.ledger_secrets->get_latest(tx).first); + // TODO: Record endorsed identity as well! + #ifdef GET_QUOTE g.trust_node_code_id(node_code_id); #endif @@ -1882,6 +1893,32 @@ namespace ccf backup_initiate_private_recovery(); } + return kv::ConsensusHookPtr(nullptr); + })); + + network.tables->set_map_hook( + network.node_endorsed_certificates.get_name(), + network.node_endorsed_certificates.wrap_map_hook( + [this]( + kv::Version hook_version, + const NodeEndorsedCertificates::Write& w) -> kv::ConsensusHookPtr { + for (auto const& [node_id, endorsed_certificate] : w) + { + if (node_id != self) + { + continue; + } + + if (!endorsed_certificate.has_value()) + { + throw std::logic_error(fmt::format( + "Could not find endorsed node certificate for {}", self)); + } + + node_cert = endorsed_certificate->endorsed_certificate; + accept_network_tls_connections(); + } + return kv::ConsensusHookPtr(nullptr); })); } @@ -1960,7 +1997,28 @@ namespace ccf cmd_forwarder->initialize(self); } - void setup_raft(ServiceStatus service_status, bool public_only = false) + void setup_history() + { + history = std::make_shared( + *network.tables.get(), + self, + *node_sign_kp, + sig_tx_interval, + sig_ms_interval, + true); + + network.tables->set_history(history); + } + + void setup_encryptor() + { + // This function makes use of ledger secrets and should be called once + // the node has joined the service + encryptor = make_encryptor(); + network.tables->set_encryptor(encryptor); + } + + void setup_consensus(ServiceStatus service_status, bool public_only = false) { setup_n2n_channels(); setup_cmd_forwarder(); @@ -2025,32 +2083,6 @@ namespace ccf setup_basic_hooks(); } - void setup_history() - { - history = std::make_shared( - *network.tables.get(), - self, - *node_sign_kp, - sig_tx_interval, - sig_ms_interval, - true); - - network.tables->set_history(history); - } - - void setup_encryptor() - { - // This function makes use of ledger secrets and should be called once - // the node has joined the service - encryptor = make_encryptor(); - network.tables->set_encryptor(encryptor); - } - - void setup_consensus(ServiceStatus service_status, bool public_only = false) - { - setup_raft(service_status, public_only); - } - void setup_progress_tracker() { if (network.consensus_type == ConsensusType::BFT) diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 1e862d54384..09ace23bfd1 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -82,7 +82,8 @@ namespace ccf ConsensusType consensus_type = ConsensusType::CFT; std::optional startup_seqno = std::nullopt; std::optional - certificate_subject_identity = std::nullopt; + certificate_subject_identity = + std::nullopt; // TODO: Remove since OpenSSL is sufficient! std::optional certificate_signing_request = std::nullopt; }; diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 403da8251db..b687a6a7be3 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -74,7 +74,7 @@ namespace ccf { NodeId node_id; std::optional ledger_secret_seqno = std::nullopt; - crypto::Pem endorsed_certificate; + std::optional endorsed_certificate = std::nullopt; }; std::optional check_node_exists( @@ -99,16 +99,13 @@ namespace ccf (!node_status.has_value() || ni.status == node_status.value())) { auto endorsed_node_certificate = endorsed_node_certificates->get(nid); - if (!endorsed_node_certificate.has_value()) - { - // TODO: Compatibility issue? - throw std::logic_error(fmt::format( - "Did not find endorsed certificate for node {}", nid)); - } existing_node_info = { nid, ni.ledger_secret_seqno, - endorsed_node_certificate->endorsed_certificate}; + endorsed_node_certificate.has_value() ? + std::make_optional( + endorsed_node_certificate->endorsed_certificate) : + std::nullopt}; return false; } return true; @@ -148,6 +145,8 @@ namespace ccf { LOG_FAIL_FMT("Service status: {}", service_status); auto nodes = tx.rw(network.nodes); + auto node_endorsed_certificates = + tx.rw(network.node_endorsed_certificates); auto conflicting_node_id = check_conflicting_node_network(tx, in.node_info_network); @@ -214,7 +213,8 @@ namespace ccf node_status, ledger_secret_seqno, ds::to_hex(code_digest.data), - in.certificate_subject_identity, + in.certificate_subject_identity, // TODO: Remove as we only need + // OpenSSL backend! in.certificate_signing_request, crypto::public_key_pem_from_cert(node_der)}); @@ -233,13 +233,29 @@ namespace ccf node_status == NodeStatus::TRUSTED || node_status == NodeStatus::LEARNER) { + // Joining node only submit a CSR from 2.x + std::optional endorsed_certificate = std::nullopt; + if (in.certificate_signing_request.has_value()) + { + LOG_FAIL_FMT("Recording endorsed identity!"); + endorsed_certificate = + context.get_node_state().generate_endorsed_certificate( + in.certificate_signing_request.value(), + in.certificate_subject_identity.value(), + this->network.identity->priv_key, + this->network.identity->cert); + node_endorsed_certificates->put( + joining_node_id, {endorsed_certificate.value()}); + } + rep.network_info = JoinNetworkNodeToNode::Out::NetworkInfo{ context.get_node_state().is_part_of_public_network(), context.get_node_state().get_last_recovered_signed_idx(), this->network.consensus_type, this->network.ledger_secrets->get(tx), *this->network.identity.get(), - service_status}; + service_status, + endorsed_certificate}; } return make_success(rep); } diff --git a/src/node/rpc/test/frontend_test_infra.h b/src/node/rpc/test/frontend_test_infra.h index 35f2ece75a7..f442c76c38b 100644 --- a/src/node/rpc/test/frontend_test_infra.h +++ b/src/node/rpc/test/frontend_test_infra.h @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. #define DOCTEST_CONFIG_IMPLEMENT -#define DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES #define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS #include "ccf/app_interface.h" #include "ccf/user_frontend.h" diff --git a/src/node/rpc/test/node_frontend_test.cpp b/src/node/rpc/test/node_frontend_test.cpp index 150a586b48b..31fc4a60ec8 100644 --- a/src/node/rpc/test/node_frontend_test.cpp +++ b/src/node/rpc/test/node_frontend_test.cpp @@ -104,7 +104,7 @@ TEST_CASE("Add a node to an opening service") // Node certificate crypto::KeyPairPtr kp = crypto::make_key_pair(); - const auto caller = kp->self_sign(fmt::format("CN=nodes")); + const auto caller = kp->self_sign(fmt::format("CN=Joiner")); const auto node_public_encryption_key = crypto::make_key_pair()->public_key_pem(); @@ -145,6 +145,8 @@ TEST_CASE("Add a node to an opening service") { JoinNetworkNodeToNode::In join_input; join_input.public_encryption_key = node_public_encryption_key; + // Join input does not include CSR (1.x) + join_input.certificate_signing_request = std::nullopt; auto http_response = frontend_process(frontend, join_input, "join", caller); CHECK(http_response.status == HTTP_STATUS_OK); @@ -157,6 +159,8 @@ TEST_CASE("Add a node to an opening service") CHECK(response.network_info.identity == *network.identity.get()); CHECK(response.node_status == NodeStatus::TRUSTED); CHECK(response.network_info.public_only == false); + // No endorsed certificate since no CSR was passed in + CHECK(response.network_info.endorsed_certificate == std::nullopt); const NodeId node_id = response.node_id; auto nodes = tx.rw(network.nodes); @@ -194,7 +198,8 @@ TEST_CASE("Add a node to an opening service") "Adding a different node with the same node network details should fail"); { crypto::KeyPairPtr kp = crypto::make_key_pair(); - auto v = crypto::make_verifier(kp->self_sign(fmt::format("CN=nodes"))); + auto v = + crypto::make_verifier(kp->self_sign(fmt::format("CN=Other Joiner"))); const auto caller = v->cert_der(); // Network node info is empty (same as before) @@ -240,12 +245,13 @@ TEST_CASE("Add a node to an open service") // Node certificate crypto::KeyPairPtr kp = crypto::make_key_pair(); - const auto caller = kp->self_sign(fmt::format("CN=nodes")); + const auto caller = kp->self_sign(fmt::format("CN=Joiner")); std::optional node_info; auto tx = network.tables->create_tx(); JoinNetworkNodeToNode::In join_input; + join_input.certificate_signing_request = kp->create_csr("CN=Joiner"); INFO("Add node once service is open"); { @@ -270,7 +276,7 @@ TEST_CASE("Add a node to an open service") "Adding a different node with the same node network details should fail"); { crypto::KeyPairPtr kp = crypto::make_key_pair(); - auto v = crypto::make_verifier(kp->self_sign(fmt::format("CN=nodes"))); + auto v = crypto::make_verifier(kp->self_sign(fmt::format("CN=Joiner"))); const auto caller = v->cert_der(); // Network node info is empty (same as before) @@ -298,9 +304,12 @@ TEST_CASE("Add a node to an open service") { // In a real scenario, nodes are trusted via member governance. GenesisGenerator g(network, tx); - g.trust_node( - crypto::Sha256Hash(kp->public_key_der()).hex_str(), - network.ledger_secrets->get_latest(tx).first); + auto joining_node_id = crypto::Sha256Hash(kp->public_key_der()).hex_str(); + g.trust_node(joining_node_id, network.ledger_secrets->get_latest(tx).first); + const auto dummy_endorsed_certificate = + crypto::make_key_pair()->self_sign("CN=dummy endorsed certificate"); + auto endorsed_certificate = tx.rw(network.node_endorsed_certificates); + endorsed_certificate->put(joining_node_id, {dummy_endorsed_certificate}); REQUIRE(tx.commit() == kv::CommitResult::SUCCESS); // In the meantime, a new ledger secret is added. The new ledger secret @@ -318,8 +327,13 @@ TEST_CASE("Add a node to an open service") require_ledger_secrets_equal( response.network_info.ledger_secrets, network.ledger_secrets->get(tx, up_to_ledger_secret_seqno)); + CHECK(response.node_id == joining_node_id); CHECK(response.network_info.identity == *network.identity.get()); CHECK(response.node_status == NodeStatus::TRUSTED); CHECK(response.network_info.public_only == true); + CHECK(response.network_info.endorsed_certificate.has_value()); + CHECK( + response.network_info.endorsed_certificate.value() == + dummy_endorsed_certificate); } } \ No newline at end of file From f69d8d7a910f9641bf7bf7248c39c8bbf0a27230 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 16 Jul 2021 14:03:55 +0100 Subject: [PATCH 012/105] Read SubjectAltName from CSR --- src/crypto/key_pair.h | 13 +- src/crypto/mbedtls/key_pair.cpp | 215 ++------------------------ src/crypto/mbedtls/key_pair.h | 6 +- src/crypto/openssl/key_pair.cpp | 82 ++++++---- src/crypto/openssl/key_pair.h | 7 +- src/crypto/openssl/openssl_wrappers.h | 26 ++++ src/crypto/test/crypto.cpp | 16 +- src/node/identity.h | 5 +- src/node/node_state.h | 12 +- src/tls/test/cert.cpp | 5 +- 10 files changed, 128 insertions(+), 259 deletions(-) diff --git a/src/crypto/key_pair.h b/src/crypto/key_pair.h index 4be59203369..70bf1f64c2d 100644 --- a/src/crypto/key_pair.h +++ b/src/crypto/key_pair.h @@ -45,12 +45,13 @@ namespace crypto virtual std::vector sign(CBuffer d, MDType md_type = {}) const = 0; - virtual Pem create_csr(const std::string& name) const = 0; + virtual Pem create_csr( + const std::string& name, + const std::vector& sans = {}) const = 0; virtual Pem sign_csr( const Pem& issuer_cert, const Pem& signing_request, - const std::vector subject_alt_names, bool ca = false) const = 0; Pem self_sign( @@ -61,8 +62,8 @@ namespace crypto std::vector sans; if (subject_alt_name.has_value()) sans.push_back(subject_alt_name.value()); - auto csr = create_csr(name); - return sign_csr(Pem(0), csr, sans, ca); + auto csr = create_csr(name, sans); + return sign_csr(Pem(0), csr, ca); } Pem self_sign( @@ -70,8 +71,8 @@ namespace crypto const std::vector subject_alt_names, bool ca = true) const { - auto csr = create_csr(name); - return sign_csr(Pem(0), csr, subject_alt_names, ca); + auto csr = create_csr(name, subject_alt_names); + return sign_csr(Pem(0), csr, ca); } }; diff --git a/src/crypto/mbedtls/key_pair.cpp b/src/crypto/mbedtls/key_pair.cpp index e8883600909..70859d5fd79 100644 --- a/src/crypto/mbedtls/key_pair.cpp +++ b/src/crypto/mbedtls/key_pair.cpp @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. -#include "key_pair.h" - #include "curve.h" #include "ds/net.h" #include "entropy.h" #include "hash.h" +#include "key_pair.h" #define FMT_HEADER_ONLY #include @@ -230,8 +229,17 @@ namespace crypto #endif } - Pem KeyPair_mbedTLS::create_csr(const std::string& name) const + Pem KeyPair_mbedTLS::create_csr( + const std::string& name, const std::vector& sans) const { + // mbedtls does not support parsing x509v3 extensions from a CSR + // (https://github.com/ARMmbed/mbedtls/issues/2912) so disallow CSR creation + // if any SAN is specified (use OpenSSL implementation instead) + if (!sans.empty()) + { + throw std::logic_error("mbedtls cannot create CSR with SAN"); + } + auto csr = mbedtls::make_unique(); mbedtls_x509write_csr_set_md_alg(csr.get(), MBEDTLS_MD_SHA512); @@ -286,192 +294,8 @@ namespace crypto registeredID = 8 }; - static inline int x509write_crt_set_subject_alt_name( - mbedtls_x509write_cert* ctx, const char* name, san_type san) - { - uint8_t san_buf[max_san_length]; - int ret = 0; - size_t len = 0; - - // mbedtls asn1 write API writes backward in san_buf - uint8_t* pc = san_buf + max_san_length; - - auto name_len = strlen(name); - if (name_len > max_san_length) - { - throw std::logic_error(fmt::format( - "Subject Alternative Name {} is too long ({}>{})", - name, - name_len, - max_san_length)); - } - - switch (san) - { - case san_type::dns_name: - { - MBEDTLS_ASN1_CHK_ADD( - len, - mbedtls_asn1_write_raw_buffer( - &pc, san_buf, (const unsigned char*)name, name_len)); - MBEDTLS_ASN1_CHK_ADD( - len, mbedtls_asn1_write_len(&pc, san_buf, name_len)); - break; - } - - // mbedtls (2.16.2) only supports parsing of subject alternative name - // that is DNS= (so no IPAddress=). When connecting to a node that has - // IPAddress set, mbedtls_ssl_set_hostname() should not be called. - // However, it should work fine with a majority of other clients (e.g. - // curl). - case san_type::ip_address: - { - auto addr = ds::ip_to_binary(name); - if (!addr.has_value()) - { - throw std ::logic_error(fmt::format( - "Subject Alternative Name {} is not a valid IPv4 or " - "IPv6 address", - name)); - } - - MBEDTLS_ASN1_CHK_ADD( - len, - mbedtls_asn1_write_raw_buffer( - &pc, san_buf, (const unsigned char*)&addr->buf, addr->size)); - MBEDTLS_ASN1_CHK_ADD( - len, mbedtls_asn1_write_len(&pc, san_buf, addr->size)); - - break; - } - - default: - { - throw std::logic_error( - fmt::format("Subject Alternative Name {} is not supported", san)); - } - } - - MBEDTLS_ASN1_CHK_ADD( - len, - mbedtls_asn1_write_tag( - &pc, san_buf, MBEDTLS_ASN1_CONTEXT_SPECIFIC | san)); - MBEDTLS_ASN1_CHK_ADD(len, mbedtls_asn1_write_len(&pc, san_buf, len)); - MBEDTLS_ASN1_CHK_ADD( - len, - mbedtls_asn1_write_tag( - &pc, san_buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); - - return mbedtls_x509write_crt_set_extension( - ctx, - MBEDTLS_OID_SUBJECT_ALT_NAME, - MBEDTLS_OID_SIZE(MBEDTLS_OID_SUBJECT_ALT_NAME), - 0, // Mark SAN as non-critical - san_buf + max_san_length - len, - len); - } - - static inline int x509write_crt_set_subject_alt_names( - mbedtls_x509write_cert* ctx, const std::vector& sans) - { - if (sans.size() == 0) - return 0; - - if (sans.size() > max_san_entries) - { - throw std::logic_error(fmt::format( - "Cannot set more than {} subject alternative names", max_san_entries)); - } - // The factor of two is an extremely conservative provision for ASN.1 - // metadata - size_t buf_len = sans.size() * max_san_length * 2; - - std::vector buf(buf_len); - uint8_t* san_buf = buf.data(); - - int ret = 0; - size_t len = 0; - - // mbedtls asn1 write API writes backward in san_buf - uint8_t* pc = san_buf + buf_len; - - for (auto& san : sans) - { - if (san.san.size() > max_san_length) - { - throw std::logic_error(fmt::format( - "Subject Alternative Name {} is too long ({}>{})", - san.san, - san.san.size(), - max_san_length)); - } - - if (san.is_ip) - { - // mbedtls (2.16.2) only supports parsing of subject alternative name - // that is DNS= (so no IPAddress=). When connecting to a node that has - // IPAddress set, mbedtls_ssl_set_hostname() should not be called. - // However, it should work fine with a majority of other clients (e.g. - // curl). - - auto addr = ds::ip_to_binary(san.san.c_str()); - if (!addr.has_value()) - { - throw std ::logic_error(fmt::format( - "Subject Alternative Name {} is not a valid IPv4 or " - "IPv6 address", - san.san)); - } - - MBEDTLS_ASN1_CHK_ADD( - len, - mbedtls_asn1_write_raw_buffer( - &pc, san_buf, (const unsigned char*)&addr->buf, addr->size)); - MBEDTLS_ASN1_CHK_ADD( - len, mbedtls_asn1_write_len(&pc, san_buf, addr->size)); - } - else - { - MBEDTLS_ASN1_CHK_ADD( - len, - mbedtls_asn1_write_raw_buffer( - &pc, - san_buf, - (const unsigned char*)san.san.data(), - san.san.size())); - MBEDTLS_ASN1_CHK_ADD( - len, mbedtls_asn1_write_len(&pc, san_buf, san.san.size())); - } - - MBEDTLS_ASN1_CHK_ADD( - len, - mbedtls_asn1_write_tag( - &pc, - san_buf, - MBEDTLS_ASN1_CONTEXT_SPECIFIC | - (san.is_ip ? san_type::ip_address : san_type::dns_name))); - } - - MBEDTLS_ASN1_CHK_ADD(len, mbedtls_asn1_write_len(&pc, san_buf, len)); - MBEDTLS_ASN1_CHK_ADD( - len, - mbedtls_asn1_write_tag( - &pc, san_buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)); - - return mbedtls_x509write_crt_set_extension( - ctx, - MBEDTLS_OID_SUBJECT_ALT_NAME, - MBEDTLS_OID_SIZE(MBEDTLS_OID_SUBJECT_ALT_NAME), - 0, // Mark SAN as non-critical - san_buf + buf_len - len, - len); - } - Pem KeyPair_mbedTLS::sign_csr( - const Pem& issuer_cert, - const Pem& signing_request, - const std::vector subject_alt_names, - bool ca) const + const Pem& issuer_cert, const Pem& signing_request, bool ca) const { auto entropy = create_entropy(); auto csr = mbedtls::make_unique(); @@ -518,18 +342,9 @@ namespace crypto MCHK(mbedtls_x509write_crt_set_subject_key_identifier(crt.get())); MCHK(mbedtls_x509write_crt_set_authority_key_identifier(crt.get())); - // Because mbedtls does not support parsing x509v3 extensions from a - // CSR (https://github.com/ARMmbed/mbedtls/issues/2912), the CA sets the - // SAN directly instead of reading it from the CSR - try - { - MCHK(x509write_crt_set_subject_alt_names(crt.get(), subject_alt_names)); - } - catch (const std::logic_error& err) - { - LOG_FAIL_FMT("Error writing SAN: {}", err.what()); - return {}; - } + // Warn: Because mbedtls does not support parsing x509v3 extensions from a + // CSR (https://github.com/ARMmbed/mbedtls/issues/2912), so those are + // ignored and not set in the certificate uint8_t buf[4096]; memset(buf, 0, sizeof(buf)); diff --git a/src/crypto/mbedtls/key_pair.h b/src/crypto/mbedtls/key_pair.h index cc46af648fb..d5a0121a3f9 100644 --- a/src/crypto/mbedtls/key_pair.h +++ b/src/crypto/mbedtls/key_pair.h @@ -3,7 +3,6 @@ #pragma once #include "../key_pair.h" - #include "../san.h" #include "mbedtls_wrappers.h" #include "public_key.h" @@ -50,12 +49,13 @@ namespace crypto size_t* sig_size, uint8_t* sig) const override; - virtual Pem create_csr(const std::string& name) const override; + virtual Pem create_csr( + const std::string& name, + const std::vector& sans = {}) const override; virtual Pem sign_csr( const Pem& issuer_cert, const Pem& signing_request, - const std::vector subject_alt_names, bool ca = false) const override; }; } diff --git a/src/crypto/openssl/key_pair.cpp b/src/crypto/openssl/key_pair.cpp index be6cef081b9..128dff173fb 100644 --- a/src/crypto/openssl/key_pair.cpp +++ b/src/crypto/openssl/key_pair.cpp @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. -#include "key_pair.h" - #include "crypto/curve.h" #include "crypto/openssl/public_key.h" #include "hash.h" +#include "key_pair.h" #include "openssl_wrappers.h" #include @@ -160,7 +159,8 @@ namespace crypto return 0; } - Pem KeyPair_OpenSSL::create_csr(const std::string& name) const + Pem KeyPair_OpenSSL::create_csr( + const std::string& name, const std::vector& sans) const { Unique_X509_REQ req; @@ -187,6 +187,43 @@ namespace crypto if (key) OpenSSL::CHECK1(X509_REQ_sign(req, key, EVP_sha512())); + if (!sans.empty()) + { + Unique_STACK_OF_X509_EXTENSIONS exts; + + std::string all_alt_names; + bool first = true; + for (const auto& san : sans) + { + LOG_FAIL_FMT("Adding san: {}", san.san); + if (first) + { + first = !first; + } + else + { + all_alt_names += ", "; + } + + if (san.is_ip) + { + all_alt_names += "IP:"; + } + else + { + all_alt_names += "DNS:"; + } + all_alt_names += san.san; + } + + X509_EXTENSION* ext = NULL; + OpenSSL::CHECKNULL( + ext = X509V3_EXT_conf_nid( + NULL, NULL, NID_subject_alt_name, all_alt_names.c_str())); + sk_X509_EXTENSION_push(exts, ext); + X509_REQ_add_extensions(req, exts); + } + Unique_BIO mem; OpenSSL::CHECK1(PEM_write_bio_X509_REQ(mem, req)); @@ -198,10 +235,7 @@ namespace crypto } Pem KeyPair_OpenSSL::sign_csr( - const Pem& issuer_cert, - const Pem& signing_request, - const std::vector subject_alt_names, - bool ca) const + const Pem& issuer_cert, const Pem& signing_request, bool ca) const { X509* icrt = NULL; Unique_BIO mem(signing_request); @@ -281,35 +315,21 @@ namespace crypto OpenSSL::CHECK1(X509_add_ext(crt, ext, -1)); X509_EXTENSION_free(ext); - // Subject alternative names (Necessary? Shouldn't they be in the CSR?) - if (!subject_alt_names.empty()) + // Add subject alternative names (read from csr) + Unique_STACK_OF_X509_EXTENSIONS exts = X509_REQ_get_extensions(csr); + int extension_count = sk_X509_EXTENSION_num(exts); + if (extension_count > 0) { - std::string all_alt_names; - bool first = true; - for (auto san : subject_alt_names) + for (size_t i = 0; i < extension_count; i++) { - if (first) + X509_EXTENSION* ext = sk_X509_EXTENSION_value(exts, i); + ASN1_OBJECT* obj = X509_EXTENSION_get_object(ext); + auto nid = OBJ_obj2nid(obj); + if (nid == NID_subject_alt_name) { - first = !first; + OpenSSL::CHECK1(X509_add_ext(crt, ext, -1)); } - else - { - all_alt_names += ", "; - } - - if (san.is_ip) - all_alt_names += "IP:"; - else - all_alt_names += "DNS:"; - - all_alt_names += san.san; } - - OpenSSL::CHECKNULL( - ext = X509V3_EXT_conf_nid( - NULL, &v3ctx, NID_subject_alt_name, all_alt_names.c_str())); - OpenSSL::CHECK1(X509_add_ext(crt, ext, -1)); - X509_EXTENSION_free(ext); } // Sign diff --git a/src/crypto/openssl/key_pair.h b/src/crypto/openssl/key_pair.h index c7fa7379f03..cdac6fe5def 100644 --- a/src/crypto/openssl/key_pair.h +++ b/src/crypto/openssl/key_pair.h @@ -3,10 +3,10 @@ #pragma once #include "../key_pair.h" - #include "openssl_wrappers.h" #include "public_key.h" +#include #include #include @@ -51,12 +51,13 @@ namespace crypto size_t* sig_size, uint8_t* sig) const override; - virtual Pem create_csr(const std::string& name) const override; + virtual Pem create_csr( + const std::string& name, + const std::vector& sans = {}) const override; virtual Pem sign_csr( const Pem& issuer_cert, const Pem& signing_request, - const std::vector subject_alt_names, bool ca = false) const override; }; } diff --git a/src/crypto/openssl/openssl_wrappers.h b/src/crypto/openssl/openssl_wrappers.h index 47203f83560..2c6def7ce65 100644 --- a/src/crypto/openssl/openssl_wrappers.h +++ b/src/crypto/openssl/openssl_wrappers.h @@ -200,6 +200,32 @@ namespace crypto } }; + class Unique_STACK_OF_X509_EXTENSIONS + { + std::unique_ptr< + STACK_OF(X509_EXTENSION), + void (*)(STACK_OF(X509_EXTENSION)*)> + p; + + public: + Unique_STACK_OF_X509_EXTENSIONS() : + p(sk_X509_EXTENSION_new_null(), + [](auto x) { sk_X509_EXTENSION_pop_free(x, X509_EXTENSION_free); }) + { + OpenSSL::CHECKNULL(p.get()); + } + + Unique_STACK_OF_X509_EXTENSIONS(STACK_OF(X509_EXTENSION) * exts) : + p(exts, + [](auto x) { sk_X509_EXTENSION_pop_free(x, X509_EXTENSION_free); }) + {} + + operator STACK_OF(X509_EXTENSION) * () + { + return p.get(); + } + }; + class Unique_ECDSA_SIG { std::unique_ptr p; diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index 5cef3349618..335a1f27ba0 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -391,15 +391,19 @@ void run_csr() const char* subject_name = "CN=myname"; - auto csr = kpm.create_csr(subject_name); - std::vector subject_alternative_names; - subject_alternative_names.push_back({"email:my-other-name", false}); - subject_alternative_names.push_back({"www.microsoft.com", false}); - subject_alternative_names.push_back({"192.168.0.1", true}); + + if constexpr (std::is_same_v) + { + subject_alternative_names.push_back({"email:my-other-name", false}); + subject_alternative_names.push_back({"www.microsoft.com", false}); + subject_alternative_names.push_back({"192.168.0.1", true}); + } + + auto csr = kpm.create_csr(subject_name, subject_alternative_names); auto icrt = kpm.self_sign("CN=issuer"); - auto crt = kpm.sign_csr(icrt, csr, subject_alternative_names); + auto crt = kpm.sign_csr(icrt, csr); std::vector content = {0, 1, 2, 3, 4}; auto signature = kpm.sign(content); diff --git a/src/node/identity.h b/src/node/identity.h index d860754b3b6..3c9f9898525 100644 --- a/src/node/identity.h +++ b/src/node/identity.h @@ -3,7 +3,7 @@ #pragma once #include "crypto/curve.h" -#include "crypto/key_pair.h" +#include "crypto/openssl/key_pair.h" #include #include @@ -24,7 +24,8 @@ namespace ccf NetworkIdentity(const std::string& name, crypto::CurveID curve_id) { - auto identity_key_pair = crypto::make_key_pair(curve_id); + auto identity_key_pair = + std::make_shared(curve_id); cert = identity_key_pair->self_sign(name); priv_key = identity_key_pair->private_key_pem(); } diff --git a/src/node/node_state.h b/src/node/node_state.h index b4d655a18ee..500580112dd 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -89,7 +89,7 @@ namespace ccf std::mutex lock; CurveID curve_id; - crypto::KeyPairPtr node_sign_kp; + std::shared_ptr node_sign_kp; NodeId self; std::shared_ptr node_encrypt_kp; crypto::Pem node_cert; @@ -257,7 +257,7 @@ namespace ccf CurveID curve_id_) : sm("NodeState", State::uninitialized), curve_id(curve_id_), - node_sign_kp(crypto::make_key_pair(curve_id_)), + node_sign_kp(std::make_shared(curve_id_)), node_encrypt_kp(crypto::make_rsa_key_pair()), writer_factory(writer_factory), to_host(writer_factory.create_writer_to_outside()), @@ -1616,10 +1616,10 @@ namespace ccf Pem create_endorsed_node_cert() { auto nw = crypto::make_key_pair(network.identity->priv_key); - auto csr = - node_sign_kp->create_csr(config.node_certificate_subject_identity.name); auto sans = get_subject_alternative_names(); - return nw->sign_csr(network.identity->cert, csr, sans); + auto csr = node_sign_kp->create_csr( + config.node_certificate_subject_identity.name, sans); + return nw->sign_csr(network.identity->cert, csr); } // TODO: Rename @@ -1631,7 +1631,7 @@ namespace ccf { LOG_FAIL_FMT("SAN count: {}", subject_identity.sans.size()); return crypto::make_key_pair(endorser_private_key) - ->sign_csr(endorser_cert, subject_csr, subject_identity.sans); + ->sign_csr(endorser_cert, subject_csr); } void accept_node_tls_connections() diff --git a/src/tls/test/cert.cpp b/src/tls/test/cert.cpp index fa3f01c1ec8..51b9e3953d1 100644 --- a/src/tls/test/cert.cpp +++ b/src/tls/test/cert.cpp @@ -26,8 +26,9 @@ int main(int argc, char** argv) auto kp = crypto::make_key_pair(); auto icrt = kp->self_sign("CN=issuer"); - auto csr = kp->create_csr(cert_subject_identity.name); - auto cert = kp->sign_csr(icrt, csr, cert_subject_identity.sans); + auto csr = + kp->create_csr(cert_subject_identity.name, cert_subject_identity.sans); + auto cert = kp->sign_csr(icrt, csr); std::cout << cert.str() << std::endl; return 0; From eaa7f71844914e0f410adabb71201bd1d7fca758 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 20 Jul 2021 15:44:03 +0100 Subject: [PATCH 013/105] Refactor arguments to CSR generation --- src/crypto/key_pair.h | 18 +++++++++--------- src/crypto/mbedtls/key_pair.cpp | 8 ++++---- src/crypto/mbedtls/key_pair.h | 3 +-- src/crypto/openssl/key_pair.cpp | 9 ++++----- src/crypto/openssl/key_pair.h | 3 +-- src/crypto/san.h | 8 ++++++-- src/crypto/test/crypto.cpp | 2 +- src/node/node_state.h | 14 +++++--------- src/node/rpc/test/node_frontend_test.cpp | 9 ++++----- src/node/test/channels.cpp | 1 - src/tls/test/cert.cpp | 3 +-- 11 files changed, 36 insertions(+), 42 deletions(-) diff --git a/src/crypto/key_pair.h b/src/crypto/key_pair.h index 70bf1f64c2d..63723ddf010 100644 --- a/src/crypto/key_pair.h +++ b/src/crypto/key_pair.h @@ -45,9 +45,12 @@ namespace crypto virtual std::vector sign(CBuffer d, MDType md_type = {}) const = 0; - virtual Pem create_csr( - const std::string& name, - const std::vector& sans = {}) const = 0; + virtual Pem create_csr(const CertificateSubjectIdentity& csi) const = 0; + + Pem create_csr(const std::string& name) + { + return create_csr(CertificateSubjectIdentity(name)); + } virtual Pem sign_csr( const Pem& issuer_cert, @@ -62,16 +65,13 @@ namespace crypto std::vector sans; if (subject_alt_name.has_value()) sans.push_back(subject_alt_name.value()); - auto csr = create_csr(name, sans); + auto csr = create_csr({name, sans}); return sign_csr(Pem(0), csr, ca); } - Pem self_sign( - const std::string& name, - const std::vector subject_alt_names, - bool ca = true) const + Pem self_sign(const CertificateSubjectIdentity& csi, bool ca = true) const { - auto csr = create_csr(name, subject_alt_names); + auto csr = create_csr(csi); return sign_csr(Pem(0), csr, ca); } }; diff --git a/src/crypto/mbedtls/key_pair.cpp b/src/crypto/mbedtls/key_pair.cpp index 70859d5fd79..f55c7fa9cfe 100644 --- a/src/crypto/mbedtls/key_pair.cpp +++ b/src/crypto/mbedtls/key_pair.cpp @@ -229,13 +229,12 @@ namespace crypto #endif } - Pem KeyPair_mbedTLS::create_csr( - const std::string& name, const std::vector& sans) const + Pem KeyPair_mbedTLS::create_csr(const CertificateSubjectIdentity& csi) const { // mbedtls does not support parsing x509v3 extensions from a CSR // (https://github.com/ARMmbed/mbedtls/issues/2912) so disallow CSR creation // if any SAN is specified (use OpenSSL implementation instead) - if (!sans.empty()) + if (!csi.sans.empty()) { throw std::logic_error("mbedtls cannot create CSR with SAN"); } @@ -243,7 +242,8 @@ namespace crypto auto csr = mbedtls::make_unique(); mbedtls_x509write_csr_set_md_alg(csr.get(), MBEDTLS_MD_SHA512); - if (mbedtls_x509write_csr_set_subject_name(csr.get(), name.c_str()) != 0) + if ( + mbedtls_x509write_csr_set_subject_name(csr.get(), csi.name.c_str()) != 0) return {}; mbedtls_x509write_csr_set_key(csr.get(), ctx.get()); diff --git a/src/crypto/mbedtls/key_pair.h b/src/crypto/mbedtls/key_pair.h index d5a0121a3f9..8574832306a 100644 --- a/src/crypto/mbedtls/key_pair.h +++ b/src/crypto/mbedtls/key_pair.h @@ -50,8 +50,7 @@ namespace crypto uint8_t* sig) const override; virtual Pem create_csr( - const std::string& name, - const std::vector& sans = {}) const override; + const CertificateSubjectIdentity& csi) const override; virtual Pem sign_csr( const Pem& issuer_cert, diff --git a/src/crypto/openssl/key_pair.cpp b/src/crypto/openssl/key_pair.cpp index d04723c6a20..8ac46ea068f 100644 --- a/src/crypto/openssl/key_pair.cpp +++ b/src/crypto/openssl/key_pair.cpp @@ -161,8 +161,7 @@ namespace crypto return 0; } - Pem KeyPair_OpenSSL::create_csr( - const std::string& name, const std::vector& sans) const + Pem KeyPair_OpenSSL::create_csr(const CertificateSubjectIdentity& csi) const { Unique_X509_REQ req; @@ -171,7 +170,7 @@ namespace crypto X509_NAME* subj_name = NULL; OpenSSL::CHECKNULL(subj_name = X509_NAME_new()); - for (const auto& [k, v] : parse_name(name)) + for (const auto& [k, v] : parse_name(csi.name)) { OpenSSL::CHECK1(X509_NAME_add_entry_by_txt( subj_name, @@ -189,7 +188,7 @@ namespace crypto if (key) OpenSSL::CHECK1(X509_REQ_sign(req, key, EVP_sha512())); - if (!sans.empty()) + if (!csi.sans.empty()) { Unique_STACK_OF_X509_EXTENSIONS exts; @@ -199,7 +198,7 @@ namespace crypto NULL, NULL, NID_subject_alt_name, - fmt::format("{}", fmt::join(sans, ", ")).c_str())); + fmt::format("{}", fmt::join(csi.sans, ", ")).c_str())); sk_X509_EXTENSION_push(exts, ext); X509_REQ_add_extensions(req, exts); } diff --git a/src/crypto/openssl/key_pair.h b/src/crypto/openssl/key_pair.h index cdac6fe5def..2193a18fd7d 100644 --- a/src/crypto/openssl/key_pair.h +++ b/src/crypto/openssl/key_pair.h @@ -52,8 +52,7 @@ namespace crypto uint8_t* sig) const override; virtual Pem create_csr( - const std::string& name, - const std::vector& sans = {}) const override; + const CertificateSubjectIdentity& csi) const override; virtual Pem sign_csr( const Pem& issuer_cert, diff --git a/src/crypto/san.h b/src/crypto/san.h index 80338cc5ed0..d7a596e074a 100644 --- a/src/crypto/san.h +++ b/src/crypto/san.h @@ -31,11 +31,15 @@ namespace crypto struct CertificateSubjectIdentity { - std::vector sans = {}; std::string name; + std::vector sans = {}; CertificateSubjectIdentity() = default; - CertificateSubjectIdentity(const std::string& name) : name(name) {} + CertificateSubjectIdentity( + const std::string& name, const std::vector& sans = {}) : + name(name), + sans(sans) + {} bool operator==(const CertificateSubjectIdentity& other) const { diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index 84d52b3cd8f..b765b80058d 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -401,7 +401,7 @@ void run_csr() subject_alternative_names.push_back({"192.168.0.1", true}); } - auto csr = kpm.create_csr(subject_name, subject_alternative_names); + auto csr = kpm.create_csr({subject_name, subject_alternative_names}); auto icrt = kpm.self_sign("CN=issuer"); auto crt = kpm.sign_csr(icrt, csr); diff --git a/src/node/node_state.h b/src/node/node_state.h index e8ba9835754..c361087f7b8 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -681,9 +681,8 @@ namespace ccf join_params.startup_seqno = startup_seqno; join_params.certificate_subject_identity = config.node_certificate_subject_identity; - join_params.certificate_signing_request = node_sign_kp->create_csr( - config.node_certificate_subject_identity.name, - config.node_certificate_subject_identity.sans); + join_params.certificate_signing_request = + node_sign_kp->create_csr(config.node_certificate_subject_identity); LOG_DEBUG_FMT( "Sending join request to {}:{}", @@ -1608,17 +1607,14 @@ namespace ccf Pem create_self_signed_node_cert() { - auto sans = get_subject_alternative_names(); - return node_sign_kp->self_sign( - config.node_certificate_subject_identity.name, sans); + return node_sign_kp->self_sign(config.node_certificate_subject_identity); } Pem create_endorsed_node_cert() { auto nw = crypto::make_key_pair(network.identity->priv_key); - auto sans = get_subject_alternative_names(); - auto csr = node_sign_kp->create_csr( - config.node_certificate_subject_identity.name, sans); + auto csr = + node_sign_kp->create_csr(config.node_certificate_subject_identity); return nw->sign_csr(network.identity->cert, csr); } diff --git a/src/node/rpc/test/node_frontend_test.cpp b/src/node/rpc/test/node_frontend_test.cpp index 31fc4a60ec8..736a7fccfb2 100644 --- a/src/node/rpc/test/node_frontend_test.cpp +++ b/src/node/rpc/test/node_frontend_test.cpp @@ -104,7 +104,7 @@ TEST_CASE("Add a node to an opening service") // Node certificate crypto::KeyPairPtr kp = crypto::make_key_pair(); - const auto caller = kp->self_sign(fmt::format("CN=Joiner")); + const auto caller = kp->self_sign("CN=Joiner"); const auto node_public_encryption_key = crypto::make_key_pair()->public_key_pem(); @@ -198,8 +198,7 @@ TEST_CASE("Add a node to an opening service") "Adding a different node with the same node network details should fail"); { crypto::KeyPairPtr kp = crypto::make_key_pair(); - auto v = - crypto::make_verifier(kp->self_sign(fmt::format("CN=Other Joiner"))); + auto v = crypto::make_verifier(kp->self_sign("CN=Other Joiner")); const auto caller = v->cert_der(); // Network node info is empty (same as before) @@ -245,7 +244,7 @@ TEST_CASE("Add a node to an open service") // Node certificate crypto::KeyPairPtr kp = crypto::make_key_pair(); - const auto caller = kp->self_sign(fmt::format("CN=Joiner")); + const auto caller = kp->self_sign("CN=Joiner"); std::optional node_info; auto tx = network.tables->create_tx(); @@ -276,7 +275,7 @@ TEST_CASE("Add a node to an open service") "Adding a different node with the same node network details should fail"); { crypto::KeyPairPtr kp = crypto::make_key_pair(); - auto v = crypto::make_verifier(kp->self_sign(fmt::format("CN=Joiner"))); + auto v = crypto::make_verifier(kp->self_sign("CN=Joiner")); const auto caller = v->cert_der(); // Network node info is empty (same as before) diff --git a/src/node/test/channels.cpp b/src/node/test/channels.cpp index e4076ff1b86..e191de4cb69 100644 --- a/src/node/test/channels.cpp +++ b/src/node/test/channels.cpp @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. #include "../channels.h" - #include "crypto/verifier.h" #include "ds/hex.h" #include "node/entities.h" diff --git a/src/tls/test/cert.cpp b/src/tls/test/cert.cpp index 51b9e3953d1..460478cba0b 100644 --- a/src/tls/test/cert.cpp +++ b/src/tls/test/cert.cpp @@ -26,8 +26,7 @@ int main(int argc, char** argv) auto kp = crypto::make_key_pair(); auto icrt = kp->self_sign("CN=issuer"); - auto csr = - kp->create_csr(cert_subject_identity.name, cert_subject_identity.sans); + auto csr = kp->create_csr(cert_subject_identity); auto cert = kp->sign_csr(icrt, csr); std::cout << cert.str() << std::endl; From 5dfedd88fbe9ba6abf55308552abe2bf41ead9b1 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 20 Jul 2021 15:53:57 +0100 Subject: [PATCH 014/105] Remove CSI from store --- src/js/wrap.cpp | 17 +---------------- src/node/node_state.h | 4 ---- src/node/nodes.h | 5 ----- src/node/rpc/node_call_types.h | 3 --- src/node/rpc/node_frontend.h | 5 +---- src/node/rpc/node_interface.h | 1 - src/node/rpc/serialization.h | 4 +--- src/node/rpc/test/node_stub.h | 1 - 8 files changed, 3 insertions(+), 37 deletions(-) diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index 4c7232460cb..01c01c3feb9 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -543,23 +543,8 @@ namespace js JS_FreeCString(ctx, certificate_subject_identity_cstr); JS_FreeValue(ctx, certificate_subject_identity_val); - crypto::CertificateSubjectIdentity certificate_subject_identity; - try - { - certificate_subject_identity = - nlohmann::json::parse(certificate_subject_identity_json) - .get(); - } - catch (std::exception& exc) - { - return JS_ThrowInternalError(ctx, "Error: %s", exc.what()); - } - auto endorsed_cert = node->generate_endorsed_certificate( - csr, - certificate_subject_identity, - network->identity->priv_key, - network->identity->cert); + csr, network->identity->priv_key, network->identity->cert); return JS_NewString(ctx, endorsed_cert.str().c_str()); } diff --git a/src/node/node_state.h b/src/node/node_state.h index c361087f7b8..b3a6a2e5be7 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -679,8 +679,6 @@ namespace ccf join_params.quote_info = quote_info; join_params.consensus_type = network.consensus_type; join_params.startup_seqno = startup_seqno; - join_params.certificate_subject_identity = - config.node_certificate_subject_identity; join_params.certificate_signing_request = node_sign_kp->create_csr(config.node_certificate_subject_identity); @@ -1621,11 +1619,9 @@ namespace ccf // TODO: Rename crypto::Pem generate_endorsed_certificate( const crypto::Pem& subject_csr, - const crypto::CertificateSubjectIdentity& subject_identity, const crypto::Pem& endorser_private_key, const crypto::Pem& endorser_cert) override { - LOG_FAIL_FMT("SAN count: {}", subject_identity.sans.size()); return crypto::make_key_pair(endorser_private_key) ->sign_csr(endorser_cert, subject_csr); } diff --git a/src/node/nodes.h b/src/node/nodes.h index 7d8a837b98a..db730082bda 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -57,10 +57,6 @@ namespace ccf * Fields below are added in 2.x */ - /// Node certificate subject identity - std::optional - certificate_subject_identity = std::nullopt; - /// Node original certificate signing request std::optional certificate_signing_request = std::nullopt; @@ -74,7 +70,6 @@ namespace ccf NodeInfo, ledger_secret_seqno, code_digest, - certificate_subject_identity, certificate_signing_request, public_key); diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 09ace23bfd1..34ce839c2d1 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -81,9 +81,6 @@ namespace ccf crypto::Pem public_encryption_key; ConsensusType consensus_type = ConsensusType::CFT; std::optional startup_seqno = std::nullopt; - std::optional - certificate_subject_identity = - std::nullopt; // TODO: Remove since OpenSSL is sufficient! std::optional certificate_signing_request = std::nullopt; }; diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index b687a6a7be3..b613415e46b 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -213,8 +213,6 @@ namespace ccf node_status, ledger_secret_seqno, ds::to_hex(code_digest.data), - in.certificate_subject_identity, // TODO: Remove as we only need - // OpenSSL backend! in.certificate_signing_request, crypto::public_key_pem_from_cert(node_der)}); @@ -237,11 +235,10 @@ namespace ccf std::optional endorsed_certificate = std::nullopt; if (in.certificate_signing_request.has_value()) { - LOG_FAIL_FMT("Recording endorsed identity!"); + LOG_FAIL_FMT("Recording endorsed identity!"); // TODO: Remove! endorsed_certificate = context.get_node_state().generate_endorsed_certificate( in.certificate_signing_request.value(), - in.certificate_subject_identity.value(), this->network.identity->priv_key, this->network.identity->cert); node_endorsed_certificates->put( diff --git a/src/node/rpc/node_interface.h b/src/node/rpc/node_interface.h index 9676a876963..c42d205ce1a 100644 --- a/src/node/rpc/node_interface.h +++ b/src/node/rpc/node_interface.h @@ -58,7 +58,6 @@ namespace ccf virtual SessionMetrics get_session_metrics() = 0; virtual crypto::Pem generate_endorsed_certificate( const crypto::Pem& subject_csr, - const crypto::CertificateSubjectIdentity& subject_identity, const crypto::Pem& endorser_private_key, const crypto::Pem& endorser_cert) = 0; }; diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index bd1e1421d12..5441b3bfff4 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -37,9 +37,7 @@ namespace ccf consensus_type, startup_seqno) DECLARE_JSON_OPTIONAL_FIELDS( - JoinNetworkNodeToNode::In, - certificate_subject_identity, - certificate_signing_request) + JoinNetworkNodeToNode::In, certificate_signing_request) DECLARE_JSON_TYPE(NetworkIdentity) DECLARE_JSON_REQUIRED_FIELDS(NetworkIdentity, cert, priv_key) diff --git a/src/node/rpc/test/node_stub.h b/src/node/rpc/test/node_stub.h index 8d991215ba0..b942ce2edff 100644 --- a/src/node/rpc/test/node_stub.h +++ b/src/node/rpc/test/node_stub.h @@ -119,7 +119,6 @@ namespace ccf crypto::Pem generate_endorsed_certificate( const crypto::Pem& subject_csr, - const crypto::CertificateSubjectIdentity& subject_identity, const crypto::Pem& endorser_private_key, const crypto::Pem& endorser_cert) override { From a85bc94a7feb9e4f61da932276dc497f3058db7d Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 21 Jul 2021 11:51:40 +0100 Subject: [PATCH 015/105] Parse public key from CSR --- src/crypto/csr.h | 32 ++++++++++++++++++++++++++++++++ src/crypto/mbedtls/key_pair.cpp | 23 ----------------------- src/crypto/test/crypto.cpp | 17 +++++++++++++++++ src/crypto/verifier.h | 3 +-- src/node/nodes.h | 8 +++++--- src/node/rpc/error.h | 1 + src/node/rpc/node_frontend.h | 20 ++++++++++++++++++-- 7 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 src/crypto/csr.h diff --git a/src/crypto/csr.h b/src/crypto/csr.h new file mode 100644 index 00000000000..bfc397a6074 --- /dev/null +++ b/src/crypto/csr.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "crypto/openssl/openssl_wrappers.h" +#include "pem.h" + +#include + +namespace crypto +{ + /** Extracts the public key from a certificate signing request (CSR). + * @param signing_request CSR to extract the public key from + * @return extracted public key + */ + Pem public_key_pem_from_csr(const Pem& signing_request) + { + X509* icrt = NULL; + OpenSSL::Unique_BIO mem(signing_request); + OpenSSL::Unique_X509_REQ csr(mem); + OpenSSL::Unique_BIO buf; + + EVP_PKEY* req_pubkey = X509_REQ_get_pubkey(csr); + + OpenSSL::CHECK1(PEM_write_bio_PUBKEY(buf, req_pubkey)); + EVP_PKEY_free(req_pubkey); + + BUF_MEM* bptr; + BIO_get_mem_ptr(buf, &bptr); + return Pem((uint8_t*)bptr->data, bptr->length); + } +} \ No newline at end of file diff --git a/src/crypto/mbedtls/key_pair.cpp b/src/crypto/mbedtls/key_pair.cpp index f55c7fa9cfe..3242ae156e5 100644 --- a/src/crypto/mbedtls/key_pair.cpp +++ b/src/crypto/mbedtls/key_pair.cpp @@ -271,29 +271,6 @@ namespace crypto } } - // Unfortunately, mbedtls does not provide a convenient API to write x509v3 - // extensions for all supported Subject Alternative Name (SAN). Until they - // do, we have to write raw ASN1 ourselves. - - // rfc5280 does not specify a maximum length for SAN, - // but rfc1035 specified that 255 bytes is enough for a DNS name - static constexpr auto max_san_length = 256; - static constexpr auto max_san_entries = 8; - - // As per https://tools.ietf.org/html/rfc5280#section-4.2.1.6 - enum san_type - { - other_name = 0, - rfc822_name = 1, - dns_name = 2, - x400_address = 3, - directory_name = 4, - edi_party_name = 5, - uniform_resource_identifier = 6, - ip_address = 7, - registeredID = 8 - }; - Pem KeyPair_mbedTLS::sign_csr( const Pem& issuer_cert, const Pem& signing_request, bool ca) const { diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index b765b80058d..18c1f35b9ed 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "crypto/csr.h" #include "crypto/entropy.h" #include "crypto/key_pair.h" #include "crypto/key_wrap.h" @@ -384,6 +385,22 @@ TEST_CASE("Extract public key from cert") } } +template +void create_csr_and_extract_pubk() +{ + T kp(CurveID::SECP384R1); + auto pk = kp.public_key_pem(); + auto csr = kp.create_csr({"CN=name"}); + auto pubk = public_key_pem_from_csr(csr); + REQUIRE(pk == pubk); +} + +TEST_CASE("Extract public key from csr") +{ + create_csr_and_extract_pubk(); + create_csr_and_extract_pubk(); +} + template void run_csr() { diff --git a/src/crypto/verifier.h b/src/crypto/verifier.h index 720b8c3b12a..e4f8b8e246a 100644 --- a/src/crypto/verifier.h +++ b/src/crypto/verifier.h @@ -229,8 +229,7 @@ namespace crypto std::vector public_key_der_from_cert( const std::vector& der); - crypto::Pem public_key_pem_from_cert( - const std::vector& der); + crypto::Pem public_key_pem_from_cert(const std::vector& der); void check_is_cert(const CBuffer& der); } diff --git a/src/node/nodes.h b/src/node/nodes.h index db730082bda..cb6a902269f 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -36,8 +36,10 @@ namespace ccf { struct NodeInfo : NodeInfoNetwork { - /** Node certificate. Only set for 1.x releases. Further releases record - * node identity in `public_key` field */ + /** Deprecated. + * Node certificate. Only set for 1.x releases. Further releases record + * node identity in `public_key` field. Service-endorsed certificate is + * recorded in "public:ccf.nodes.endorsed_certificates" table */ crypto::Pem cert; /// Node enclave quote QuoteInfo quote_info; @@ -57,7 +59,7 @@ namespace ccf * Fields below are added in 2.x */ - /// Node original certificate signing request + /// Node certificate signing request std::optional certificate_signing_request = std::nullopt; /// Public key diff --git a/src/node/rpc/error.h b/src/node/rpc/error.h index 0f1473edb51..7ad9f2f0526 100644 --- a/src/node/rpc/error.h +++ b/src/node/rpc/error.h @@ -76,6 +76,7 @@ namespace ccf ERROR(InvalidNodeState) ERROR(NodeAlreadyExists) ERROR(StartupSnapshotIsOld) + ERROR(CSRPublicKeyInvalid) #undef ERROR } diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index b613415e46b..4d459fd3e1f 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -7,6 +7,7 @@ #include "ccf/http_query.h" #include "ccf/json_handler.h" #include "ccf/version.h" +#include "crypto/csr.h" #include "crypto/hash.h" #include "frontend.h" #include "node/entities.h" @@ -202,7 +203,22 @@ namespace ccf this->network.ledger_secrets->get_latest(tx).first; } - // TODO: Check that the public key in the CSR matches the TLS identity + // Note: All new nodes should specify a CSR from 2.x + // TODO: Should we enforce this? + auto client_public_key_pem = crypto::public_key_pem_from_cert(node_der); + if (in.certificate_signing_request.has_value()) + { + // Verify that client's public key matches the one specified in the CSR) + auto csr_public_key_pem = crypto::public_key_pem_from_csr( + in.certificate_signing_request.value()); + if (client_public_key_pem != csr_public_key_pem) + { + return make_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::CSRPublicKeyInvalid, + "Public key in CSR does not match TLS client identity."); + } + } nodes->put( joining_node_id, @@ -214,7 +230,7 @@ namespace ccf ledger_secret_seqno, ds::to_hex(code_digest.data), in.certificate_signing_request, - crypto::public_key_pem_from_cert(node_der)}); + client_public_key_pem}); kv::NetworkConfiguration nc = get_latest_network_configuration(network, tx); From 258550a3db470949bce2cddad593221734aae1a3 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 21 Jul 2021 15:29:52 +0100 Subject: [PATCH 016/105] Add missing & --- src/crypto/key_pair.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/key_pair.h b/src/crypto/key_pair.h index 63723ddf010..0f4966e06b9 100644 --- a/src/crypto/key_pair.h +++ b/src/crypto/key_pair.h @@ -93,7 +93,7 @@ namespace crypto * @param der Sequence of bytes containing the key in DER format * @return Public key */ - PublicKeyPtr make_public_key(const std::vector der); + PublicKeyPtr make_public_key(const std::vector& der); /** * Create a new public / private ECDSA key pair on specified curve and From 9eac639b7f58ffdffbe291d427b8bdc42ea5531f Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 21 Jul 2021 15:40:54 +0100 Subject: [PATCH 017/105] Signature verification works --- python/ccf/ledger.py | 21 +++++++- src/node/history.h | 34 ++++++++++--- src/node/rpc/member_frontend.h | 9 +++- src/node/rpc/node_frontend.h | 15 +++--- src/node/rpc/test/node_frontend_test.cpp | 4 +- tests/reconfiguration.py | 63 ++++++++++++------------ 6 files changed, 96 insertions(+), 50 deletions(-) diff --git a/python/ccf/ledger.py b/python/ccf/ledger.py index 74bd066880c..1f4dfd8d067 100644 --- a/python/ccf/ledger.py +++ b/python/ccf/ledger.py @@ -31,6 +31,7 @@ # https://github.com/microsoft/CCF/blob/main/src/node/entities.h SIGNATURE_TX_TABLE_NAME = "public:ccf.internal.signatures" NODES_TABLE_NAME = "public:ccf.gov.nodes.info" +ENDORSED_NODE_CERTIFICATES_TABLE_NAME = "public:ccf.gov.nodes.endorsed_certificates" # Key used by CCF to record single-key tables WELL_KNOWN_SINGLETON_TABLE_KEY = bytes(bytearray(8)) @@ -289,8 +290,10 @@ def add_transaction(self, transaction): for node_id, node_info in node_table.items(): node_id = node_id.decode() node_info = json.loads(node_info) - # Add the node certificate - self.node_certificates[node_id] = node_info["cert"].encode() + # Add the self-signed node certificate (only available in 1.x, + # refer to node endorsed certificates table otherwise) + if node_info["cert"]: + self.node_certificates[node_id] = node_info["cert"].encode() # Update node trust status # Also record the seqno at which the node status changed to # track when a primary node should stop issuing signatures @@ -299,6 +302,20 @@ def add_transaction(self, transaction): transaction_public_domain.get_seqno(), ) + if ENDORSED_NODE_CERTIFICATES_TABLE_NAME in tables: + node_endorsed_certificates_tables = tables[ + ENDORSED_NODE_CERTIFICATES_TABLE_NAME + ] + for node_id, node_cert in node_endorsed_certificates_tables.items(): + node_id = node_id.decode() + node_cert = json.loads(node_cert) + assert ( + node_id not in self.node_certificates + ), "Only one of node self-signed certificate and endorsed certificate should be recorded" + self.node_certificates[node_id] = node_cert[ + "endorsed_certificate" + ].encode() + # This is a merkle root/signature tx if the table exists if SIGNATURE_TX_TABLE_NAME in tables: self.signature_count += 1 diff --git a/src/node/history.h b/src/node/history.h index 27dcf0849f8..42845f26ba2 100644 --- a/src/node/history.h +++ b/src/node/history.h @@ -693,7 +693,6 @@ namespace ccf auto tx = store.create_tx(); auto signatures = tx.template ro(ccf::Tables::SIGNATURES); - auto nodes = tx.template ro(ccf::Tables::NODES); auto sig = signatures->get(); if (!sig.has_value()) { @@ -711,15 +710,36 @@ namespace ccf *signature = sig_value; } - auto ni = nodes->get(sig_value.node); - if (!ni.has_value()) + // Find node certificate from unique node ID recorded in signature table + crypto::Pem node_cert; + auto node_endorsed_certs = tx.template ro( + ccf::Tables::NODE_ENDORSED_CERTIFICATES); + auto node_endorsed_cert = node_endorsed_certs->get(sig_value.node); + if (!node_endorsed_cert.has_value()) { - LOG_FAIL_FMT( - "No node info, and therefore no cert for node {}", sig_value.node); - return false; + // No endorsed certificate for node. Its (self-signed) certificate may + // be stored in the nodes table (1.x ledger only) + + auto nodes = tx.template ro(ccf::Tables::NODES); + auto node = nodes->get(sig_value.node); + if (!node.has_value()) + { + LOG_FAIL_FMT( + "Signature cannot be verified: no certificate found for node {}", + sig_value.node); + return false; + } + + node_cert = node->cert; } + else + { + node_cert = node_endorsed_cert->endorsed_certificate; + } + + LOG_FAIL_FMT("Node cert: {}", node_cert.str()); - crypto::VerifierPtr from_cert = crypto::make_verifier(ni.value().cert); + crypto::VerifierPtr from_cert = crypto::make_verifier(node_cert); crypto::Sha256Hash root = get_replicated_state_root(); log_hash(root, VERIFY); bool result = diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 3432142d2f9..7737ec8ebe1 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -839,11 +839,12 @@ namespace ccf // recovery member is added before the service is opened. g.init_configuration(in.configuration); - // TODO: Record SAN and public key as well + // TODO: Record CSR, public key and endorsed certificate g.add_node( in.node_id, {in.node_info_network, - in.node_cert, + Pem(), // This field was used in 1.x to record self-signed node + // certificate {in.quote_info}, in.public_encryption_key, NodeStatus::TRUSTED, @@ -851,6 +852,10 @@ namespace ccf ds::to_hex(in.code_digest.data), std::nullopt}); + auto endorsed_certificates = + ctx.tx.rw(network.node_endorsed_certificates); + endorsed_certificates->put(in.node_id, {in.node_cert}); + #ifdef GET_QUOTE g.trust_node_code_id(in.code_digest); #endif diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 4d459fd3e1f..22e450b838a 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -80,23 +80,25 @@ namespace ccf std::optional check_node_exists( kv::Tx& tx, - const std::vector& node_der, + const std::vector& self_signed_node_der, std::optional node_status = std::nullopt) { + // Check that a node exists by looking up its public key in the nodes + // table. auto nodes = tx.ro(network.nodes); auto endorsed_node_certificates = tx.ro(network.node_endorsed_certificates); - auto node_pem = crypto::cert_der_to_pem(node_der); + auto pk_pem = crypto::public_key_pem_from_cert(self_signed_node_der); std::optional existing_node_info = std::nullopt; nodes->foreach([&existing_node_info, - &node_pem, + &pk_pem, &node_status, &endorsed_node_certificates]( const NodeId& nid, const NodeInfo& ni) { if ( - ni.cert == node_pem && + ni.public_key == pk_pem && (!node_status.has_value() || ni.status == node_status.value())) { auto endorsed_node_certificate = endorsed_node_certificates->get(nid); @@ -144,7 +146,7 @@ namespace ccf NodeStatus node_status, ServiceStatus service_status) { - LOG_FAIL_FMT("Service status: {}", service_status); + LOG_FAIL_FMT("Service status: {}", service_status); // TODO: Delete auto nodes = tx.rw(network.nodes); auto node_endorsed_certificates = tx.rw(network.node_endorsed_certificates); @@ -223,7 +225,8 @@ namespace ccf nodes->put( joining_node_id, {in.node_info_network, - crypto::cert_der_to_pem(node_der), + Pem(), // This field was used in 1.x to record self-signed node + // certificate in.quote_info, in.public_encryption_key, node_status, diff --git a/src/node/rpc/test/node_frontend_test.cpp b/src/node/rpc/test/node_frontend_test.cpp index 736a7fccfb2..e16152666e4 100644 --- a/src/node/rpc/test/node_frontend_test.cpp +++ b/src/node/rpc/test/node_frontend_test.cpp @@ -168,7 +168,7 @@ TEST_CASE("Add a node to an opening service") CHECK(node_info.has_value()); CHECK(node_info->status == NodeStatus::TRUSTED); - CHECK(caller == node_info->cert); + CHECK(kp->public_key_pem() == node_info->public_key); } INFO("Adding the same node should return the same result"); @@ -268,7 +268,7 @@ TEST_CASE("Add a node to an open service") node_info = nodes->get(node_id); CHECK(node_info.has_value()); CHECK(node_info->status == NodeStatus::PENDING); - CHECK(caller == node_info->cert); + CHECK(kp->public_key_pem() == node_info->public_key); } INFO( diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index a6946ccb79e..2e35331b2a2 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -427,35 +427,36 @@ def run(args): network.start_and_join(args) test_version(network, args) - - if args.consensus != "bft": - test_join_straddling_primary_replacement(network, args) - test_node_replacement(network, args) - test_add_node_from_backup(network, args) - test_add_node(network, args) - test_add_node_on_other_curve(network, args) - test_retire_backup(network, args) - test_add_as_many_pending_nodes(network, args) - test_add_node(network, args) - test_retire_primary(network, args) - - test_add_node_from_snapshot(network, args) - test_add_node_from_snapshot(network, args, from_backup=True) - test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) - latest_node_log = network.get_joined_nodes()[-1].remote.log_path() - with open(latest_node_log, "r+") as log: - assert any( - "No snapshot found: Node will replay all historical transactions" - in l - for l in log.readlines() - ), "New nodes shouldn't join from snapshot if snapshot evidence cannot be verified" - - test_node_filter(network, args) - test_retiring_nodes_emit_at_most_one_signature(network, args) - else: - test_learner_catches_up(network, args) - test_learner_does_not_take_part(network, args) - test_retire_backup(network, args) + test_add_node(network, args) + + # if args.consensus != "bft": + # test_join_straddling_primary_replacement(network, args) + # test_node_replacement(network, args) + # test_add_node_from_backup(network, args) + # test_add_node(network, args) + # test_add_node_on_other_curve(network, args) + # test_retire_backup(network, args) + # test_add_as_many_pending_nodes(network, args) + # test_add_node(network, args) + # test_retire_primary(network, args) + + # test_add_node_from_snapshot(network, args) + # test_add_node_from_snapshot(network, args, from_backup=True) + # test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) + # latest_node_log = network.get_joined_nodes()[-1].remote.log_path() + # with open(latest_node_log, "r+") as log: + # assert any( + # "No snapshot found: Node will replay all historical transactions" + # in l + # for l in log.readlines() + # ), "New nodes shouldn't join from snapshot if snapshot evidence cannot be verified" + + # test_node_filter(network, args) + # test_retiring_nodes_emit_at_most_one_signature(network, args) + # else: + # test_learner_catches_up(network, args) + # test_learner_does_not_take_part(network, args) + # test_retire_backup(network, args) def run_join_old_snapshot(args): @@ -527,5 +528,5 @@ def run_join_old_snapshot(args): run(args) - if args.consensus != "bft": - run_join_old_snapshot(args) + # if args.consensus != "bft": + # run_join_old_snapshot(args) From cd9d8a1280f597b1671ec68c81ba4ddad3585e45 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 23 Jul 2021 16:56:07 +0100 Subject: [PATCH 018/105] Change type of endorsed certificates KV map --- doc/audit/builtin_maps.rst | 7 +++++++ python/ccf/ledger.py | 10 +++++----- src/node/historical_queries.h | 3 +++ src/node/history.h | 2 +- src/node/node_state.h | 2 +- src/node/nodes.h | 10 ++-------- src/node/rpc/node_frontend.h | 8 +------- src/runtime_config/default/actions.js | 12 +++++------- 8 files changed, 25 insertions(+), 29 deletions(-) diff --git a/doc/audit/builtin_maps.rst b/doc/audit/builtin_maps.rst index bac03225cb3..ae92715384f 100644 --- a/doc/audit/builtin_maps.rst +++ b/doc/audit/builtin_maps.rst @@ -108,6 +108,13 @@ Identity, status and attestations (endorsed quotes) of the nodes hosting the net .. doxygenenum:: ccf::QuoteFormat :project: CCF +``nodes.endorsed_certificates`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Key** Node ID: SHA-256 digest of the node public key, represented as a hex-encoded string. + +**Value** Node service-endorsed certificate, represented as a PEM-encoded string. + ``network.configurations`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/python/ccf/ledger.py b/python/ccf/ledger.py index 1f4dfd8d067..309b338873a 100644 --- a/python/ccf/ledger.py +++ b/python/ccf/ledger.py @@ -306,15 +306,15 @@ def add_transaction(self, transaction): node_endorsed_certificates_tables = tables[ ENDORSED_NODE_CERTIFICATES_TABLE_NAME ] - for node_id, node_cert in node_endorsed_certificates_tables.items(): + for ( + node_id, + endorsed_node_cert, + ) in node_endorsed_certificates_tables.items(): node_id = node_id.decode() - node_cert = json.loads(node_cert) assert ( node_id not in self.node_certificates ), "Only one of node self-signed certificate and endorsed certificate should be recorded" - self.node_certificates[node_id] = node_cert[ - "endorsed_certificate" - ].encode() + self.node_certificates[node_id] = endorsed_node_cert # This is a merkle root/signature tx if the table exists if SIGNATURE_TX_TABLE_NAME in tables: diff --git a/src/node/historical_queries.h b/src/node/historical_queries.h index 16ce3cf7f9f..cbc1ffd2628 100644 --- a/src/node/historical_queries.h +++ b/src/node/historical_queries.h @@ -411,6 +411,9 @@ namespace ccf::historical return false; } + // TODO: Fix this! + // 2.x onwards should use node endorsed certificate, or public key? + // Refactor with history verify const auto node_info = get_node_info(sig->node); if (!node_info.has_value()) { diff --git a/src/node/history.h b/src/node/history.h index 42845f26ba2..f03ac782567 100644 --- a/src/node/history.h +++ b/src/node/history.h @@ -734,7 +734,7 @@ namespace ccf } else { - node_cert = node_endorsed_cert->endorsed_certificate; + node_cert = node_endorsed_cert.value(); } LOG_FAIL_FMT("Node cert: {}", node_cert.str()); diff --git a/src/node/node_state.h b/src/node/node_state.h index b3a6a2e5be7..47344b5946a 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1907,7 +1907,7 @@ namespace ccf "Could not find endorsed node certificate for {}", self)); } - node_cert = endorsed_certificate->endorsed_certificate; + node_cert = endorsed_certificate.value(); accept_network_tls_connections(); } diff --git a/src/node/nodes.h b/src/node/nodes.h index cb6a902269f..3f085260a30 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -76,14 +76,8 @@ namespace ccf public_key); using Nodes = ServiceMap; - - struct NodeEndorsedCertificate - { - crypto::Pem endorsed_certificate; - }; - DECLARE_JSON_TYPE(NodeEndorsedCertificate); - DECLARE_JSON_REQUIRED_FIELDS(NodeEndorsedCertificate, endorsed_certificate); - using NodeEndorsedCertificates = ServiceMap; + using NodeEndorsedCertificates = + kv::RawCopySerialisedMap; } FMT_BEGIN_NAMESPACE diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 22e450b838a..4b0739c8cb5 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -101,14 +101,8 @@ namespace ccf ni.public_key == pk_pem && (!node_status.has_value() || ni.status == node_status.value())) { - auto endorsed_node_certificate = endorsed_node_certificates->get(nid); existing_node_info = { - nid, - ni.ledger_secret_seqno, - endorsed_node_certificate.has_value() ? - std::make_optional( - endorsed_node_certificate->endorsed_certificate) : - std::nullopt}; + nid, ni.ledger_secret_seqno, endorsed_node_certificates->get(nid)}; return false; } return true; diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index f8dd69bf2a0..bf7306e7db6 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -779,15 +779,13 @@ const actions = new Map([ // Also endorse and record node certificate // TODO: For now, assume that node public key is always present, which isn't true for 1.x! - let node_cert = {}; - node_cert.endorsed_certificate = - ccf.network.generateEndorsedCertificate( - nodeInfo.certificate_signing_request, - nodeInfo.certificate_subject_identity - ); + const endorsed_node_cert = ccf.network.generateEndorsedCertificate( + nodeInfo.certificate_signing_request, + nodeInfo.certificate_subject_identity + ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), - ccf.jsonCompatibleToBuf(node_cert) + ccf.strToBuf(endorsed_node_cert) ); } } From 8acc546a26ef538de5ff873aac0b5a6d3ca104da Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 23 Jul 2021 17:17:50 +0100 Subject: [PATCH 019/105] Historical queries work again --- src/node/historical_queries.h | 33 ++++------------- src/node/history.h | 67 +++++++++++++++++------------------ 2 files changed, 40 insertions(+), 60 deletions(-) diff --git a/src/node/historical_queries.h b/src/node/historical_queries.h index cbc1ffd2628..725e6e29cca 100644 --- a/src/node/historical_queries.h +++ b/src/node/historical_queries.h @@ -373,17 +373,6 @@ namespace ccf::historical } } - std::optional get_node_info(const ccf::NodeId& node_id) - { - // Current solution: Use current state of Nodes table from real store. - // This only works while entries are never deleted from this table, and - // makes no check that the signing node was active at the point it - // produced this signature - auto tx = source_store.create_read_only_tx(); - auto nodes = tx.ro(ccf::Tables::NODES); - return nodes->get(node_id); - } - // Returns true if this is a valid signature that passes our verification // checks bool verify_signature(const StorePtr& sig_store, ccf::SeqNo sig_seqno) @@ -411,21 +400,13 @@ namespace ccf::historical return false; } - // TODO: Fix this! - // 2.x onwards should use node endorsed certificate, or public key? - // Refactor with history verify - const auto node_info = get_node_info(sig->node); - if (!node_info.has_value()) - { - LOG_FAIL_FMT( - "Signature at {}: Node {} is unknown", sig_seqno, sig->node); - return false; - } - - auto verifier = crypto::make_verifier(node_info->cert); - const auto verified = - verifier->verify_hash(real_root.h, sig->sig, crypto::MDType::SHA256); - if (!verified) + // Current solution: Use current state of Nodes tables from real store. + // This only works while entries are never deleted from this table, and + // makes no check that the signing node was active at the point it + // produced this signature + auto tx = source_store.create_read_only_tx(); + if (!ccf::MerkleTxHistory::verify_node_signature( + tx, sig->node, sig->sig, real_root)) { LOG_FAIL_FMT("Signature at {}: Signature invalid", sig_seqno); return false; diff --git a/src/node/history.h b/src/node/history.h index f03ac782567..eeef764f12e 100644 --- a/src/node/history.h +++ b/src/node/history.h @@ -687,46 +687,28 @@ namespace ccf return result; } - bool verify( - kv::Term* term = nullptr, PrimarySignature* signature = nullptr) override + static bool verify_node_signature( + kv::ReadOnlyTx& tx, + const NodeId& node_id, + const std::vector& expected_sig, + const crypto::Sha256Hash& expected_root) { - auto tx = store.create_tx(); - auto signatures = - tx.template ro(ccf::Tables::SIGNATURES); - auto sig = signatures->get(); - if (!sig.has_value()) - { - LOG_FAIL_FMT("No signature found in signatures map"); - return false; - } - auto& sig_value = sig.value(); - if (term) - { - *term = sig_value.view; - } - - if (signature) - { - *signature = sig_value; - } - - // Find node certificate from unique node ID recorded in signature table crypto::Pem node_cert; auto node_endorsed_certs = tx.template ro( ccf::Tables::NODE_ENDORSED_CERTIFICATES); - auto node_endorsed_cert = node_endorsed_certs->get(sig_value.node); + auto node_endorsed_cert = node_endorsed_certs->get(node_id); if (!node_endorsed_cert.has_value()) { // No endorsed certificate for node. Its (self-signed) certificate may // be stored in the nodes table (1.x ledger only) auto nodes = tx.template ro(ccf::Tables::NODES); - auto node = nodes->get(sig_value.node); + auto node = nodes->get(node_id); if (!node.has_value()) { LOG_FAIL_FMT( "Signature cannot be verified: no certificate found for node {}", - sig_value.node); + node_id); return false; } @@ -737,20 +719,37 @@ namespace ccf node_cert = node_endorsed_cert.value(); } - LOG_FAIL_FMT("Node cert: {}", node_cert.str()); - crypto::VerifierPtr from_cert = crypto::make_verifier(node_cert); - crypto::Sha256Hash root = get_replicated_state_root(); - log_hash(root, VERIFY); - bool result = - from_cert->verify_hash(root.h, sig_value.sig, crypto::MDType::SHA256); + return from_cert->verify_hash( + expected_root.h, expected_sig, crypto::MDType::SHA256); + } - if (!result) + bool verify( + kv::Term* term = nullptr, PrimarySignature* signature = nullptr) override + { + auto tx = store.create_read_only_tx(); + auto signatures = + tx.template ro(ccf::Tables::SIGNATURES); + auto sig = signatures->get(); + if (!sig.has_value()) { + LOG_FAIL_FMT("No signature found in signatures map"); return false; } + auto& sig_value = sig.value(); + if (term) + { + *term = sig_value.view; + } - return true; + if (signature) + { + *signature = sig_value; + } + + auto root = get_replicated_state_root(); + log_hash(root, VERIFY); + return verify_node_signature(tx, sig_value.node, sig_value.sig, root); } std::vector serialise_tree(size_t from, size_t to) override From be4cd7871e4b296d41a0fafd7c6d8ec20ce9689f Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 5 Aug 2021 16:30:20 +0100 Subject: [PATCH 020/105] Record public key --- src/node/node_state.h | 1 + src/node/rpc/node_call_types.h | 1 + src/node/rpc/node_frontend.h | 13 +++++++++++-- src/node/rpc/serialization.h | 1 + 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/node/node_state.h b/src/node/node_state.h index d7ef9240e8e..2715cdc826f 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1652,6 +1652,7 @@ namespace ccf create_params.node_id = self; create_params.node_cert = node_cert; + create_params.public_key = node_sign_kp->public_key_pem(); create_params.network_cert = network.identity->cert; create_params.quote_info = quote_info; create_params.public_encryption_key = node_encrypt_kp->public_key_pem(); diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 81a7f9b95d7..546a7b52786 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -61,6 +61,7 @@ namespace ccf { NodeId node_id; crypto::Pem node_cert; + crypto::Pem public_key; crypto::Pem network_cert; QuoteInfo quote_info; crypto::Pem public_encryption_key; diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 900f6196997..98ebeb7c4a2 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -1109,18 +1109,27 @@ namespace ccf // Retire all nodes, in case there are any (i.e. post recovery) g.retire_active_nodes(); + // TODO: Record CSR, public key and endorsed certificate + g.add_node( in.node_id, {in.node_info_network, - in.node_cert, + Pem(), // This field was used in 1.x to record self-signed node + // certificate {in.quote_info}, in.public_encryption_key, NodeStatus::PENDING, std::nullopt, - ds::to_hex(in.code_digest.data)}); + ds::to_hex(in.code_digest.data), + std::nullopt, + in.public_key}); g.trust_node( in.node_id, network.ledger_secrets->get_latest(ctx.tx).first); + auto endorsed_certificates = + ctx.tx.rw(network.node_endorsed_certificates); + endorsed_certificates->put(in.node_id, {in.node_cert}); + #ifdef GET_QUOTE g.trust_node_code_id(in.code_digest); #endif diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index f2e3f954756..5550399e5c3 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -69,6 +69,7 @@ namespace ccf CreateNetworkNodeToNode::In, node_id, node_cert, + public_key, network_cert, quote_info, public_encryption_key, From f6c93ee9c9ea8a18b2b6e7f8ff03c4a16b24ffae Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 5 Aug 2021 16:58:39 +0100 Subject: [PATCH 021/105] Also register CSR --- src/node/genesis_gen.h | 5 ++--- src/node/node_state.h | 3 ++- src/node/nodes.h | 3 +++ src/node/rpc/node_call_types.h | 2 +- src/node/rpc/node_frontend.h | 15 ++++++++------- src/node/rpc/serialization.h | 2 +- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/node/genesis_gen.h b/src/node/genesis_gen.h index 1ce1c30ea8e..6a4e298fd31 100644 --- a/src/node/genesis_gen.h +++ b/src/node/genesis_gen.h @@ -396,13 +396,12 @@ namespace ccf throw std::logic_error(fmt::format("Node {} is retired", node_id)); } - kv::NetworkConfiguration nc = - get_latest_network_configuration(tables, tx); - node_info->status = NodeStatus::TRUSTED; node_info->ledger_secret_seqno = latest_ledger_secret_seqno; nodes->put(node_id, node_info.value()); + kv::NetworkConfiguration nc = + get_latest_network_configuration(tables, tx); nc.nodes.insert(node_id); add_new_network_reconfiguration(tables, tx, nc); diff --git a/src/node/node_state.h b/src/node/node_state.h index 2715cdc826f..55b46c54105 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1651,7 +1651,8 @@ namespace ccf } create_params.node_id = self; - create_params.node_cert = node_cert; + create_params.certificate_signing_request = + node_sign_kp->create_csr(config.node_certificate_subject_identity); create_params.public_key = node_sign_kp->public_key_pem(); create_params.network_cert = network.identity->cert; create_params.quote_info = quote_info; diff --git a/src/node/nodes.h b/src/node/nodes.h index 3f085260a30..fcdd8e5b955 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -40,6 +40,9 @@ namespace ccf * Node certificate. Only set for 1.x releases. Further releases record * node identity in `public_key` field. Service-endorsed certificate is * recorded in "public:ccf.nodes.endorsed_certificates" table */ + // TODO: Could we change the `to_json` function here so that we don't + // serialise this for future ledger? Or make this optional with a default + // value of none?? crypto::Pem cert; /// Node enclave quote QuoteInfo quote_info; diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 546a7b52786..15b442cfb67 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -60,7 +60,7 @@ namespace ccf struct In { NodeId node_id; - crypto::Pem node_cert; + crypto::Pem certificate_signing_request; crypto::Pem public_key; crypto::Pem network_cert; QuoteInfo quote_info; diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 98ebeb7c4a2..ab95b041974 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -1109,8 +1109,6 @@ namespace ccf // Retire all nodes, in case there are any (i.e. post recovery) g.retire_active_nodes(); - // TODO: Record CSR, public key and endorsed certificate - g.add_node( in.node_id, {in.node_info_network, @@ -1118,17 +1116,20 @@ namespace ccf // certificate {in.quote_info}, in.public_encryption_key, - NodeStatus::PENDING, + NodeStatus::TRUSTED, std::nullopt, ds::to_hex(in.code_digest.data), - std::nullopt, + in.certificate_signing_request, in.public_key}); - g.trust_node( - in.node_id, network.ledger_secrets->get_latest(ctx.tx).first); auto endorsed_certificates = ctx.tx.rw(network.node_endorsed_certificates); - endorsed_certificates->put(in.node_id, {in.node_cert}); + endorsed_certificates->put( + in.node_id, + context.get_node_state().generate_endorsed_certificate( + in.certificate_signing_request, + this->network.identity->priv_key, + this->network.identity->cert)); #ifdef GET_QUOTE g.trust_node_code_id(in.code_digest); diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index 5550399e5c3..1ec561bc4fd 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -68,7 +68,7 @@ namespace ccf DECLARE_JSON_REQUIRED_FIELDS( CreateNetworkNodeToNode::In, node_id, - node_cert, + certificate_signing_request, public_key, network_cert, quote_info, From c155191024658373d6c0a0a1e8b6256d8c185539 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 5 Aug 2021 17:04:52 +0100 Subject: [PATCH 022/105] Pick up TLS cert from hook --- src/node/node_state.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/node/node_state.h b/src/node/node_state.h index 55b46c54105..8f2556083d2 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -357,7 +357,7 @@ namespace ccf network.identity = std::make_unique("CN=CCF Network", curve_id); - node_cert = create_endorsed_node_cert(); + node_cert = create_self_signed_node_cert(); network.ledger_secrets->init(); @@ -392,7 +392,7 @@ namespace ccf "Genesis transaction could not be committed"); } - accept_network_tls_connections(); + accept_node_tls_connections(); auto_refresh_jwt_keys(); reset_data(quote_info.quote); @@ -426,7 +426,7 @@ namespace ccf network.identity = std::make_unique("CN=CCF Network", curve_id); - node_cert = create_endorsed_node_cert(); + node_cert = create_self_signed_node_cert(); setup_history(); @@ -447,7 +447,7 @@ namespace ccf snapshotter->set_last_snapshot_idx(ledger_idx); } - accept_network_tls_connections(); + accept_node_tls_connections(); sm.advance(State::readingPublicLedger); @@ -539,14 +539,14 @@ namespace ccf std::make_unique(resp.network_info.identity); // Endorsed node certificate is included in join response from 2.x. - // When joining an existing 1.x service, self-sign own certificate - // and use it to endorse TLS connections if (resp.network_info.endorsed_certificate.has_value()) { node_cert = resp.network_info.endorsed_certificate.value(); } else { + // When joining an existing 1.x service, self-sign own certificate + // and use it to endorse TLS connections node_cert = create_endorsed_node_cert(); accept_network_tls_connections(); } From 1b2ca95d7b5eb7391685c3b0ce0c24955e7d90d7 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 6 Aug 2021 15:46:30 +0100 Subject: [PATCH 023/105] Remove duplicated changelog entry --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b94f1fdda90..c63333d4ca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,10 +28,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Remove long-deprecated `--domain` argument from `cchost`. Node certificate Subject Alternative Names should be passed in via existing `--san` argument (#2798). -### Removed - -- Remove long-deprecated `--domain` argument from `cchost`. Node certificate Subject Alternative Names should be passed in via existing `--san` argument. - ## [2.0.0-dev2] ### Changed From b02627ff266b0d5d0352874728c7e5237f281ccb Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 6 Aug 2021 16:33:16 +0100 Subject: [PATCH 024/105] Refactor verify node signature --- python/ccf/ledger.py | 2 +- src/crypto/openssl/key_pair.h | 1 - src/node/historical_queries.h | 4 +- src/node/history.h | 38 +--------------- src/node/node_signature_verify.h | 73 +++++++++++++++++++++++++++++++ src/node/node_state.h | 3 +- src/node/nodes.h | 17 ++++--- src/node/progress_tracker.h | 4 +- src/node/progress_tracker_types.h | 45 ++++--------------- src/node/rpc/node_frontend.h | 4 -- 10 files changed, 97 insertions(+), 94 deletions(-) create mode 100644 src/node/node_signature_verify.h diff --git a/python/ccf/ledger.py b/python/ccf/ledger.py index 309b338873a..926d9c1f1b3 100644 --- a/python/ccf/ledger.py +++ b/python/ccf/ledger.py @@ -292,7 +292,7 @@ def add_transaction(self, transaction): node_info = json.loads(node_info) # Add the self-signed node certificate (only available in 1.x, # refer to node endorsed certificates table otherwise) - if node_info["cert"]: + if "cert" in node_info: self.node_certificates[node_id] = node_info["cert"].encode() # Update node trust status # Also record the seqno at which the node status changed to diff --git a/src/crypto/openssl/key_pair.h b/src/crypto/openssl/key_pair.h index 2193a18fd7d..3f11294d8db 100644 --- a/src/crypto/openssl/key_pair.h +++ b/src/crypto/openssl/key_pair.h @@ -6,7 +6,6 @@ #include "openssl_wrappers.h" #include "public_key.h" -#include #include #include diff --git a/src/node/historical_queries.h b/src/node/historical_queries.h index 725e6e29cca..90de1cb5535 100644 --- a/src/node/historical_queries.h +++ b/src/node/historical_queries.h @@ -9,6 +9,7 @@ #include "node/encryptor.h" #include "node/history.h" #include "node/ledger_secrets.h" +#include "node/node_signature.h" #include "node/rpc/node_interface.h" #include @@ -405,8 +406,7 @@ namespace ccf::historical // makes no check that the signing node was active at the point it // produced this signature auto tx = source_store.create_read_only_tx(); - if (!ccf::MerkleTxHistory::verify_node_signature( - tx, sig->node, sig->sig, real_root)) + if (!verify_node_signature(tx, sig->node, sig->sig, real_root)) { LOG_FAIL_FMT("Signature at {}: Signature invalid", sig_seqno); return false; diff --git a/src/node/history.h b/src/node/history.h index 2f40c1e3eee..de0fcdcf73a 100644 --- a/src/node/history.h +++ b/src/node/history.h @@ -11,6 +11,7 @@ #include "entities.h" #include "kv/kv_types.h" #include "kv/store.h" +#include "node_signature_verify.h" #include "nodes.h" #include "signatures.h" #include "tls/tls.h" @@ -689,43 +690,6 @@ namespace ccf return result; } - static bool verify_node_signature( - kv::ReadOnlyTx& tx, - const NodeId& node_id, - const std::vector& expected_sig, - const crypto::Sha256Hash& expected_root) - { - crypto::Pem node_cert; - auto node_endorsed_certs = tx.template ro( - ccf::Tables::NODE_ENDORSED_CERTIFICATES); - auto node_endorsed_cert = node_endorsed_certs->get(node_id); - if (!node_endorsed_cert.has_value()) - { - // No endorsed certificate for node. Its (self-signed) certificate may - // be stored in the nodes table (1.x ledger only) - - auto nodes = tx.template ro(ccf::Tables::NODES); - auto node = nodes->get(node_id); - if (!node.has_value()) - { - LOG_FAIL_FMT( - "Signature cannot be verified: no certificate found for node {}", - node_id); - return false; - } - - node_cert = node->cert; - } - else - { - node_cert = node_endorsed_cert.value(); - } - - crypto::VerifierPtr from_cert = crypto::make_verifier(node_cert); - return from_cert->verify_hash( - expected_root.h, expected_sig, crypto::MDType::SHA256); - } - bool verify( kv::Term* term = nullptr, PrimarySignature* signature = nullptr) override { diff --git a/src/node/node_signature_verify.h b/src/node/node_signature_verify.h new file mode 100644 index 00000000000..1c1be1b15dc --- /dev/null +++ b/src/node/node_signature_verify.h @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "ccf/tx.h" +#include "crypto/verifier.h" +#include "node_signature_verify.h" + +namespace ccf +{ + static bool verify_node_signature( + kv::ReadOnlyTx& tx, + const NodeId& node_id, + const uint8_t* expected_sig, + size_t expected_sig_size, + const uint8_t* expected_root, + size_t expected_root_size) + { + crypto::Pem node_cert; + auto node_endorsed_certs = tx.template ro( + ccf::Tables::NODE_ENDORSED_CERTIFICATES); + auto node_endorsed_cert = node_endorsed_certs->get(node_id); + if (!node_endorsed_cert.has_value()) + { + // No endorsed certificate for node. Its (self-signed) certificate + // must be stored in the nodes table (1.x ledger only) + + auto nodes = tx.template ro(ccf::Tables::NODES); + auto node = nodes->get(node_id); + if (!node.has_value()) + { + LOG_FAIL_FMT( + "Signature cannot be verified: no certificate found for node {}", + node_id); + return false; + } + + CCF_ASSERT_FMT( + node->cert.has_value(), + "No certificate recorded in nodes table for {} (1.x ledger)", + node_id); + + node_cert = node->cert.value(); + } + else + { + node_cert = node_endorsed_cert.value(); + } + + crypto::VerifierPtr from_cert = crypto::make_verifier(node_cert); + return from_cert->verify_hash( + expected_root, + expected_root_size, + expected_sig, + expected_sig_size, + crypto::MDType::SHA256); + } + + static bool verify_node_signature( + kv::ReadOnlyTx& tx, + const NodeId& node_id, + const std::vector& expected_sig, + const crypto::Sha256Hash& expected_root) + { + return verify_node_signature( + tx, + node_id, + expected_sig.data(), + expected_sig.size(), + expected_root.h.data(), + expected_root.h.size()); + } +} \ No newline at end of file diff --git a/src/node/node_state.h b/src/node/node_state.h index 8f2556083d2..7154029327c 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -375,9 +375,9 @@ namespace ccf setup_snapshotter(); setup_encryptor(); - setup_consensus(ServiceStatus::OPENING); setup_progress_tracker(); setup_history(); + setup_consensus(ServiceStatus::OPENING); // Become the primary and force replication consensus->force_become_primary(); @@ -1570,6 +1570,7 @@ namespace ccf Pem create_endorsed_node_cert() { + // 1.x only auto nw = crypto::make_key_pair(network.identity->priv_key); auto csr = node_sign_kp->create_csr(config.node_certificate_subject_identity); diff --git a/src/node/nodes.h b/src/node/nodes.h index fcdd8e5b955..41786118138 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -36,14 +36,6 @@ namespace ccf { struct NodeInfo : NodeInfoNetwork { - /** Deprecated. - * Node certificate. Only set for 1.x releases. Further releases record - * node identity in `public_key` field. Service-endorsed certificate is - * recorded in "public:ccf.nodes.endorsed_certificates" table */ - // TODO: Could we change the `to_json` function here so that we don't - // serialise this for future ledger? Or make this optional with a default - // value of none?? - crypto::Pem cert; /// Node enclave quote QuoteInfo quote_info; /// Node encryption public key, used to distribute ledger re-keys. @@ -67,12 +59,19 @@ namespace ccf /// Public key std::optional public_key = std::nullopt; + + /** Deprecated. + * Node certificate. Only set for 1.x releases. Further releases record + * node identity in `public_key` field. Service-endorsed certificate is + * recorded in "public:ccf.nodes.endorsed_certificates" table */ + std::optional cert = std::nullopt; }; DECLARE_JSON_TYPE_WITH_BASE_AND_OPTIONAL_FIELDS(NodeInfo, NodeInfoNetwork); DECLARE_JSON_REQUIRED_FIELDS( - NodeInfo, cert, quote_info, encryption_pub_key, status); + NodeInfo, quote_info, encryption_pub_key, status); DECLARE_JSON_OPTIONAL_FIELDS( NodeInfo, + cert, ledger_secret_seqno, code_digest, certificate_signing_request, diff --git a/src/node/progress_tracker.h b/src/node/progress_tracker.h index 2f200736391..d69d3413a04 100644 --- a/src/node/progress_tracker.h +++ b/src/node/progress_tracker.h @@ -34,7 +34,7 @@ namespace ccf kv::TxHistory::Result add_signature( ccf::TxID tx_id, const NodeId& node_id, - uint32_t signature_size, + size_t signature_size, std::array& sig, Nonce hashed_nonce, const kv::Configuration::Nodes& config, @@ -693,7 +693,7 @@ namespace ccf kv::TxHistory::Result add_signature_internal( ccf::TxID tx_id, const NodeId& node_id, - uint32_t signature_size, + size_t signature_size, std::array& sig, Nonce hashed_nonce, const kv::Configuration::Nodes& config, diff --git a/src/node/progress_tracker_types.h b/src/node/progress_tracker_types.h index dac75d30278..669842e98c7 100644 --- a/src/node/progress_tracker_types.h +++ b/src/node/progress_tracker_types.h @@ -9,6 +9,7 @@ #include "crypto/verifier.h" #include "kv/committable_tx.h" #include "node_signature.h" +#include "node_signature_verify.h" #include "tls/tls.h" #include "view_change.h" @@ -68,7 +69,7 @@ namespace ccf virtual bool verify_signature( const NodeId& node_id, crypto::Sha256Hash& root, - uint32_t sig_size, + size_t sig_size, uint8_t* sig) = 0; virtual void sign_view_change_request( ViewChangeRequest& view_change, ccf::View view) = 0; @@ -168,22 +169,12 @@ namespace ccf bool verify_signature( const NodeId& node_id, crypto::Sha256Hash& root, - uint32_t sig_size, + size_t sig_size, uint8_t* sig) override { kv::ReadOnlyTx tx(&store); - auto ni_tv = tx.ro(nodes); - - auto ni = ni_tv->get(node_id); - if (!ni.has_value()) - { - LOG_FAIL_FMT( - "No node info, and therefore no cert for node {}", node_id); - return false; - } - crypto::VerifierPtr from_cert = crypto::make_verifier(ni.value().cert); - return from_cert->verify_hash( - root.h.data(), root.h.size(), sig, sig_size, crypto::MDType::SHA256); + return verify_node_signature( + tx, node_id, sig, sig_size, root.h.data(), root.h.size()); } void sign_view_change_request( @@ -202,35 +193,15 @@ namespace ccf crypto::Sha256Hash h = hash_view_change(view_change, view); kv::ReadOnlyTx tx(&store); - auto ni_tv = tx.ro(nodes); - - auto ni = ni_tv->get(from); - if (!ni.has_value()) - { - LOG_FAIL_FMT("No node info, and therefore no cert for node {}", from); - return false; - } - crypto::VerifierPtr from_cert = crypto::make_verifier(ni.value().cert); - return from_cert->verify_hash( - h.h, view_change.signature, crypto::MDType::SHA256); + return verify_node_signature(tx, from, view_change.signature, h); } bool verify_view_change_request_confirmation( ViewChangeConfirmation& new_view, const NodeId& from) override { + crypto::Sha256Hash h = hash_new_view(new_view); kv::ReadOnlyTx tx(&store); - auto ni_tv = tx.ro(nodes); - - auto ni = ni_tv->get(from); - if (!ni.has_value()) - { - LOG_FAIL_FMT("No node info, and therefore no cert for node {}", from); - return false; - } - crypto::VerifierPtr from_cert = crypto::make_verifier(ni.value().cert); - auto h = hash_new_view(new_view); - return from_cert->verify_hash( - h.h, new_view.signature, crypto::MDType::SHA256); + return verify_node_signature(tx, from, new_view.signature, h); } void sign_view_change_confirmation( diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index ab95b041974..c66c7f95616 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -219,8 +219,6 @@ namespace ccf nodes->put( joining_node_id, {in.node_info_network, - Pem(), // This field was used in 1.x to record self-signed node - // certificate in.quote_info, in.public_encryption_key, node_status, @@ -1112,8 +1110,6 @@ namespace ccf g.add_node( in.node_id, {in.node_info_network, - Pem(), // This field was used in 1.x to record self-signed node - // certificate {in.quote_info}, in.public_encryption_key, NodeStatus::TRUSTED, From d91342ac3d6cffee4f4463f97d4ac1b78c0e51b9 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 6 Aug 2021 17:15:11 +0100 Subject: [PATCH 025/105] Fix n2n channel non-endorsed certificate issue --- src/node/channels.h | 31 +++++++++++++++++++++++-------- src/node/node_state.h | 10 ++++++---- src/node/node_to_node.h | 25 +++++++++++++++++-------- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/node/channels.h b/src/node/channels.h index 1dfecfcc9cf..a4e4ad3bc53 100644 --- a/src/node/channels.h +++ b/src/node/channels.h @@ -893,8 +893,8 @@ namespace ccf ringbuffer::AbstractWriterFactory& writer_factory; const crypto::Pem& network_cert; crypto::KeyPairPtr node_kp; - const crypto::Pem& node_cert; NodeId self; + std::optional endorsed_node_cert = std::nullopt; std::mutex lock; public: @@ -902,15 +902,21 @@ namespace ccf ringbuffer::AbstractWriterFactory& writer_factory_, const crypto::Pem& network_cert_, crypto::KeyPairPtr node_kp_, - const crypto::Pem& node_cert_, - const NodeId& self_) : + const NodeId& self_, + std::optional endorsed_node_cert_ = std::nullopt) : writer_factory(writer_factory_), network_cert(network_cert_), node_kp(node_kp_), - node_cert(node_cert_), - self(self_) + self(self_), + endorsed_node_cert(endorsed_node_cert_) {} + void set_endorsed_node_cert(const crypto::Pem& endorsed_node_cert_) + { + std::lock_guard guard(lock); + endorsed_node_cert = endorsed_node_cert_; + } + void create_channel( const NodeId& peer_id, const std::string& hostname, @@ -918,6 +924,10 @@ namespace ccf size_t message_limit = Channel::default_message_limit) { std::lock_guard guard(lock); + CCF_ASSERT_FMT( + endorsed_node_cert.has_value(), + "Endorsed node certificate has not yet been set"); + auto search = channels.find(peer_id); if (search == channels.end()) { @@ -930,7 +940,7 @@ namespace ccf writer_factory, network_cert, node_kp, - node_cert, + endorsed_node_cert.value(), self, peer_id, hostname, @@ -957,7 +967,7 @@ namespace ccf writer_factory, network_cert, node_kp, - node_cert, + endorsed_node_cert.value(), self, peer_id, hostname, @@ -1012,7 +1022,12 @@ namespace ccf channels.try_emplace( peer_id, std::make_shared( - writer_factory, network_cert, node_kp, node_cert, self, peer_id)); + writer_factory, + network_cert, + node_kp, + endorsed_node_cert.value(), + self, + peer_id)); return channels.at(peer_id); } }; diff --git a/src/node/node_state.h b/src/node/node_state.h index 7154029327c..21848f77d8b 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -382,16 +382,16 @@ namespace ccf // Become the primary and force replication consensus->force_become_primary(); - // Open member frontend for members to configure and open the - // network - open_frontend(ActorsType::members); - if (!create_and_send_boot_request(true /* Create new consortium */)) { throw std::runtime_error( "Genesis transaction could not be committed"); } + // Open member frontend for members to configure and open the + // network + open_frontend(ActorsType::members); + accept_node_tls_connections(); auto_refresh_jwt_keys(); @@ -1881,6 +1881,7 @@ namespace ccf } node_cert = endorsed_certificate.value(); + n2n_channels->set_endorsed_node_cert(node_cert); accept_network_tls_connections(); } @@ -1953,6 +1954,7 @@ namespace ccf void setup_n2n_channels() { + // TODO: Endorsed node certificate should be passed in from join response n2n_channels->initialize( self, network.identity->cert, node_sign_kp, node_cert); } diff --git a/src/node/node_to_node.h b/src/node/node_to_node.h index 9285bef2470..a01656e21f2 100644 --- a/src/node/node_to_node.h +++ b/src/node/node_to_node.h @@ -102,6 +102,9 @@ namespace ccf crypto::KeyPairPtr node_kp, const crypto::Pem& node_cert) = 0; + virtual void set_endorsed_node_cert( + const crypto::Pem& endorsed_node_cert) = 0; + virtual bool send_encrypted( const NodeId& to, NodeMsgType type, @@ -156,17 +159,23 @@ namespace ccf self.value(), self_id); - if (make_verifier(node_cert)->is_self_signed()) - { - LOG_INFO_FMT( - "Refusing to initialize node-to-node channels with self-signed node " - "certificate."); - return; - } + // TODO: Remove + // if (make_verifier(node_cert)->is_self_signed()) + // { + // LOG_INFO_FMT( + // "Refusing to initialize node-to-node channels with self-signed node + // " "certificate."); + // return; + // } self = self_id; channels = std::make_unique( - writer_factory, network_cert, node_kp, node_cert, self.value()); + writer_factory, network_cert, node_kp, self.value(), node_cert); + } + + void set_endorsed_node_cert(const crypto::Pem& endorsed_node_cert) override + { + channels->set_endorsed_node_cert(endorsed_node_cert); } void create_channel( From 7dcfc8d1096869235a121eea517af3ef512494c8 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 9 Aug 2021 08:30:56 +0100 Subject: [PATCH 026/105] WIP --- src/node/node_state.h | 21 +++++++++++++++------ src/node/node_to_node.h | 4 ++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/node/node_state.h b/src/node/node_state.h index 21848f77d8b..c7d33e7f1ce 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -541,6 +541,7 @@ namespace ccf // Endorsed node certificate is included in join response from 2.x. if (resp.network_info.endorsed_certificate.has_value()) { + // TODO: This should be picked up later on, on hook node_cert = resp.network_info.endorsed_certificate.value(); } else @@ -573,7 +574,9 @@ namespace ccf setup_snapshotter(); setup_encryptor(); setup_consensus( - resp.network_info.service_status, resp.network_info.public_only); + resp.network_info.service_status, + resp.network_info.public_only, + resp.network_info.endorsed_certificate); setup_progress_tracker(); setup_history(); auto_refresh_jwt_keys(); @@ -638,7 +641,7 @@ namespace ccf sig->view); } - // TODO: Move later? + // TODO: Move later! open_frontend(ActorsType::members); if (resp.network_info.public_only) @@ -1952,11 +1955,13 @@ namespace ccf network.encrypted_ledger_secrets.get_name()); } - void setup_n2n_channels() + void setup_n2n_channels( + const std::optional& endorsed_node_certificate_ = + std::nullopt) { // TODO: Endorsed node certificate should be passed in from join response n2n_channels->initialize( - self, network.identity->cert, node_sign_kp, node_cert); + self, network.identity->cert, node_sign_kp, endorsed_node_certificate_); } void setup_cmd_forwarder() @@ -1985,9 +1990,13 @@ namespace ccf network.tables->set_encryptor(encryptor); } - void setup_consensus(ServiceStatus service_status, bool public_only = false) + void setup_consensus( + ServiceStatus service_status, + bool public_only = false, + const std::optional& endorsed_node_certificate_ = + std::nullopt) { - setup_n2n_channels(); + setup_n2n_channels(endorsed_node_certificate_); setup_cmd_forwarder(); setup_tracker_store(); diff --git a/src/node/node_to_node.h b/src/node/node_to_node.h index a01656e21f2..6088fad1dad 100644 --- a/src/node/node_to_node.h +++ b/src/node/node_to_node.h @@ -100,7 +100,7 @@ namespace ccf const NodeId& self_id, const crypto::Pem& network_cert, crypto::KeyPairPtr node_kp, - const crypto::Pem& node_cert) = 0; + const std::optional& node_cert = std::nullopt) = 0; virtual void set_endorsed_node_cert( const crypto::Pem& endorsed_node_cert) = 0; @@ -151,7 +151,7 @@ namespace ccf const NodeId& self_id, const crypto::Pem& network_cert, crypto::KeyPairPtr node_kp, - const crypto::Pem& node_cert) override + const std::optional& node_cert = std::nullopt) override { CCF_ASSERT_FMT( !self.has_value(), From e20c8aace8fe3cad7777496c4e10091a57b01379 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 9 Aug 2021 08:51:06 +0100 Subject: [PATCH 027/105] operator default --- src/crypto/san.h | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/crypto/san.h b/src/crypto/san.h index d7a596e074a..b54d7e0e9d1 100644 --- a/src/crypto/san.h +++ b/src/crypto/san.h @@ -16,15 +16,8 @@ namespace crypto std::string san; bool is_ip; - bool operator==(const SubjectAltName& other) const - { - return san == other.san && is_ip == other.is_ip; - } - - bool operator!=(const SubjectAltName& other) const - { - return !(*this == other); - } + bool operator==(const SubjectAltName& other) const = default; + bool operator!=(const SubjectAltName& other) const = default; }; DECLARE_JSON_TYPE(SubjectAltName); DECLARE_JSON_REQUIRED_FIELDS(SubjectAltName, san, is_ip); @@ -41,15 +34,8 @@ namespace crypto sans(sans) {} - bool operator==(const CertificateSubjectIdentity& other) const - { - return sans == other.sans && name == other.name; - } - - bool operator!=(const CertificateSubjectIdentity& other) const - { - return !(*this == other); - } + bool operator==(const CertificateSubjectIdentity& other) const = default; + bool operator!=(const CertificateSubjectIdentity& other) const = default; }; DECLARE_JSON_TYPE(CertificateSubjectIdentity); DECLARE_JSON_REQUIRED_FIELDS(CertificateSubjectIdentity, sans, name); From 837a1c1cc5043f95480817bbb739b134094e5469 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 9 Aug 2021 09:04:55 +0100 Subject: [PATCH 028/105] Minor tweaks --- src/crypto/san.h | 1 - src/node/node_state.h | 22 ++++++++++------------ src/node/node_to_node.h | 15 +++++++-------- src/node/rpc/node_frontend.h | 1 - 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/crypto/san.h b/src/crypto/san.h index b54d7e0e9d1..a7a79029d99 100644 --- a/src/crypto/san.h +++ b/src/crypto/san.h @@ -8,7 +8,6 @@ #include #include -// TODO: Rename file? namespace crypto { struct SubjectAltName diff --git a/src/node/node_state.h b/src/node/node_state.h index c7d33e7f1ce..98d9bbd9965 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -350,6 +350,7 @@ namespace ccf } #endif + node_cert = create_self_signed_node_cert(); switch (start_type) { case StartType::New: @@ -357,8 +358,6 @@ namespace ccf network.identity = std::make_unique("CN=CCF Network", curve_id); - node_cert = create_self_signed_node_cert(); - network.ledger_secrets->init(); if (network.consensus_type == ConsensusType::BFT) @@ -392,7 +391,6 @@ namespace ccf // network open_frontend(ActorsType::members); - accept_node_tls_connections(); auto_refresh_jwt_keys(); reset_data(quote_info.quote); @@ -400,13 +398,11 @@ namespace ccf sm.advance(State::partOfNetwork); LOG_INFO_FMT("Created new node {}", self); + accept_node_tls_connections(); return {node_cert, network.identity->cert}; } case StartType::Join: { - node_cert = create_self_signed_node_cert(); - accept_node_tls_connections(); - if (!config.startup_snapshot.empty()) { initialise_startup_snapshot(); @@ -418,6 +414,7 @@ namespace ccf } LOG_INFO_FMT("Created join node {}", self); + accept_node_tls_connections(); return {node_cert, {}}; } case StartType::Recover: @@ -426,7 +423,6 @@ namespace ccf network.identity = std::make_unique("CN=CCF Network", curve_id); - node_cert = create_self_signed_node_cert(); setup_history(); @@ -447,11 +443,10 @@ namespace ccf snapshotter->set_last_snapshot_idx(ledger_idx); } - accept_node_tls_connections(); - sm.advance(State::readingPublicLedger); LOG_INFO_FMT("Created recovery node {}", self); + accept_node_tls_connections(); return {node_cert, network.identity->cert}; } default: @@ -1573,14 +1568,14 @@ namespace ccf Pem create_endorsed_node_cert() { - // 1.x only + // Only used by a 2.x node joining an existing 1.x service which will not + // endorsed the identity of the new joiner. auto nw = crypto::make_key_pair(network.identity->priv_key); auto csr = node_sign_kp->create_csr(config.node_certificate_subject_identity); return nw->sign_csr(network.identity->cert, csr); } - // TODO: Rename crypto::Pem generate_endorsed_certificate( const crypto::Pem& subject_csr, const crypto::Pem& endorser_private_key, @@ -1959,7 +1954,10 @@ namespace ccf const std::optional& endorsed_node_certificate_ = std::nullopt) { - // TODO: Endorsed node certificate should be passed in from join response + // If the endorsed node certificate is available at the time the + // consensus/node-to-node channels are initialised, use it (i.e. join). + // Otherwise, specify it later, on endorsed certificate table hook (i.e. + // start or recover). n2n_channels->initialize( self, network.identity->cert, node_sign_kp, endorsed_node_certificate_); } diff --git a/src/node/node_to_node.h b/src/node/node_to_node.h index 6088fad1dad..d02ee1e9c45 100644 --- a/src/node/node_to_node.h +++ b/src/node/node_to_node.h @@ -159,14 +159,13 @@ namespace ccf self.value(), self_id); - // TODO: Remove - // if (make_verifier(node_cert)->is_self_signed()) - // { - // LOG_INFO_FMT( - // "Refusing to initialize node-to-node channels with self-signed node - // " "certificate."); - // return; - // } + if (node_cert.has_value() && make_verifier(node_cert)->is_self_signed()) + { + LOG_FAIL_FMT( + "Refusing to initialize node-to-node channels with self-signed node " + "certificate"); + return; + } self = self_id; channels = std::make_unique( diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index c66c7f95616..7ae8497cb11 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -246,7 +246,6 @@ namespace ccf std::optional endorsed_certificate = std::nullopt; if (in.certificate_signing_request.has_value()) { - LOG_FAIL_FMT("Recording endorsed identity!"); // TODO: Remove! endorsed_certificate = context.get_node_state().generate_endorsed_certificate( in.certificate_signing_request.value(), From 7a5e02a4c745e09ddb017ffd7528c1417ba2476c Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 9 Aug 2021 10:40:21 +0100 Subject: [PATCH 029/105] More cleanup --- src/node/node_state.h | 18 ++++++++---------- src/node/node_to_node.h | 4 +++- src/node/nodes.h | 6 +++++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/node/node_state.h b/src/node/node_state.h index 98d9bbd9965..a9359eead1d 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -533,16 +533,11 @@ namespace ccf network.identity = std::make_unique(resp.network_info.identity); - // Endorsed node certificate is included in join response from 2.x. - if (resp.network_info.endorsed_certificate.has_value()) + if (!resp.network_info.endorsed_certificate.has_value()) { - // TODO: This should be picked up later on, on hook - node_cert = resp.network_info.endorsed_certificate.value(); - } - else - { - // When joining an existing 1.x service, self-sign own certificate - // and use it to endorse TLS connections + // Endorsed node certificate is included in join response + // from 2.x. When joining an existing 1.x service, self-sign own + // certificate and use it to endorse TLS connections. node_cert = create_endorsed_node_cert(); accept_network_tls_connections(); } @@ -574,6 +569,8 @@ namespace ccf resp.network_info.endorsed_certificate); setup_progress_tracker(); setup_history(); + + // TODO: Does this need the endorsed or self-signed cert? auto_refresh_jwt_keys(); if (resp.network_info.public_only) @@ -1878,6 +1875,7 @@ namespace ccf "Could not find endorsed node certificate for {}", self)); } + LOG_FAIL_FMT("Picking up endorsed identity from hook"); node_cert = endorsed_certificate.value(); n2n_channels->set_endorsed_node_cert(node_cert); accept_network_tls_connections(); @@ -2018,7 +2016,7 @@ namespace ccf snapshotter, rpcsessions, rpc_map, - node_cert.raw(), + node_cert.raw(), // TODO: Delete? shared_state, std::make_shared(shared_state, rpc_map, rpcsessions), request_tracker, diff --git a/src/node/node_to_node.h b/src/node/node_to_node.h index d02ee1e9c45..c1f88d48e41 100644 --- a/src/node/node_to_node.h +++ b/src/node/node_to_node.h @@ -159,7 +159,9 @@ namespace ccf self.value(), self_id); - if (node_cert.has_value() && make_verifier(node_cert)->is_self_signed()) + if ( + node_cert.has_value() && + make_verifier(node_cert.value())->is_self_signed()) { LOG_FAIL_FMT( "Refusing to initialize node-to-node channels with self-signed node " diff --git a/src/node/nodes.h b/src/node/nodes.h index 41786118138..6dade76de8e 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -60,7 +60,11 @@ namespace ccf /// Public key std::optional public_key = std::nullopt; - /** Deprecated. + /** + * Fields below are deprecated + */ + + /** Deprecated as of 2.x. * Node certificate. Only set for 1.x releases. Further releases record * node identity in `public_key` field. Service-endorsed certificate is * recorded in "public:ccf.nodes.endorsed_certificates" table */ From cccaf65b0b546e2ffd13503411284a15b08e613c Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 9 Aug 2021 10:51:36 +0100 Subject: [PATCH 030/105] Fix LTS compatibility --- src/node/node_state.h | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/node/node_state.h b/src/node/node_state.h index a9359eead1d..fbcb46320cc 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -530,8 +530,19 @@ namespace ccf resp.node_status == NodeStatus::TRUSTED || resp.node_status == NodeStatus::LEARNER) { + if (resp.network_info.consensus_type != network.consensus_type) + { + throw std::logic_error(fmt::format( + "Enclave initiated with consensus type {} but target node " + "responded with consensus {}", + network.consensus_type, + resp.network_info.consensus_type)); + } + network.identity = std::make_unique(resp.network_info.identity); + network.ledger_secrets->init_from_map( + std::move(resp.network_info.ledger_secrets)); if (!resp.network_info.endorsed_certificate.has_value()) { @@ -542,18 +553,6 @@ namespace ccf accept_network_tls_connections(); } - network.ledger_secrets->init_from_map( - std::move(resp.network_info.ledger_secrets)); - - if (resp.network_info.consensus_type != network.consensus_type) - { - throw std::logic_error(fmt::format( - "Enclave initiated with consensus type {} but target node " - "responded with consensus {}", - network.consensus_type, - resp.network_info.consensus_type)); - } - if (network.consensus_type == ConsensusType::BFT) { // In CFT, the node id is computed at startup, as the hash of the @@ -566,7 +565,7 @@ namespace ccf setup_consensus( resp.network_info.service_status, resp.network_info.public_only, - resp.network_info.endorsed_certificate); + resp.network_info.endorsed_certificate.value_or(node_cert)); setup_progress_tracker(); setup_history(); From 08f0ac0f8f1090104d023756666d708a1723a6e1 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 9 Aug 2021 17:09:51 +0100 Subject: [PATCH 031/105] Fix unit test --- src/consensus/aft/test/logging_stub.h | 4 +++- src/node/test/channels.cpp | 2 +- src/node/test/progress_tracker.cpp | 5 ++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/consensus/aft/test/logging_stub.h b/src/consensus/aft/test/logging_stub.h index 8634f9ebf9f..cd322783314 100644 --- a/src/consensus/aft/test/logging_stub.h +++ b/src/consensus/aft/test/logging_stub.h @@ -175,6 +175,8 @@ namespace aft void close_all_outgoing() override {} + void set_endorsed_node_cert(const crypto::Pem&) override {} + bool send_authenticated( const ccf::NodeId& to, ccf::NodeMsgType msg_type, @@ -203,7 +205,7 @@ namespace aft const ccf::NodeId& self_id, const crypto::Pem& network_cert, crypto::KeyPairPtr node_kp, - const crypto::Pem& node_cert) override + const std::optional& node_cert = std::nullopt) override {} bool send_encrypted( diff --git a/src/node/test/channels.cpp b/src/node/test/channels.cpp index fd05363d756..f01dd9514b7 100644 --- a/src/node/test/channels.cpp +++ b/src/node/test/channels.cpp @@ -520,7 +520,7 @@ TEST_CASE("Host connections") auto channel_kp = crypto::make_key_pair(default_curve); auto channel_cert = channel_kp->self_sign("CN=Node"); auto channel_manager = - ChannelManager(wf1, network_cert, channel_kp, channel_cert, self); + ChannelManager(wf1, network_cert, channel_kp, self, channel_cert); INFO("New channel creates host connection"); { diff --git a/src/node/test/progress_tracker.cpp b/src/node/test/progress_tracker.cpp index 1e24edf9faa..394ea767269 100644 --- a/src/node/test/progress_tracker.cpp +++ b/src/node/test/progress_tracker.cpp @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. -#include "node/progress_tracker.h" - #include "consensus/aft/impl/view_change_tracker.h" #include "kv/store.h" #include "kv/test/stub_consensus.h" #include "node/nodes.h" +#include "node/progress_tracker.h" #include "node/request_tracker.h" #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN @@ -33,7 +32,7 @@ class StoreMock : public ccf::ProgressTrackerStore MAKE_MOCK0(get_nonces, std::optional(), override); MAKE_MOCK4( verify_signature, - bool(const kv::NodeId&, crypto::Sha256Hash&, uint32_t, uint8_t*), + bool(const kv::NodeId&, crypto::Sha256Hash&, size_t, uint8_t*), override); MAKE_MOCK2( sign_view_change_request, From cbb41e022cb80ba5446feb6ad17120f2b9481076 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 9 Aug 2021 17:26:43 +0100 Subject: [PATCH 032/105] Postpone frontend opening to hook --- src/node/node_state.h | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/node/node_state.h b/src/node/node_state.h index fbcb46320cc..1d13b175c8b 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -387,10 +387,6 @@ namespace ccf "Genesis transaction could not be committed"); } - // Open member frontend for members to configure and open the - // network - open_frontend(ActorsType::members); - auto_refresh_jwt_keys(); reset_data(quote_info.quote); @@ -568,8 +564,6 @@ namespace ccf resp.network_info.endorsed_certificate.value_or(node_cert)); setup_progress_tracker(); setup_history(); - - // TODO: Does this need the endorsed or self-signed cert? auto_refresh_jwt_keys(); if (resp.network_info.public_only) @@ -632,9 +626,6 @@ namespace ccf sig->view); } - // TODO: Move later! - open_frontend(ActorsType::members); - if (resp.network_info.public_only) { sm.advance(State::partOfPublicNetwork); @@ -650,9 +641,6 @@ namespace ccf "Node has now joined the network as node {}: {}", self, (resp.network_info.public_only ? "public only" : "all domains")); - - // TODO: Move later? - open_user_frontend(); } else if (resp.node_status == NodeStatus::PENDING) { @@ -1032,8 +1020,6 @@ namespace ccf "End of public recovery transaction could not be committed"); } - open_frontend(ActorsType::members); - sm.advance(State::partOfPublicNetwork); } @@ -1878,6 +1864,9 @@ namespace ccf node_cert = endorsed_certificate.value(); n2n_channels->set_endorsed_node_cert(node_cert); accept_network_tls_connections(); + + open_frontend(ActorsType::members); + open_user_frontend(); } return kv::ConsensusHookPtr(nullptr); From 1f2adfa5368e581bc40e8943ac33f62a094a56d1 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 9 Aug 2021 17:31:54 +0100 Subject: [PATCH 033/105] Remove cert argument to Raft ctor --- src/consensus/aft/raft.h | 1 - src/consensus/aft/test/committable_suffix.cpp | 1 - src/consensus/aft/test/driver.h | 3 --- src/consensus/aft/test/main.cpp | 16 ---------------- src/node/node_state.h | 1 - 5 files changed, 22 deletions(-) diff --git a/src/consensus/aft/raft.h b/src/consensus/aft/raft.h index 0f5671a2147..079574ff7a1 100644 --- a/src/consensus/aft/raft.h +++ b/src/consensus/aft/raft.h @@ -187,7 +187,6 @@ namespace aft std::shared_ptr snapshotter_, std::shared_ptr rpc_sessions_, std::shared_ptr rpc_map_, - const std::vector& /*cert*/, std::shared_ptr state_, std::shared_ptr executor_, std::shared_ptr request_tracker_, diff --git a/src/consensus/aft/test/committable_suffix.cpp b/src/consensus/aft/test/committable_suffix.cpp index f9ba87f1956..666ff29c8b7 100644 --- a/src/consensus/aft/test/committable_suffix.cpp +++ b/src/consensus/aft/test/committable_suffix.cpp @@ -110,7 +110,6 @@ void keep_earliest_append_entries_for_each_target( std::make_shared(), \ nullptr, \ nullptr, \ - cert, \ std::make_shared(node_id##N), \ nullptr, \ nullptr, \ diff --git a/src/consensus/aft/test/driver.h b/src/consensus/aft/test/driver.h index 0eb4066bd90..04117a5035d 100644 --- a/src/consensus/aft/test/driver.h +++ b/src/consensus/aft/test/driver.h @@ -100,8 +100,6 @@ using TRaft = aft:: using Store = LoggingStubStoreSig_Mermaid; using Adaptor = aft::Adaptor; -std::vector cert; - aft::ChannelStubProxy* channel_stub_proxy(const TRaft& r) { return (aft::ChannelStubProxy*)r.channels.get(); @@ -137,7 +135,6 @@ class RaftDriver std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id), nullptr, std::make_shared(), diff --git a/src/consensus/aft/test/main.cpp b/src/consensus/aft/test/main.cpp index 9036399f61d..551d8e05c24 100644 --- a/src/consensus/aft/test/main.cpp +++ b/src/consensus/aft/test/main.cpp @@ -26,7 +26,6 @@ DOCTEST_TEST_CASE("Single node startup" * doctest::test_suite("single")) std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id), nullptr, nullptr, @@ -71,7 +70,6 @@ DOCTEST_TEST_CASE("Single node commit" * doctest::test_suite("single")) std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id), nullptr, nullptr, @@ -127,7 +125,6 @@ DOCTEST_TEST_CASE( std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id0), nullptr, nullptr, @@ -143,7 +140,6 @@ DOCTEST_TEST_CASE( std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id1), nullptr, nullptr, @@ -159,7 +155,6 @@ DOCTEST_TEST_CASE( std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id2), nullptr, nullptr, @@ -299,7 +294,6 @@ DOCTEST_TEST_CASE( std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id0), nullptr, nullptr, @@ -315,7 +309,6 @@ DOCTEST_TEST_CASE( std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id1), nullptr, nullptr, @@ -331,7 +324,6 @@ DOCTEST_TEST_CASE( std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id2), nullptr, nullptr, @@ -455,7 +447,6 @@ DOCTEST_TEST_CASE("Multiple nodes late join" * doctest::test_suite("multiple")) std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id0), nullptr, nullptr, @@ -471,7 +462,6 @@ DOCTEST_TEST_CASE("Multiple nodes late join" * doctest::test_suite("multiple")) std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id1), nullptr, nullptr, @@ -487,7 +477,6 @@ DOCTEST_TEST_CASE("Multiple nodes late join" * doctest::test_suite("multiple")) std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id2), nullptr, nullptr, @@ -598,7 +587,6 @@ DOCTEST_TEST_CASE("Recv append entries logic" * doctest::test_suite("multiple")) std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id0), nullptr, nullptr, @@ -614,7 +602,6 @@ DOCTEST_TEST_CASE("Recv append entries logic" * doctest::test_suite("multiple")) std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id1), nullptr, nullptr, @@ -774,7 +761,6 @@ DOCTEST_TEST_CASE("Exceed append entries limit") std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id0), nullptr, nullptr, @@ -790,7 +776,6 @@ DOCTEST_TEST_CASE("Exceed append entries limit") std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id1), nullptr, nullptr, @@ -806,7 +791,6 @@ DOCTEST_TEST_CASE("Exceed append entries limit") std::make_shared(), nullptr, nullptr, - cert, std::make_shared(node_id2), nullptr, nullptr, diff --git a/src/node/node_state.h b/src/node/node_state.h index 1d13b175c8b..f42c1907b59 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -2004,7 +2004,6 @@ namespace ccf snapshotter, rpcsessions, rpc_map, - node_cert.raw(), // TODO: Delete? shared_state, std::make_shared(shared_state, rpc_map, rpcsessions), request_tracker, From 6075d5026a365d6158c6de157d9c38260b4c8c13 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 10 Aug 2021 09:06:08 +0100 Subject: [PATCH 034/105] Fix merge conflict --- tests/governance_history.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/governance_history.py b/tests/governance_history.py index a79a21c8da8..f41d510cc3e 100644 --- a/tests/governance_history.py +++ b/tests/governance_history.py @@ -14,10 +14,7 @@ from loguru import logger as LOG import suite.test_requirements as reqs import ccf.read_ledger -<<<<<<< HEAD -======= import infra.logging_app as app ->>>>>>> 38267cd76e333b964946e6ac02f27ba02b1e0e42 def check_operations(ledger, operations): From b8e332b7e4b88f9e9a117c7cfa7585a72d70ef18 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 10 Aug 2021 09:17:46 +0100 Subject: [PATCH 035/105] Update constitution --- src/node/rpc/node_frontend.h | 3 +-- src/runtime_config/default/actions.js | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 83d84d114ce..5d9a0b7f66a 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -136,7 +136,6 @@ namespace ccf NodeStatus node_status, ServiceStatus service_status) { - LOG_FAIL_FMT("Service status: {}", service_status); // TODO: Delete auto nodes = tx.rw(network.nodes); auto node_endorsed_certificates = tx.rw(network.node_endorsed_certificates); @@ -273,7 +272,7 @@ namespace ccf openapi_info.description = "This API provides public, uncredentialed access to service and node " "state."; - openapi_info.document_version = "1.4.0"; // TODO: Bump up + openapi_info.document_version = "1.4.0"; } void init_handlers() override diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index bf7306e7db6..160c89fd8cb 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -777,16 +777,18 @@ const actions = new Map([ ccf.jsonCompatibleToBuf(nodeInfo) ); - // Also endorse and record node certificate - // TODO: For now, assume that node public key is always present, which isn't true for 1.x! - const endorsed_node_cert = ccf.network.generateEndorsedCertificate( - nodeInfo.certificate_signing_request, - nodeInfo.certificate_subject_identity - ); - ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( - ccf.strToBuf(args.node_id), - ccf.strToBuf(endorsed_node_cert) - ); + // Also generate and record service-endorsed node certificate from node CSR + if (nodeInfo.certificate_signing_request !== undefined) { + // Note: CSR is only present from 2.x + const endorsed_node_cert = ccf.network.generateEndorsedCertificate( + nodeInfo.certificate_signing_request, + nodeInfo.certificate_subject_identity + ); + ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( + ccf.strToBuf(args.node_id), + ccf.strToBuf(endorsed_node_cert) + ); + } } } ), From 5712eb8879e98365ae4dd368667d27c90ad0ae53 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 10 Aug 2021 10:16:22 +0100 Subject: [PATCH 036/105] Open frontend early if no endorsemen (fixes LTS) --- src/node/node_state.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/node/node_state.h b/src/node/node_state.h index 69a314abaab..62677f826a0 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -546,6 +546,8 @@ namespace ccf // from 2.x. When joining an existing 1.x service, self-sign own // certificate and use it to endorse TLS connections. node_cert = create_endorsed_node_cert(); + open_frontend(ActorsType::members); + open_user_frontend(); accept_network_tls_connections(); } From 68073349ae4a5b3dbc2f9542c7e0643bf7918e79 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 11 Aug 2021 14:04:16 +0100 Subject: [PATCH 037/105] . --- src/node/rpc/node_call_types.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 15b442cfb67..e5a15847c09 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -107,7 +107,7 @@ namespace ccf LedgerSecretsMap ledger_secrets; NetworkIdentity identity; - ServiceStatus service_status; // TODO: This isn't serialised! + ServiceStatus service_status; std::optional endorsed_certificate = std::nullopt; From b3a18836634507b46c46e9d60e882f5ff78e5c75 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 11 Aug 2021 14:37:37 +0100 Subject: [PATCH 038/105] .. --- tests/reconfiguration.py | 63 +++++++++++++++++++-------------------- tests/suite/test_suite.py | 3 +- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 3cd7ccaad8d..b3f3da70242 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -430,36 +430,35 @@ def run(args): network.start_and_join(args) test_version(network, args) - test_add_node(network, args) - - # if args.consensus != "bft": - # test_join_straddling_primary_replacement(network, args) - # test_node_replacement(network, args) - # test_add_node_from_backup(network, args) - # test_add_node(network, args) - # test_add_node_on_other_curve(network, args) - # test_retire_backup(network, args) - # test_add_as_many_pending_nodes(network, args) - # test_add_node(network, args) - # test_retire_primary(network, args) - - # test_add_node_from_snapshot(network, args) - # test_add_node_from_snapshot(network, args, from_backup=True) - # test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) - # latest_node_log = network.get_joined_nodes()[-1].remote.log_path() - # with open(latest_node_log, "r+") as log: - # assert any( - # "No snapshot found: Node will replay all historical transactions" - # in l - # for l in log.readlines() - # ), "New nodes shouldn't join from snapshot if snapshot evidence cannot be verified" - - # test_node_filter(network, args) - # test_retiring_nodes_emit_at_most_one_signature(network, args) - # else: - # test_learner_catches_up(network, args) - # test_learner_does_not_take_part(network, args) - # test_retire_backup(network, args) + + if args.consensus != "bft": + test_join_straddling_primary_replacement(network, args) + test_node_replacement(network, args) + test_add_node_from_backup(network, args) + test_add_node(network, args) + test_add_node_on_other_curve(network, args) + test_retire_backup(network, args) + test_add_as_many_pending_nodes(network, args) + test_add_node(network, args) + test_retire_primary(network, args) + + test_add_node_from_snapshot(network, args) + test_add_node_from_snapshot(network, args, from_backup=True) + test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) + latest_node_log = network.get_joined_nodes()[-1].remote.log_path() + with open(latest_node_log, "r+") as log: + assert any( + "No snapshot found: Node will replay all historical transactions" + in l + for l in log.readlines() + ), "New nodes shouldn't join from snapshot if snapshot evidence cannot be verified" + + test_node_filter(network, args) + test_retiring_nodes_emit_at_most_one_signature(network, args) + else: + test_learner_catches_up(network, args) + test_learner_does_not_take_part(network, args) + test_retire_backup(network, args) def run_join_old_snapshot(args): @@ -531,5 +530,5 @@ def run_join_old_snapshot(args): run(args) - # if args.consensus != "bft": - # run_join_old_snapshot(args) + if args.consensus != "bft": + run_join_old_snapshot(args) diff --git a/tests/suite/test_suite.py b/tests/suite/test_suite.py index 3f159338188..f3a930b7017 100644 --- a/tests/suite/test_suite.py +++ b/tests/suite/test_suite.py @@ -116,7 +116,8 @@ # # # - # Below tests should always stay last + # Below tests should always stay last, so that they + # test the most complete ledgers governance_history.test_ledger_is_readable, governance_history.test_tables_doc, ] From de6aa15b8d2837ef015610b73fc2d1c99c43c5d5 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 12 Aug 2021 10:24:49 +0100 Subject: [PATCH 039/105] WIP fix for BFT --- src/node/config.h | 8 ++- src/node/node_state.h | 33 ++++++++-- src/node/rpc/node_call_types.h | 10 ++- src/node/rpc/node_frontend.h | 91 ++++++++++++++++++--------- src/node/rpc/serialization.h | 3 +- src/runtime_config/default/actions.js | 7 ++- 6 files changed, 112 insertions(+), 40 deletions(-) diff --git a/src/node/config.h b/src/node/config.h index f521257af72..124bfecf867 100644 --- a/src/node/config.h +++ b/src/node/config.h @@ -15,14 +15,20 @@ namespace ccf size_t recovery_threshold = 0; ConsensusType consensus = ConsensusType::CFT; + std::optional reconfiguration_type = std::nullopt; + // If true, the service endorses the certificate of new trusted nodes, and + // records them in the store + std::optional node_endorsement_on_trust = std::nullopt; + bool operator==(const ServiceConfiguration&) const = default; }; DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(ServiceConfiguration) DECLARE_JSON_REQUIRED_FIELDS( ServiceConfiguration, recovery_threshold, consensus) - DECLARE_JSON_OPTIONAL_FIELDS(ServiceConfiguration, reconfiguration_type) + DECLARE_JSON_OPTIONAL_FIELDS( + ServiceConfiguration, reconfiguration_type, node_endorsement_on_trust) // The there is always only one active configuration, so this is a single // Value diff --git a/src/node/node_state.h b/src/node/node_state.h index 62677f826a0..c9e2162130a 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -350,7 +350,9 @@ namespace ccf } #endif + // TODO: Split self-signed from endorsed certificate node_cert = create_self_signed_node_cert(); + switch (start_type) { case StartType::New: @@ -360,6 +362,9 @@ namespace ccf network.ledger_secrets->init(); + setup_snapshotter(); + setup_encryptor(); + if (network.consensus_type == ConsensusType::BFT) { // BFT consensus requires a stable order of node IDs so that the @@ -370,13 +375,15 @@ namespace ccf // Pad node id string to avoid memory alignment issues on // node-to-node messages self = NodeId(fmt::format("{:#064}", 0)); + + node_cert = create_endorsed_node_cert(); + open_frontend(ActorsType::members); } - setup_snapshotter(); - setup_encryptor(); + // TODO: Do not pass node_cert if CFT! + setup_consensus(ServiceStatus::OPENING, false); setup_progress_tracker(); setup_history(); - setup_consensus(ServiceStatus::OPENING); // Become the primary and force replication consensus->force_become_primary(); @@ -969,6 +976,9 @@ namespace ccf auto values = tx.ro(network.values); auto id = values->get(0); self = NodeId(fmt::format("{:#064}", id.value())); + + node_cert = create_endorsed_node_cert(); + open_frontend(ActorsType::members); } network.ledger_secrets->init(last_recovered_signed_idx + 1); @@ -1660,10 +1670,16 @@ namespace ccf ReconfigurationType::TWO_TRANSACTION : ReconfigurationType::ONE_TRANSACTION; + // Because certificate signature scheme is not deterministic, endorsed + // node certificate is not recorded in BFT + auto node_endorsement_on_trust = + network.consensus_type != ConsensusType::BFT; + genesis_info.configuration = { config.genesis.recovery_threshold, network.consensus_type, - reconf_type}; + reconf_type, + node_endorsement_on_trust}; create_params.genesis_info = genesis_info; } @@ -1677,6 +1693,14 @@ namespace ccf create_params.code_digest = node_code_id; create_params.node_info_network = config.node_info_network; + // Record self-signed certificate in genesis if the node does not require + // endorsement by the service, so that node signatures can be verified + if (!create_params.genesis_info->configuration.node_endorsement_on_trust + .value()) + { + create_params.node_cert = node_cert; + } + const auto body = serdes::pack(create_params, serdes::Pack::Text); http::Request request(fmt::format( @@ -1896,7 +1920,6 @@ namespace ccf "Could not find endorsed node certificate for {}", self)); } - LOG_FAIL_FMT("Picking up endorsed identity from hook"); node_cert = endorsed_certificate.value(); n2n_channels->set_endorsed_node_cert(node_cert); accept_network_tls_connections(); diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index e5a15847c09..9260d144c7b 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -68,6 +68,9 @@ namespace ccf CodeDigest code_digest; NodeInfoNetwork node_info_network; + // Only set if node does _not_ require endorsement by the service + std::optional node_cert = std::nullopt; + // Only set on genesis transaction, but not on recovery struct GenesisInfo { @@ -75,7 +78,12 @@ namespace ccf std::string constitution; ServiceConfiguration configuration; - bool operator==(const GenesisInfo&) const = default; + bool operator==(const GenesisInfo& other) const + { + return members_info == other.members_info && + constitution == other.constitution && + configuration == other.configuration; + } }; std::optional genesis_info = std::nullopt; }; diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 5d9a0b7f66a..c4c59bc60be 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -139,6 +139,7 @@ namespace ccf auto nodes = tx.rw(network.nodes); auto node_endorsed_certificates = tx.rw(network.node_endorsed_certificates); + auto config = tx.rw(network.config)->get(); auto conflicting_node_id = check_conflicting_node_network(tx, in.node_info_network); @@ -195,7 +196,6 @@ namespace ccf } // Note: All new nodes should specify a CSR from 2.x - // TODO: Should we enforce this? auto client_public_key_pem = crypto::public_key_pem_from_cert(node_der); if (in.certificate_signing_request.has_value()) { @@ -211,16 +211,26 @@ namespace ccf } } - nodes->put( - joining_node_id, - {in.node_info_network, - in.quote_info, - in.public_encryption_key, - node_status, - ledger_secret_seqno, - ds::to_hex(code_digest.data), - in.certificate_signing_request, - client_public_key_pem}); + NodeInfo node_info = { + in.node_info_network, + in.quote_info, + in.public_encryption_key, + node_status, + ledger_secret_seqno, + ds::to_hex(code_digest.data), + in.certificate_signing_request, + client_public_key_pem}; + + // Only record self-signed node certificate if the node does not require + // endorsement by the service when it is trusted + if ( + config->node_endorsement_on_trust.has_value() && + !config->node_endorsement_on_trust.value()) + { + node_info.cert = crypto::cert_der_to_pem(node_der); + } + + nodes->put(joining_node_id, node_info); kv::NetworkConfiguration nc = get_latest_network_configuration(network, tx); @@ -239,7 +249,10 @@ namespace ccf { // Joining node only submit a CSR from 2.x std::optional endorsed_certificate = std::nullopt; - if (in.certificate_signing_request.has_value()) + if ( + in.certificate_signing_request.has_value() && + (!config->node_endorsement_on_trust.has_value() || + config->node_endorsement_on_trust.value())) { endorsed_certificate = context.get_node_state().generate_endorsed_certificate( @@ -1117,25 +1130,41 @@ namespace ccf // Retire all nodes, in case there are any (i.e. post recovery) g.retire_active_nodes(); - g.add_node( - in.node_id, - {in.node_info_network, - {in.quote_info}, - in.public_encryption_key, - NodeStatus::TRUSTED, - std::nullopt, - ds::to_hex(in.code_digest.data), - in.certificate_signing_request, - in.public_key}); - - auto endorsed_certificates = - ctx.tx.rw(network.node_endorsed_certificates); - endorsed_certificates->put( - in.node_id, - context.get_node_state().generate_endorsed_certificate( - in.certificate_signing_request, - this->network.identity->priv_key, - this->network.identity->cert)); + NodeInfo node_info = { + in.node_info_network, + {in.quote_info}, + in.public_encryption_key, + NodeStatus::TRUSTED, + std::nullopt, + ds::to_hex(in.code_digest.data), + in.certificate_signing_request, + in.public_key}; + + CCF_ASSERT_FMT( + in.genesis_info->configuration.node_endorsement_on_trust.has_value(), + "Node endorsement configuration should always be set from 2.x"); + + if (in.genesis_info->configuration.node_endorsement_on_trust.value()) + { + auto endorsed_certificates = + ctx.tx.rw(network.node_endorsed_certificates); + endorsed_certificates->put( + in.node_id, + context.get_node_state().generate_endorsed_certificate( + in.certificate_signing_request, + this->network.identity->priv_key, + this->network.identity->cert)); + } + else + { + CCF_ASSERT_FMT( + in.node_cert.value(), + "Self-signed node certificate should be set if configuration does " + "not require endorsement"); + node_info.cert = in.node_cert.value(); + } + + g.add_node(in.node_id, node_info); #ifdef GET_QUOTE g.trust_node_code_id(in.code_digest); diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index 1ec561bc4fd..2c68cdcdd47 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -75,7 +75,8 @@ namespace ccf public_encryption_key, code_digest, node_info_network) - DECLARE_JSON_OPTIONAL_FIELDS(CreateNetworkNodeToNode::In, genesis_info) + DECLARE_JSON_OPTIONAL_FIELDS( + CreateNetworkNodeToNode::In, node_cert, genesis_info) DECLARE_JSON_TYPE(GetCommit::Out) DECLARE_JSON_REQUIRED_FIELDS(GetCommit::Out, transaction_id) diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 160c89fd8cb..06b61cbfc9e 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -778,7 +778,12 @@ const actions = new Map([ ); // Also generate and record service-endorsed node certificate from node CSR - if (nodeInfo.certificate_signing_request !== undefined) { + + if ( + nodeInfo.certificate_signing_request !== undefined && + serviceConfig.node_endorsement_on_trust !== undefined && + serviceConfig.node_endorsement_on_trust + ) { // Note: CSR is only present from 2.x const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, From 566dcb57b984d5b6efc3b11199928a762ad1275a Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 12 Aug 2021 11:50:18 +0100 Subject: [PATCH 040/105] Differentiate self signed from endorsed node cert --- src/enclave/enclave.h | 11 ++++--- src/node/node_state.h | 75 +++++++++++++++++++++++-------------------- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/enclave/enclave.h b/src/enclave/enclave.h index e76269fc97e..2cb449cd098 100644 --- a/src/enclave/enclave.h +++ b/src/enclave/enclave.h @@ -175,16 +175,19 @@ namespace enclave } // Copy node and network certs out - if (r.node_cert.size() > node_cert_size) + if (r.self_signed_node_cert.size() > node_cert_size) { LOG_FAIL_FMT( "Insufficient space ({}) to copy node_cert out ({})", node_cert_size, - r.node_cert.size()); + r.self_signed_node_cert.size()); return false; } - ::memcpy(node_cert, r.node_cert.data(), r.node_cert.size()); - *node_cert_len = r.node_cert.size(); + ::memcpy( + node_cert, + r.self_signed_node_cert.data(), + r.self_signed_node_cert.size()); + *node_cert_len = r.self_signed_node_cert.size(); if (start_type == StartType::New || start_type == StartType::Recover) { diff --git a/src/node/node_state.h b/src/node/node_state.h index c9e2162130a..2bfcc63a345 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -69,7 +69,7 @@ namespace ccf struct NodeCreateInfo { - crypto::Pem node_cert; + crypto::Pem self_signed_node_cert; crypto::Pem network_cert; }; @@ -92,7 +92,8 @@ namespace ccf std::shared_ptr node_sign_kp; NodeId self; std::shared_ptr node_encrypt_kp; - crypto::Pem node_cert; + crypto::Pem self_signed_node_cert; + std::optional endorsed_node_cert = std::nullopt; QuoteInfo quote_info; CodeDigest node_code_id; CCFConfig config; @@ -334,6 +335,8 @@ namespace ccf get_subject_alternative_names(); js::register_class_ids(); + self_signed_node_cert = create_self_signed_node_cert(); + accept_node_tls_connections(); open_frontend(ActorsType::nodes); #ifdef GET_QUOTE @@ -350,9 +353,6 @@ namespace ccf } #endif - // TODO: Split self-signed from endorsed certificate - node_cert = create_self_signed_node_cert(); - switch (start_type) { case StartType::New: @@ -376,12 +376,12 @@ namespace ccf // node-to-node messages self = NodeId(fmt::format("{:#064}", 0)); - node_cert = create_endorsed_node_cert(); + endorsed_node_cert = create_endorsed_node_cert(); + accept_network_tls_connections(); open_frontend(ActorsType::members); } - // TODO: Do not pass node_cert if CFT! - setup_consensus(ServiceStatus::OPENING, false); + setup_consensus(ServiceStatus::OPENING, false, endorsed_node_cert); setup_progress_tracker(); setup_history(); @@ -401,8 +401,8 @@ namespace ccf sm.advance(State::partOfNetwork); LOG_INFO_FMT("Created new node {}", self); - accept_node_tls_connections(); - return {node_cert, network.identity->cert}; + + return {self_signed_node_cert, network.identity->cert}; } case StartType::Join: { @@ -417,8 +417,7 @@ namespace ccf } LOG_INFO_FMT("Created join node {}", self); - accept_node_tls_connections(); - return {node_cert, {}}; + return {self_signed_node_cert, {}}; } case StartType::Recover: { @@ -449,8 +448,7 @@ namespace ccf sm.advance(State::readingPublicLedger); LOG_INFO_FMT("Created recovery node {}", self); - accept_node_tls_connections(); - return {node_cert, network.identity->cert}; + return {self_signed_node_cert, network.identity->cert}; } default: { @@ -467,7 +465,7 @@ namespace ccf { auto network_ca = std::make_shared(config.joining.network_cert); auto join_client_cert = std::make_unique( - network_ca, node_cert, node_sign_kp->private_key_pem()); + network_ca, self_signed_node_cert, node_sign_kp->private_key_pem()); // Create RPC client and connect to remote node auto join_client = @@ -547,16 +545,24 @@ namespace ccf network.ledger_secrets->init_from_map( std::move(resp.network_info.ledger_secrets)); + crypto::Pem n2n_channels_cert; if (!resp.network_info.endorsed_certificate.has_value()) { // Endorsed node certificate is included in join response - // from 2.x. When joining an existing 1.x service, self-sign own - // certificate and use it to endorse TLS connections. - node_cert = create_endorsed_node_cert(); + // from 2.x (CFT only). When joining an existing 1.x service, + // self-sign own certificate and use it to endorse TLS + // connections. + endorsed_node_cert = create_endorsed_node_cert(); + n2n_channels_cert = endorsed_node_cert.value(); open_frontend(ActorsType::members); open_user_frontend(); accept_network_tls_connections(); } + else + { + n2n_channels_cert = + resp.network_info.endorsed_certificate.value(); + } if (network.consensus_type == ConsensusType::BFT) { @@ -570,7 +576,7 @@ namespace ccf setup_consensus( resp.network_info.service_status, resp.network_info.public_only, - resp.network_info.endorsed_certificate.value_or(node_cert)); + n2n_channels_cert); setup_progress_tracker(); setup_history(); auto_refresh_jwt_keys(); @@ -741,7 +747,7 @@ namespace ccf rpcsessions, rpc_map, node_sign_kp, - node_cert); + self_signed_node_cert); jwt_key_auto_refresh->start(); network.tables->set_map_hook( @@ -977,7 +983,7 @@ namespace ccf auto id = values->get(0); self = NodeId(fmt::format("{:#064}", id.value())); - node_cert = create_endorsed_node_cert(); + endorsed_node_cert = create_endorsed_node_cert(); open_frontend(ActorsType::members); } @@ -1616,11 +1622,9 @@ namespace ccf void accept_node_tls_connections() { // Accept TLS connections, presenting self-signed (i.e. non-endorsed) - // node certificate. Once the node is part of the network, this - // certificate should be replaced with network-endorsed counterpart - - assert(!node_cert.empty()); - rpcsessions->set_cert(node_cert, node_sign_kp->private_key_pem()); + // node certificate. + rpcsessions->set_cert( + self_signed_node_cert, node_sign_kp->private_key_pem()); LOG_INFO_FMT("Node TLS connections now accepted"); } @@ -1628,9 +1632,12 @@ namespace ccf { // Accept TLS connections, presenting node certificate signed by network // certificate - - assert(!node_cert.empty() && !make_verifier(node_cert)->is_self_signed()); - rpcsessions->set_cert(node_cert, node_sign_kp->private_key_pem()); + CCF_ASSERT_FMT( + endorsed_node_cert.has_value(), + "Node certificate should be endorsed before accepting endorsed client " + "connections"); + rpcsessions->set_cert( + endorsed_node_cert.value(), node_sign_kp->private_key_pem()); LOG_INFO_FMT("Network TLS connections now accepted"); } @@ -1698,7 +1705,7 @@ namespace ccf if (!create_params.genesis_info->configuration.node_endorsement_on_trust .value()) { - create_params.node_cert = node_cert; + create_params.node_cert = self_signed_node_cert; } const auto body = serdes::pack(create_params, serdes::Pack::Text); @@ -1710,7 +1717,7 @@ namespace ccf request.set_body(&body); - auto node_cert_der = crypto::cert_pem_to_der(node_cert); + auto node_cert_der = crypto::cert_pem_to_der(self_signed_node_cert); const auto key_id = crypto::Sha256Hash(node_cert_der).hex_str(); http::sign_request(request, node_sign_kp, key_id); @@ -1758,7 +1765,7 @@ namespace ccf bool send_create_request(const std::vector& packed) { auto node_session = std::make_shared( - enclave::InvalidSessionId, node_cert.raw()); + enclave::InvalidSessionId, self_signed_node_cert.raw()); auto ctx = enclave::make_rpc_context(node_session, packed); ctx->is_create_request = true; @@ -1920,8 +1927,8 @@ namespace ccf "Could not find endorsed node certificate for {}", self)); } - node_cert = endorsed_certificate.value(); - n2n_channels->set_endorsed_node_cert(node_cert); + endorsed_node_cert = endorsed_certificate.value(); + n2n_channels->set_endorsed_node_cert(endorsed_node_cert.value()); accept_network_tls_connections(); open_frontend(ActorsType::members); From 10577a7b20c540c1994ec066ae6194e86f156894 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 12 Aug 2021 14:09:38 +0100 Subject: [PATCH 041/105] Fix recovery --- src/node/node_state.h | 19 ++++++++++--------- src/node/rpc/node_frontend.h | 36 ++++++++++++++---------------------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/src/node/node_state.h b/src/node/node_state.h index 59959fcc634..e0c1bbec79c 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -985,6 +985,7 @@ namespace ccf self = NodeId(fmt::format("{:#064}", id.value())); endorsed_node_cert = create_endorsed_node_cert(); + accept_network_tls_connections(); open_frontend(ActorsType::members); } @@ -1664,6 +1665,11 @@ namespace ccf { CreateNetworkNodeToNode::In create_params; + // Because certificate signature scheme is not deterministic, endorsed + // node certificate is not recorded in BFT + auto node_endorsement_on_trust = + network.consensus_type != ConsensusType::BFT; + // False on recovery where the consortium is read from the existing // ledger if (create_consortium) @@ -1678,11 +1684,6 @@ namespace ccf ReconfigurationType::TWO_TRANSACTION : ReconfigurationType::ONE_TRANSACTION; - // Because certificate signature scheme is not deterministic, endorsed - // node certificate is not recorded in BFT - auto node_endorsement_on_trust = - network.consensus_type != ConsensusType::BFT; - genesis_info.configuration = { config.genesis.recovery_threshold, network.consensus_type, @@ -1701,10 +1702,10 @@ namespace ccf create_params.code_digest = node_code_id; create_params.node_info_network = config.node_info_network; - // Record self-signed certificate in genesis if the node does not require - // endorsement by the service, so that node signatures can be verified - if (!create_params.genesis_info->configuration.node_endorsement_on_trust - .value()) + // Record self-signed certificate in create request if the node does not + // require endorsement by the service, so that node signatures can be + // verified + if (!node_endorsement_on_trust) { create_params.node_cert = self_signed_node_cert; } diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 25b0f2c27c7..b86fc75a83e 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -1101,6 +1101,19 @@ namespace ccf g.create_service(in.network_cert); + // Retire all nodes, in case there are any (i.e. post recovery) + g.retire_active_nodes(); + + NodeInfo node_info = { + in.node_info_network, + {in.quote_info}, + in.public_encryption_key, + NodeStatus::TRUSTED, + std::nullopt, + ds::to_hex(in.code_digest.data), + in.certificate_signing_request, + in.public_key}; + // Genesis transaction (i.e. not after recovery) if (in.genesis_info.has_value()) { @@ -1130,24 +1143,7 @@ namespace ccf g.set_constitution(in.genesis_info->constitution); } - // Retire all nodes, in case there are any (i.e. post recovery) - g.retire_active_nodes(); - - NodeInfo node_info = { - in.node_info_network, - {in.quote_info}, - in.public_encryption_key, - NodeStatus::TRUSTED, - std::nullopt, - ds::to_hex(in.code_digest.data), - in.certificate_signing_request, - in.public_key}; - - CCF_ASSERT_FMT( - in.genesis_info->configuration.node_endorsement_on_trust.has_value(), - "Node endorsement configuration should always be set from 2.x"); - - if (in.genesis_info->configuration.node_endorsement_on_trust.value()) + if (!in.node_cert.has_value()) { auto endorsed_certificates = ctx.tx.rw(network.node_endorsed_certificates); @@ -1160,10 +1156,6 @@ namespace ccf } else { - CCF_ASSERT_FMT( - in.node_cert.value(), - "Self-signed node certificate should be set if configuration does " - "not require endorsement"); node_info.cert = in.node_cert.value(); } From 86b8091fbef8b7dcdfe5b1212c648cc717655bf0 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 12 Aug 2021 14:09:56 +0100 Subject: [PATCH 042/105] Canary --- .daily_canary | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.daily_canary b/.daily_canary index 57c8746a1d4..10e30a5ac75 100644 --- a/.daily_canary +++ b/.daily_canary @@ -1 +1 @@ -Allo Allo \ No newline at end of file +Hey there \ No newline at end of file From 33b00cf42e1e848f8ae4403448f4898cd20baaa9 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 12 Aug 2021 14:22:21 +0100 Subject: [PATCH 043/105] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e470f097fda..9873e68bdbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Added support for listening on multiple interfaces for incoming client RPCs, with individual session caps (#2628). +- Service-endorsed node certificates are now recorded in a new `public:ccf.gov.nodes.endorsed_certificates` table, while existing `cert` field in the `public:ccf.gov.nodes.info` table is now deprecated (#2844). +- Joining nodes now present service-endorsed certificate in client TLS sessions _after_ they have observed their own addition to the store, rathen than as soon as they have joined the service. Operators should monitor the initial progress of a new node using its self-signed certificate as TLS session certificate authority (#2844). +- Updated `actions.js` constitution fragment (#2844). ### Changed From 4712e9908ee5e902e471c7d6fcc01d0d554607ac Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 12 Aug 2021 14:43:50 +0100 Subject: [PATCH 044/105] Update docs --- doc/operations/start_network.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/operations/start_network.rst b/doc/operations/start_network.rst index 62a5f14ae25..fdc98c262e0 100644 --- a/doc/operations/start_network.rst +++ b/doc/operations/start_network.rst @@ -76,7 +76,7 @@ Otherwise, if the network has already been opened to users, members need to trus The ``Pending`` joining node will automatically poll the service (interval configurable via ``--join-timer`` option) until the members have successfully transitioned the node to the ``Trusted`` state. It is only then that the joining node will transition to the ``PartOfNetwork`` state and start updating its ledger. -.. tip:: After the node has been trusted by members, operators should poll the ``/node/state`` endpoint on the newly added node until the ``{"state": "PartOfNetwork"}`` is reported. This status confirms that the replication of the ledger has started on this node. +.. tip:: After the node has been trusted by members, operators should poll the ``/node/state`` endpoint on the newly added node, using the node's self-signed certificate as TLS CA, until the ``{"state": "PartOfNetwork"}`` is reported. This status confirms that the replication of the ledger has started on this node. .. note:: To accelerate the joining procedure, it is possible for new nodes to join from a snapshot. More information on snapshots :ref:`here `. @@ -103,33 +103,33 @@ The following diagram summarises the steps that operators and members should fol Node 0-->>-Node 1: "Pending" state end - Operators->>+Node 1: Poll /node/state for "PartOfNetwork" + Operators->>+Node 1: Poll /node/state for "PartOfNetwork" (using self-signed certificate as CA) Node 1-->>-Operators: "Pending" state Members->>+Node 0: transition_node_to_trusted proposal for Node 1 and votes Node 0-->>-Members: Proposal Accepted - Operators->>+Node 1: Poll /node/state for "PartOfNetwork" + Operators->>+Node 1: Poll /node/state for "PartOfNetwork" (using self-signed certificate as CA) Node 1-->>-Operators: "Pending" state Node 1->>+Node 0: Poll for "Trusted" state Node 0-->>-Node 1: "Trusted" state (includes ledger secrets and service private key) - Node 1->>+Node 1: Endorse TLS with service private key - Note over Node 1: State: "PartOfNetwork"
Ledger replication started
Application open to users loop Node 1 ledger replication Node 0->>+Node 1: Ledger replication end - Operators->>+Node 1: Poll /node/state for "PartOfNetwork" + Operators->>+Node 1: Poll /node/state for "PartOfNetwork" (using self-signed certificate as CA) Node 1-->>-Operators: "PartOfNetwork" state loop Node 1 ledger replication Node 0->>+Node 1: Ledger replication end + Node 1->>+Node 1: Endorse TLS with service private key + Note over Operators: Operators monitor progress of ledger replication Operators->>+Node 1: Poll /node/commit Node 1-->>-Operators: "commit": ... From 0297c5fe576ba3b82c144ec45a24dc31f641e799 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 12 Aug 2021 14:46:22 +0100 Subject: [PATCH 045/105] fmt --- src/crypto/mbedtls/key_pair.cpp | 3 ++- src/crypto/openssl/key_pair.cpp | 3 ++- src/crypto/verifier.cpp | 1 + src/js/wrap.cpp | 3 ++- src/node/test/channels.cpp | 1 + src/node/test/progress_tracker.cpp | 3 ++- 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/crypto/mbedtls/key_pair.cpp b/src/crypto/mbedtls/key_pair.cpp index 9d9ea85a070..c1e6257f4a9 100644 --- a/src/crypto/mbedtls/key_pair.cpp +++ b/src/crypto/mbedtls/key_pair.cpp @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. +#include "key_pair.h" + #include "curve.h" #include "ds/net.h" #include "entropy.h" #include "hash.h" -#include "key_pair.h" #define FMT_HEADER_ONLY #include diff --git a/src/crypto/openssl/key_pair.cpp b/src/crypto/openssl/key_pair.cpp index d7bf60d6677..390e5414cfd 100644 --- a/src/crypto/openssl/key_pair.cpp +++ b/src/crypto/openssl/key_pair.cpp @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. +#include "key_pair.h" + #include "crypto/curve.h" #include "crypto/openssl/public_key.h" #include "hash.h" -#include "key_pair.h" #include "openssl_wrappers.h" #define FMT_HEADER_ONLY diff --git a/src/crypto/verifier.cpp b/src/crypto/verifier.cpp index 635ca68467b..8b93286b0d6 100644 --- a/src/crypto/verifier.cpp +++ b/src/crypto/verifier.cpp @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 License. #include "crypto/mbedtls/verifier.h" + #include "crypto/openssl/verifier.h" #include "verifier.h" diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index 01c01c3feb9..b4ce330a15d 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -1,5 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. +#include "js/wrap.h" + #include "ccf/tx_id.h" #include "ccf/version.h" #include "ds/logger.h" @@ -7,7 +9,6 @@ #include "js/conv.cpp" #include "js/crypto.cpp" #include "js/oe.cpp" -#include "js/wrap.h" #include "kv/untyped_map.h" #include "node/jwt.h" #include "node/rpc/call_types.h" diff --git a/src/node/test/channels.cpp b/src/node/test/channels.cpp index f01dd9514b7..f4eddc90cbc 100644 --- a/src/node/test/channels.cpp +++ b/src/node/test/channels.cpp @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. #include "../channels.h" + #include "crypto/verifier.h" #include "ds/hex.h" #include "node/entities.h" diff --git a/src/node/test/progress_tracker.cpp b/src/node/test/progress_tracker.cpp index 394ea767269..309849544b2 100644 --- a/src/node/test/progress_tracker.cpp +++ b/src/node/test/progress_tracker.cpp @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. +#include "node/progress_tracker.h" + #include "consensus/aft/impl/view_change_tracker.h" #include "kv/store.h" #include "kv/test/stub_consensus.h" #include "node/nodes.h" -#include "node/progress_tracker.h" #include "node/request_tracker.h" #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN From b7b30460216e826ae04828d74be8014fab02e08d Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 12 Aug 2021 14:52:20 +0100 Subject: [PATCH 046/105] Tweaks --- doc/operations/start_network.rst | 2 +- src/node/node_state.h | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/operations/start_network.rst b/doc/operations/start_network.rst index fdc98c262e0..9f5afb5bd95 100644 --- a/doc/operations/start_network.rst +++ b/doc/operations/start_network.rst @@ -128,7 +128,7 @@ The following diagram summarises the steps that operators and members should fol Node 0->>+Node 1: Ledger replication end - Node 1->>+Node 1: Endorse TLS with service private key + Node 1->>+Node 1: Observe own addition to store
Endorse TLS with service private key Note over Operators: Operators monitor progress of ledger replication Operators->>+Node 1: Poll /node/commit diff --git a/src/node/node_state.h b/src/node/node_state.h index e0c1bbec79c..743c97aed8b 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -362,9 +362,6 @@ namespace ccf network.ledger_secrets->init(); - setup_snapshotter(); - setup_encryptor(); - if (network.consensus_type == ConsensusType::BFT) { // BFT consensus requires a stable order of node IDs so that the @@ -381,6 +378,8 @@ namespace ccf open_frontend(ActorsType::members); } + setup_snapshotter(); + setup_encryptor(); setup_consensus(ServiceStatus::OPENING, false, endorsed_node_cert); setup_progress_tracker(); setup_history(); From 16392d9ebb8404e5f007c28beb906c57ba1751e1 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 12 Aug 2021 17:05:32 +0100 Subject: [PATCH 047/105] Fix UB --- src/node/rpc/test/node_frontend_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node/rpc/test/node_frontend_test.cpp b/src/node/rpc/test/node_frontend_test.cpp index 82fec954da1..993599488b2 100644 --- a/src/node/rpc/test/node_frontend_test.cpp +++ b/src/node/rpc/test/node_frontend_test.cpp @@ -87,6 +87,7 @@ TEST_CASE("Add a node to an opening service") auto gen_tx = network.tables->create_tx(); GenesisGenerator gen(network, gen_tx); gen.init_values(); + gen.init_configuration({0, ConsensusType::CFT, std::nullopt, true}); ShareManager share_manager(network); StubNodeContext context; From a1b0e018acd4c17e79bb21c3956db8028931974a Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 23 Aug 2021 07:56:25 +0000 Subject: [PATCH 048/105] Fix `node_frontent_test` --- src/node/rpc/node_call_types.h | 4 +++- src/node/rpc/test/node_frontend_test.cpp | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 1da3a994274..8f3dcc28919 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -104,7 +104,9 @@ namespace ccf struct Out { NodeStatus node_status; - std::optional node_id; + + // Deprecated in 2.x + std::optional node_id = std::nullopt; struct NetworkInfo { diff --git a/src/node/rpc/test/node_frontend_test.cpp b/src/node/rpc/test/node_frontend_test.cpp index 6446e7b60d7..4ff8cfd706f 100644 --- a/src/node/rpc/test/node_frontend_test.cpp +++ b/src/node/rpc/test/node_frontend_test.cpp @@ -305,7 +305,7 @@ TEST_CASE("Add a node to an open service") { // In a real scenario, nodes are trusted via member governance. GenesisGenerator g(network, tx); - auto joining_node_id = crypto::Sha256Hash(kp->public_key_der()).hex_str(); + auto joining_node_id = ccf::compute_node_id_from_kp(kp); g.trust_node(joining_node_id, network.ledger_secrets->get_latest(tx).first); const auto dummy_endorsed_certificate = crypto::make_key_pair()->self_sign("CN=dummy endorsed certificate"); @@ -330,7 +330,6 @@ TEST_CASE("Add a node to an open service") require_ledger_secrets_equal( response.network_info->ledger_secrets, network.ledger_secrets->get(tx, up_to_ledger_secret_seqno)); - CHECK(response.node_id == joining_node_id); CHECK(response.network_info->identity == *network.identity.get()); CHECK(response.node_status == NodeStatus::TRUSTED); CHECK(response.network_info->public_only == true); From 6aa3e88a144f625c76e5d02ede8537729866b904 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 23 Aug 2021 08:12:33 +0000 Subject: [PATCH 049/105] WIP - where is certificate_subject_identity? --- src/runtime_config/default/actions.js | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 06b61cbfc9e..b41e1aaeb42 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -841,4 +841,41 @@ const actions = new Map([ } ), ], + [ + "renew_node_certificate", + new Action( + function (args) { + checkEntityId(args.node_id, "node_id"); + }, + function (args) { + const node = ccf.kv["public:ccf.gov.nodes.info"].get( + ccf.strToBuf(args.node_id) + ); + if (node === undefined) { + throw new Error(`No such node: ${args.node_id}`); + } + const nodeInfo = ccf.bufToJsonCompatible(node); + if (nodeInfo.status !== "Trusted") { + throw new Error(`Node ${args.node_id} is not trusted`); + } + + // Note: CSR is only present from 2.x + if (nodeInfo.certificate_signing_request === undefined) { + throw new Error( + `Node ${args.node_id} has no certificate signing request` + ); + } + + // Note: CSR is only present from 2.x + const endorsed_node_cert = ccf.network.generateEndorsedCertificate( + nodeInfo.certificate_signing_request, + nodeInfo.certificate_subject_identity + ); + ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( + ccf.strToBuf(args.node_id), + ccf.strToBuf(endorsed_node_cert) + ); + } + ), + ], ]); From 301837ec81b8f644e6221716c4441d5fe5b40f20 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 23 Aug 2021 08:22:34 +0000 Subject: [PATCH 050/105] Remove certificate_subject_identity` field from governance --- src/js/wrap.cpp | 18 ++---------------- src/runtime_config/default/actions.js | 3 +-- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index b4ce330a15d..eb1fbf7aba4 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -495,9 +495,9 @@ namespace js int argc, [[maybe_unused]] JSValueConst* argv) { - if (argc != 2) + if (argc != 1) { - return JS_ThrowTypeError(ctx, "Passed %d arguments but expected 2", argc); + return JS_ThrowTypeError(ctx, "Passed %d arguments but expected 1", argc); } auto network = @@ -530,20 +530,6 @@ namespace js } auto csr = crypto::Pem(csr_cstr); - JSValue certificate_subject_identity_val = - JS_JSONStringify(ctx, argv[1], JS_NULL, JS_NULL); - if (JS_IsException(certificate_subject_identity_val)) - { - return JS_ThrowTypeError( - ctx, "certificate subject identity argument is not a JSON object"); - } - auto certificate_subject_identity_cstr = - JS_ToCString(ctx, certificate_subject_identity_val); - std::string certificate_subject_identity_json( - certificate_subject_identity_cstr); - JS_FreeCString(ctx, certificate_subject_identity_cstr); - JS_FreeValue(ctx, certificate_subject_identity_val); - auto endorsed_cert = node->generate_endorsed_certificate( csr, network->identity->priv_key, network->identity->cert); diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 06b61cbfc9e..6880583bf5d 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -786,8 +786,7 @@ const actions = new Map([ ) { // Note: CSR is only present from 2.x const endorsed_node_cert = ccf.network.generateEndorsedCertificate( - nodeInfo.certificate_signing_request, - nodeInfo.certificate_subject_identity + nodeInfo.certificate_signing_request ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), From e48584b1394095acd137bfd736d4e1a4c4578efa Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 23 Aug 2021 14:09:38 +0000 Subject: [PATCH 051/105] . --- python/ccf/proposal_generator.py | 5 +++ src/runtime_config/default/actions.js | 3 +- tests/governance.py | 61 ++++++++++++++++++++++----- tests/infra/consortium.py | 7 +++ 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/python/ccf/proposal_generator.py b/python/ccf/proposal_generator.py index 784fceade8b..4d65bf3482c 100644 --- a/python/ccf/proposal_generator.py +++ b/python/ccf/proposal_generator.py @@ -325,6 +325,11 @@ def set_jwt_public_signing_keys(issuer: str, jwks_path: str, **kwargs): return build_proposal("set_jwt_public_signing_keys", args, **kwargs) +@cli_proposal +def renew_node_certificate(node_id: str, **kwargs): + return build_proposal("renew_node_certificate", {"node_id": node_id}, **kwargs) + + if __name__ == "__main__": parser = argparse.ArgumentParser() diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index df2bec10d5a..65721c57389 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -867,8 +867,7 @@ const actions = new Map([ // Note: CSR is only present from 2.x const endorsed_node_cert = ccf.network.generateEndorsedCertificate( - nodeInfo.certificate_signing_request, - nodeInfo.certificate_subject_identity + nodeInfo.certificate_signing_request ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), diff --git a/tests/governance.py b/tests/governance.py index 230e0cb94e3..311b8e5ed0b 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -13,6 +13,7 @@ import infra.logging_app as app import json import requests +import infra.crypto from loguru import logger as LOG @@ -265,22 +266,62 @@ def post_proposal_request_raw(node, headers=None, expected_error_msg=None): ) +# TODO: Add to suite +@reqs.description("Update certificates of all nodes") +def test_node_cert_renewal(network, args): + def get_node_cert_tls(node): + import ssl + + return ssl.get_server_certificate((node.host, node.rpc_port)) + + for node in network.get_joined_nodes(): + + with node.client() as c: + c.get("/node/network/nodes") + + node_cert_tls_before = get_node_cert_tls(node) + assert ( + infra.crypto.compute_public_key_der_hash_hex_from_pem( + node_cert_tls_before + ) + == node.node_id + ) + network.consortium.renew_node_certificate(node, node.node_id) + node_cert_tls_after = get_node_cert_tls(node) + assert ( + node_cert_tls_before != node_cert_tls_after + ), "Node TLS certificate should be updated after renewal" + + # Long-connected client is still connected after certificate renewal + c.get("/node/network/nodes") + + assert ( + infra.crypto.compute_public_key_der_hash_hex_from_pem( + node_cert_tls_before + ) + == node.node_id + ) + + # TODO: Extract public key and check that validity period changed too + + def run(args): with infra.network.network( args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb ) as network: network.start_and_join(args) - test_create_endpoint(network, args) - test_consensus_status(network, args) - test_node_ids(network, args) - test_member_data(network, args) - test_quote(network, args) - test_user(network, args) - test_no_quote(network, args) - test_service_principals(network, args) - test_ack_state_digest_update(network, args) - test_invalid_client_signature(network, args) + # test_create_endpoint(network, args) + # test_consensus_status(network, args) + # test_node_ids(network, args) + # test_member_data(network, args) + # test_quote(network, args) + # test_user(network, args) + # test_no_quote(network, args) + # test_service_principals(network, args) + # test_ack_state_digest_update(network, args) + # test_invalid_client_signature(network, args) + test_node_cert_renewal(network, args) if __name__ == "__main__": diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 5d8df0fd886..7420c108918 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -521,6 +521,13 @@ def retire_code(self, remote_node, code_id): proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) + def renew_node_certificate(self, remote_node, node_id): + proposal_body, careful_vote = self.make_proposal( + "renew_node_certificate", node_id + ) + proposal = self.get_any_active_member().propose(remote_node, proposal_body) + return self.vote_using_majority(remote_node, proposal, careful_vote) + def check_for_service(self, remote_node, status): """ Check the certificate associated with current CCF service signing key has been recorded in From 8fa9951e990da9e15f1751b4c9920615d0e2cc8f Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 23 Aug 2021 14:09:55 +0000 Subject: [PATCH 052/105] Fixes --- python/ccf/ledger.py | 9 ++++++--- src/js/wrap.cpp | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/python/ccf/ledger.py b/python/ccf/ledger.py index 926d9c1f1b3..52064ecfebe 100644 --- a/python/ccf/ledger.py +++ b/python/ccf/ledger.py @@ -285,6 +285,7 @@ def add_transaction(self, transaction): tables = transaction_public_domain.get_tables() # Add contributing nodes certs and update nodes network trust status for verification + node_cert = None if NODES_TABLE_NAME in tables: node_table = tables[NODES_TABLE_NAME] for node_id, node_info in node_table.items(): @@ -293,7 +294,8 @@ def add_transaction(self, transaction): # Add the self-signed node certificate (only available in 1.x, # refer to node endorsed certificates table otherwise) if "cert" in node_info: - self.node_certificates[node_id] = node_info["cert"].encode() + node_cert = node_info["cert"].encode() + self.node_certificates[node_id] = node_cert # Update node trust status # Also record the seqno at which the node status changed to # track when a primary node should stop issuing signatures @@ -312,9 +314,10 @@ def add_transaction(self, transaction): ) in node_endorsed_certificates_tables.items(): node_id = node_id.decode() assert ( - node_id not in self.node_certificates + node_cert is None ), "Only one of node self-signed certificate and endorsed certificate should be recorded" - self.node_certificates[node_id] = endorsed_node_cert + node_cert = endorsed_node_cert + self.node_certificates[node_id] = node_cert # This is a merkle root/signature tx if the table exists if SIGNATURE_TX_TABLE_NAME in tables: diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index eb1fbf7aba4..a4225310021 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -529,6 +529,7 @@ namespace js throw JS_ThrowTypeError(ctx, "csr argument is not a string"); } auto csr = crypto::Pem(csr_cstr); + JS_FreeCString(ctx, csr_cstr); auto endorsed_cert = node->generate_endorsed_certificate( csr, network->identity->priv_key, network->identity->cert); From 86f108bd3e082675b84a03e1a2a94e5255c45191 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 23 Aug 2021 14:34:42 +0000 Subject: [PATCH 053/105] . --- tests/governance.py | 23 +++++++++++------------ tests/suite/test_suite.py | 3 +++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/governance.py b/tests/governance.py index 311b8e5ed0b..adbc2a9610c 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -266,7 +266,6 @@ def post_proposal_request_raw(node, headers=None, expected_error_msg=None): ) -# TODO: Add to suite @reqs.description("Update certificates of all nodes") def test_node_cert_renewal(network, args): def get_node_cert_tls(node): @@ -302,7 +301,7 @@ def get_node_cert_tls(node): == node.node_id ) - # TODO: Extract public key and check that validity period changed too + # TODO: Validty period def run(args): @@ -311,16 +310,16 @@ def run(args): ) as network: network.start_and_join(args) - # test_create_endpoint(network, args) - # test_consensus_status(network, args) - # test_node_ids(network, args) - # test_member_data(network, args) - # test_quote(network, args) - # test_user(network, args) - # test_no_quote(network, args) - # test_service_principals(network, args) - # test_ack_state_digest_update(network, args) - # test_invalid_client_signature(network, args) + test_create_endpoint(network, args) + test_consensus_status(network, args) + test_node_ids(network, args) + test_member_data(network, args) + test_quote(network, args) + test_user(network, args) + test_no_quote(network, args) + test_service_principals(network, args) + test_ack_state_digest_update(network, args) + test_invalid_client_signature(network, args) test_node_cert_renewal(network, args) diff --git a/tests/suite/test_suite.py b/tests/suite/test_suite.py index f3a930b7017..240a2472987 100644 --- a/tests/suite/test_suite.py +++ b/tests/suite/test_suite.py @@ -10,6 +10,7 @@ import membership import governance_history import jwt_test +import governance from inspect import signature, Parameter @@ -113,6 +114,8 @@ recovery.test, # jwt jwt_test.test_refresh_jwt_issuer, + # governance + governance.test_test_node_cert_renewal, # # # From 672cae891f1f55b2808e76f18466fcc97183560a Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 24 Aug 2021 14:17:47 +0000 Subject: [PATCH 054/105] Add valid_from and valid_to --- python/ccf/proposal_generator.py | 8 ++++++-- src/crypto/key_pair.h | 4 +++- src/crypto/mbedtls/key_pair.cpp | 6 +++++- src/crypto/mbedtls/key_pair.h | 4 +++- src/crypto/openssl/key_pair.cpp | 27 ++++++++++++++++++++++++--- src/crypto/openssl/key_pair.h | 4 +++- src/js/wrap.cpp | 26 +++++++++++++++++++++++--- src/node/node_state.h | 7 +++++-- src/node/rpc/node_interface.h | 4 +++- src/node/rpc/test/node_stub.h | 4 +++- src/runtime_config/default/actions.js | 4 +++- tests/governance.py | 27 +++++++++++++++------------ tests/infra/consortium.py | 4 ++-- 13 files changed, 98 insertions(+), 31 deletions(-) diff --git a/python/ccf/proposal_generator.py b/python/ccf/proposal_generator.py index 4d65bf3482c..b2859663b07 100644 --- a/python/ccf/proposal_generator.py +++ b/python/ccf/proposal_generator.py @@ -326,8 +326,12 @@ def set_jwt_public_signing_keys(issuer: str, jwks_path: str, **kwargs): @cli_proposal -def renew_node_certificate(node_id: str, **kwargs): - return build_proposal("renew_node_certificate", {"node_id": node_id}, **kwargs) +def renew_node_certificate(node_id: str, valid_from: str, valid_to: str, **kwargs): + return build_proposal( + "renew_node_certificate", + {"node_id": node_id, "valid_from": valid_from, "valid_to": valid_to}, + **kwargs, + ) if __name__ == "__main__": diff --git a/src/crypto/key_pair.h b/src/crypto/key_pair.h index 0f4966e06b9..cec02e7ca2a 100644 --- a/src/crypto/key_pair.h +++ b/src/crypto/key_pair.h @@ -55,7 +55,9 @@ namespace crypto virtual Pem sign_csr( const Pem& issuer_cert, const Pem& signing_request, - bool ca = false) const = 0; + bool ca = false, + const std::optional& valid_from = std::nullopt, + const std::optional& valid_to = std::nullopt) const = 0; Pem self_sign( const std::string& name, diff --git a/src/crypto/mbedtls/key_pair.cpp b/src/crypto/mbedtls/key_pair.cpp index c1e6257f4a9..5bf3553196a 100644 --- a/src/crypto/mbedtls/key_pair.cpp +++ b/src/crypto/mbedtls/key_pair.cpp @@ -273,7 +273,11 @@ namespace crypto } Pem KeyPair_mbedTLS::sign_csr( - const Pem& issuer_cert, const Pem& signing_request, bool ca) const + const Pem& issuer_cert, + const Pem& signing_request, + bool ca, + const std::optional& valid_from, + const std::optional& valid_to) const { auto entropy = create_entropy(); auto csr = mbedtls::make_unique(); diff --git a/src/crypto/mbedtls/key_pair.h b/src/crypto/mbedtls/key_pair.h index 8574832306a..84393018d3e 100644 --- a/src/crypto/mbedtls/key_pair.h +++ b/src/crypto/mbedtls/key_pair.h @@ -55,6 +55,8 @@ namespace crypto virtual Pem sign_csr( const Pem& issuer_cert, const Pem& signing_request, - bool ca = false) const override; + bool ca = false, + const std::optional& valid_from = std::nullopt, + const std::optional& valid_to = std::nullopt) const override; }; } diff --git a/src/crypto/openssl/key_pair.cpp b/src/crypto/openssl/key_pair.cpp index 390e5414cfd..202596a1e63 100644 --- a/src/crypto/openssl/key_pair.cpp +++ b/src/crypto/openssl/key_pair.cpp @@ -215,7 +215,11 @@ namespace crypto } Pem KeyPair_OpenSSL::sign_csr( - const Pem& issuer_cert, const Pem& signing_request, bool ca) const + const Pem& issuer_cert, + const Pem& signing_request, + bool ca, + const std::optional& valid_from, + const std::optional& valid_to) const { X509* icrt = NULL; Unique_BIO mem(signing_request); @@ -259,8 +263,25 @@ namespace crypto ASN1_TIME *before = NULL, *after = NULL; OpenSSL::CHECKNULL(before = ASN1_TIME_new()); OpenSSL::CHECKNULL(after = ASN1_TIME_new()); - OpenSSL::CHECK1(ASN1_TIME_set_string(before, "20210311000000Z")); - OpenSSL::CHECK1(ASN1_TIME_set_string(after, "20230611235959Z")); + + // TODO: Fix! + if (valid_from.has_value()) + { + OpenSSL::CHECK1(ASN1_TIME_set_string(before, valid_from->c_str())); + } + else + { + OpenSSL::CHECK1(ASN1_TIME_set_string(before, "20210311000000Z")); + } + + if (valid_to.has_value()) + { + OpenSSL::CHECK1(ASN1_TIME_set_string(after, valid_to->c_str())); + } + else + { + OpenSSL::CHECK1(ASN1_TIME_set_string(after, "20230611235959Z")); + } OpenSSL::CHECK1(ASN1_TIME_normalize(before)); OpenSSL::CHECK1(ASN1_TIME_normalize(after)); OpenSSL::CHECK1(X509_set1_notBefore(crt, before)); diff --git a/src/crypto/openssl/key_pair.h b/src/crypto/openssl/key_pair.h index 3f11294d8db..c976c9d0590 100644 --- a/src/crypto/openssl/key_pair.h +++ b/src/crypto/openssl/key_pair.h @@ -56,6 +56,8 @@ namespace crypto virtual Pem sign_csr( const Pem& issuer_cert, const Pem& signing_request, - bool ca = false) const override; + bool ca = false, + const std::optional& valid_from = std::nullopt, + const std::optional& valid_to = std::nullopt) const override; }; } diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index a4225310021..961f33ee288 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -495,9 +495,9 @@ namespace js int argc, [[maybe_unused]] JSValueConst* argv) { - if (argc != 1) + if (argc != 3) { - return JS_ThrowTypeError(ctx, "Passed %d arguments but expected 1", argc); + return JS_ThrowTypeError(ctx, "Passed %d arguments but expected 3", argc); } auto network = @@ -531,8 +531,28 @@ namespace js auto csr = crypto::Pem(csr_cstr); JS_FreeCString(ctx, csr_cstr); + auto valid_from_cstr = JS_ToCString(ctx, argv[1]); + if (valid_from_cstr == nullptr) + { + throw JS_ThrowTypeError(ctx, "valid from argument is not a string"); + } + auto valid_from = std::string(valid_from_cstr); + JS_FreeCString(ctx, valid_from_cstr); + + auto valid_to_cstr = JS_ToCString(ctx, argv[2]); + if (valid_to_cstr == nullptr) + { + throw JS_ThrowTypeError(ctx, "valid to argument is not a string"); + } + auto valid_to = std::string(valid_to_cstr); + JS_FreeCString(ctx, valid_to_cstr); + auto endorsed_cert = node->generate_endorsed_certificate( - csr, network->identity->priv_key, network->identity->cert); + csr, + network->identity->priv_key, + network->identity->cert, + valid_from, + valid_to); return JS_NewString(ctx, endorsed_cert.str().c_str()); } diff --git a/src/node/node_state.h b/src/node/node_state.h index fd033bf3f7f..cd30a40d0bd 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1588,10 +1588,13 @@ namespace ccf crypto::Pem generate_endorsed_certificate( const crypto::Pem& subject_csr, const crypto::Pem& endorser_private_key, - const crypto::Pem& endorser_cert) override + const crypto::Pem& endorser_cert, + const std::optional& valid_from = std::nullopt, + const std::optional& valid_to = + std::nullopt) override // TODO: Create date format type { return crypto::make_key_pair(endorser_private_key) - ->sign_csr(endorser_cert, subject_csr); + ->sign_csr(endorser_cert, subject_csr, false, valid_from, valid_to); } void accept_node_tls_connections() diff --git a/src/node/rpc/node_interface.h b/src/node/rpc/node_interface.h index 777fd715899..c05992e1a62 100644 --- a/src/node/rpc/node_interface.h +++ b/src/node/rpc/node_interface.h @@ -53,6 +53,8 @@ namespace ccf virtual crypto::Pem generate_endorsed_certificate( const crypto::Pem& subject_csr, const crypto::Pem& endorser_private_key, - const crypto::Pem& endorser_cert) = 0; + const crypto::Pem& endorser_cert, + const std::optional& valid_from = std::nullopt, + const std::optional& valid_to = std::nullopt) = 0; }; } \ No newline at end of file diff --git a/src/node/rpc/test/node_stub.h b/src/node/rpc/test/node_stub.h index 9a176f838ee..3840cdb10b4 100644 --- a/src/node/rpc/test/node_stub.h +++ b/src/node/rpc/test/node_stub.h @@ -125,7 +125,9 @@ namespace ccf crypto::Pem generate_endorsed_certificate( const crypto::Pem& subject_csr, const crypto::Pem& endorser_private_key, - const crypto::Pem& endorser_cert) override + const crypto::Pem& endorser_cert, + std::optional& valid_from = std::nullopt, + std::optional& valid_to = std::nullopt) override { throw std::logic_error("Unimplemented"); } diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 65721c57389..34fb2ffc1c3 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -867,7 +867,9 @@ const actions = new Map([ // Note: CSR is only present from 2.x const endorsed_node_cert = ccf.network.generateEndorsedCertificate( - nodeInfo.certificate_signing_request + nodeInfo.certificate_signing_request, + args.valid_from, + args.valid_to ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), diff --git a/tests/governance.py b/tests/governance.py index adbc2a9610c..308220965e1 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -285,8 +285,11 @@ def get_node_cert_tls(node): ) == node.node_id ) - network.consortium.renew_node_certificate(node, node.node_id) + network.consortium.renew_node_certificate( + node, node.node_id, valid_from="fdsfsd", valid_to="fdsfsd" + ) node_cert_tls_after = get_node_cert_tls(node) + LOG.error(node_cert_tls_after) assert ( node_cert_tls_before != node_cert_tls_after ), "Node TLS certificate should be updated after renewal" @@ -301,7 +304,7 @@ def get_node_cert_tls(node): == node.node_id ) - # TODO: Validty period + # TODO: Validity period def run(args): @@ -310,16 +313,16 @@ def run(args): ) as network: network.start_and_join(args) - test_create_endpoint(network, args) - test_consensus_status(network, args) - test_node_ids(network, args) - test_member_data(network, args) - test_quote(network, args) - test_user(network, args) - test_no_quote(network, args) - test_service_principals(network, args) - test_ack_state_digest_update(network, args) - test_invalid_client_signature(network, args) + # test_create_endpoint(network, args) + # test_consensus_status(network, args) + # test_node_ids(network, args) + # test_member_data(network, args) + # test_quote(network, args) + # test_user(network, args) + # test_no_quote(network, args) + # test_service_principals(network, args) + # test_ack_state_digest_update(network, args) + # test_invalid_client_signature(network, args) test_node_cert_renewal(network, args) diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 7420c108918..3d0ec6325ee 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -521,9 +521,9 @@ def retire_code(self, remote_node, code_id): proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) - def renew_node_certificate(self, remote_node, node_id): + def renew_node_certificate(self, remote_node, *args, **kwargs): proposal_body, careful_vote = self.make_proposal( - "renew_node_certificate", node_id + "renew_node_certificate", *args, **kwargs ) proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) From f5a729c54319e9a9706b6cc5b4d9990abe1790c5 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 24 Aug 2021 15:37:36 +0000 Subject: [PATCH 055/105] Specify validity period in proposal --- src/crypto/openssl/key_pair.cpp | 27 +------------- src/crypto/openssl/openssl_wrappers.h | 54 +++++++++++++++++++-------- src/runtime_config/default/actions.js | 1 + tests/governance.py | 5 ++- 4 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/crypto/openssl/key_pair.cpp b/src/crypto/openssl/key_pair.cpp index 202596a1e63..1f5bb8ae0ae 100644 --- a/src/crypto/openssl/key_pair.cpp +++ b/src/crypto/openssl/key_pair.cpp @@ -260,34 +260,11 @@ namespace crypto // Note: 825-day validity range // https://support.apple.com/en-us/HT210176 - ASN1_TIME *before = NULL, *after = NULL; - OpenSSL::CHECKNULL(before = ASN1_TIME_new()); - OpenSSL::CHECKNULL(after = ASN1_TIME_new()); + Unique_ASN1_TIME before(valid_from.value_or("20210311000000Z")); + Unique_ASN1_TIME after(valid_to.value_or("20230611235959Z")); - // TODO: Fix! - if (valid_from.has_value()) - { - OpenSSL::CHECK1(ASN1_TIME_set_string(before, valid_from->c_str())); - } - else - { - OpenSSL::CHECK1(ASN1_TIME_set_string(before, "20210311000000Z")); - } - - if (valid_to.has_value()) - { - OpenSSL::CHECK1(ASN1_TIME_set_string(after, valid_to->c_str())); - } - else - { - OpenSSL::CHECK1(ASN1_TIME_set_string(after, "20230611235959Z")); - } - OpenSSL::CHECK1(ASN1_TIME_normalize(before)); - OpenSSL::CHECK1(ASN1_TIME_normalize(after)); OpenSSL::CHECK1(X509_set1_notBefore(crt, before)); OpenSSL::CHECK1(X509_set1_notAfter(crt, after)); - ASN1_TIME_free(before); - ASN1_TIME_free(after); X509_set_subject_name(crt, X509_REQ_get_subject_name(csr)); X509_set_pubkey(crt, req_pubkey); diff --git a/src/crypto/openssl/openssl_wrappers.h b/src/crypto/openssl/openssl_wrappers.h index 2c6def7ce65..f58157b8450 100644 --- a/src/crypto/openssl/openssl_wrappers.h +++ b/src/crypto/openssl/openssl_wrappers.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -53,22 +54,22 @@ namespace crypto public: Unique_BIO() : p(BIO_new(BIO_s_mem()), [](auto x) { BIO_free(x); }) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } Unique_BIO(const void* buf, int len) : p(BIO_new_mem_buf(buf, len), [](auto x) { BIO_free(x); }) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } Unique_BIO(const std::vector& d) : p(BIO_new_mem_buf(d.data(), d.size()), [](auto x) { BIO_free(x); }) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } Unique_BIO(const Pem& pem) : p(BIO_new_mem_buf(pem.data(), -1), [](auto x) { BIO_free(x); }) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } operator BIO*() { @@ -84,12 +85,12 @@ namespace crypto Unique_EVP_PKEY_CTX(EVP_PKEY* key) : p(EVP_PKEY_CTX_new(key, NULL), EVP_PKEY_CTX_free) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } Unique_EVP_PKEY_CTX() : p(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL), EVP_PKEY_CTX_free) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } operator EVP_PKEY_CTX*() { @@ -104,12 +105,12 @@ namespace crypto public: Unique_X509_REQ() : p(X509_REQ_new(), X509_REQ_free) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } Unique_X509_REQ(BIO* mem) : p(PEM_read_bio_X509_REQ(mem, NULL, NULL, NULL), X509_REQ_free) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } operator X509_REQ*() { @@ -124,7 +125,7 @@ namespace crypto public: Unique_X509() : p(X509_new(), X509_free) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } Unique_X509(BIO* mem, bool pem) : p(pem ? PEM_read_bio_X509(mem, NULL, NULL, NULL) : @@ -146,7 +147,7 @@ namespace crypto public: Unique_X509_STORE() : p(X509_STORE_new(), X509_STORE_free) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } operator X509_STORE*() { @@ -161,7 +162,7 @@ namespace crypto public: Unique_X509_STORE_CTX() : p(X509_STORE_CTX_new(), X509_STORE_CTX_free) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } operator X509_STORE_CTX*() { @@ -176,7 +177,7 @@ namespace crypto public: Unique_EVP_CIPHER_CTX() : p(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } operator EVP_CIPHER_CTX*() { @@ -192,7 +193,7 @@ namespace crypto Unique_STACK_OF_X509() : p(sk_X509_new_null(), [](auto x) { sk_X509_pop_free(x, X509_free); }) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } operator STACK_OF(X509) * () { @@ -212,7 +213,7 @@ namespace crypto p(sk_X509_EXTENSION_new_null(), [](auto x) { sk_X509_EXTENSION_pop_free(x, X509_EXTENSION_free); }) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } Unique_STACK_OF_X509_EXTENSIONS(STACK_OF(X509_EXTENSION) * exts) : @@ -233,7 +234,7 @@ namespace crypto public: Unique_ECDSA_SIG() : p(ECDSA_SIG_new(), ECDSA_SIG_free) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } operator ECDSA_SIG*() { @@ -248,7 +249,7 @@ namespace crypto public: Unique_BIGNUM() : p(BN_new(), BN_free) { - OpenSSL::CHECKNULL(p.get()); + CHECKNULL(p.get()); } operator BIGNUM*() { @@ -260,6 +261,27 @@ namespace crypto } }; + class Unique_ASN1_TIME + { + std::unique_ptr p; + + public: + Unique_ASN1_TIME() : p(ASN1_TIME_new(), ASN1_TIME_free) + { + CHECKNULL(p.get()); + } + Unique_ASN1_TIME(const std::string& s) : + p(ASN1_TIME_new(), ASN1_TIME_free) + { + CHECK1(ASN1_TIME_set_string(*this, s.c_str())); + CHECK1(ASN1_TIME_normalize(*this)); + } + operator ASN1_TIME*() + { + return p.get(); + } + }; + inline std::string error_string(int ec) { return ERR_error_string((unsigned long)ec, NULL); diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 34fb2ffc1c3..f74d49a7c66 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -845,6 +845,7 @@ const actions = new Map([ new Action( function (args) { checkEntityId(args.node_id, "node_id"); + // TODO: Check format of valid_from and valid_to }, function (args) { const node = ccf.kv["public:ccf.gov.nodes.info"].get( diff --git a/tests/governance.py b/tests/governance.py index 308220965e1..6d9dd70a115 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -286,7 +286,10 @@ def get_node_cert_tls(node): == node.node_id ) network.consortium.renew_node_certificate( - node, node.node_id, valid_from="fdsfsd", valid_to="fdsfsd" + node, + node.node_id, + valid_from="210311000000Z", + valid_to="220311000000Z", ) node_cert_tls_after = get_node_cert_tls(node) LOG.error(node_cert_tls_after) From c74634122edb4db6e90e4a88927624265527a4bb Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 25 Aug 2021 13:51:48 +0000 Subject: [PATCH 056/105] Check for time validity --- src/crypto/openssl/openssl_wrappers.h | 3 +- src/crypto/openssl/x509_time.h | 52 +++++++++++++++++++++++++++ src/crypto/test/crypto.cpp | 48 +++++++++++++++++++++++++ src/js/crypto.cpp | 50 ++++++++++++++++++++++++++ src/js/wrap.cpp | 16 +++++---- src/node/config.h | 41 ++++++++++++++++++--- src/node/node_state.h | 8 +++-- src/node/rpc/node_frontend.h | 7 ++-- src/runtime_config/default/actions.js | 35 ++++++++++++++++-- tests/governance.py | 2 -- 10 files changed, 239 insertions(+), 23 deletions(-) create mode 100644 src/crypto/openssl/x509_time.h diff --git a/src/crypto/openssl/openssl_wrappers.h b/src/crypto/openssl/openssl_wrappers.h index f58157b8450..8996eecbf41 100644 --- a/src/crypto/openssl/openssl_wrappers.h +++ b/src/crypto/openssl/openssl_wrappers.h @@ -276,7 +276,8 @@ namespace crypto CHECK1(ASN1_TIME_set_string(*this, s.c_str())); CHECK1(ASN1_TIME_normalize(*this)); } - operator ASN1_TIME*() + Unique_ASN1_TIME(ASN1_TIME* t) : p(t, ASN1_TIME_free) {} + operator ASN1_TIME*() const { return p.get(); } diff --git a/src/crypto/openssl/x509_time.h b/src/crypto/openssl/x509_time.h new file mode 100644 index 00000000000..0ac824fdffe --- /dev/null +++ b/src/crypto/openssl/x509_time.h @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "openssl_wrappers.h" + +#include + +namespace crypto +{ + namespace OpenSSL + { + /** Validates that @time_before is not after @time_after. + * If @allowed_diff_days is set, the time difference (in days) between + * @time_before and @time_after should be less than or equal to its value. + * + * @param time_before The time to check. + * @param time_after The time to check against. + * @param allowed_diff_days The maximum allowed difference in days + * (optional). + * + * @return True if @time_before is not after @time_after @allowed_diff_days. + */ + static inline bool validate_chronological_times( + const Unique_ASN1_TIME& time_before, + const Unique_ASN1_TIME& time_after, + const std::optional& allowed_diff_days = std::nullopt) + { + int diff_days = 0; + int diff_secs = 0; + CHECK1(ASN1_TIME_diff(&diff_days, &diff_secs, time_before, time_after)); + + return diff_days > 0 && + (!allowed_diff_days.has_value() || + (unsigned int)diff_days <= allowed_diff_days.value()); + } + + static inline Unique_ASN1_TIME from_time_t(const time_t& t) + { + return Unique_ASN1_TIME(ASN1_TIME_set(nullptr, t)); + } + + static inline time_t to_time_t(const Unique_ASN1_TIME& time) + { + tm tm_time; + CHECK1(ASN1_TIME_to_tm(time, &tm_time)); + return std::mktime(&tm_time); + } + + } + +} \ No newline at end of file diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index b9229830673..0d5513f7421 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -14,6 +14,7 @@ #include "crypto/openssl/rsa_key_pair.h" #include "crypto/openssl/symmetric_key.h" #include "crypto/openssl/verifier.h" +#include "crypto/openssl/x509_time.h" #include "crypto/rsa_key_pair.h" #include "crypto/symmetric_key.h" #include "crypto/verifier.h" @@ -21,7 +22,9 @@ #include #include +#include #include +#include using namespace std; using namespace tls; @@ -629,4 +632,49 @@ TEST_CASE("AES-GCM convenience functions") auto encrypted = aes_gcm_encrypt(key, contents); auto decrypted = aes_gcm_decrypt(key, encrypted); REQUIRE(decrypted == contents); +} + +TEST_CASE("ASN1 time") +{ + struct TimeTest + { + struct Input + { + std::tm from; + std::tm to; + std::optional maximum_validity_period_days = std::nullopt; + }; + Input input; + + bool expected_verification_result; + }; + + auto current_time_t = + std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + auto time = *std::localtime(¤t_time_t); + + auto next_day_time = time; + next_day_time.tm_mday++; + auto next_year_time = time; + next_year_time.tm_year++; + + std::vector test_vector{ + {{time, next_day_time}, true}, // Valid: Next day + {{time, time}, false}, // Invalid: Same date + {{next_day_time, time}, false}, // Invalid: to is before from + {{time, next_day_time, 100}, true}, // Valid: Next day within 100 days + {{time, next_year_time, 100}, false}, // Valid: Next day not within 100 days + }; + + for (auto& data : test_vector) + { + auto* from = &data.input.from; + auto* to = &data.input.to; + REQUIRE( + crypto::OpenSSL::validate_chronological_times( + crypto::OpenSSL::from_time_t(std::mktime(from)), + crypto::OpenSSL::from_time_t(std::mktime(to)), + data.input.maximum_validity_period_days) == + data.expected_verification_result); + } } \ No newline at end of file diff --git a/src/js/crypto.cpp b/src/js/crypto.cpp index bd3a0bb1063..c2b01bf0fed 100644 --- a/src/js/crypto.cpp +++ b/src/js/crypto.cpp @@ -483,6 +483,56 @@ namespace js } } + JSValue js_validate_certificate_validity_period( + JSContext* ctx, + JSValueConst this_val, + int argc, + [[maybe_unused]] JSValueConst* argv) + { + if (argc < 2 || argc > 3) + { + return JS_ThrowTypeError( + ctx, + "Passed %d arguments but expected at least 2 and less than 3", + argc); + } + + auto valid_from_cstr = JS_ToCString(ctx, argv[0]); + if (valid_from_cstr == nullptr) + { + throw JS_ThrowTypeError(ctx, "valid_from argument is not a string"); + } + auto valid_from = std::string(valid_from_cstr); + JS_FreeCString(ctx, valid_from_cstr); + + auto valid_to_cstr = JS_ToCString(ctx, argv[1]); + if (valid_to_cstr == nullptr) + { + throw JS_ThrowTypeError(ctx, "valid_to argument is not a string"); + } + auto valid_to = std::string(valid_to_cstr); + JS_FreeCString(ctx, valid_to_cstr); + + LOG_FAIL_FMT("argc: {}", argc); + + std::optional allowed_validity_period_days = std::nullopt; + if (argc > 2 && !JS_IsUndefined(argv[2])) + { + uint32_t allowed_validity_period_days_ = 0; + if (JS_ToUint32(ctx, &allowed_validity_period_days_, argv[2]) < 0) + { + js::js_dump_error(ctx); + return JS_EXCEPTION; + } + allowed_validity_period_days = allowed_validity_period_days_; + } + + return JS_NewBool( + ctx, + crypto::OpenSSL::validate_chronological_times( + valid_from, valid_to, allowed_validity_period_days)); + } + #pragma clang diagnostic pop } \ No newline at end of file diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index 961f33ee288..e9983921803 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -4,6 +4,7 @@ #include "ccf/tx_id.h" #include "ccf/version.h" +#include "crypto/openssl/x509_time.h" #include "ds/logger.h" #include "enclave/rpc_context.h" #include "js/conv.cpp" @@ -1302,16 +1303,19 @@ namespace js "refreshAppBytecodeCache", JS_NewCFunction( ctx, js_refresh_app_bytecode_cache, "refreshAppBytecodeCache", 0)); + JS_SetPropertyStr( + ctx, + ccf, + "validateCertificateValidityPeriod", + JS_NewCFunction( + ctx, + js_validate_certificate_validity_period, + "validateCertificateValidityPeriod", + 0)); auto crypto = JS_NewObject(ctx); JS_SetPropertyStr(ctx, ccf, "crypto", crypto); - JS_SetPropertyStr( - ctx, - crypto, - "verifySignature", - JS_NewCFunction(ctx, js_verify_signature, "verifySignature", 4)); - if (txctx != nullptr) { auto kv = JS_NewObjectClass(ctx, kv_class_id); diff --git a/src/node/config.h b/src/node/config.h index 6753b26978b..3a39e1bb178 100644 --- a/src/node/config.h +++ b/src/node/config.h @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 License. #pragma once +#include "crypto/openssl/x509_time.h" #include "ds/json.h" #include "enclave/consensus_type.h" #include "enclave/reconfiguration_type.h" @@ -9,6 +10,8 @@ namespace ccf { + static constexpr auto default_node_certificate_validity_period_days = 365; + struct ServiceConfiguration { // Number of recovery shares required to decrypt the latest ledger secret @@ -18,22 +21,50 @@ namespace ccf std::optional reconfiguration_type = std::nullopt; - // If true, the service endorses the certificate of new trusted nodes, and - // records them in the store - std::optional node_endorsement_on_trust = std::nullopt; + struct Nodes + { + // If true, the service endorses the certificate of new trusted nodes, and + // records them in the store + bool node_endorsement_on_trust = true; + + size_t cert_maximum_validity_period_days = + default_node_certificate_validity_period_days; + + Nodes() {} + + bool operator==(const Nodes& other) const + { + return node_endorsement_on_trust == other.node_endorsement_on_trust && + cert_maximum_validity_period_days == + other.cert_maximum_validity_period_days; + } + + bool operator!=(const Nodes& other) const + { + return !(*this == other); + } + }; + std::optional nodes = std::nullopt; bool operator==(const ServiceConfiguration& other) const { return recovery_threshold == other.recovery_threshold && consensus == other.consensus && - reconfiguration_type == other.reconfiguration_type; + reconfiguration_type == other.reconfiguration_type && + nodes == other.nodes; } }; + DECLARE_JSON_TYPE(ServiceConfiguration::Nodes) + DECLARE_JSON_REQUIRED_FIELDS( + ServiceConfiguration::Nodes, + node_endorsement_on_trust, + cert_maximum_validity_period_days) + DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(ServiceConfiguration) DECLARE_JSON_REQUIRED_FIELDS( ServiceConfiguration, recovery_threshold, consensus) DECLARE_JSON_OPTIONAL_FIELDS( - ServiceConfiguration, reconfiguration_type, node_endorsement_on_trust) + ServiceConfiguration, reconfiguration_type, nodes) // The there is always only one active configuration, so this is a single // Value diff --git a/src/node/node_state.h b/src/node/node_state.h index cd30a40d0bd..fb1054d9077 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1663,8 +1663,12 @@ namespace ccf genesis_info.configuration = { config.genesis.recovery_threshold, network.consensus_type, - reconf_type, - node_endorsement_on_trust}; + reconf_type}; + + ServiceConfiguration::Nodes nodes; + nodes.node_endorsement_on_trust = node_endorsement_on_trust; + + genesis_info.configuration.nodes = nodes; create_params.genesis_info = genesis_info; } diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 8b59f40bead..3578f00f1e2 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -221,8 +221,7 @@ namespace ccf // Only record self-signed node certificate if the node does not require // endorsement by the service when it is trusted if ( - config->node_endorsement_on_trust.has_value() && - !config->node_endorsement_on_trust.value()) + config->nodes.has_value() && !config->nodes->node_endorsement_on_trust) { node_info.cert = crypto::cert_der_to_pem(node_der); } @@ -248,8 +247,8 @@ namespace ccf std::optional endorsed_certificate = std::nullopt; if ( in.certificate_signing_request.has_value() && - (!config->node_endorsement_on_trust.has_value() || - config->node_endorsement_on_trust.value())) + (!config->nodes.has_value() || + config->nodes->node_endorsement_on_trust)) { endorsed_certificate = context.get_node_state().generate_endorsed_certificate( diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index f74d49a7c66..88fac69dbb8 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -137,6 +137,14 @@ function checkX509CertBundle(value, field) { } } +function validateCertificateValidityPeriod(from, to, from_field, to_field) { + checkType(from, "string", from_field); + checkType(to, "string", to_field); + if (!ccf.validateCertificateValidityPeriod(from, to)) { + throw new Error(`Date ${from_field} must be before date ${to_field}`); + } +} + function invalidateOtherOpenProposals(proposalIdToRetain) { let proposals = ccf.kv["public:ccf.gov.proposals_info"]; const proposalsMap = ccf.kv["public:ccf.gov.proposals_info"]; @@ -845,7 +853,12 @@ const actions = new Map([ new Action( function (args) { checkEntityId(args.node_id, "node_id"); - // TODO: Check format of valid_from and valid_to + validateCertificateValidityPeriod( + args.valid_from, + args.valid_to, + "valid_from", + "valid_to" + ); }, function (args) { const node = ccf.kv["public:ccf.gov.nodes.info"].get( @@ -859,14 +872,30 @@ const actions = new Map([ throw new Error(`Node ${args.node_id} is not trusted`); } - // Note: CSR is only present from 2.x if (nodeInfo.certificate_signing_request === undefined) { throw new Error( `Node ${args.node_id} has no certificate signing request` ); } - // Note: CSR is only present from 2.x + const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( + getSingletonKvKey() + ); + if (rawConfig === undefined) { + throw new Error("Service configuration could not be found"); + } + const serviceConfig = ccf.bufToJsonCompatible(rawConfig); + + if ( + !ccf.validateCertificateValidityPeriod( + args.valid_from, + args.valid_to, + serviceConfig.nodes.cert_maximum_validity_period_days + ) + ) { + throw new Error(`Date valid_from must be before date valid_to`); + } + const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, args.valid_from, diff --git a/tests/governance.py b/tests/governance.py index 6d9dd70a115..79f257d0722 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -307,8 +307,6 @@ def get_node_cert_tls(node): == node.node_id ) - # TODO: Validity period - def run(args): with infra.network.network( From 5ca652b81da8a63940361cdef675b164d7b457e6 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 25 Aug 2021 15:45:47 +0000 Subject: [PATCH 057/105] Better Python test --- src/js/crypto.cpp | 2 -- tests/governance.py | 22 ++++++++++++---------- tests/infra/crypto.py | 6 ++++++ tests/requirements.txt | 4 ++-- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/js/crypto.cpp b/src/js/crypto.cpp index c2b01bf0fed..37ea228abbc 100644 --- a/src/js/crypto.cpp +++ b/src/js/crypto.cpp @@ -513,8 +513,6 @@ namespace js auto valid_to = std::string(valid_to_cstr); JS_FreeCString(ctx, valid_to_cstr); - LOG_FAIL_FMT("argc: {}", argc); - std::optional allowed_validity_period_days = std::nullopt; if (argc > 2 && !JS_IsUndefined(argv[2])) { diff --git a/tests/governance.py b/tests/governance.py index 79f257d0722..1b620c04421 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -14,6 +14,7 @@ import json import requests import infra.crypto +from datetime import datetime, timedelta from loguru import logger as LOG @@ -268,31 +269,32 @@ def post_proposal_request_raw(node, headers=None, expected_error_msg=None): @reqs.description("Update certificates of all nodes") def test_node_cert_renewal(network, args): - def get_node_cert_tls(node): - import ssl - - return ssl.get_server_certificate((node.host, node.rpc_port)) for node in network.get_joined_nodes(): - with node.client() as c: c.get("/node/network/nodes") - node_cert_tls_before = get_node_cert_tls(node) + node_cert_tls_before = node.get_tls_certificate_pem() assert ( infra.crypto.compute_public_key_der_hash_hex_from_pem( node_cert_tls_before ) == node.node_id ) + now = datetime.now() + six_months_from_now = now + timedelta(days=180) + utc_now = infra.crypto.datetime_as_UTCtime(now) + utc_six_months_from_now_utc = infra.crypto.datetime_as_UTCtime( + six_months_from_now + ) + network.consortium.renew_node_certificate( node, node.node_id, - valid_from="210311000000Z", - valid_to="220311000000Z", + valid_from=str(utc_now), + valid_to=str(utc_six_months_from_now_utc), ) - node_cert_tls_after = get_node_cert_tls(node) - LOG.error(node_cert_tls_after) + node_cert_tls_after = node.get_tls_certificate_pem() assert ( node_cert_tls_before != node_cert_tls_after ), "Node TLS certificate should be updated after renewal" diff --git a/tests/infra/crypto.py b/tests/infra/crypto.py index 5dc088ce6c6..5a02ad36462 100644 --- a/tests/infra/crypto.py +++ b/tests/infra/crypto.py @@ -7,6 +7,8 @@ import secrets import datetime import hashlib +from datetime import datetime +from pyasn1.type.useful import UTCTime from cryptography import x509 from cryptography.x509.oid import NameOID @@ -271,3 +273,7 @@ def check_key_pair_pem(private: str, public: str, password=None) -> bool: ) pub_der = pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) return prv_pub_der == pub_der + +def datetime_as_UTCtime(datetime): + return UTCTime.fromDateTime(datetime) + diff --git a/tests/requirements.txt b/tests/requirements.txt index c71b2b53965..724f95ac332 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -3,10 +3,10 @@ paramiko loguru psutil cimetrics>=0.2.1 -pynacl openapi-spec-validator PyJWT docutils python-iptables py-spy -GitPython \ No newline at end of file +GitPython +pyasn1 \ No newline at end of file From d5dda14854374708f0460ee8dc39dda466bf883b Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 25 Aug 2021 16:05:38 +0000 Subject: [PATCH 058/105] Check that validity period is reflected in node certificate --- tests/governance.py | 20 ++++++++++++++------ tests/infra/crypto.py | 6 +++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/governance.py b/tests/governance.py index 1b620c04421..2987cae3164 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -281,10 +281,12 @@ def test_node_cert_renewal(network, args): ) == node.node_id ) - now = datetime.now() + now = datetime.now().replace( + microsecond=0 + ) # Truncate microseconds which are not reflected in RFC5280 UTCTime six_months_from_now = now + timedelta(days=180) utc_now = infra.crypto.datetime_as_UTCtime(now) - utc_six_months_from_now_utc = infra.crypto.datetime_as_UTCtime( + utc_six_months_from_now = infra.crypto.datetime_as_UTCtime( six_months_from_now ) @@ -292,15 +294,18 @@ def test_node_cert_renewal(network, args): node, node.node_id, valid_from=str(utc_now), - valid_to=str(utc_six_months_from_now_utc), + valid_to=str(utc_six_months_from_now), ) + node_cert_tls_after = node.get_tls_certificate_pem() assert ( node_cert_tls_before != node_cert_tls_after ), "Node TLS certificate should be updated after renewal" - - # Long-connected client is still connected after certificate renewal - c.get("/node/network/nodes") + valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( + node_cert_tls_after + ) + assert valid_from == now + assert valid_to == six_months_from_now assert ( infra.crypto.compute_public_key_der_hash_hex_from_pem( @@ -309,6 +314,9 @@ def test_node_cert_renewal(network, args): == node.node_id ) + # Long-connected client is still connected after certificate renewal + c.get("/node/network/nodes") + def run(args): with infra.network.network( diff --git a/tests/infra/crypto.py b/tests/infra/crypto.py index 5a02ad36462..641a935c3b8 100644 --- a/tests/infra/crypto.py +++ b/tests/infra/crypto.py @@ -274,6 +274,10 @@ def check_key_pair_pem(private: str, public: str, password=None) -> bool: pub_der = pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) return prv_pub_der == pub_der -def datetime_as_UTCtime(datetime): +def get_validity_period_from_pem_cert(pem:str): + cert = load_pem_x509_certificate(pem.encode(), default_backend()) + return cert.not_valid_before, cert.not_valid_after + +def datetime_as_UTCtime(datetime:datetime): return UTCTime.fromDateTime(datetime) From cb8570dd0298807ad88e14b2464e6f1b3559f0cc Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 25 Aug 2021 16:25:24 +0000 Subject: [PATCH 059/105] Pass maximum validity period from CLI --- src/enclave/interface.h | 7 ++++++- src/host/main.cpp | 11 +++++++++++ src/node/node_state.h | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/enclave/interface.h b/src/enclave/interface.h index 7898643a44e..2da8aedb9bf 100644 --- a/src/enclave/interface.h +++ b/src/enclave/interface.h @@ -63,6 +63,7 @@ struct CCFConfig std::vector members_info; std::string constitution; size_t recovery_threshold; + size_t node_cert_maximum_validity_period_days; }; Genesis genesis = {}; @@ -88,7 +89,11 @@ DECLARE_JSON_REQUIRED_FIELDS( DECLARE_JSON_TYPE(CCFConfig::Genesis); DECLARE_JSON_REQUIRED_FIELDS( - CCFConfig::Genesis, members_info, constitution, recovery_threshold); + CCFConfig::Genesis, + members_info, + constitution, + recovery_threshold, + node_cert_maximum_validity_period_days); DECLARE_JSON_TYPE(CCFConfig::Joining); DECLARE_JSON_REQUIRED_FIELDS( diff --git a/src/host/main.cpp b/src/host/main.cpp index 63d69b6b86b..c9f919f051a 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -446,6 +446,15 @@ int main(int argc, char** argv) ->check(CLI::PositiveNumber) ->type_name("UINT"); + size_t node_cert_maximum_validity_period_days = 365; + start + ->add_option( + "--node-cert-max-validity-days", + node_cert_maximum_validity_period_days, + "Maximum number of days node certificates should be valid for.") + ->check(CLI::PositiveNumber) + ->type_name("UINT"); + auto join = app.add_subcommand("join", "Join existing network"); join->configurable(); @@ -821,6 +830,8 @@ int main(int argc, char** argv) files::slurp_string(constitution_path); } ccf_config.genesis.recovery_threshold = recovery_threshold.value(); + ccf_config.genesis.node_cert_maximum_validity_period_days = + node_cert_maximum_validity_period_days; LOG_INFO_FMT( "Creating new node: new network (with {} initial member(s) and {} " "member(s) required for recovery)", diff --git a/src/node/node_state.h b/src/node/node_state.h index 851596d9c4e..8ab5f195843 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1667,6 +1667,8 @@ namespace ccf ServiceConfiguration::Nodes nodes; nodes.node_endorsement_on_trust = node_endorsement_on_trust; + nodes.cert_maximum_validity_period_days = + config.genesis.node_cert_maximum_validity_period_days; genesis_info.configuration.nodes = nodes; create_params.genesis_info = genesis_info; From c619772cc230149d5c8efae67710c6244a136bd8 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 26 Aug 2021 10:22:02 +0000 Subject: [PATCH 060/105] Update comment --- src/crypto/openssl/x509_time.h | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/crypto/openssl/x509_time.h b/src/crypto/openssl/x509_time.h index 0ac824fdffe..2be16c256ca 100644 --- a/src/crypto/openssl/x509_time.h +++ b/src/crypto/openssl/x509_time.h @@ -10,16 +10,17 @@ namespace crypto { namespace OpenSSL { - /** Validates that @time_before is not after @time_after. - * If @allowed_diff_days is set, the time difference (in days) between - * @time_before and @time_after should be less than or equal to its value. + /** Checks that two times are in chronological order, and optionally within + * a certain time range. * - * @param time_before The time to check. - * @param time_after The time to check against. + * @param time_before The time to check + * @param time_after The time to check against, which should be later than + * \p time_before * @param allowed_diff_days The maximum allowed difference in days - * (optional). + * (optional) * - * @return True if @time_before is not after @time_after @allowed_diff_days. + * @return True if \p time_before is chronologically before \p time_after, + * and within \p allowed_diff_days days. */ static inline bool validate_chronological_times( const Unique_ASN1_TIME& time_before, From 407873c8efb2abc010607be4ba2c645af3eec491 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 26 Aug 2021 10:57:08 +0000 Subject: [PATCH 061/105] More comprehensive e2e testing --- src/crypto/openssl/x509_time.h | 3 ++ src/host/main.cpp | 2 +- tests/governance.py | 99 +++++++++++++++++++--------------- tests/infra/e2e_args.py | 5 ++ tests/infra/network.py | 1 + tests/infra/remote.py | 5 ++ 6 files changed, 72 insertions(+), 43 deletions(-) diff --git a/src/crypto/openssl/x509_time.h b/src/crypto/openssl/x509_time.h index 2be16c256ca..0ac6be3986c 100644 --- a/src/crypto/openssl/x509_time.h +++ b/src/crypto/openssl/x509_time.h @@ -10,6 +10,9 @@ namespace crypto { namespace OpenSSL { + /** Set of utilites functions for working with x509 time, as defined in RFC + 5280 (https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5.1) */ + /** Checks that two times are in chronological order, and optionally within * a certain time range. * diff --git a/src/host/main.cpp b/src/host/main.cpp index c9f919f051a..82f69928f6c 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -451,7 +451,7 @@ int main(int argc, char** argv) ->add_option( "--node-cert-max-validity-days", node_cert_maximum_validity_period_days, - "Maximum number of days node certificates should be valid for.") + "Maximum number of days node certificates must be valid for.") ->check(CLI::PositiveNumber) ->type_name("UINT"); diff --git a/tests/governance.py b/tests/governance.py index 2987cae3164..64fb04249ec 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -270,55 +270,70 @@ def post_proposal_request_raw(node, headers=None, expected_error_msg=None): @reqs.description("Update certificates of all nodes") def test_node_cert_renewal(network, args): - for node in network.get_joined_nodes(): - with node.client() as c: - c.get("/node/network/nodes") - - node_cert_tls_before = node.get_tls_certificate_pem() - assert ( - infra.crypto.compute_public_key_der_hash_hex_from_pem( - node_cert_tls_before + now = datetime.now().replace( + microsecond=0 + ) # Truncate microseconds which are not reflected in RFC5280 UTCTime + future_allowed = now + timedelta(days=args.node_cert_max_validity_days - 1) + future_forbidden = now + timedelta(days=args.node_cert_max_validity_days + 1) + + test_vector = [ + (now, future_allowed, None), + (now, future_forbidden, infra.proposal.ProposalNotAccepted), + (future_allowed, now, infra.proposal.ProposalNotCreated), + ] + + for (before_date, after_date, expected_exception) in test_vector: + for node in network.get_joined_nodes(): + with node.client() as c: + c.get("/node/network/nodes") + + node_cert_tls_before = node.get_tls_certificate_pem() + assert ( + infra.crypto.compute_public_key_der_hash_hex_from_pem( + node_cert_tls_before + ) + == node.node_id ) - == node.node_id - ) - now = datetime.now().replace( - microsecond=0 - ) # Truncate microseconds which are not reflected in RFC5280 UTCTime - six_months_from_now = now + timedelta(days=180) - utc_now = infra.crypto.datetime_as_UTCtime(now) - utc_six_months_from_now = infra.crypto.datetime_as_UTCtime( - six_months_from_now - ) - - network.consortium.renew_node_certificate( - node, - node.node_id, - valid_from=str(utc_now), - valid_to=str(utc_six_months_from_now), - ) - - node_cert_tls_after = node.get_tls_certificate_pem() - assert ( - node_cert_tls_before != node_cert_tls_after - ), "Node TLS certificate should be updated after renewal" - valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( - node_cert_tls_after - ) - assert valid_from == now - assert valid_to == six_months_from_now - assert ( - infra.crypto.compute_public_key_der_hash_hex_from_pem( - node_cert_tls_before + try: + network.consortium.renew_node_certificate( + node, + node.node_id, + valid_from=str(infra.crypto.datetime_as_UTCtime(before_date)), + valid_to=str(infra.crypto.datetime_as_UTCtime(after_date)), + ) + except Exception as e: + assert isinstance(e, expected_exception) + continue + else: + assert ( + expected_exception is None + ), "Proposal should have not succeeded" + + node_cert_tls_after = node.get_tls_certificate_pem() + assert ( + node_cert_tls_before != node_cert_tls_after + ), "Node TLS certificate should be updated after renewal" + valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( + node_cert_tls_after + ) + assert valid_from == before_date + assert valid_to == after_date + + assert ( + infra.crypto.compute_public_key_der_hash_hex_from_pem( + node_cert_tls_before + ) + == node.node_id ) - == node.node_id - ) - # Long-connected client is still connected after certificate renewal - c.get("/node/network/nodes") + # Long-connected client is still connected after certificate renewal + c.get("/node/network/nodes") def run(args): + args.node_cert_max_validity_days = 10 + with infra.network.network( args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb ) as network: diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index 262d7627d0f..9f3e81724a7 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -350,6 +350,11 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): help="TCP client connection timeout in ms", default=None, ) + parser.add_argument( + "--node-cert-max-validity-days", + help="Maximum number of days node certificates must be valid for.", + default=None, + ) add(parser) diff --git a/tests/infra/network.py b/tests/infra/network.py index 593c85e2609..f6297626d2c 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -100,6 +100,7 @@ class Network: "common_read_only_ledger_dir", "curve_id", "client_connection_timeout_ms", + "node_cert_max_validity_days", ] # Maximum delay (seconds) for updates to propagate from the primary to backups diff --git a/tests/infra/remote.py b/tests/infra/remote.py index cde156e516b..6c9e2d77011 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -586,6 +586,7 @@ def __init__( jwt_key_refresh_interval_s=None, curve_id=None, client_connection_timeout_ms=None, + node_cert_max_validity_days=None, additional_raw_node_args=None, ): """ @@ -717,6 +718,10 @@ def __init__( data_files += [ os.path.join(self.common_dir, os.path.basename(fragment)) ] + + if node_cert_max_validity_days: + cmd += [f"--node-cert-max-validity-days={node_cert_max_validity_days}"] + if members_info is None: raise ValueError( "Starting node should be given at least one member info" From ac8d484d0eedca674955136d88cba74383b81ce7 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 26 Aug 2021 13:33:47 +0000 Subject: [PATCH 062/105] . --- src/crypto/test/crypto.cpp | 4 ++-- tests/governance.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index 0d5513f7421..717593f9c4d 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -658,7 +658,7 @@ TEST_CASE("ASN1 time") auto next_year_time = time; next_year_time.tm_year++; - std::vector test_vector{ + std::vector test_vectors{ {{time, next_day_time}, true}, // Valid: Next day {{time, time}, false}, // Invalid: Same date {{next_day_time, time}, false}, // Invalid: to is before from @@ -666,7 +666,7 @@ TEST_CASE("ASN1 time") {{time, next_year_time, 100}, false}, // Valid: Next day not within 100 days }; - for (auto& data : test_vector) + for (auto& data : test_vectors) { auto* from = &data.input.from; auto* to = &data.input.to; diff --git a/tests/governance.py b/tests/governance.py index 64fb04249ec..1594c0edb47 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -276,13 +276,13 @@ def test_node_cert_renewal(network, args): future_allowed = now + timedelta(days=args.node_cert_max_validity_days - 1) future_forbidden = now + timedelta(days=args.node_cert_max_validity_days + 1) - test_vector = [ + test_vectors = [ (now, future_allowed, None), (now, future_forbidden, infra.proposal.ProposalNotAccepted), (future_allowed, now, infra.proposal.ProposalNotCreated), ] - for (before_date, after_date, expected_exception) in test_vector: + for (before_date, after_date, expected_exception) in test_vectors: for node in network.get_joined_nodes(): with node.client() as c: c.get("/node/network/nodes") From 644c0533023ba03279e98362b6eb2bf1fe736ff7 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 27 Aug 2021 10:20:13 +0000 Subject: [PATCH 063/105] Oops --- src/js/wrap.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index e42166286bb..d3cdf023659 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -1314,6 +1314,12 @@ namespace js auto crypto = JS_NewObject(ctx); JS_SetPropertyStr(ctx, ccf, "crypto", crypto); + JS_SetPropertyStr( + ctx, + crypto, + "verifySignature", + JS_NewCFunction(ctx, js_verify_signature, "verifySignature", 4)); + if (txctx != nullptr) { auto kv = JS_NewObjectClass(ctx, kv_class_id); From a9a27a2bfeee7d87d54cb8b8da97bab2b8d92b9a Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 27 Sep 2021 15:07:12 +0000 Subject: [PATCH 064/105] Minor tweeks --- src/crypto/mbedtls/key_pair.cpp | 1 + src/crypto/openssl/key_pair.cpp | 1 + src/crypto/openssl/x509_time.h | 2 - src/node/config.h | 31 ++--------- src/node/node_state.h | 8 +-- src/runtime_config/default/actions.js | 8 ++- tests/governance.py | 80 ++++++++++++++------------- tests/infra/e2e_args.py | 5 +- 8 files changed, 58 insertions(+), 78 deletions(-) diff --git a/src/crypto/mbedtls/key_pair.cpp b/src/crypto/mbedtls/key_pair.cpp index 5bf3553196a..d13b7944a12 100644 --- a/src/crypto/mbedtls/key_pair.cpp +++ b/src/crypto/mbedtls/key_pair.cpp @@ -325,6 +325,7 @@ namespace crypto // Note: 825-day validity range // https://support.apple.com/en-us/HT210176 + // TODO: Use valid_from and valid_to MCHK(mbedtls_x509write_crt_set_validity( crt.get(), "20210311000000", "20230611235959")); diff --git a/src/crypto/openssl/key_pair.cpp b/src/crypto/openssl/key_pair.cpp index 1f5bb8ae0ae..74f52f9659f 100644 --- a/src/crypto/openssl/key_pair.cpp +++ b/src/crypto/openssl/key_pair.cpp @@ -260,6 +260,7 @@ namespace crypto // Note: 825-day validity range // https://support.apple.com/en-us/HT210176 + // TODO: valid_from and valid_to should always have a value Unique_ASN1_TIME before(valid_from.value_or("20210311000000Z")); Unique_ASN1_TIME after(valid_to.value_or("20230611235959Z")); diff --git a/src/crypto/openssl/x509_time.h b/src/crypto/openssl/x509_time.h index 0ac6be3986c..2caa39641c2 100644 --- a/src/crypto/openssl/x509_time.h +++ b/src/crypto/openssl/x509_time.h @@ -50,7 +50,5 @@ namespace crypto CHECK1(ASN1_TIME_to_tm(time, &tm_time)); return std::mktime(&tm_time); } - } - } \ No newline at end of file diff --git a/src/node/config.h b/src/node/config.h index 1daa879d702..fbc1b477eae 100644 --- a/src/node/config.h +++ b/src/node/config.h @@ -21,43 +21,22 @@ namespace ccf std::optional reconfiguration_type = std::nullopt; - struct Nodes - { - size_t cert_maximum_validity_period_days = - default_node_certificate_validity_period_days; - - Nodes() {} - - bool operator==(const Nodes& other) const - { - return cert_maximum_validity_period_days == - other.cert_maximum_validity_period_days; - } - - bool operator!=(const Nodes& other) const - { - return !(*this == other); - } - }; - std::optional nodes = std::nullopt; + std::optional cert_maximum_validity_period_days = std::nullopt; bool operator==(const ServiceConfiguration& other) const { return recovery_threshold == other.recovery_threshold && consensus == other.consensus && - reconfiguration_type == other.reconfiguration_type && - nodes == other.nodes; + reconfiguration_type == other.reconfiguration_type; } }; - DECLARE_JSON_TYPE(ServiceConfiguration::Nodes) - DECLARE_JSON_REQUIRED_FIELDS( - ServiceConfiguration::Nodes, cert_maximum_validity_period_days) - DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(ServiceConfiguration) DECLARE_JSON_REQUIRED_FIELDS( ServiceConfiguration, recovery_threshold, consensus) DECLARE_JSON_OPTIONAL_FIELDS( - ServiceConfiguration, reconfiguration_type, nodes) + ServiceConfiguration, + reconfiguration_type, + cert_maximum_validity_period_days) // The there is always only one active configuration, so this is a single // Value diff --git a/src/node/node_state.h b/src/node/node_state.h index b1f8ae4385f..07d6ed236d7 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1638,13 +1638,9 @@ namespace ccf genesis_info.configuration = { config.genesis.recovery_threshold, network.consensus_type, - reconf_type}; + reconf_type, + config.genesis.node_cert_maximum_validity_period_days}; - ServiceConfiguration::Nodes nodes; - nodes.cert_maximum_validity_period_days = - config.genesis.node_cert_maximum_validity_period_days; - - genesis_info.configuration.nodes = nodes; create_params.genesis_info = genesis_info; } diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 629c09a9061..126f7677137 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -141,7 +141,7 @@ function validateCertificateValidityPeriod(from, to, from_field, to_field) { checkType(from, "string", from_field); checkType(to, "string", to_field); if (!ccf.validateCertificateValidityPeriod(from, to)) { - throw new Error(`Date ${from_field} must be before date ${to_field}`); + throw new Error(`Date ${to_field} must be after date ${from_field}`); } } @@ -959,10 +959,12 @@ const actions = new Map([ !ccf.validateCertificateValidityPeriod( args.valid_from, args.valid_to, - serviceConfig.nodes.cert_maximum_validity_period_days + serviceConfig.cert_maximum_validity_period_days ) ) { - throw new Error(`Date valid_from must be before date valid_to`); + throw new Error( + `Date valid_to ${args.valid_to} must be after date valid_from ${args.valid_from}, and within ${serviceConfig.cert_maximum_validity_period_days} days ` + ); } const endorsed_node_cert = ccf.network.generateEndorsedCertificate( diff --git a/tests/governance.py b/tests/governance.py index 5e2aa661169..c9f1c2ff3e2 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -230,8 +230,8 @@ def test_node_cert_renewal(network, args): test_vectors = [ (now, future_allowed, None), - (now, future_forbidden, infra.proposal.ProposalNotAccepted), (future_allowed, now, infra.proposal.ProposalNotCreated), + (now, future_forbidden, infra.proposal.ProposalNotAccepted), ] for (before_date, after_date, expected_exception) in test_vectors: @@ -262,6 +262,7 @@ def test_node_cert_renewal(network, args): expected_exception is None ), "Proposal should have not succeeded" + # Verify that node certificate has been renewed node_cert_tls_after = node.get_tls_certificate_pem() assert ( node_cert_tls_before != node_cert_tls_after @@ -278,28 +279,29 @@ def test_node_cert_renewal(network, args): ) == node.node_id ) + LOG.info( + f"Certificate for node {node.local_node_id} has successfully been renewed" + ) # Long-connected client is still connected after certificate renewal c.get("/node/network/nodes") def gov(args): - args.node_cert_max_validity_days = 10 - with infra.network.network( args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb ) as network: network.start_and_join(args) network.consortium.set_authenticate_session(args.authenticate_session) - test_create_endpoint(network, args) - test_consensus_status(network, args) - test_node_ids(network, args) - test_member_data(network, args) - test_quote(network, args) - test_user(network, args) - test_no_quote(network, args) - test_ack_state_digest_update(network, args) - test_invalid_client_signature(network, args) + # test_create_endpoint(network, args) + # test_consensus_status(network, args) + # test_node_ids(network, args) + # test_member_data(network, args) + # test_quote(network, args) + # test_user(network, args) + # test_no_quote(network, args) + # test_ack_state_digest_update(network, args) + # test_invalid_client_signature(network, args) test_node_cert_renewal(network, args) @@ -333,32 +335,32 @@ def js_gov(args): authenticate_session=True, ) - cr.add( - "session_noauth", - gov, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), - initial_user_count=3, - authenticate_session=False, - ) - - cr.add( - "js", - js_gov, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), - initial_user_count=3, - authenticate_session=True, - ) - - cr.add( - "history", - governance_history.run, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), - # Higher snapshot interval as snapshots trigger new ledger chunks, which - # may result in latest chunk being partially written - snapshot_tx_interval=10000, - ) + # cr.add( + # "session_noauth", + # gov, + # package="samples/apps/logging/liblogging", + # nodes=infra.e2e_args.max_nodes(cr.args, f=0), + # initial_user_count=3, + # authenticate_session=False, + # ) + + # cr.add( + # "js", + # js_gov, + # package="samples/apps/logging/liblogging", + # nodes=infra.e2e_args.max_nodes(cr.args, f=0), + # initial_user_count=3, + # authenticate_session=True, + # ) + + # cr.add( + # "history", + # governance_history.run, + # package="samples/apps/logging/liblogging", + # nodes=infra.e2e_args.max_nodes(cr.args, f=0), + # # Higher snapshot interval as snapshots trigger new ledger chunks, which + # # may result in latest chunk being partially written + # snapshot_tx_interval=10000, + # ) cr.run(2) diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index 0b5603a3ecb..26afbcea1cc 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -352,8 +352,9 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): ) parser.add_argument( "--node-cert-max-validity-days", - help="Maximum number of days node certificates must be valid for.", - default=None, + help="Maximum number of days node certificates must be valid for", + type=int, + default=365, ) add(parser) From adb3e68f63afb4b581afaf52a94f6a037a8f63d1 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 27 Sep 2021 16:29:15 +0000 Subject: [PATCH 065/105] Add ability to adjust time by a number of days --- src/crypto/key_pair.h | 2 + src/crypto/openssl/x509_time.h | 9 +++ src/crypto/test/crypto.cpp | 83 +++++++++++++++++---------- src/runtime_config/default/actions.js | 2 +- 4 files changed, 66 insertions(+), 30 deletions(-) diff --git a/src/crypto/key_pair.h b/src/crypto/key_pair.h index cec02e7ca2a..3ff9050f49e 100644 --- a/src/crypto/key_pair.h +++ b/src/crypto/key_pair.h @@ -59,6 +59,8 @@ namespace crypto const std::optional& valid_from = std::nullopt, const std::optional& valid_to = std::nullopt) const = 0; + // TODO: Self-signed cert should also include valid_from and valid_to as + // arguments Pem self_sign( const std::string& name, const std::optional subject_alt_name = std::nullopt, diff --git a/src/crypto/openssl/x509_time.h b/src/crypto/openssl/x509_time.h index 2caa39641c2..eaef46f78e8 100644 --- a/src/crypto/openssl/x509_time.h +++ b/src/crypto/openssl/x509_time.h @@ -8,6 +8,8 @@ namespace crypto { + // TODO: ASN1_TIME or ASN1_GENERALIZEDTIME? + namespace OpenSSL { /** Set of utilites functions for working with x509 time, as defined in RFC @@ -50,5 +52,12 @@ namespace crypto CHECK1(ASN1_TIME_to_tm(time, &tm_time)); return std::mktime(&tm_time); } + + static inline Unique_ASN1_TIME adjust_time( + const Unique_ASN1_TIME& time, size_t offset_days) + { + return Unique_ASN1_TIME( + ASN1_TIME_adj(nullptr, to_time_t(time), offset_days, 0)); + } } } \ No newline at end of file diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index 717593f9c4d..11bec4b3f95 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -636,19 +636,6 @@ TEST_CASE("AES-GCM convenience functions") TEST_CASE("ASN1 time") { - struct TimeTest - { - struct Input - { - std::tm from; - std::tm to; - std::optional maximum_validity_period_days = std::nullopt; - }; - Input input; - - bool expected_verification_result; - }; - auto current_time_t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); auto time = *std::localtime(¤t_time_t); @@ -658,23 +645,61 @@ TEST_CASE("ASN1 time") auto next_year_time = time; next_year_time.tm_year++; - std::vector test_vectors{ - {{time, next_day_time}, true}, // Valid: Next day - {{time, time}, false}, // Invalid: Same date - {{next_day_time, time}, false}, // Invalid: to is before from - {{time, next_day_time, 100}, true}, // Valid: Next day within 100 days - {{time, next_year_time, 100}, false}, // Valid: Next day not within 100 days - }; + auto current_time = crypto::OpenSSL::from_time_t(current_time_t); + auto next_day = crypto::OpenSSL::from_time_t(std::mktime(&next_day_time)); + auto next_year = crypto::OpenSSL::from_time_t(std::mktime(&next_year_time)); + + INFO("Validate chronological times"); + { + struct TimeTest + { + struct Input + { + std::tm from; + std::tm to; + std::optional maximum_validity_period_days = std::nullopt; + }; + Input input; + + bool expected_verification_result; + }; + + std::vector test_vectors{ + {{time, next_day_time}, true}, // Valid: Next day + {{time, time}, false}, // Invalid: Same date + {{next_day_time, time}, false}, // Invalid: to is before from + {{time, next_day_time, 100}, true}, // Valid: Next day within 100 days + {{time, next_year_time, 100}, + false}, // Valid: Next day not within 100 days + }; - for (auto& data : test_vectors) + for (auto& data : test_vectors) + { + auto* from = &data.input.from; + auto* to = &data.input.to; + REQUIRE( + crypto::OpenSSL::validate_chronological_times( + crypto::OpenSSL::from_time_t(std::mktime(from)), + crypto::OpenSSL::from_time_t(std::mktime(to)), + data.input.maximum_validity_period_days) == + data.expected_verification_result); + } + } + + INFO("Adjust time"); { - auto* from = &data.input.from; - auto* to = &data.input.to; - REQUIRE( - crypto::OpenSSL::validate_chronological_times( - crypto::OpenSSL::from_time_t(std::mktime(from)), - crypto::OpenSSL::from_time_t(std::mktime(to)), - data.input.maximum_validity_period_days) == - data.expected_verification_result); + std::vector times = {time, next_day_time, next_day_time}; + + for (auto& t : times) + { + size_t days_offset = 100; + time_t t_ = std::mktime(&t); + auto adjusted_time = crypto::OpenSSL::adjust_time( + crypto::OpenSSL::from_time_t(t_), days_offset); + auto days_diff = + std::difftime(crypto::OpenSSL::to_time_t(adjusted_time), t_) / + (60 * 60 * 24); + REQUIRE(days_diff == days_offset); + } } } \ No newline at end of file diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 126f7677137..97b8f523f3a 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -963,7 +963,7 @@ const actions = new Map([ ) ) { throw new Error( - `Date valid_to ${args.valid_to} must be after date valid_from ${args.valid_from}, and within ${serviceConfig.cert_maximum_validity_period_days} days ` + `Date valid_to ${args.valid_to} must be after date valid_from ${args.valid_from}, and within ${serviceConfig.cert_maximum_validity_period_days} days` ); } From 94c58916bac5ae35f66651a94b6a47ec0ff9e115 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 28 Sep 2021 10:11:25 +0000 Subject: [PATCH 066/105] Self-signed certificate uses time from host --- src/crypto/key_pair.h | 14 ++++++++++---- src/crypto/openssl/x509_time.h | 12 ++++++++++-- src/crypto/test/crypto.cpp | 21 +++++++++++++++++++-- src/enclave/interface.h | 5 ++++- src/host/main.cpp | 7 +++++-- src/node/node_state.h | 15 ++++++++++++++- 6 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/crypto/key_pair.h b/src/crypto/key_pair.h index 3ff9050f49e..8cc8c775f42 100644 --- a/src/crypto/key_pair.h +++ b/src/crypto/key_pair.h @@ -64,19 +64,25 @@ namespace crypto Pem self_sign( const std::string& name, const std::optional subject_alt_name = std::nullopt, - bool ca = true) const + bool ca = true, + const std::optional& valid_from = std::nullopt, + const std::optional& valid_to = std::nullopt) const { std::vector sans; if (subject_alt_name.has_value()) sans.push_back(subject_alt_name.value()); auto csr = create_csr({name, sans}); - return sign_csr(Pem(0), csr, ca); + return sign_csr(Pem(0), csr, ca, valid_from, valid_to); } - Pem self_sign(const CertificateSubjectIdentity& csi, bool ca = true) const + Pem self_sign( + const CertificateSubjectIdentity& csi, + bool ca = true, + const std::optional& valid_from = std::nullopt, + const std::optional& valid_to = std::nullopt) const { auto csr = create_csr(csi); - return sign_csr(Pem(0), csr, ca); + return sign_csr(Pem(0), csr, ca, valid_from, valid_to); } }; diff --git a/src/crypto/openssl/x509_time.h b/src/crypto/openssl/x509_time.h index eaef46f78e8..30d30e44ab0 100644 --- a/src/crypto/openssl/x509_time.h +++ b/src/crypto/openssl/x509_time.h @@ -4,6 +4,7 @@ #include "openssl_wrappers.h" +#include #include namespace crypto @@ -54,10 +55,17 @@ namespace crypto } static inline Unique_ASN1_TIME adjust_time( - const Unique_ASN1_TIME& time, size_t offset_days) + const Unique_ASN1_TIME& time, size_t offset_days, int64_t offset_secs = 0) { return Unique_ASN1_TIME( - ASN1_TIME_adj(nullptr, to_time_t(time), offset_days, 0)); + ASN1_TIME_adj(nullptr, to_time_t(time), offset_days, offset_secs)); + } + + static inline std::string to_x509_time_string(const time_t& time) + { + // Returns ASN1 time string (YYYYMMDDHHMMSSZ) from time_t, as per + // https://www.openssl.org/docs/man1.1.1/man3/ASN1_UTCTIME_set.html + return fmt::format("{:%Y%m%d%H%M%SZ}", fmt::gmtime(time)); } } } \ No newline at end of file diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index 11bec4b3f95..27b358fba63 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -638,7 +638,7 @@ TEST_CASE("ASN1 time") { auto current_time_t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - auto time = *std::localtime(¤t_time_t); + auto time = *std::gmtime(¤t_time_t); auto next_day_time = time; next_day_time.tm_mday++; @@ -689,10 +689,10 @@ TEST_CASE("ASN1 time") INFO("Adjust time"); { std::vector times = {time, next_day_time, next_day_time}; + size_t days_offset = 100; for (auto& t : times) { - size_t days_offset = 100; time_t t_ = std::mktime(&t); auto adjusted_time = crypto::OpenSSL::adjust_time( crypto::OpenSSL::from_time_t(t_), days_offset); @@ -702,4 +702,21 @@ TEST_CASE("ASN1 time") REQUIRE(days_diff == days_offset); } } + + INFO("String to time conversion and back"); + { + std::vector days_offsets = {0, 1, 10, 100, 365, 1000, 10000}; + + for (auto const& days_offset : days_offsets) + { + auto adjusted_time = crypto::OpenSSL::adjust_time( + crypto::OpenSSL::from_time_t(current_time_t), days_offset); + auto adjusted_time_t = crypto::OpenSSL::to_time_t(adjusted_time); + + auto x509_str = crypto::OpenSSL::to_x509_time_string(adjusted_time_t); + auto asn1_time = crypto::OpenSSL::Unique_ASN1_TIME(x509_str); + auto converted_time_t = crypto::OpenSSL::to_time_t(asn1_time); + REQUIRE(converted_time_t == adjusted_time_t); + } + } } \ No newline at end of file diff --git a/src/enclave/interface.h b/src/enclave/interface.h index 2da8aedb9bf..3bf625f2e46 100644 --- a/src/enclave/interface.h +++ b/src/enclave/interface.h @@ -81,6 +81,8 @@ struct CCFConfig size_t jwt_key_refresh_interval_s; crypto::CurveID curve_id; + + std::string startup_host_time; }; DECLARE_JSON_TYPE(CCFConfig::SignatureIntervals); @@ -112,7 +114,8 @@ DECLARE_JSON_REQUIRED_FIELDS( joining, node_certificate_subject_identity, jwt_key_refresh_interval_s, - curve_id); + curve_id, + startup_host_time); /// General administrative messages enum AdminMessage : ringbuffer::Message diff --git a/src/host/main.cpp b/src/host/main.cpp index 1a38880cd6d..8e492f00edc 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. #include "ccf/version.h" +#include "crypto/openssl/x509_time.h" #include "ds/cli_helper.h" #include "ds/files.h" #include "ds/logger.h" @@ -786,10 +787,12 @@ int main(int argc, char** argv) ccf_config.node_certificate_subject_identity = node_certificate_subject_identity; - ccf_config.jwt_key_refresh_interval_s = jwt_key_refresh_interval_s; - ccf_config.curve_id = curve_id; + ccf_config.startup_host_time = crypto::OpenSSL::to_x509_time_string( + std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())); + + LOG_FAIL_FMT("Current host time: {}", ccf_config.startup_host_time); if (*start) { diff --git a/src/node/node_state.h b/src/node/node_state.h index 07d6ed236d7..4ee2f3a2dfc 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -334,6 +334,7 @@ namespace ccf js::register_class_ids(); self_signed_node_cert = create_self_signed_node_cert(); + LOG_FAIL_FMT("{}", self_signed_node_cert.str()); accept_node_tls_connections(); open_frontend(ActorsType::nodes); @@ -1552,7 +1553,19 @@ namespace ccf Pem create_self_signed_node_cert() { - return node_sign_kp->self_sign(config.node_certificate_subject_identity); + // TODO: Determine valid_to + auto valid_to = crypto::OpenSSL::adjust_time( + config.startup_host_time, + config.genesis.node_cert_maximum_validity_period_days, + -1); + auto valid_to_str = crypto::OpenSSL::to_x509_time_string( + crypto::OpenSSL::to_time_t(valid_to)); + + return node_sign_kp->self_sign( + config.node_certificate_subject_identity, + true, + config.startup_host_time, + valid_to_str); } Pem create_endorsed_node_cert() From f0c99c73e7a0656a1dec11b86a67bef4068d0af9 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 28 Sep 2021 15:56:56 +0000 Subject: [PATCH 067/105] Use time from host as `valid_from` --- include/ccf/endpoint_registry.h | 1 - src/enclave/interface.h | 11 +--- src/host/main.cpp | 30 +++++----- src/http/authentication/cert_auth.h | 1 - src/node/cert_bundles.h | 10 ++++ src/node/certs.h | 31 ++++++++++- src/node/network_tables.h | 2 +- src/node/node_state.h | 36 ++++++------ src/node/rpc/node_call_types.h | 1 + src/node/rpc/node_frontend.h | 9 ++- src/node/rpc/serialization.h | 3 +- tests/infra/crypto.py | 7 ++- tests/infra/remote.py | 6 +- tests/reconfiguration.py | 85 +++++++++++++++++++---------- 14 files changed, 147 insertions(+), 86 deletions(-) create mode 100644 src/node/cert_bundles.h diff --git a/include/ccf/endpoint_registry.h b/include/ccf/endpoint_registry.h index 32d0217e65d..5fd5b0ae8d0 100644 --- a/include/ccf/endpoint_registry.h +++ b/include/ccf/endpoint_registry.h @@ -9,7 +9,6 @@ #include "ds/json_schema.h" #include "ds/openapi.h" #include "http/http_consts.h" -#include "node/certs.h" #include "node/endpoint_metrics.h" #include "node/rpc/serialization.h" diff --git a/src/enclave/interface.h b/src/enclave/interface.h index 3bf625f2e46..53f27c8d93f 100644 --- a/src/enclave/interface.h +++ b/src/enclave/interface.h @@ -63,7 +63,6 @@ struct CCFConfig std::vector members_info; std::string constitution; size_t recovery_threshold; - size_t node_cert_maximum_validity_period_days; }; Genesis genesis = {}; @@ -77,11 +76,10 @@ struct CCFConfig Joining joining = {}; crypto::CertificateSubjectIdentity node_certificate_subject_identity; - size_t jwt_key_refresh_interval_s; - crypto::CurveID curve_id; + size_t node_cert_maximum_validity_period_days; std::string startup_host_time; }; @@ -91,11 +89,7 @@ DECLARE_JSON_REQUIRED_FIELDS( DECLARE_JSON_TYPE(CCFConfig::Genesis); DECLARE_JSON_REQUIRED_FIELDS( - CCFConfig::Genesis, - members_info, - constitution, - recovery_threshold, - node_cert_maximum_validity_period_days); + CCFConfig::Genesis, members_info, constitution, recovery_threshold); DECLARE_JSON_TYPE(CCFConfig::Joining); DECLARE_JSON_REQUIRED_FIELDS( @@ -115,6 +109,7 @@ DECLARE_JSON_REQUIRED_FIELDS( node_certificate_subject_identity, jwt_key_refresh_interval_s, curve_id, + node_cert_maximum_validity_period_days, startup_host_time); /// General administrative messages diff --git a/src/host/main.cpp b/src/host/main.cpp index 8e492f00edc..a849eaf24fe 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -401,6 +401,15 @@ int main(int argc, char** argv) ->transform(CLI::CheckedTransformer(curve_id_map, CLI::ignore_case)) ->capture_default_str(); + size_t node_cert_maximum_validity_period_days = 365; + app + .add_option( + "--node-cert-max-validity-days", + node_cert_maximum_validity_period_days, + "Maximum number of days node certificates must be valid for.") + ->check(CLI::PositiveNumber) + ->type_name("UINT"); + // The network certificate file can either be an input or output parameter, // depending on the subcommand. std::string network_cert_file = "networkcert.pem"; @@ -445,15 +454,6 @@ int main(int argc, char** argv) ->check(CLI::PositiveNumber) ->type_name("UINT"); - size_t node_cert_maximum_validity_period_days = 365; - start - ->add_option( - "--node-cert-max-validity-days", - node_cert_maximum_validity_period_days, - "Maximum number of days node certificates must be valid for.") - ->check(CLI::PositiveNumber) - ->type_name("UINT"); - auto join = app.add_subcommand("join", "Join existing network"); join->configurable(); @@ -789,10 +789,14 @@ int main(int argc, char** argv) node_certificate_subject_identity; ccf_config.jwt_key_refresh_interval_s = jwt_key_refresh_interval_s; ccf_config.curve_id = curve_id; - ccf_config.startup_host_time = crypto::OpenSSL::to_x509_time_string( - std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())); + ccf_config.node_cert_maximum_validity_period_days = + node_cert_maximum_validity_period_days; - LOG_FAIL_FMT("Current host time: {}", ccf_config.startup_host_time); + auto current_host_time = std::chrono::system_clock::now(); + LOG_INFO_FMT("Current host time: {}", current_host_time); + + ccf_config.startup_host_time = crypto::OpenSSL::to_x509_time_string( + std::chrono::system_clock::to_time_t(current_host_time)); if (*start) { @@ -831,8 +835,6 @@ int main(int argc, char** argv) files::slurp_string(constitution_path); } ccf_config.genesis.recovery_threshold = recovery_threshold.value(); - ccf_config.genesis.node_cert_maximum_validity_period_days = - node_cert_maximum_validity_period_days; LOG_INFO_FMT( "Creating new node: new network (with {} initial member(s) and {} " "member(s) required for recovery)", diff --git a/src/http/authentication/cert_auth.h b/src/http/authentication/cert_auth.h index e33159eb507..385df1c8eb5 100644 --- a/src/http/authentication/cert_auth.h +++ b/src/http/authentication/cert_auth.h @@ -6,7 +6,6 @@ #include "crypto/pem.h" #include "crypto/verifier.h" #include "node/blit.h" -#include "node/certs.h" #include "node/members.h" #include "node/nodes.h" #include "node/users.h" diff --git a/src/node/cert_bundles.h b/src/node/cert_bundles.h new file mode 100644 index 00000000000..f46e0ec726e --- /dev/null +++ b/src/node/cert_bundles.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "service_map.h" + +namespace ccf +{ + using CACertBundlePEMs = ServiceMap; +} diff --git a/src/node/certs.h b/src/node/certs.h index f46e0ec726e..e11c76529ef 100644 --- a/src/node/certs.h +++ b/src/node/certs.h @@ -2,9 +2,34 @@ // Licensed under the Apache 2.0 License. #pragma once -#include "service_map.h" +#include "crypto/openssl/x509_time.h" +#include "crypto/pem.h" + +#include namespace ccf { - using CACertBundlePEMs = ServiceMap; -} + static std::string compute_cert_valid_to_string( + const std::string& valid_from, size_t validity_period_days) + { + // Note: As per RFC 5280, the validity period runs until "notAfter" + // _inclusive_ so substract one second from the validity period. + auto valid_to = + crypto::OpenSSL::adjust_time(valid_from, validity_period_days, -1); + return crypto::OpenSSL::to_x509_time_string( + crypto::OpenSSL::to_time_t(valid_to)); + } + + crypto::Pem create_self_signed_cert( + const crypto::KeyPairPtr& key_pair, + const crypto::CertificateSubjectIdentity& csi, + const std::string& valid_from, + size_t validity_period_days) + { + return key_pair->self_sign( + csi, + true /* CA */, + valid_from, + compute_cert_valid_to_string(valid_from, validity_period_days)); + } +} \ No newline at end of file diff --git a/src/node/network_tables.h b/src/node/network_tables.h index 2ca8059b7cd..f67ccee3d69 100644 --- a/src/node/network_tables.h +++ b/src/node/network_tables.h @@ -2,7 +2,7 @@ // Licensed under the Apache 2.0 License. #pragma once #include "backup_signatures.h" -#include "certs.h" +#include "cert_bundles.h" #include "client_signatures.h" #include "code_id.h" #include "config.h" diff --git a/src/node/node_state.h b/src/node/node_state.h index 4ee2f3a2dfc..17a5a93c345 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -20,6 +20,7 @@ #include "hooks.h" #include "js/wrap.h" #include "network_state.h" +#include "node/certs.h" #include "node/http_node_client.h" #include "node/jwt_key_auto_refresh.h" #include "node/progress_tracker.h" @@ -333,8 +334,13 @@ namespace ccf get_subject_alternative_names(); js::register_class_ids(); - self_signed_node_cert = create_self_signed_node_cert(); + self_signed_node_cert = create_self_signed_cert( + node_sign_kp, + config.node_certificate_subject_identity, + config.startup_host_time, + config.node_cert_maximum_validity_period_days); LOG_FAIL_FMT("{}", self_signed_node_cert.str()); + accept_node_tls_connections(); open_frontend(ActorsType::nodes); @@ -1551,23 +1557,6 @@ namespace ccf } } - Pem create_self_signed_node_cert() - { - // TODO: Determine valid_to - auto valid_to = crypto::OpenSSL::adjust_time( - config.startup_host_time, - config.genesis.node_cert_maximum_validity_period_days, - -1); - auto valid_to_str = crypto::OpenSSL::to_x509_time_string( - crypto::OpenSSL::to_time_t(valid_to)); - - return node_sign_kp->self_sign( - config.node_certificate_subject_identity, - true, - config.startup_host_time, - valid_to_str); - } - Pem create_endorsed_node_cert() { // Only used by a 2.x node joining an existing 1.x service which will not @@ -1575,7 +1564,13 @@ namespace ccf auto nw = crypto::make_key_pair(network.identity->priv_key); auto csr = node_sign_kp->create_csr(config.node_certificate_subject_identity); - return nw->sign_csr(network.identity->cert, csr); + return nw->sign_csr( + network.identity->cert, + csr, + false, + compute_cert_valid_to_string( + config.startup_host_time, + config.node_cert_maximum_validity_period_days)); } crypto::Pem generate_endorsed_certificate( @@ -1652,7 +1647,7 @@ namespace ccf config.genesis.recovery_threshold, network.consensus_type, reconf_type, - config.genesis.node_cert_maximum_validity_period_days}; + config.node_cert_maximum_validity_period_days}; create_params.genesis_info = genesis_info; } @@ -1666,6 +1661,7 @@ namespace ccf create_params.public_encryption_key = node_encrypt_kp->public_key_pem(); create_params.code_digest = node_code_id; create_params.node_info_network = config.node_info_network; + create_params.node_cert_valid_from = config.startup_host_time; // Record self-signed certificate in create request if the node does not // require endorsement by the service (i.e. BFT) diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index f611d95b865..5abbc7923e4 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -67,6 +67,7 @@ namespace ccf crypto::Pem public_encryption_key; CodeDigest code_digest; NodeInfoNetwork node_info_network; + std::string node_cert_valid_from; // Only set if node does _not_ require endorsement by the service std::optional node_cert = std::nullopt; diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 6e5466c0467..e0154597901 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -10,6 +10,7 @@ #include "crypto/csr.h" #include "crypto/hash.h" #include "frontend.h" +#include "node/certs.h" #include "node/entities.h" #include "node/network_state.h" #include "node/quote.h" @@ -1160,7 +1161,13 @@ namespace ccf context.get_node_state().generate_endorsed_certificate( in.certificate_signing_request, this->network.identity->priv_key, - this->network.identity->cert)); + this->network.identity->cert, + in.node_cert_valid_from, + compute_cert_valid_to_string( + in.node_cert_valid_from, + in.genesis_info->configuration.cert_maximum_validity_period_days + .value()))); + // TODO: What to do for recovery? Read existing value from store? } else { diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index ed6d2b544a4..845065e5ed3 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -85,7 +85,8 @@ namespace ccf quote_info, public_encryption_key, code_digest, - node_info_network) + node_info_network, + node_cert_valid_from) DECLARE_JSON_OPTIONAL_FIELDS( CreateNetworkNodeToNode::In, node_cert, genesis_info) diff --git a/tests/infra/crypto.py b/tests/infra/crypto.py index 641a935c3b8..a5c4b6ab6ae 100644 --- a/tests/infra/crypto.py +++ b/tests/infra/crypto.py @@ -274,10 +274,11 @@ def check_key_pair_pem(private: str, public: str, password=None) -> bool: pub_der = pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) return prv_pub_der == pub_der -def get_validity_period_from_pem_cert(pem:str): + +def get_validity_period_from_pem_cert(pem: str): cert = load_pem_x509_certificate(pem.encode(), default_backend()) return cert.not_valid_before, cert.not_valid_after -def datetime_as_UTCtime(datetime:datetime): - return UTCTime.fromDateTime(datetime) +def datetime_as_UTCtime(datetime: datetime): + return UTCTime.fromDateTime(datetime) diff --git a/tests/infra/remote.py b/tests/infra/remote.py index 90571ebcbdc..5fadaa0fc95 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -705,6 +705,9 @@ def __init__( if client_connection_timeout_ms: cmd += [f"--client-connection-timeout-ms={client_connection_timeout_ms}"] + if node_cert_max_validity_days: + cmd += [f"--node-cert-max-validity-days={node_cert_max_validity_days}"] + if additional_raw_node_args: for s in additional_raw_node_args: cmd += [str(s)] @@ -717,9 +720,6 @@ def __init__( os.path.join(self.common_dir, os.path.basename(fragment)) ] - if node_cert_max_validity_days: - cmd += [f"--node-cert-max-validity-days={node_cert_max_validity_days}"] - if members_info is None: raise ValueError( "Starting node should be given at least one member info" diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 4b7aa1a0fd9..e7490556b1f 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -11,6 +11,8 @@ from infra.checker import check_can_progress, check_does_not_progress import ccf.ledger import json +import infra.crypto +from datetime import datetime, timedelta from loguru import logger as LOG @@ -35,10 +37,26 @@ def count_nodes(configs, network): return len(nodes) +def verify_node_certificate_validity_period(node, args): + # Verify self-signed certificate validity period + valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( + node.get_tls_certificate_pem() + ) + expected_valid_to = valid_from + timedelta( + days=args.node_cert_max_validity_days, seconds=-1 + ) + if valid_to != expected_valid_to: + raise ValueError( + f"Node {node.local_node_id}: validity period for certiticate is not as expected: from {valid_from} to {valid_to} but expected to {expected_valid_to}" + ) + + @reqs.description("Adding a valid node without snapshot") def test_add_node(network, args): new_node = network.create_node("local://localhost") network.join_node(new_node, args.package, args, from_snapshot=False) + # Verify self-signed node certificate validity period + verify_node_certificate_validity_period(new_node, args) network.trust_node(new_node, args) with new_node.client() as c: s = c.get("/node/state") @@ -412,6 +430,12 @@ def test_learner_does_not_take_part(network, args): return network +@reqs.description("Test node certificates validity period") +def test_node_certificates_validity_period(network, args): + for node in network.get_joined_nodes(): + verify_node_certificate_validity_period(node, args) + + def run(args): txs = app.LoggingTxs("user0") with infra.network.network( @@ -426,34 +450,35 @@ def run(args): test_version(network, args) - if args.consensus != "bft": - test_join_straddling_primary_replacement(network, args) - test_node_replacement(network, args) - test_add_node_from_backup(network, args) - test_add_node(network, args) - test_add_node_on_other_curve(network, args) - test_retire_backup(network, args) - test_add_as_many_pending_nodes(network, args) - test_add_node(network, args) - test_retire_primary(network, args) - - test_add_node_from_snapshot(network, args) - test_add_node_from_snapshot(network, args, from_backup=True) - test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) - latest_node_log = network.get_joined_nodes()[-1].remote.log_path() - with open(latest_node_log, "r+", encoding="utf-8") as log: - assert any( - "No snapshot found: Node will replay all historical transactions" - in l - for l in log.readlines() - ), "New nodes shouldn't join from snapshot if snapshot evidence cannot be verified" - - test_node_filter(network, args) - test_retiring_nodes_emit_at_most_one_signature(network, args) - else: - test_learner_catches_up(network, args) - # test_learner_does_not_take_part(network, args) - test_retire_backup(network, args) + # if args.consensus != "bft": + # test_join_straddling_primary_replacement(network, args) + # test_node_replacement(network, args) + # test_add_node_from_backup(network, args) + # test_add_node(network, args) + # test_add_node_on_other_curve(network, args) + # test_retire_backup(network, args) + # test_add_as_many_pending_nodes(network, args) + # test_add_node(network, args) + # test_retire_primary(network, args) + + # test_add_node_from_snapshot(network, args) + # test_add_node_from_snapshot(network, args, from_backup=True) + # test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) + # latest_node_log = network.get_joined_nodes()[-1].remote.log_path() + # with open(latest_node_log, "r+", encoding="utf-8") as log: + # assert any( + # "No snapshot found: Node will replay all historical transactions" + # in l + # for l in log.readlines() + # ), "New nodes shouldn't join from snapshot if snapshot evidence cannot be verified" + + # test_node_filter(network, args) + # test_retiring_nodes_emit_at_most_one_signature(network, args) + # else: + # test_learner_catches_up(network, args) + # # test_learner_does_not_take_part(network, args) + # test_retire_backup(network, args) + test_node_certificates_validity_period(network, args) def run_join_old_snapshot(args): @@ -525,5 +550,5 @@ def run_join_old_snapshot(args): run(args) - if args.consensus != "bft": - run_join_old_snapshot(args) + # if args.consensus != "bft": + # run_join_old_snapshot(args) From 7bb8173070f4f052fac1e9d6f5a0902cd7f084fa Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 29 Sep 2021 08:17:44 +0000 Subject: [PATCH 068/105] . --- src/node/rpc/node_frontend.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index e0154597901..3e0f6cb2fcf 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -1167,7 +1167,7 @@ namespace ccf in.node_cert_valid_from, in.genesis_info->configuration.cert_maximum_validity_period_days .value()))); - // TODO: What to do for recovery? Read existing value from store? + // TODO: What to do for recovery? Read existing value from store! } else { From 0e2e6bcab8d2269404b5e1ca19066aa38bf035d6 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 30 Sep 2021 14:18:32 +0000 Subject: [PATCH 069/105] Pass validity_period_days in proposal --- python/ccf/proposal_generator.py | 10 ++++-- src/crypto/openssl/x509_time.h | 1 + src/host/main.cpp | 4 +++ src/js/crypto.cpp | 48 --------------------------- src/js/wrap.cpp | 22 ++++-------- src/node/node_state.h | 13 ++++++-- src/node/nodes.h | 6 +++- src/node/rpc/node_call_types.h | 1 + src/node/rpc/node_frontend.h | 15 +++++---- src/node/rpc/node_interface.h | 2 +- src/node/rpc/serialization.h | 4 ++- src/node/rpc/test/node_stub.h | 2 +- src/runtime_config/default/actions.js | 33 ++++++------------ tests/governance.py | 33 +++++++++++------- tests/reconfiguration.py | 12 ++++++- tests/suite/test_suite.py | 1 + 16 files changed, 93 insertions(+), 114 deletions(-) diff --git a/python/ccf/proposal_generator.py b/python/ccf/proposal_generator.py index b2859663b07..24ba9f8bcfa 100644 --- a/python/ccf/proposal_generator.py +++ b/python/ccf/proposal_generator.py @@ -326,10 +326,16 @@ def set_jwt_public_signing_keys(issuer: str, jwks_path: str, **kwargs): @cli_proposal -def renew_node_certificate(node_id: str, valid_from: str, valid_to: str, **kwargs): +def renew_node_certificate( + node_id: str, valid_from: str, validity_period_days: int, **kwargs +): return build_proposal( "renew_node_certificate", - {"node_id": node_id, "valid_from": valid_from, "valid_to": valid_to}, + { + "node_id": node_id, + "valid_from": valid_from, + "validity_period_days": validity_period_days, + }, **kwargs, ) diff --git a/src/crypto/openssl/x509_time.h b/src/crypto/openssl/x509_time.h index 30d30e44ab0..5d25dd9e7cf 100644 --- a/src/crypto/openssl/x509_time.h +++ b/src/crypto/openssl/x509_time.h @@ -63,6 +63,7 @@ namespace crypto static inline std::string to_x509_time_string(const time_t& time) { + // TODO: Change format to YY instead of YYYY // Returns ASN1 time string (YYYYMMDDHHMMSSZ) from time_t, as per // https://www.openssl.org/docs/man1.1.1/man3/ASN1_UTCTIME_set.html return fmt::format("{:%Y%m%d%H%M%SZ}", fmt::gmtime(time)); diff --git a/src/host/main.cpp b/src/host/main.cpp index a849eaf24fe..19c845e40e3 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -401,6 +401,10 @@ int main(int argc, char** argv) ->transform(CLI::CheckedTransformer(curve_id_map, CLI::ignore_case)) ->capture_default_str(); + // TODO: Should this be under a different name? + // Also, because there's no way to renew a self-signed cert, should this be + // hardcoded instead, assuming that the self-signed certificate gets + // overridden by the service-endorsed one pretty quickly? size_t node_cert_maximum_validity_period_days = 365; app .add_option( diff --git a/src/js/crypto.cpp b/src/js/crypto.cpp index 6dc339c3b63..0a6ba1282bf 100644 --- a/src/js/crypto.cpp +++ b/src/js/crypto.cpp @@ -483,54 +483,6 @@ namespace ccf::js } } - JSValue js_validate_certificate_validity_period( - JSContext* ctx, - JSValueConst this_val, - int argc, - [[maybe_unused]] JSValueConst* argv) - { - if (argc < 2 || argc > 3) - { - return JS_ThrowTypeError( - ctx, - "Passed %d arguments but expected at least 2 and less than 3", - argc); - } - - auto valid_from_cstr = JS_ToCString(ctx, argv[0]); - if (valid_from_cstr == nullptr) - { - throw JS_ThrowTypeError(ctx, "valid_from argument is not a string"); - } - auto valid_from = std::string(valid_from_cstr); - JS_FreeCString(ctx, valid_from_cstr); - - auto valid_to_cstr = JS_ToCString(ctx, argv[1]); - if (valid_to_cstr == nullptr) - { - throw JS_ThrowTypeError(ctx, "valid_to argument is not a string"); - } - auto valid_to = std::string(valid_to_cstr); - JS_FreeCString(ctx, valid_to_cstr); - - std::optional allowed_validity_period_days = std::nullopt; - if (argc > 2 && !JS_IsUndefined(argv[2])) - { - uint32_t allowed_validity_period_days_ = 0; - if (JS_ToUint32(ctx, &allowed_validity_period_days_, argv[2]) < 0) - { - js::js_dump_error(ctx); - return JS_EXCEPTION; - } - allowed_validity_period_days = allowed_validity_period_days_; - } - - return JS_NewBool( - ctx, - crypto::OpenSSL::validate_chronological_times( - valid_from, valid_to, allowed_validity_period_days)); - } - #pragma clang diagnostic pop } diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index 43e4d182754..480da427510 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -562,20 +562,21 @@ namespace ccf::js auto valid_from = std::string(valid_from_cstr); JS_FreeCString(ctx, valid_from_cstr); - auto valid_to_cstr = JS_ToCString(ctx, argv[2]); - if (valid_to_cstr == nullptr) + size_t validity_period_days = 0; + if (JS_ToIndex(ctx, &validity_period_days, argv[2]) < 0) { - throw JS_ThrowTypeError(ctx, "valid to argument is not a string"); + js::js_dump_error(ctx); + return JS_EXCEPTION; } - auto valid_to = std::string(valid_to_cstr); - JS_FreeCString(ctx, valid_to_cstr); + + LOG_FAIL_FMT("Validity period: {}", validity_period_days); auto endorsed_cert = node->generate_endorsed_certificate( csr, network->identity->priv_key, network->identity->cert, valid_from, - valid_to); + validity_period_days); return JS_NewString(ctx, endorsed_cert.str().c_str()); } @@ -1303,15 +1304,6 @@ namespace ccf::js "refreshAppBytecodeCache", JS_NewCFunction( ctx, js_refresh_app_bytecode_cache, "refreshAppBytecodeCache", 0)); - JS_SetPropertyStr( - ctx, - ccf, - "validateCertificateValidityPeriod", - JS_NewCFunction( - ctx, - js_validate_certificate_validity_period, - "validateCertificateValidityPeriod", - 0)); auto crypto = JS_NewObject(ctx); JS_SetPropertyStr(ctx, ccf, "crypto", crypto); diff --git a/src/node/node_state.h b/src/node/node_state.h index 17a5a93c345..d35b9cb0bdb 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -655,6 +655,7 @@ namespace ccf join_params.startup_seqno = startup_seqno; join_params.certificate_signing_request = node_sign_kp->create_csr(config.node_certificate_subject_identity); + join_params.node_cert_valid_from = config.startup_host_time; LOG_DEBUG_FMT( "Sending join request to {}:{}", @@ -1557,6 +1558,7 @@ namespace ccf } } + // TODO: Move to certs.h Pem create_endorsed_node_cert() { // Only used by a 2.x node joining an existing 1.x service which will not @@ -1573,14 +1575,21 @@ namespace ccf config.node_cert_maximum_validity_period_days)); } + // TODO: Remove optional? crypto::Pem generate_endorsed_certificate( const crypto::Pem& subject_csr, const crypto::Pem& endorser_private_key, const crypto::Pem& endorser_cert, const std::optional& valid_from = std::nullopt, - const std::optional& valid_to = - std::nullopt) override // TODO: Create date format type + const std::optional& validity_period_days = std::nullopt) override { + std::optional valid_to = std::nullopt; + if (validity_period_days.has_value()) + { + valid_to = compute_cert_valid_to_string( + valid_from.value(), validity_period_days.value()); + } + return crypto::make_key_pair(endorser_private_key) ->sign_csr(endorser_cert, subject_csr, false, valid_from, valid_to); } diff --git a/src/node/nodes.h b/src/node/nodes.h index 885dcf93c49..26ca453b2db 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -62,6 +62,9 @@ namespace ccf /// Public key std::optional public_key = std::nullopt; + /// Initial node certificate valid from + std::optional certificate_initial_valid_from = std::nullopt; + /** * Fields below are deprecated */ @@ -81,7 +84,8 @@ namespace ccf ledger_secret_seqno, code_digest, certificate_signing_request, - public_key); + public_key, + certificate_initial_valid_from); using Nodes = ServiceMap; using NodeEndorsedCertificates = diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 5abbc7923e4..68d95f9fde1 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -100,6 +100,7 @@ namespace ccf ConsensusType consensus_type = ConsensusType::CFT; std::optional startup_seqno = std::nullopt; std::optional certificate_signing_request = std::nullopt; + std::optional node_cert_valid_from = std::nullopt; }; struct Out diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 3e0f6cb2fcf..fb1c47099b9 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -197,7 +197,7 @@ namespace ccf auto client_public_key_pem = crypto::public_key_pem_from_cert(node_der); if (in.certificate_signing_request.has_value()) { - // Verify that client's public key matches the one specified in the CSR) + // Verify that client's public key matches the one specified in the CSR auto csr_public_key_pem = crypto::public_key_pem_from_csr( in.certificate_signing_request.value()); if (client_public_key_pem != csr_public_key_pem) @@ -217,7 +217,8 @@ namespace ccf ledger_secret_seqno, ds::to_hex(code_digest.data), in.certificate_signing_request, - client_public_key_pem}; + client_public_key_pem, + in.node_cert_valid_from}; // Because the certificate signature scheme is non-deterministic, only // self-signed node certificate is recorded in the node info table @@ -255,6 +256,7 @@ namespace ccf in.certificate_signing_request.has_value() && this->network.consensus_type == ConsensusType::CFT) { + // TODO: Add validity period endorsed_certificate = context.get_node_state().generate_endorsed_certificate( in.certificate_signing_request.value(), @@ -429,7 +431,8 @@ namespace ccf // If the node already exists, return network secrets if is already // trusted. Otherwise, only return its status - auto node_status = nodes->get(existing_node_info->node_id)->status; + auto node_info = nodes->get(existing_node_info->node_id); + auto node_status = node_info->status; rep.node_status = node_status; if ( node_status == NodeStatus::TRUSTED || @@ -1163,10 +1166,8 @@ namespace ccf this->network.identity->priv_key, this->network.identity->cert, in.node_cert_valid_from, - compute_cert_valid_to_string( - in.node_cert_valid_from, - in.genesis_info->configuration.cert_maximum_validity_period_days - .value()))); + in.genesis_info->configuration.cert_maximum_validity_period_days + .value())); // TODO: What to do for recovery? Read existing value from store! } else diff --git a/src/node/rpc/node_interface.h b/src/node/rpc/node_interface.h index c05992e1a62..3ee2087ba23 100644 --- a/src/node/rpc/node_interface.h +++ b/src/node/rpc/node_interface.h @@ -55,6 +55,6 @@ namespace ccf const crypto::Pem& endorser_private_key, const crypto::Pem& endorser_cert, const std::optional& valid_from = std::nullopt, - const std::optional& valid_to = std::nullopt) = 0; + const std::optional& validity_period_days = std::nullopt) = 0; }; } \ No newline at end of file diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index 845065e5ed3..be7ed4bae85 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -37,7 +37,9 @@ namespace ccf consensus_type, startup_seqno) DECLARE_JSON_OPTIONAL_FIELDS( - JoinNetworkNodeToNode::In, certificate_signing_request) + JoinNetworkNodeToNode::In, + certificate_signing_request, + node_cert_valid_from) DECLARE_JSON_ENUM( ccf::IdentityType, diff --git a/src/node/rpc/test/node_stub.h b/src/node/rpc/test/node_stub.h index ea81ea9e2eb..f497e10179c 100644 --- a/src/node/rpc/test/node_stub.h +++ b/src/node/rpc/test/node_stub.h @@ -127,7 +127,7 @@ namespace ccf const crypto::Pem& endorser_private_key, const crypto::Pem& endorser_cert, const std::optional& valid_from = std::nullopt, - const std::optional& valid_to = std::nullopt) override + const std::optional& validity_period_days = std::nullopt) override { throw std::logic_error("Unimplemented"); } diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 97b8f523f3a..fcd8adb36f1 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -137,16 +137,7 @@ function checkX509CertBundle(value, field) { } } -function validateCertificateValidityPeriod(from, to, from_field, to_field) { - checkType(from, "string", from_field); - checkType(to, "string", to_field); - if (!ccf.validateCertificateValidityPeriod(from, to)) { - throw new Error(`Date ${to_field} must be after date ${from_field}`); - } -} - function invalidateOtherOpenProposals(proposalIdToRetain) { - let proposals = ccf.kv["public:ccf.gov.proposals_info"]; const proposalsMap = ccf.kv["public:ccf.gov.proposals_info"]; proposalsMap.forEach((v, k) => { let proposalId = ccf.bufToStr(k); @@ -792,7 +783,9 @@ const actions = new Map([ ) { // Note: CSR is only present from 2.x const endorsed_node_cert = ccf.network.generateEndorsedCertificate( - nodeInfo.certificate_signing_request + nodeInfo.certificate_signing_request, + nodeInfo.certificate_initial_valid_from, + serviceConfig.cert_maximum_validity_period_days ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), @@ -922,12 +915,9 @@ const actions = new Map([ new Action( function (args) { checkEntityId(args.node_id, "node_id"); - validateCertificateValidityPeriod( - args.valid_from, - args.valid_to, - "valid_from", - "valid_to" - ); + checkType(args.valid_from, "string", "valid_from"); + checkType(args.validity_period_days, "integer", "validity_period_days"); + checkBounds(args.validity_period_days, 0, null, "validity_period_days"); }, function (args) { const node = ccf.kv["public:ccf.gov.nodes.info"].get( @@ -956,21 +946,18 @@ const actions = new Map([ const serviceConfig = ccf.bufToJsonCompatible(rawConfig); if ( - !ccf.validateCertificateValidityPeriod( - args.valid_from, - args.valid_to, - serviceConfig.cert_maximum_validity_period_days - ) + args.validity_period_days > + serviceConfig.cert_maximum_validity_period_days ) { throw new Error( - `Date valid_to ${args.valid_to} must be after date valid_from ${args.valid_from}, and within ${serviceConfig.cert_maximum_validity_period_days} days` + `Validity period ${args.validity_period_days} (days) must be less than or equal to service node certificate maximum validity period ${serviceConfig.cert_maximum_validity_period_days} (days)` ); } const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, args.valid_from, - args.valid_to + args.validity_period_days ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), diff --git a/tests/governance.py b/tests/governance.py index c9f1c2ff3e2..4bee889b308 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -225,16 +225,16 @@ def test_node_cert_renewal(network, args): now = datetime.now().replace( microsecond=0 ) # Truncate microseconds which are not reflected in RFC5280 UTCTime - future_allowed = now + timedelta(days=args.node_cert_max_validity_days - 1) - future_forbidden = now + timedelta(days=args.node_cert_max_validity_days + 1) + validity_period_allowed = args.node_cert_max_validity_days - 1 + validity_period_forbidden = args.node_cert_max_validity_days + 1 test_vectors = [ - (now, future_allowed, None), - (future_allowed, now, infra.proposal.ProposalNotCreated), - (now, future_forbidden, infra.proposal.ProposalNotAccepted), + (now, validity_period_allowed, None), + (now, -1, infra.proposal.ProposalNotCreated), + (now, validity_period_forbidden, infra.proposal.ProposalNotAccepted), ] - for (before_date, after_date, expected_exception) in test_vectors: + for (valid_from, validity_period_days, expected_exception) in test_vectors: for node in network.get_joined_nodes(): with node.client() as c: c.get("/node/network/nodes") @@ -251,8 +251,8 @@ def test_node_cert_renewal(network, args): network.consortium.renew_node_certificate( node, node.node_id, - valid_from=str(infra.crypto.datetime_as_UTCtime(before_date)), - valid_to=str(infra.crypto.datetime_as_UTCtime(after_date)), + valid_from=str(infra.crypto.datetime_as_UTCtime(valid_from)), + validity_period_days=validity_period_days, ) except Exception as e: assert isinstance(e, expected_exception) @@ -267,11 +267,20 @@ def test_node_cert_renewal(network, args): assert ( node_cert_tls_before != node_cert_tls_after ), "Node TLS certificate should be updated after renewal" - valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( - node_cert_tls_after + ( + cert_valid_from, + cert_valid_to, + ) = infra.crypto.get_validity_period_from_pem_cert(node_cert_tls_after) + # Note: CCF automatically substracts one second from validity period + expected_valid_to = valid_from + timedelta( + days=validity_period_days, seconds=-1 ) - assert valid_from == before_date, f"{valid_from} != {before_date}" - assert valid_to == after_date, f"{valid_to} != {after_date}" + assert ( + cert_valid_from == valid_from + ), f"{cert_valid_from} != {valid_from}" + assert ( + cert_valid_to == expected_valid_to + ), f"{cert_valid_to} != {expected_valid_to}" assert ( infra.crypto.compute_public_key_der_hash_hex_from_pem( diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index e7490556b1f..a74e612a80b 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -42,12 +42,22 @@ def verify_node_certificate_validity_period(node, args): valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( node.get_tls_certificate_pem() ) + + # Node certificate should have been issued within this test run (generous window in case + # the node was spun a while ago) + if valid_from < datetime.utcnow() - timedelta(hours=3): + raise ValueError( + f'Node {node.local_node_id} certificate is too old: valid from "{valid_from}"' + ) + + # Note: CCF substracts one second from validity period since x509 + # specifies that validity dates are inclusive. expected_valid_to = valid_from + timedelta( days=args.node_cert_max_validity_days, seconds=-1 ) if valid_to != expected_valid_to: raise ValueError( - f"Node {node.local_node_id}: validity period for certiticate is not as expected: from {valid_from} to {valid_to} but expected to {expected_valid_to}" + f'Validity period for node {node.local_node_id} certiticate is not as expected: from "{valid_from}"" to "{valid_to}"" but expected to "{expected_valid_to}"' ) diff --git a/tests/suite/test_suite.py b/tests/suite/test_suite.py index 240a2472987..ee3d361385f 100644 --- a/tests/suite/test_suite.py +++ b/tests/suite/test_suite.py @@ -99,6 +99,7 @@ reconfiguration.test_add_node_from_backup, reconfiguration.test_add_as_many_pending_nodes, reconfiguration.test_retire_backup, + reconfiguration.test_node_certificates_validity_period, # recovery: recovery.test, # rekey: From 09cd2eceb64482ec16fd0bd9f0d9091fdc43cae2 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 30 Sep 2021 14:51:28 +0000 Subject: [PATCH 070/105] All tests work --- src/js/wrap.cpp | 2 -- src/node/node_state.h | 19 +++++------ src/node/rpc/node_frontend.h | 10 ++++-- src/node/rpc/node_interface.h | 4 +-- src/node/rpc/test/node_stub.h | 4 +-- tests/reconfiguration.py | 59 ++++++++++++++++++----------------- 6 files changed, 51 insertions(+), 47 deletions(-) diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index 480da427510..c81deac547d 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -569,8 +569,6 @@ namespace ccf::js return JS_EXCEPTION; } - LOG_FAIL_FMT("Validity period: {}", validity_period_days); - auto endorsed_cert = node->generate_endorsed_certificate( csr, network->identity->priv_key, diff --git a/src/node/node_state.h b/src/node/node_state.h index d35b9cb0bdb..2856309d359 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1575,23 +1575,20 @@ namespace ccf config.node_cert_maximum_validity_period_days)); } - // TODO: Remove optional? crypto::Pem generate_endorsed_certificate( const crypto::Pem& subject_csr, const crypto::Pem& endorser_private_key, const crypto::Pem& endorser_cert, - const std::optional& valid_from = std::nullopt, - const std::optional& validity_period_days = std::nullopt) override + const std::string& valid_from, + size_t validity_period_days) override { - std::optional valid_to = std::nullopt; - if (validity_period_days.has_value()) - { - valid_to = compute_cert_valid_to_string( - valid_from.value(), validity_period_days.value()); - } - return crypto::make_key_pair(endorser_private_key) - ->sign_csr(endorser_cert, subject_csr, false, valid_from, valid_to); + ->sign_csr( + endorser_cert, + subject_csr, + false, + valid_from, + compute_cert_valid_to_string(valid_from, validity_period_days)); } void accept_node_tls_connections() diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index fb1c47099b9..da7e75832fc 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -150,6 +150,7 @@ namespace ccf auto nodes = tx.rw(network.nodes); auto node_endorsed_certificates = tx.rw(network.node_endorsed_certificates); + auto config = tx.ro(network.config)->get(); auto conflicting_node_id = check_conflicting_node_network(tx, in.node_info_network); @@ -256,12 +257,17 @@ namespace ccf in.certificate_signing_request.has_value() && this->network.consensus_type == ConsensusType::CFT) { - // TODO: Add validity period + // TODO: What if the configuration has no validity period (i.e. 1.x + // ledger)? Default to 365 days? + assert(config->cert_maximum_validity_period_days.has_value()); endorsed_certificate = context.get_node_state().generate_endorsed_certificate( in.certificate_signing_request.value(), this->network.identity->priv_key, - this->network.identity->cert); + this->network.identity->cert, + in.node_cert_valid_from.value(), + config->cert_maximum_validity_period_days.value()); + node_endorsed_certificates->put( joining_node_id, {endorsed_certificate.value()}); } diff --git a/src/node/rpc/node_interface.h b/src/node/rpc/node_interface.h index 3ee2087ba23..4b4f372f705 100644 --- a/src/node/rpc/node_interface.h +++ b/src/node/rpc/node_interface.h @@ -54,7 +54,7 @@ namespace ccf const crypto::Pem& subject_csr, const crypto::Pem& endorser_private_key, const crypto::Pem& endorser_cert, - const std::optional& valid_from = std::nullopt, - const std::optional& validity_period_days = std::nullopt) = 0; + const std::string& valid_from, + size_t validity_period_days) = 0; }; } \ No newline at end of file diff --git a/src/node/rpc/test/node_stub.h b/src/node/rpc/test/node_stub.h index f497e10179c..af310023321 100644 --- a/src/node/rpc/test/node_stub.h +++ b/src/node/rpc/test/node_stub.h @@ -126,8 +126,8 @@ namespace ccf const crypto::Pem& subject_csr, const crypto::Pem& endorser_private_key, const crypto::Pem& endorser_cert, - const std::optional& valid_from = std::nullopt, - const std::optional& validity_period_days = std::nullopt) override + const std::string& valid_from, + size_t validity_period_days) override { throw std::logic_error("Unimplemented"); } diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index a74e612a80b..969a2b4fa93 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -59,6 +59,9 @@ def verify_node_certificate_validity_period(node, args): raise ValueError( f'Validity period for node {node.local_node_id} certiticate is not as expected: from "{valid_from}"" to "{valid_to}"" but expected to "{expected_valid_to}"' ) + LOG.info( + f"Verified validity period for node {node.local_node_id} certificate: {valid_from} - {valid_to}" + ) @reqs.description("Adding a valid node without snapshot") @@ -460,34 +463,34 @@ def run(args): test_version(network, args) - # if args.consensus != "bft": - # test_join_straddling_primary_replacement(network, args) - # test_node_replacement(network, args) - # test_add_node_from_backup(network, args) - # test_add_node(network, args) - # test_add_node_on_other_curve(network, args) - # test_retire_backup(network, args) - # test_add_as_many_pending_nodes(network, args) - # test_add_node(network, args) - # test_retire_primary(network, args) - - # test_add_node_from_snapshot(network, args) - # test_add_node_from_snapshot(network, args, from_backup=True) - # test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) - # latest_node_log = network.get_joined_nodes()[-1].remote.log_path() - # with open(latest_node_log, "r+", encoding="utf-8") as log: - # assert any( - # "No snapshot found: Node will replay all historical transactions" - # in l - # for l in log.readlines() - # ), "New nodes shouldn't join from snapshot if snapshot evidence cannot be verified" - - # test_node_filter(network, args) - # test_retiring_nodes_emit_at_most_one_signature(network, args) - # else: - # test_learner_catches_up(network, args) - # # test_learner_does_not_take_part(network, args) - # test_retire_backup(network, args) + if args.consensus != "bft": + test_join_straddling_primary_replacement(network, args) + test_node_replacement(network, args) + test_add_node_from_backup(network, args) + test_add_node(network, args) + test_add_node_on_other_curve(network, args) + test_retire_backup(network, args) + test_add_as_many_pending_nodes(network, args) + test_add_node(network, args) + test_retire_primary(network, args) + + test_add_node_from_snapshot(network, args) + test_add_node_from_snapshot(network, args, from_backup=True) + test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) + latest_node_log = network.get_joined_nodes()[-1].remote.log_path() + with open(latest_node_log, "r+", encoding="utf-8") as log: + assert any( + "No snapshot found: Node will replay all historical transactions" + in l + for l in log.readlines() + ), "New nodes shouldn't join from snapshot if snapshot evidence cannot be verified" + + test_node_filter(network, args) + test_retiring_nodes_emit_at_most_one_signature(network, args) + else: + test_learner_catches_up(network, args) + # test_learner_does_not_take_part(network, args) + test_retire_backup(network, args) test_node_certificates_validity_period(network, args) From 122ea8d3c09ace5850951156b1f32574e74a46a2 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 30 Sep 2021 15:14:50 +0000 Subject: [PATCH 071/105] Recovery too --- src/node/config.h | 7 +++++-- src/node/rpc/node_frontend.h | 5 ++--- tests/recovery.py | 5 +++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/node/config.h b/src/node/config.h index fbc1b477eae..ac78c6135b1 100644 --- a/src/node/config.h +++ b/src/node/config.h @@ -21,13 +21,16 @@ namespace ccf std::optional reconfiguration_type = std::nullopt; - std::optional cert_maximum_validity_period_days = std::nullopt; + std::optional cert_maximum_validity_period_days = + std::nullopt; // TODO: Simply rename to cert_validity_period_days? bool operator==(const ServiceConfiguration& other) const { return recovery_threshold == other.recovery_threshold && consensus == other.consensus && - reconfiguration_type == other.reconfiguration_type; + reconfiguration_type == other.reconfiguration_type && + cert_maximum_validity_period_days == + other.cert_maximum_validity_period_days; } }; DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(ServiceConfiguration) diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index da7e75832fc..8a39afaa57e 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -1161,6 +1161,7 @@ namespace ccf g.set_constitution(in.genesis_info->constitution); } + auto config = ctx.tx.ro(this->network.config)->get(); if (!in.node_cert.has_value()) { auto endorsed_certificates = @@ -1172,9 +1173,7 @@ namespace ccf this->network.identity->priv_key, this->network.identity->cert, in.node_cert_valid_from, - in.genesis_info->configuration.cert_maximum_validity_period_days - .value())); - // TODO: What to do for recovery? Read existing value from store! + config->cert_maximum_validity_period_days.value_or(10))); } else { diff --git a/tests/recovery.py b/tests/recovery.py index fefdf041da7..4084e6fb20c 100644 --- a/tests/recovery.py +++ b/tests/recovery.py @@ -7,6 +7,8 @@ import infra.checker import suite.test_requirements as reqs +import reconfiguration + from loguru import logger as LOG @@ -119,6 +121,9 @@ def run(args): network.txs.issue(network, number_txs=1) network.txs.issue(network, number_txs=1, repeat=True) + # TODO: Delete + reconfiguration.test_node_certificates_validity_period(network, args) + # Alternate between recovery with primary change and stable primary-ship, # with and without snapshots if i % 2 == 0: From 9dc3e3ded845cf38421becae3d926ffbd41d4fe2 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 30 Sep 2021 15:24:16 +0000 Subject: [PATCH 072/105] . --- src/node/config.h | 11 +++++------ src/node/rpc/node_frontend.h | 9 ++++----- src/runtime_config/default/actions.js | 6 +++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/node/config.h b/src/node/config.h index ac78c6135b1..ef0f0601c6f 100644 --- a/src/node/config.h +++ b/src/node/config.h @@ -10,7 +10,7 @@ namespace ccf { - static constexpr auto default_node_certificate_validity_period_days = 365; + static constexpr auto default_node_cert_validity_period_days = 365; struct ServiceConfiguration { @@ -21,16 +21,15 @@ namespace ccf std::optional reconfiguration_type = std::nullopt; - std::optional cert_maximum_validity_period_days = - std::nullopt; // TODO: Simply rename to cert_validity_period_days? + std::optional node_cert_allowed_validity_period_days = std::nullopt; bool operator==(const ServiceConfiguration& other) const { return recovery_threshold == other.recovery_threshold && consensus == other.consensus && reconfiguration_type == other.reconfiguration_type && - cert_maximum_validity_period_days == - other.cert_maximum_validity_period_days; + node_cert_allowed_validity_period_days == + other.node_cert_allowed_validity_period_days; } }; DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(ServiceConfiguration) @@ -39,7 +38,7 @@ namespace ccf DECLARE_JSON_OPTIONAL_FIELDS( ServiceConfiguration, reconfiguration_type, - cert_maximum_validity_period_days) + node_cert_allowed_validity_period_days) // The there is always only one active configuration, so this is a single // Value diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 8a39afaa57e..c81e7b684d5 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -257,16 +257,14 @@ namespace ccf in.certificate_signing_request.has_value() && this->network.consensus_type == ConsensusType::CFT) { - // TODO: What if the configuration has no validity period (i.e. 1.x - // ledger)? Default to 365 days? - assert(config->cert_maximum_validity_period_days.has_value()); endorsed_certificate = context.get_node_state().generate_endorsed_certificate( in.certificate_signing_request.value(), this->network.identity->priv_key, this->network.identity->cert, in.node_cert_valid_from.value(), - config->cert_maximum_validity_period_days.value()); + config->node_cert_allowed_validity_period_days.value_or( + default_node_cert_validity_period_days)); node_endorsed_certificates->put( joining_node_id, {endorsed_certificate.value()}); @@ -1173,7 +1171,8 @@ namespace ccf this->network.identity->priv_key, this->network.identity->cert, in.node_cert_valid_from, - config->cert_maximum_validity_period_days.value_or(10))); + config->node_cert_allowed_validity_period_days.value_or( + default_node_cert_validity_period_days))); } else { diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index fcd8adb36f1..ae4f92c67de 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -785,7 +785,7 @@ const actions = new Map([ const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, nodeInfo.certificate_initial_valid_from, - serviceConfig.cert_maximum_validity_period_days + serviceConfig.node_cert_allowed_validity_period_days ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), @@ -947,10 +947,10 @@ const actions = new Map([ if ( args.validity_period_days > - serviceConfig.cert_maximum_validity_period_days + serviceConfig.node_cert_allowed_validity_period_days ) { throw new Error( - `Validity period ${args.validity_period_days} (days) must be less than or equal to service node certificate maximum validity period ${serviceConfig.cert_maximum_validity_period_days} (days)` + `Validity period ${args.validity_period_days} (days) must be less than or equal to service node certificate maximum validity period ${serviceConfig.node_cert_allowed_validity_period_days} (days)` ); } From 6ac4701a6e4f52d98653a8538d99fd2db70d8026 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 30 Sep 2021 16:21:11 +0000 Subject: [PATCH 073/105] Refactor certificate generation --- src/crypto/certs.h | 67 +++++++++++++++++++++++++++++++++++ src/js/wrap.cpp | 37 +++++++------------ src/node/certs.h | 35 ------------------ src/node/node_state.h | 38 +++++--------------- src/node/rpc/node_frontend.h | 25 +++++++------ src/node/rpc/node_interface.h | 6 ---- src/node/rpc/test/node_stub.h | 10 ------ 7 files changed, 100 insertions(+), 118 deletions(-) create mode 100644 src/crypto/certs.h delete mode 100644 src/node/certs.h diff --git a/src/crypto/certs.h b/src/crypto/certs.h new file mode 100644 index 00000000000..f4645351497 --- /dev/null +++ b/src/crypto/certs.h @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "key_pair.h" +#include "openssl/x509_time.h" +#include "pem.h" + +#include + +namespace crypto +{ + static std::string compute_cert_valid_to_string( + const std::string& valid_from, size_t validity_period_days) + { + // Note: As per RFC 5280, the validity period runs until "notAfter" + // _inclusive_ so substract one second from the validity period. + auto valid_to = OpenSSL::adjust_time(valid_from, validity_period_days, -1); + return OpenSSL::to_x509_time_string(OpenSSL::to_time_t(valid_to)); + } + + static Pem create_self_signed_cert( + const KeyPairPtr& key_pair, + const CertificateSubjectIdentity& csi, + const std::string& valid_from, + size_t validity_period_days) + { + return key_pair->self_sign( + csi, + true /* CA */, + valid_from, + compute_cert_valid_to_string(valid_from, validity_period_days)); + } + + static Pem create_endorsed_cert( + const Pem& csr, + const std::string& valid_from, + size_t validity_period_days, + const Pem& issuer_key_pair, + const Pem& issuer_cert) + { + return make_key_pair(issuer_key_pair) + ->sign_csr( + issuer_cert, + csr, + false /* Not CA */, + valid_from, + compute_cert_valid_to_string(valid_from, validity_period_days)); + } + + static Pem create_endorsed_cert( + const KeyPairPtr& subject_key_pair, + const CertificateSubjectIdentity& csi, + const std::string& valid_from, + size_t validity_period_days, + const Pem& issuer_key_pair, + const Pem& issuer_cert) + { + return create_endorsed_cert( + subject_key_pair->create_csr(csi), + valid_from, + validity_period_days, + issuer_key_pair, + issuer_cert); + } + +} \ No newline at end of file diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index c81deac547d..cbaa98643ae 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -4,6 +4,7 @@ #include "ccf/tx_id.h" #include "ccf/version.h" +#include "crypto/certs.h" #include "crypto/openssl/x509_time.h" #include "ds/logger.h" #include "enclave/rpc_context.h" @@ -535,16 +536,6 @@ namespace ccf::js auto global_obj = Context::JSWrappedValue(ctx, JS_GetGlobalObject(ctx)); auto ccf = Context::JSWrappedValue(ctx, JS_GetPropertyStr(ctx, global_obj, "ccf")); - auto node_ = - Context::JSWrappedValue(ctx, JS_GetPropertyStr(ctx, ccf, "node")); - - auto node = - static_cast(JS_GetOpaque(node_, node_class_id)); - - if (node == nullptr) - { - return JS_ThrowInternalError(ctx, "Node state is not set"); - } auto csr_cstr = JS_ToCString(ctx, argv[0]); if (csr_cstr == nullptr) @@ -569,12 +560,12 @@ namespace ccf::js return JS_EXCEPTION; } - auto endorsed_cert = node->generate_endorsed_certificate( + auto endorsed_cert = create_endorsed_cert( csr, - network->identity->priv_key, - network->identity->cert, valid_from, - validity_period_days); + validity_period_days, + network->identity->priv_key, + network->identity->cert); return JS_NewString(ctx, endorsed_cert.str().c_str()); } @@ -1464,19 +1455,15 @@ namespace ccf::js js_network_latest_ledger_secret_seqno, "getLatestLedgerSecretSeqno", 0)); - - if (node_state != nullptr) - { - JS_SetPropertyStr( + JS_SetPropertyStr( + ctx, + network, + "generateEndorsedCertificate", + JS_NewCFunction( ctx, - network, + js_network_generate_endorsed_certificate, "generateEndorsedCertificate", - JS_NewCFunction( - ctx, - js_network_generate_endorsed_certificate, - "generateEndorsedCertificate", - 0)); - } + 0)); } if (rpc_ctx != nullptr) diff --git a/src/node/certs.h b/src/node/certs.h deleted file mode 100644 index e11c76529ef..00000000000 --- a/src/node/certs.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the Apache 2.0 License. -#pragma once - -#include "crypto/openssl/x509_time.h" -#include "crypto/pem.h" - -#include - -namespace ccf -{ - static std::string compute_cert_valid_to_string( - const std::string& valid_from, size_t validity_period_days) - { - // Note: As per RFC 5280, the validity period runs until "notAfter" - // _inclusive_ so substract one second from the validity period. - auto valid_to = - crypto::OpenSSL::adjust_time(valid_from, validity_period_days, -1); - return crypto::OpenSSL::to_x509_time_string( - crypto::OpenSSL::to_time_t(valid_to)); - } - - crypto::Pem create_self_signed_cert( - const crypto::KeyPairPtr& key_pair, - const crypto::CertificateSubjectIdentity& csi, - const std::string& valid_from, - size_t validity_period_days) - { - return key_pair->self_sign( - csi, - true /* CA */, - valid_from, - compute_cert_valid_to_string(valid_from, validity_period_days)); - } -} \ No newline at end of file diff --git a/src/node/node_state.h b/src/node/node_state.h index 2856309d359..43ad7112e78 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -5,6 +5,7 @@ #include "blit.h" #include "consensus/aft/raft_consensus.h" #include "consensus/ledger_enclave.h" +#include "crypto/certs.h" #include "crypto/entropy.h" #include "crypto/pem.h" #include "crypto/symmetric_key.h" @@ -20,7 +21,6 @@ #include "hooks.h" #include "js/wrap.h" #include "network_state.h" -#include "node/certs.h" #include "node/http_node_client.h" #include "node/jwt_key_auto_refresh.h" #include "node/progress_tracker.h" @@ -1558,37 +1558,17 @@ namespace ccf } } - // TODO: Move to certs.h - Pem create_endorsed_node_cert() + crypto::Pem create_endorsed_node_cert() { // Only used by a 2.x node joining an existing 1.x service which will not // endorsed the identity of the new joiner. - auto nw = crypto::make_key_pair(network.identity->priv_key); - auto csr = - node_sign_kp->create_csr(config.node_certificate_subject_identity); - return nw->sign_csr( - network.identity->cert, - csr, - false, - compute_cert_valid_to_string( - config.startup_host_time, - config.node_cert_maximum_validity_period_days)); - } - - crypto::Pem generate_endorsed_certificate( - const crypto::Pem& subject_csr, - const crypto::Pem& endorser_private_key, - const crypto::Pem& endorser_cert, - const std::string& valid_from, - size_t validity_period_days) override - { - return crypto::make_key_pair(endorser_private_key) - ->sign_csr( - endorser_cert, - subject_csr, - false, - valid_from, - compute_cert_valid_to_string(valid_from, validity_period_days)); + return create_endorsed_cert( + node_sign_kp, + config.node_certificate_subject_identity, + config.startup_host_time, + config.node_cert_maximum_validity_period_days, + network.identity->priv_key, + network.identity->cert); } void accept_node_tls_connections() diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index c81e7b684d5..2b91ec7dfd3 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -7,10 +7,10 @@ #include "ccf/http_query.h" #include "ccf/json_handler.h" #include "ccf/version.h" +#include "crypto/certs.h" #include "crypto/csr.h" #include "crypto/hash.h" #include "frontend.h" -#include "node/certs.h" #include "node/entities.h" #include "node/network_state.h" #include "node/quote.h" @@ -257,14 +257,13 @@ namespace ccf in.certificate_signing_request.has_value() && this->network.consensus_type == ConsensusType::CFT) { - endorsed_certificate = - context.get_node_state().generate_endorsed_certificate( - in.certificate_signing_request.value(), - this->network.identity->priv_key, - this->network.identity->cert, - in.node_cert_valid_from.value(), - config->node_cert_allowed_validity_period_days.value_or( - default_node_cert_validity_period_days)); + endorsed_certificate = create_endorsed_cert( + in.certificate_signing_request.value(), + in.node_cert_valid_from.value(), + config->node_cert_allowed_validity_period_days.value_or( + default_node_cert_validity_period_days), + this->network.identity->priv_key, + this->network.identity->cert); node_endorsed_certificates->put( joining_node_id, {endorsed_certificate.value()}); @@ -1166,13 +1165,13 @@ namespace ccf ctx.tx.rw(network.node_endorsed_certificates); endorsed_certificates->put( in.node_id, - context.get_node_state().generate_endorsed_certificate( + create_endorsed_cert( in.certificate_signing_request, - this->network.identity->priv_key, - this->network.identity->cert, in.node_cert_valid_from, config->node_cert_allowed_validity_period_days.value_or( - default_node_cert_validity_period_days))); + default_node_cert_validity_period_days), + this->network.identity->priv_key, + this->network.identity->cert)); } else { diff --git a/src/node/rpc/node_interface.h b/src/node/rpc/node_interface.h index 4b4f372f705..48093a8ef14 100644 --- a/src/node/rpc/node_interface.h +++ b/src/node/rpc/node_interface.h @@ -50,11 +50,5 @@ namespace ccf CodeDigest& code_digest) = 0; virtual std::optional get_startup_snapshot_seqno() = 0; virtual SessionMetrics get_session_metrics() = 0; - virtual crypto::Pem generate_endorsed_certificate( - const crypto::Pem& subject_csr, - const crypto::Pem& endorser_private_key, - const crypto::Pem& endorser_cert, - const std::string& valid_from, - size_t validity_period_days) = 0; }; } \ No newline at end of file diff --git a/src/node/rpc/test/node_stub.h b/src/node/rpc/test/node_stub.h index af310023321..f378d7486e3 100644 --- a/src/node/rpc/test/node_stub.h +++ b/src/node/rpc/test/node_stub.h @@ -121,16 +121,6 @@ namespace ccf { return {}; } - - crypto::Pem generate_endorsed_certificate( - const crypto::Pem& subject_csr, - const crypto::Pem& endorser_private_key, - const crypto::Pem& endorser_cert, - const std::string& valid_from, - size_t validity_period_days) override - { - throw std::logic_error("Unimplemented"); - } }; class StubNodeStateCache : public historical::AbstractStateCache From f130861dd03b3278dcf1aeb1093bad0699a0bfb2 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 30 Sep 2021 16:24:04 +0000 Subject: [PATCH 074/105] Tweaks --- src/crypto/key_pair.h | 2 -- src/crypto/mbedtls/key_pair.cpp | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/crypto/key_pair.h b/src/crypto/key_pair.h index 8cc8c775f42..dca7f63ad6e 100644 --- a/src/crypto/key_pair.h +++ b/src/crypto/key_pair.h @@ -59,8 +59,6 @@ namespace crypto const std::optional& valid_from = std::nullopt, const std::optional& valid_to = std::nullopt) const = 0; - // TODO: Self-signed cert should also include valid_from and valid_to as - // arguments Pem self_sign( const std::string& name, const std::optional subject_alt_name = std::nullopt, diff --git a/src/crypto/mbedtls/key_pair.cpp b/src/crypto/mbedtls/key_pair.cpp index d13b7944a12..ff6c34ce82e 100644 --- a/src/crypto/mbedtls/key_pair.cpp +++ b/src/crypto/mbedtls/key_pair.cpp @@ -325,9 +325,10 @@ namespace crypto // Note: 825-day validity range // https://support.apple.com/en-us/HT210176 - // TODO: Use valid_from and valid_to MCHK(mbedtls_x509write_crt_set_validity( - crt.get(), "20210311000000", "20230611235959")); + crt.get(), + valid_from.value_or("20210311000000"), + valid_to.value_or("20230611235959"))); MCHK(mbedtls_x509write_crt_set_basic_constraints(crt.get(), ca ? 1 : 0, 0)); MCHK(mbedtls_x509write_crt_set_subject_key_identifier(crt.get())); From b69cb1c7de4323a28438773485d63321540a6e45 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 1 Oct 2021 10:40:09 +0000 Subject: [PATCH 075/105] Use primary host time on join --- src/crypto/mbedtls/key_pair.cpp | 5 ++-- src/crypto/openssl/key_pair.cpp | 1 - src/crypto/openssl/x509_time.h | 29 +-------------------- src/crypto/test/crypto.cpp | 37 --------------------------- src/host/main.cpp | 6 ++--- src/node/node_state.h | 3 +-- src/node/nodes.h | 6 +---- src/node/rpc/node_call_types.h | 1 - src/node/rpc/node_frontend.h | 16 +++++++++--- src/node/rpc/serialization.h | 4 +-- src/runtime_config/default/actions.js | 2 +- 11 files changed, 23 insertions(+), 87 deletions(-) diff --git a/src/crypto/mbedtls/key_pair.cpp b/src/crypto/mbedtls/key_pair.cpp index ff6c34ce82e..d13b7944a12 100644 --- a/src/crypto/mbedtls/key_pair.cpp +++ b/src/crypto/mbedtls/key_pair.cpp @@ -325,10 +325,9 @@ namespace crypto // Note: 825-day validity range // https://support.apple.com/en-us/HT210176 + // TODO: Use valid_from and valid_to MCHK(mbedtls_x509write_crt_set_validity( - crt.get(), - valid_from.value_or("20210311000000"), - valid_to.value_or("20230611235959"))); + crt.get(), "20210311000000", "20230611235959")); MCHK(mbedtls_x509write_crt_set_basic_constraints(crt.get(), ca ? 1 : 0, 0)); MCHK(mbedtls_x509write_crt_set_subject_key_identifier(crt.get())); diff --git a/src/crypto/openssl/key_pair.cpp b/src/crypto/openssl/key_pair.cpp index 74f52f9659f..1f5bb8ae0ae 100644 --- a/src/crypto/openssl/key_pair.cpp +++ b/src/crypto/openssl/key_pair.cpp @@ -260,7 +260,6 @@ namespace crypto // Note: 825-day validity range // https://support.apple.com/en-us/HT210176 - // TODO: valid_from and valid_to should always have a value Unique_ASN1_TIME before(valid_from.value_or("20210311000000Z")); Unique_ASN1_TIME after(valid_to.value_or("20230611235959Z")); diff --git a/src/crypto/openssl/x509_time.h b/src/crypto/openssl/x509_time.h index 5d25dd9e7cf..962e86ddd98 100644 --- a/src/crypto/openssl/x509_time.h +++ b/src/crypto/openssl/x509_time.h @@ -16,32 +16,6 @@ namespace crypto /** Set of utilites functions for working with x509 time, as defined in RFC 5280 (https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5.1) */ - /** Checks that two times are in chronological order, and optionally within - * a certain time range. - * - * @param time_before The time to check - * @param time_after The time to check against, which should be later than - * \p time_before - * @param allowed_diff_days The maximum allowed difference in days - * (optional) - * - * @return True if \p time_before is chronologically before \p time_after, - * and within \p allowed_diff_days days. - */ - static inline bool validate_chronological_times( - const Unique_ASN1_TIME& time_before, - const Unique_ASN1_TIME& time_after, - const std::optional& allowed_diff_days = std::nullopt) - { - int diff_days = 0; - int diff_secs = 0; - CHECK1(ASN1_TIME_diff(&diff_days, &diff_secs, time_before, time_after)); - - return diff_days > 0 && - (!allowed_diff_days.has_value() || - (unsigned int)diff_days <= allowed_diff_days.value()); - } - static inline Unique_ASN1_TIME from_time_t(const time_t& t) { return Unique_ASN1_TIME(ASN1_TIME_set(nullptr, t)); @@ -63,10 +37,9 @@ namespace crypto static inline std::string to_x509_time_string(const time_t& time) { - // TODO: Change format to YY instead of YYYY // Returns ASN1 time string (YYYYMMDDHHMMSSZ) from time_t, as per // https://www.openssl.org/docs/man1.1.1/man3/ASN1_UTCTIME_set.html - return fmt::format("{:%Y%m%d%H%M%SZ}", fmt::gmtime(time)); + return fmt::format("{:%y%m%d%H%M%SZ}", fmt::gmtime(time)); } } } \ No newline at end of file diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index 27b358fba63..d30f98f17b3 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -649,43 +649,6 @@ TEST_CASE("ASN1 time") auto next_day = crypto::OpenSSL::from_time_t(std::mktime(&next_day_time)); auto next_year = crypto::OpenSSL::from_time_t(std::mktime(&next_year_time)); - INFO("Validate chronological times"); - { - struct TimeTest - { - struct Input - { - std::tm from; - std::tm to; - std::optional maximum_validity_period_days = std::nullopt; - }; - Input input; - - bool expected_verification_result; - }; - - std::vector test_vectors{ - {{time, next_day_time}, true}, // Valid: Next day - {{time, time}, false}, // Invalid: Same date - {{next_day_time, time}, false}, // Invalid: to is before from - {{time, next_day_time, 100}, true}, // Valid: Next day within 100 days - {{time, next_year_time, 100}, - false}, // Valid: Next day not within 100 days - }; - - for (auto& data : test_vectors) - { - auto* from = &data.input.from; - auto* to = &data.input.to; - REQUIRE( - crypto::OpenSSL::validate_chronological_times( - crypto::OpenSSL::from_time_t(std::mktime(from)), - crypto::OpenSSL::from_time_t(std::mktime(to)), - data.input.maximum_validity_period_days) == - data.expected_verification_result); - } - } - INFO("Adjust time"); { std::vector times = {time, next_day_time, next_day_time}; diff --git a/src/host/main.cpp b/src/host/main.cpp index 19c845e40e3..33aee9d7163 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -796,11 +796,11 @@ int main(int argc, char** argv) ccf_config.node_cert_maximum_validity_period_days = node_cert_maximum_validity_period_days; - auto current_host_time = std::chrono::system_clock::now(); - LOG_INFO_FMT("Current host time: {}", current_host_time); + auto startup_host_time = std::chrono::system_clock::now(); + LOG_INFO_FMT("Startup host time: {}", startup_host_time); ccf_config.startup_host_time = crypto::OpenSSL::to_x509_time_string( - std::chrono::system_clock::to_time_t(current_host_time)); + std::chrono::system_clock::to_time_t(startup_host_time)); if (*start) { diff --git a/src/node/node_state.h b/src/node/node_state.h index 43ad7112e78..1bf06ae47cf 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -339,7 +339,6 @@ namespace ccf config.node_certificate_subject_identity, config.startup_host_time, config.node_cert_maximum_validity_period_days); - LOG_FAIL_FMT("{}", self_signed_node_cert.str()); accept_node_tls_connections(); open_frontend(ActorsType::nodes); @@ -655,7 +654,6 @@ namespace ccf join_params.startup_seqno = startup_seqno; join_params.certificate_signing_request = node_sign_kp->create_csr(config.node_certificate_subject_identity); - join_params.node_cert_valid_from = config.startup_host_time; LOG_DEBUG_FMT( "Sending join request to {}:{}", @@ -1844,6 +1842,7 @@ namespace ccf return kv::ConsensusHookPtr(nullptr); })); + // TODO: Should be global hook network.tables->set_map_hook( network.node_endorsed_certificates.get_name(), network.node_endorsed_certificates.wrap_map_hook( diff --git a/src/node/nodes.h b/src/node/nodes.h index 26ca453b2db..885dcf93c49 100644 --- a/src/node/nodes.h +++ b/src/node/nodes.h @@ -62,9 +62,6 @@ namespace ccf /// Public key std::optional public_key = std::nullopt; - /// Initial node certificate valid from - std::optional certificate_initial_valid_from = std::nullopt; - /** * Fields below are deprecated */ @@ -84,8 +81,7 @@ namespace ccf ledger_secret_seqno, code_digest, certificate_signing_request, - public_key, - certificate_initial_valid_from); + public_key); using Nodes = ServiceMap; using NodeEndorsedCertificates = diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 68d95f9fde1..5abbc7923e4 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -100,7 +100,6 @@ namespace ccf ConsensusType consensus_type = ConsensusType::CFT; std::optional startup_seqno = std::nullopt; std::optional certificate_signing_request = std::nullopt; - std::optional node_cert_valid_from = std::nullopt; }; struct Out diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 2b91ec7dfd3..d75b3fd9f26 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -218,8 +218,7 @@ namespace ccf ledger_secret_seqno, ds::to_hex(code_digest.data), in.certificate_signing_request, - client_public_key_pem, - in.node_cert_valid_from}; + client_public_key_pem}; // Because the certificate signature scheme is non-deterministic, only // self-signed node certificate is recorded in the node info table @@ -257,9 +256,20 @@ namespace ccf in.certificate_signing_request.has_value() && this->network.consensus_type == ConsensusType::CFT) { + ::timespec time; + ccf::ApiResult result = get_untrusted_host_time_v1(time); + if (result != ccf::ApiResult::OK) + { + return ccf::make_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Unable to get time: {}", ccf::api_result_to_str(result))); + } + endorsed_certificate = create_endorsed_cert( in.certificate_signing_request.value(), - in.node_cert_valid_from.value(), + crypto::OpenSSL::to_x509_time_string(time.tv_sec), config->node_cert_allowed_validity_period_days.value_or( default_node_cert_validity_period_days), this->network.identity->priv_key, diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index be7ed4bae85..845065e5ed3 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -37,9 +37,7 @@ namespace ccf consensus_type, startup_seqno) DECLARE_JSON_OPTIONAL_FIELDS( - JoinNetworkNodeToNode::In, - certificate_signing_request, - node_cert_valid_from) + JoinNetworkNodeToNode::In, certificate_signing_request) DECLARE_JSON_ENUM( ccf::IdentityType, diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index ae4f92c67de..50d0a8f6082 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -784,7 +784,7 @@ const actions = new Map([ // Note: CSR is only present from 2.x const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, - nodeInfo.certificate_initial_valid_from, + "211001100000Z", // TODO: Change to host time then serviceConfig.node_cert_allowed_validity_period_days ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( From c1f09b65ee9e8cf6cf12b70bb43705bdf95b0c6c Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 1 Oct 2021 10:45:25 +0000 Subject: [PATCH 076/105] Global hook --- src/node/node_state.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/node/node_state.h b/src/node/node_state.h index 1bf06ae47cf..034e5a0ad98 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1842,13 +1842,12 @@ namespace ccf return kv::ConsensusHookPtr(nullptr); })); - // TODO: Should be global hook - network.tables->set_map_hook( + network.tables->set_global_hook( network.node_endorsed_certificates.get_name(), - network.node_endorsed_certificates.wrap_map_hook( + network.node_endorsed_certificates.wrap_commit_hook( [this]( kv::Version hook_version, - const NodeEndorsedCertificates::Write& w) -> kv::ConsensusHookPtr { + const NodeEndorsedCertificates::Write& w) { for (auto const& [node_id, endorsed_certificate] : w) { if (node_id != self) @@ -1870,8 +1869,6 @@ namespace ccf open_frontend(ActorsType::members); open_user_frontend(); } - - return kv::ConsensusHookPtr(nullptr); })); } From 52f0058eb1c0f1557d2f5f61dec967921e211632 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 1 Oct 2021 10:50:44 +0000 Subject: [PATCH 077/105] Rename node certificate renewal proposal action --- python/ccf/proposal_generator.py | 4 ++-- src/node/node_state.h | 9 ++++++--- src/runtime_config/default/actions.js | 2 +- tests/governance.py | 2 +- tests/infra/consortium.py | 4 ++-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/python/ccf/proposal_generator.py b/python/ccf/proposal_generator.py index 24ba9f8bcfa..89e15d5d58e 100644 --- a/python/ccf/proposal_generator.py +++ b/python/ccf/proposal_generator.py @@ -326,11 +326,11 @@ def set_jwt_public_signing_keys(issuer: str, jwks_path: str, **kwargs): @cli_proposal -def renew_node_certificate( +def trigger_node_certificate_renewal( node_id: str, valid_from: str, validity_period_days: int, **kwargs ): return build_proposal( - "renew_node_certificate", + "trigger_node_certificate_renewal", { "node_id": node_id, "valid_from": valid_from, diff --git a/src/node/node_state.h b/src/node/node_state.h index 034e5a0ad98..1bf06ae47cf 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1842,12 +1842,13 @@ namespace ccf return kv::ConsensusHookPtr(nullptr); })); - network.tables->set_global_hook( + // TODO: Should be global hook + network.tables->set_map_hook( network.node_endorsed_certificates.get_name(), - network.node_endorsed_certificates.wrap_commit_hook( + network.node_endorsed_certificates.wrap_map_hook( [this]( kv::Version hook_version, - const NodeEndorsedCertificates::Write& w) { + const NodeEndorsedCertificates::Write& w) -> kv::ConsensusHookPtr { for (auto const& [node_id, endorsed_certificate] : w) { if (node_id != self) @@ -1869,6 +1870,8 @@ namespace ccf open_frontend(ActorsType::members); open_user_frontend(); } + + return kv::ConsensusHookPtr(nullptr); })); } diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 50d0a8f6082..a01d26086e7 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -911,7 +911,7 @@ const actions = new Map([ ), ], [ - "renew_node_certificate", + "trigger_node_certificate_renewal", new Action( function (args) { checkEntityId(args.node_id, "node_id"); diff --git a/tests/governance.py b/tests/governance.py index 4bee889b308..de32bc79436 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -248,7 +248,7 @@ def test_node_cert_renewal(network, args): ) try: - network.consortium.renew_node_certificate( + network.consortium.trigger_node_certificate_renewal( node, node.node_id, valid_from=str(infra.crypto.datetime_as_UTCtime(valid_from)), diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index ca9020476b5..0c46650c914 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -535,9 +535,9 @@ def retire_code(self, remote_node, code_id): proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) - def renew_node_certificate(self, remote_node, *args, **kwargs): + def trigger_node_certificate_renewal(self, remote_node, *args, **kwargs): proposal_body, careful_vote = self.make_proposal( - "renew_node_certificate", *args, **kwargs + "trigger_node_certificate_renewal", *args, **kwargs ) proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) From 0ce2f0d648577f5b639d32b15c7873be9fff5b79 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 5 Oct 2021 15:06:33 +0000 Subject: [PATCH 078/105] Rename proposal --- python/ccf/proposal_generator.py | 4 ++-- src/host/main.cpp | 8 ++++---- src/runtime_config/default/actions.js | 8 +++++++- tests/governance.py | 2 +- tests/infra/consortium.py | 4 ++-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/python/ccf/proposal_generator.py b/python/ccf/proposal_generator.py index 89e15d5d58e..209d3ff6d4c 100644 --- a/python/ccf/proposal_generator.py +++ b/python/ccf/proposal_generator.py @@ -326,11 +326,11 @@ def set_jwt_public_signing_keys(issuer: str, jwks_path: str, **kwargs): @cli_proposal -def trigger_node_certificate_renewal( +def set_node_certificate_validity( node_id: str, valid_from: str, validity_period_days: int, **kwargs ): return build_proposal( - "trigger_node_certificate_renewal", + "set_node_certificate_validity", { "node_id": node_id, "valid_from": valid_from, diff --git a/src/host/main.cpp b/src/host/main.cpp index 33aee9d7163..8d4fa89a45f 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -402,10 +402,10 @@ int main(int argc, char** argv) ->capture_default_str(); // TODO: Should this be under a different name? - // Also, because there's no way to renew a self-signed cert, should this be - // hardcoded instead, assuming that the self-signed certificate gets - // overridden by the service-endorsed one pretty quickly? - size_t node_cert_maximum_validity_period_days = 365; + // By default, node certificates are only valid for one day. It is expected + // that members will submit a proposal to renew the node certificates before + // expiry, at the point the service is open. + size_t node_cert_maximum_validity_period_days = 1; app .add_option( "--node-cert-max-validity-days", diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index a01d26086e7..9ace6b96eb8 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -151,6 +151,11 @@ function invalidateOtherOpenProposals(proposalIdToRetain) { }); } +// TODO: Only implement non-ACL-specific proposals for now +// 1. [DONE] Rename proposal to `set_node_certificate_validity` +// 2. Pass `valid_to` to `transition_node_to_trusted` +// 3. Rename things and set initial default period to 24h + const actions = new Map([ [ "set_constitution", @@ -911,7 +916,7 @@ const actions = new Map([ ), ], [ - "trigger_node_certificate_renewal", + "set_node_certificate_validity", new Action( function (args) { checkEntityId(args.node_id, "node_id"); @@ -937,6 +942,7 @@ const actions = new Map([ ); } + // TODO: Do we still need a service-wide configuration value? const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( getSingletonKvKey() ); diff --git a/tests/governance.py b/tests/governance.py index de32bc79436..01f7be03c4c 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -248,7 +248,7 @@ def test_node_cert_renewal(network, args): ) try: - network.consortium.trigger_node_certificate_renewal( + network.consortium.set_node_certificate_validity( node, node.node_id, valid_from=str(infra.crypto.datetime_as_UTCtime(valid_from)), diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 0c46650c914..925d63cd4ee 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -535,9 +535,9 @@ def retire_code(self, remote_node, code_id): proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) - def trigger_node_certificate_renewal(self, remote_node, *args, **kwargs): + def set_node_certificate_validity(self, remote_node, *args, **kwargs): proposal_body, careful_vote = self.make_proposal( - "trigger_node_certificate_renewal", *args, **kwargs + "set_node_certificate_validity", *args, **kwargs ) proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) From 014d4bc5d0f145157c7a1ccfb5b1aa3d5c70fd8d Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 5 Oct 2021 15:29:04 +0000 Subject: [PATCH 079/105] Rename cchost argument --- src/enclave/interface.h | 4 ++-- src/host/main.cpp | 13 ++++++------- src/node/node_state.h | 7 ++++--- src/runtime_config/default/actions.js | 2 +- tests/governance.py | 4 ++-- tests/infra/e2e_args.py | 4 ++-- tests/infra/network.py | 2 +- tests/infra/remote.py | 8 +++++--- tests/reconfiguration.py | 2 +- 9 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/enclave/interface.h b/src/enclave/interface.h index 53f27c8d93f..36dbe917a3f 100644 --- a/src/enclave/interface.h +++ b/src/enclave/interface.h @@ -79,7 +79,7 @@ struct CCFConfig size_t jwt_key_refresh_interval_s; crypto::CurveID curve_id; - size_t node_cert_maximum_validity_period_days; + size_t initial_node_certificate_validity_period_days; std::string startup_host_time; }; @@ -109,7 +109,7 @@ DECLARE_JSON_REQUIRED_FIELDS( node_certificate_subject_identity, jwt_key_refresh_interval_s, curve_id, - node_cert_maximum_validity_period_days, + initial_node_certificate_validity_period_days, startup_host_time); /// General administrative messages diff --git a/src/host/main.cpp b/src/host/main.cpp index 8d4fa89a45f..540f8172da0 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -401,16 +401,15 @@ int main(int argc, char** argv) ->transform(CLI::CheckedTransformer(curve_id_map, CLI::ignore_case)) ->capture_default_str(); - // TODO: Should this be under a different name? // By default, node certificates are only valid for one day. It is expected // that members will submit a proposal to renew the node certificates before // expiry, at the point the service is open. - size_t node_cert_maximum_validity_period_days = 1; + size_t initial_node_certificate_validity_period_days = 1; app .add_option( - "--node-cert-max-validity-days", - node_cert_maximum_validity_period_days, - "Maximum number of days node certificates must be valid for.") + "--initial-node-cert-validity-days", + initial_node_certificate_validity_period_days, + "Number of days node certificates are initially valid for") ->check(CLI::PositiveNumber) ->type_name("UINT"); @@ -793,8 +792,8 @@ int main(int argc, char** argv) node_certificate_subject_identity; ccf_config.jwt_key_refresh_interval_s = jwt_key_refresh_interval_s; ccf_config.curve_id = curve_id; - ccf_config.node_cert_maximum_validity_period_days = - node_cert_maximum_validity_period_days; + ccf_config.initial_node_certificate_validity_period_days = + initial_node_certificate_validity_period_days; auto startup_host_time = std::chrono::system_clock::now(); LOG_INFO_FMT("Startup host time: {}", startup_host_time); diff --git a/src/node/node_state.h b/src/node/node_state.h index 1bf06ae47cf..91bfc0e757c 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -338,7 +338,7 @@ namespace ccf node_sign_kp, config.node_certificate_subject_identity, config.startup_host_time, - config.node_cert_maximum_validity_period_days); + config.initial_node_certificate_validity_period_days); accept_node_tls_connections(); open_frontend(ActorsType::nodes); @@ -1564,7 +1564,7 @@ namespace ccf node_sign_kp, config.node_certificate_subject_identity, config.startup_host_time, - config.node_cert_maximum_validity_period_days, + config.initial_node_certificate_validity_period_days, network.identity->priv_key, network.identity->cert); } @@ -1631,7 +1631,8 @@ namespace ccf config.genesis.recovery_threshold, network.consensus_type, reconf_type, - config.node_cert_maximum_validity_period_days}; + config.initial_node_certificate_validity_period_days}; // TODO: Change + // this create_params.genesis_info = genesis_info; } diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 9ace6b96eb8..dacf5495b66 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -789,7 +789,7 @@ const actions = new Map([ // Note: CSR is only present from 2.x const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, - "211001100000Z", // TODO: Change to host time then + "211001100000Z", // TODO: Get argument from proposal paramters serviceConfig.node_cert_allowed_validity_period_days ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( diff --git a/tests/governance.py b/tests/governance.py index 01f7be03c4c..3ff2053e300 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -225,8 +225,8 @@ def test_node_cert_renewal(network, args): now = datetime.now().replace( microsecond=0 ) # Truncate microseconds which are not reflected in RFC5280 UTCTime - validity_period_allowed = args.node_cert_max_validity_days - 1 - validity_period_forbidden = args.node_cert_max_validity_days + 1 + validity_period_allowed = args.initial_node_cert_validity_days - 1 + validity_period_forbidden = args.initial_node_cert_validity_days + 1 test_vectors = [ (now, validity_period_allowed, None), diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index 26afbcea1cc..57669495d2f 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -351,8 +351,8 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): default=None, ) parser.add_argument( - "--node-cert-max-validity-days", - help="Maximum number of days node certificates must be valid for", + "--initial-node-cert-validity-days", + help="Number of days node certificates are initially valid for", type=int, default=365, ) diff --git a/tests/infra/network.py b/tests/infra/network.py index 7f1989b881e..3b98ab1e1e0 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -104,7 +104,7 @@ class Network: "common_read_only_ledger_dir", "curve_id", "client_connection_timeout_ms", - "node_cert_max_validity_days", + "initial_node_cert_validity_days", ] # Maximum delay (seconds) for updates to propagate from the primary to backups diff --git a/tests/infra/remote.py b/tests/infra/remote.py index 5fadaa0fc95..547d699c655 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -578,7 +578,7 @@ def __init__( jwt_key_refresh_interval_s=None, curve_id=None, client_connection_timeout_ms=None, - node_cert_max_validity_days=None, + initial_node_cert_validity_days=None, version=None, include_addresses=True, additional_raw_node_args=None, @@ -705,8 +705,10 @@ def __init__( if client_connection_timeout_ms: cmd += [f"--client-connection-timeout-ms={client_connection_timeout_ms}"] - if node_cert_max_validity_days: - cmd += [f"--node-cert-max-validity-days={node_cert_max_validity_days}"] + if initial_node_cert_validity_days: + cmd += [ + f"--initial-node-cert-validity-days={initial_node_cert_validity_days}" + ] if additional_raw_node_args: for s in additional_raw_node_args: diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 969a2b4fa93..5c726a4c5eb 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -53,7 +53,7 @@ def verify_node_certificate_validity_period(node, args): # Note: CCF substracts one second from validity period since x509 # specifies that validity dates are inclusive. expected_valid_to = valid_from + timedelta( - days=args.node_cert_max_validity_days, seconds=-1 + days=args.initial_node_cert_validity_days, seconds=-1 ) if valid_to != expected_valid_to: raise ValueError( From 60929a2e60116329041d857f367cafc1ae9a785f Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 5 Oct 2021 16:09:06 +0000 Subject: [PATCH 080/105] Add separate CLI argument for max allowed certificate validity period --- src/enclave/interface.h | 7 ++++++- src/host/main.cpp | 14 +++++++++++++- src/node/node_state.h | 4 +--- tests/infra/e2e_args.py | 9 ++++++++- tests/infra/network.py | 1 + tests/infra/remote.py | 6 ++++++ 6 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/enclave/interface.h b/src/enclave/interface.h index 36dbe917a3f..f68a02913d5 100644 --- a/src/enclave/interface.h +++ b/src/enclave/interface.h @@ -63,6 +63,7 @@ struct CCFConfig std::vector members_info; std::string constitution; size_t recovery_threshold; + size_t max_allowed_node_cert_validity_days; }; Genesis genesis = {}; @@ -89,7 +90,11 @@ DECLARE_JSON_REQUIRED_FIELDS( DECLARE_JSON_TYPE(CCFConfig::Genesis); DECLARE_JSON_REQUIRED_FIELDS( - CCFConfig::Genesis, members_info, constitution, recovery_threshold); + CCFConfig::Genesis, + members_info, + constitution, + recovery_threshold, + max_allowed_node_cert_validity_days); DECLARE_JSON_TYPE(CCFConfig::Joining); DECLARE_JSON_REQUIRED_FIELDS( diff --git a/src/host/main.cpp b/src/host/main.cpp index 540f8172da0..2ef5016dbae 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -409,7 +409,8 @@ int main(int argc, char** argv) .add_option( "--initial-node-cert-validity-days", initial_node_certificate_validity_period_days, - "Number of days node certificates are initially valid for") + "Initial validity period (days) for certificates of nodes before the " + "service is open by members") ->check(CLI::PositiveNumber) ->type_name("UINT"); @@ -457,6 +458,15 @@ int main(int argc, char** argv) ->check(CLI::PositiveNumber) ->type_name("UINT"); + size_t max_allowed_node_cert_validity_days = 365; + start + ->add_option( + "--max-allowed-node-cert-validity-days", + max_allowed_node_cert_validity_days, + "Maximum validity period (days) for certificates of trusted nodes") + ->check(CLI::PositiveNumber) + ->type_name("UINT"); + auto join = app.add_subcommand("join", "Join existing network"); join->configurable(); @@ -838,6 +848,8 @@ int main(int argc, char** argv) files::slurp_string(constitution_path); } ccf_config.genesis.recovery_threshold = recovery_threshold.value(); + ccf_config.genesis.max_allowed_node_cert_validity_days = + max_allowed_node_cert_validity_days; LOG_INFO_FMT( "Creating new node: new network (with {} initial member(s) and {} " "member(s) required for recovery)", diff --git a/src/node/node_state.h b/src/node/node_state.h index 91bfc0e757c..300d275fde3 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1631,9 +1631,7 @@ namespace ccf config.genesis.recovery_threshold, network.consensus_type, reconf_type, - config.initial_node_certificate_validity_period_days}; // TODO: Change - // this - + config.genesis.max_allowed_node_cert_validity_days}; create_params.genesis_info = genesis_info; } diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index 57669495d2f..2278902a303 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -352,7 +352,14 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): ) parser.add_argument( "--initial-node-cert-validity-days", - help="Number of days node certificates are initially valid for", + help="Initial validity period (days) for certificates of nodes before the " + "service is open by members", + type=int, + default=365, + ) + parser.add_argument( + "--max-allowed-node-cert-validity-days", + help="Maximum validity period (days) for certificates of trusted nodes", type=int, default=365, ) diff --git a/tests/infra/network.py b/tests/infra/network.py index 3b98ab1e1e0..e3886b45941 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -105,6 +105,7 @@ class Network: "curve_id", "client_connection_timeout_ms", "initial_node_cert_validity_days", + "max_allowed_node_cert_validity_days", ] # Maximum delay (seconds) for updates to propagate from the primary to backups diff --git a/tests/infra/remote.py b/tests/infra/remote.py index 547d699c655..4742fbb49dc 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -579,6 +579,7 @@ def __init__( curve_id=None, client_connection_timeout_ms=None, initial_node_cert_validity_days=None, + max_allowed_node_cert_validity_days=None, version=None, include_addresses=True, additional_raw_node_args=None, @@ -739,6 +740,11 @@ def __init__( data_files.append(os.path.join(self.common_dir, mf)) cmd += [member_info_cmd] + if max_allowed_node_cert_validity_days: + cmd += [ + f"--max-allowed-node-cert-validity-days={max_allowed_node_cert_validity_days}" + ] + elif start_type == StartType.join: cmd += [ "join", From 5947b80f0022204805c314c602523dcea5fb48d8 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 14 Oct 2021 13:33:22 +0000 Subject: [PATCH 081/105] Improve verification of node cert validity in e2e tests --- src/node/node_state.h | 14 +++++---- src/node/rpc/node_frontend.h | 4 +-- src/runtime_config/default/actions.js | 1 + tests/infra/certs.py | 43 +++++++++++++++++++++++++++ tests/infra/crypto.py | 11 +------ tests/infra/e2e_args.py | 2 +- tests/lts_compatibility.py | 7 +++++ tests/reconfiguration.py | 41 +++++++------------------ 8 files changed, 73 insertions(+), 50 deletions(-) create mode 100644 tests/infra/certs.py diff --git a/src/node/node_state.h b/src/node/node_state.h index 25506dd30b1..0a27ed579c0 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -326,7 +326,8 @@ namespace ccf if (network.consensus_type == ConsensusType::BFT) { - endorsed_node_cert = create_endorsed_node_cert(); + endorsed_node_cert = create_endorsed_node_cert( + config.initial_node_certificate_validity_period_days); history->set_endorsed_certificate(endorsed_node_cert.value()); accept_network_tls_connections(); open_frontend(ActorsType::members); @@ -493,7 +494,8 @@ namespace ccf // from 2.x (CFT only). When joining an existing 1.x service, // self-sign own certificate and use it to endorse TLS // connections. - endorsed_node_cert = create_endorsed_node_cert(); + endorsed_node_cert = create_endorsed_node_cert( + default_node_cert_validity_period_days); history->set_endorsed_certificate(endorsed_node_cert.value()); n2n_channels_cert = endorsed_node_cert.value(); open_frontend(ActorsType::members); @@ -904,7 +906,8 @@ namespace ccf auto tx = network.tables->create_read_only_tx(); if (network.consensus_type == ConsensusType::BFT) { - endorsed_node_cert = create_endorsed_node_cert(); + endorsed_node_cert = create_endorsed_node_cert( + config.initial_node_certificate_validity_period_days); history->set_endorsed_certificate(endorsed_node_cert.value()); accept_network_tls_connections(); open_frontend(ActorsType::members); @@ -1513,7 +1516,7 @@ namespace ccf } } - crypto::Pem create_endorsed_node_cert() + crypto::Pem create_endorsed_node_cert(size_t validity_period_days) { // Only used by a 2.x node joining an existing 1.x service which will not // endorsed the identity of the new joiner. @@ -1521,7 +1524,7 @@ namespace ccf node_sign_kp, config.node_certificate_subject_identity, config.startup_host_time, - config.initial_node_certificate_validity_period_days, + validity_period_days, network.identity->priv_key, network.identity->cert); } @@ -1798,7 +1801,6 @@ namespace ccf return kv::ConsensusHookPtr(nullptr); })); - // TODO: Should be global hook network.tables->set_map_hook( network.node_endorsed_certificates.get_name(), network.node_endorsed_certificates.wrap_map_hook( diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 4509567e31c..ab434b7da41 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -267,7 +267,7 @@ namespace ccf "Unable to get time: {}", ccf::api_result_to_str(result))); } - endorsed_certificate = create_endorsed_cert( + endorsed_certificate = crypto::create_endorsed_cert( in.certificate_signing_request.value(), crypto::OpenSSL::to_x509_time_string(time.tv_sec), config->node_cert_allowed_validity_period_days.value_or( @@ -1179,7 +1179,7 @@ namespace ccf ctx.tx.rw(network.node_endorsed_certificates); endorsed_certificates->put( in.node_id, - create_endorsed_cert( + crypto::create_endorsed_cert( in.certificate_signing_request, in.node_cert_valid_from, config->node_cert_allowed_validity_period_days.value_or( diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index dacf5495b66..a23ba7c656f 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -155,6 +155,7 @@ function invalidateOtherOpenProposals(proposalIdToRetain) { // 1. [DONE] Rename proposal to `set_node_certificate_validity` // 2. Pass `valid_to` to `transition_node_to_trusted` // 3. Rename things and set initial default period to 24h +// 4. Create set_all_nodes_certificate_validity proposal const actions = new Map([ [ diff --git a/tests/infra/certs.py b/tests/infra/certs.py new file mode 100644 index 00000000000..e48cf4723da --- /dev/null +++ b/tests/infra/certs.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the Apache 2.0 License. + +from datetime import datetime, timedelta +from cryptography.hazmat.backends import default_backend +from cryptography.x509 import ( + load_pem_x509_certificate, +) +from pyasn1.type.useful import UTCTime + + +def get_validity_period_from_pem_cert(pem: str): + cert = load_pem_x509_certificate(pem.encode(), default_backend()) + return cert.not_valid_before, cert.not_valid_after + + +def datetime_as_UTCtime(datetime: datetime): + return UTCTime.fromDateTime(datetime) + + +def verify_certificate_validity_period( + pem: str, expected_validity_period_days: int, expected_valid_from=None +): + valid_from, valid_to = get_validity_period_from_pem_cert(pem) + + # By default, assume that certificate has been issued within this test run + expected_valid_from = expected_valid_from or ( + datetime.utcnow() - timedelta(hours=1) + ) + if valid_from < expected_valid_from: + raise ValueError( + f'Certificate is too old: valid from "{valid_from}", expected "{expected_valid_from}"' + ) + + # Note: CCF substracts one second from validity period since x509 + # specifies that validity dates are inclusive. + expected_valid_to = valid_from + timedelta( + days=expected_validity_period_days, seconds=-1 + ) + if valid_to != expected_valid_to: + raise ValueError( + f'Validity period for certiticate is not as expected: valid to "{valid_to}, expected to "{expected_valid_to}"' + ) diff --git a/tests/infra/crypto.py b/tests/infra/crypto.py index a5c4b6ab6ae..7fa69046693 100644 --- a/tests/infra/crypto.py +++ b/tests/infra/crypto.py @@ -8,7 +8,7 @@ import datetime import hashlib from datetime import datetime -from pyasn1.type.useful import UTCTime + from cryptography import x509 from cryptography.x509.oid import NameOID @@ -273,12 +273,3 @@ def check_key_pair_pem(private: str, public: str, password=None) -> bool: ) pub_der = pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) return prv_pub_der == pub_der - - -def get_validity_period_from_pem_cert(pem: str): - cert = load_pem_x509_certificate(pem.encode(), default_backend()) - return cert.not_valid_before, cert.not_valid_after - - -def datetime_as_UTCtime(datetime: datetime): - return UTCTime.fromDateTime(datetime) diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index 2278902a303..a72a170e398 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -355,7 +355,7 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): help="Initial validity period (days) for certificates of nodes before the " "service is open by members", type=int, - default=365, + default=365, # TODO: Should be 1 ) parser.add_argument( "--max-allowed-node-cert-validity-days", diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index 7ea911815ff..82a8703d83b 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -167,6 +167,13 @@ def run_code_upgrade_from( new_node, args.package, args, from_snapshot=from_snapshot ) network.trust_node(new_node, args) + # Note: validity period for 2.x node joining 1.x service is hardcoded + # to 365 days since existing service is not capable of issuing endorsed + # node certificate + infra.certs.verify_certificate_validity_period( + new_node.get_tls_certificate_pem(), + expected_validity_period_days=365, + ) from_snapshot = not from_snapshot new_nodes.append(new_node) diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 116e8029f86..71d1ee3cec5 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -12,7 +12,7 @@ import ccf.ledger import json import infra.crypto -from datetime import datetime, timedelta +import infra.certs from loguru import logger as LOG @@ -37,39 +37,14 @@ def count_nodes(configs, network): return len(nodes) -def verify_node_certificate_validity_period(node, args): - # Verify self-signed certificate validity period - valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( - node.get_tls_certificate_pem() - ) - - # Node certificate should have been issued within this test run (generous window in case - # the node was spun a while ago) - if valid_from < datetime.utcnow() - timedelta(hours=3): - raise ValueError( - f'Node {node.local_node_id} certificate is too old: valid from "{valid_from}"' - ) - - # Note: CCF substracts one second from validity period since x509 - # specifies that validity dates are inclusive. - expected_valid_to = valid_from + timedelta( - days=args.initial_node_cert_validity_days, seconds=-1 - ) - if valid_to != expected_valid_to: - raise ValueError( - f'Validity period for node {node.local_node_id} certiticate is not as expected: from "{valid_from}"" to "{valid_to}"" but expected to "{expected_valid_to}"' - ) - LOG.info( - f"Verified validity period for node {node.local_node_id} certificate: {valid_from} - {valid_to}" - ) - - @reqs.description("Adding a valid node without snapshot") def test_add_node(network, args): new_node = network.create_node("local://localhost") network.join_node(new_node, args.package, args, from_snapshot=False) # Verify self-signed node certificate validity period - verify_node_certificate_validity_period(new_node, args) + infra.certs.verify_certificate_validity_period( + new_node.get_tls_certificate_pem(), args.initial_node_cert_validity_days + ) network.trust_node(new_node, args) with new_node.client() as c: s = c.get("/node/state") @@ -81,7 +56,6 @@ def test_add_node(network, args): return network -@reqs.description("Adding a node on different curve") def test_add_node_on_other_curve(network, args): original_curve = args.curve_id args.curve_id = ( @@ -444,7 +418,12 @@ def test_learner_does_not_take_part(network, args): @reqs.description("Test node certificates validity period") def test_node_certificates_validity_period(network, args): for node in network.get_joined_nodes(): - verify_node_certificate_validity_period(node, args) + infra.certs.verify_certificate_validity_period( + node.get_tls_certificate_pem(), args.max_allowed_node_cert_validity_days + ) + LOG.info( + f"Certificate validity period for node {node.local_node_id} successfully verified" + ) def run(args): From ad69b0f05736e26d5478b8bedc997c1b4f6988dd Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Thu, 14 Oct 2021 15:45:08 +0000 Subject: [PATCH 082/105] More tests are now passing --- src/runtime_config/default/actions.js | 1 + tests/infra/certs.py | 2 ++ tests/infra/crypto.py | 1 - tests/infra/e2e_args.py | 2 +- tests/infra/node.py | 1 + tests/infra/remote.py | 27 ++++++++++++++++----------- tests/lts_compatibility.py | 8 +++++++- tests/reconfiguration.py | 4 ++-- tests/recovery.py | 6 +++--- 9 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index a23ba7c656f..9eb760020a0 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -156,6 +156,7 @@ function invalidateOtherOpenProposals(proposalIdToRetain) { // 2. Pass `valid_to` to `transition_node_to_trusted` // 3. Rename things and set initial default period to 24h // 4. Create set_all_nodes_certificate_validity proposal +// 5. Add proposal to set max allowed node certificate validity const actions = new Map([ [ diff --git a/tests/infra/certs.py b/tests/infra/certs.py index e48cf4723da..00e8ece65d5 100644 --- a/tests/infra/certs.py +++ b/tests/infra/certs.py @@ -41,3 +41,5 @@ def verify_certificate_validity_period( raise ValueError( f'Validity period for certiticate is not as expected: valid to "{valid_to}, expected to "{expected_valid_to}"' ) + + return valid_from, valid_to diff --git a/tests/infra/crypto.py b/tests/infra/crypto.py index 7fa69046693..928f1276e1c 100644 --- a/tests/infra/crypto.py +++ b/tests/infra/crypto.py @@ -7,7 +7,6 @@ import secrets import datetime import hashlib -from datetime import datetime from cryptography import x509 diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index a72a170e398..b037b3868bd 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -361,7 +361,7 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): "--max-allowed-node-cert-validity-days", help="Maximum validity period (days) for certificates of trusted nodes", type=int, - default=365, + default=365, # TODO: Set to something random and see if tests still work ) add(parser) diff --git a/tests/infra/node.py b/tests/infra/node.py index 4974819b2d4..05984e9c6ab 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -268,6 +268,7 @@ def _start( binary_dir=self.binary_dir, additional_raw_node_args=self.additional_raw_node_args, version=self.version, + major_version=self.major_version, **kwargs, ) self.remote.setup() diff --git a/tests/infra/remote.py b/tests/infra/remote.py index 4742fbb49dc..c0f0bc905bd 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -581,6 +581,7 @@ def __init__( initial_node_cert_validity_days=None, max_allowed_node_cert_validity_days=None, version=None, + major_version=None, include_addresses=True, additional_raw_node_args=None, ): @@ -660,9 +661,6 @@ def __init__( f"--public-rpc-address={make_address(pub_host, rpc_port)}", ] - if node_client_host: - cmd += [f"--node-client-interface={node_client_host}"] - if log_format_json: cmd += ["--log-format-json"] @@ -706,10 +704,15 @@ def __init__( if client_connection_timeout_ms: cmd += [f"--client-connection-timeout-ms={client_connection_timeout_ms}"] - if initial_node_cert_validity_days: - cmd += [ - f"--initial-node-cert-validity-days={initial_node_cert_validity_days}" - ] + # Added in 1.x + if not major_version or major_version > 1: + if initial_node_cert_validity_days: + cmd += [ + f"--initial-node-cert-validity-days={initial_node_cert_validity_days}" + ] + + if node_client_host: + cmd += [f"--node-client-interface={node_client_host}"] if additional_raw_node_args: for s in additional_raw_node_args: @@ -740,10 +743,12 @@ def __init__( data_files.append(os.path.join(self.common_dir, mf)) cmd += [member_info_cmd] - if max_allowed_node_cert_validity_days: - cmd += [ - f"--max-allowed-node-cert-validity-days={max_allowed_node_cert_validity_days}" - ] + # Added in 1.x + if not major_version or major_version > 1: + if max_allowed_node_cert_validity_days: + cmd += [ + f"--max-allowed-node-cert-validity-days={max_allowed_node_cert_validity_days}" + ] elif start_type == StartType.join: cmd += [ diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index 82a8703d83b..dfa92f63d73 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -3,6 +3,7 @@ import infra.network import infra.e2e_args import infra.proc +import infra.certs import infra.logging_app as app import infra.utils import infra.github @@ -348,7 +349,8 @@ def run_ledger_compatibility_since_first(args, local_branch, use_snapshot): # Verify that all nodes run the expected CCF version for node in nodes: - # Note: /node/version endpoint was added in 2.x + # Note: /node/version endpoint and custom certificate validity + # were added in 2.x if not node.major_version or node.major_version > 1: with node.client() as c: r = c.get("/node/version") @@ -357,6 +359,10 @@ def run_ledger_compatibility_since_first(args, local_branch, use_snapshot): assert ( r.body.json()["ccf_version"] == expected_version ), f"Node version is not {expected_version}" + infra.certs.verify_certificate_validity_period( + node.get_tls_certificate_pem(), + expected_validity_period_days=args.max_allowed_node_cert_validity_days, + ) # Rollover JWKS so that new primary must read historical CA bundle table # and retrieve new keys via auto refresh diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 71d1ee3cec5..70d2fd3f876 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -418,11 +418,11 @@ def test_learner_does_not_take_part(network, args): @reqs.description("Test node certificates validity period") def test_node_certificates_validity_period(network, args): for node in network.get_joined_nodes(): - infra.certs.verify_certificate_validity_period( + valid_from, valid_to = infra.certs.verify_certificate_validity_period( node.get_tls_certificate_pem(), args.max_allowed_node_cert_validity_days ) LOG.info( - f"Certificate validity period for node {node.local_node_id} successfully verified" + f"Certificate validity period for node {node.local_node_id} successfully verified: {valid_from} - {valid_to}" ) diff --git a/tests/recovery.py b/tests/recovery.py index 4084e6fb20c..066fd9dc43d 100644 --- a/tests/recovery.py +++ b/tests/recovery.py @@ -121,9 +121,6 @@ def run(args): network.txs.issue(network, number_txs=1) network.txs.issue(network, number_txs=1, repeat=True) - # TODO: Delete - reconfiguration.test_node_certificates_validity_period(network, args) - # Alternate between recovery with primary change and stable primary-ship, # with and without snapshots if i % 2 == 0: @@ -136,6 +133,9 @@ def run(args): else: recovered_network = test(network, args, from_snapshot=False) network = recovered_network + + reconfiguration.test_node_certificates_validity_period(network, args) + LOG.success("Recovery complete on all nodes") From a70b856a8b7322d3b34b06560ddc32772c2f02bf Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 15 Oct 2021 14:28:34 +0000 Subject: [PATCH 083/105] Pass valid_from in transition_node_to_trusted proposal --- python/ccf/proposal_generator.py | 9 +++- src/node/config.h | 12 ++++- src/node/node_state.h | 5 +- src/node/rpc/node_call_types.h | 1 + src/node/rpc/node_frontend.h | 12 +++-- src/node/rpc/serialization.h | 3 +- src/runtime_config/default/actions.js | 9 ++-- tests/governance.py | 35 ++------------ tests/infra/certs.py | 45 ------------------ tests/infra/consortium.py | 19 ++++++-- tests/infra/crypto.py | 10 ++++ tests/infra/e2e_args.py | 4 +- tests/infra/network.py | 6 ++- tests/infra/node.py | 32 +++++++++++++ tests/lts_compatibility.py | 11 ++--- tests/reconfiguration.py | 66 +++++++++++++-------------- 16 files changed, 143 insertions(+), 136 deletions(-) delete mode 100644 tests/infra/certs.py diff --git a/python/ccf/proposal_generator.py b/python/ccf/proposal_generator.py index 209d3ff6d4c..117948586d7 100644 --- a/python/ccf/proposal_generator.py +++ b/python/ccf/proposal_generator.py @@ -226,8 +226,13 @@ def refresh_js_app_bytecode_cache(**kwargs): @cli_proposal -def transition_node_to_trusted(node_id: str, **kwargs): - return build_proposal("transition_node_to_trusted", {"node_id": node_id}, **kwargs) +def transition_node_to_trusted( + node_id: str, valid_from: str, validity_period_days=None, **kwargs +): + args = {"node_id": node_id, "valid_from": valid_from} + if validity_period_days is not None: + args["validity_period_days"] = validity_period_days + return build_proposal("transition_node_to_trusted", args, **kwargs) @cli_proposal diff --git a/src/node/config.h b/src/node/config.h index ef0f0601c6f..3b38f1c2388 100644 --- a/src/node/config.h +++ b/src/node/config.h @@ -11,6 +11,7 @@ namespace ccf { static constexpr auto default_node_cert_validity_period_days = 365; + static constexpr auto default_node_cert_initial_validity_period_days = 1; struct ServiceConfiguration { @@ -23,13 +24,19 @@ namespace ccf std::optional node_cert_allowed_validity_period_days = std::nullopt; + // TODO: Only required if initial node join (when service is opening) uses + // primary's host time + std::optional node_cert_initial_validity_period_days = std::nullopt; + bool operator==(const ServiceConfiguration& other) const { return recovery_threshold == other.recovery_threshold && consensus == other.consensus && reconfiguration_type == other.reconfiguration_type && node_cert_allowed_validity_period_days == - other.node_cert_allowed_validity_period_days; + other.node_cert_allowed_validity_period_days && + node_cert_initial_validity_period_days == + other.node_cert_initial_validity_period_days; } }; DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(ServiceConfiguration) @@ -38,7 +45,8 @@ namespace ccf DECLARE_JSON_OPTIONAL_FIELDS( ServiceConfiguration, reconfiguration_type, - node_cert_allowed_validity_period_days) + node_cert_allowed_validity_period_days, + node_cert_initial_validity_period_days) // The there is always only one active configuration, so this is a single // Value diff --git a/src/node/node_state.h b/src/node/node_state.h index 0a27ed579c0..65af19f29db 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1591,7 +1591,8 @@ namespace ccf config.genesis.recovery_threshold, network.consensus_type, reconf_type, - config.genesis.max_allowed_node_cert_validity_days}; + config.genesis.max_allowed_node_cert_validity_days, + config.initial_node_certificate_validity_period_days}; create_params.genesis_info = genesis_info; } @@ -1605,6 +1606,8 @@ namespace ccf create_params.code_digest = node_code_id; create_params.node_info_network = config.node_info_network; create_params.node_cert_valid_from = config.startup_host_time; + create_params.initial_node_cert_validity_period_days = + config.initial_node_certificate_validity_period_days; // Record self-signed certificate in create request if the node does not // require endorsement by the service (i.e. BFT) diff --git a/src/node/rpc/node_call_types.h b/src/node/rpc/node_call_types.h index 5abbc7923e4..b0e16901f01 100644 --- a/src/node/rpc/node_call_types.h +++ b/src/node/rpc/node_call_types.h @@ -68,6 +68,7 @@ namespace ccf CodeDigest code_digest; NodeInfoNetwork node_info_network; std::string node_cert_valid_from; + size_t initial_node_cert_validity_period_days; // Only set if node does _not_ require endorsement by the service std::optional node_cert = std::nullopt; diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index ab434b7da41..96b4756ceda 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -267,11 +267,16 @@ namespace ccf "Unable to get time: {}", ccf::api_result_to_str(result))); } + // TODO: Joining node while service is opening + // Should the validity period be: + // 1. [this node's host time, this node's host time + initial validity + // period?] + // 2. validity period extracted from self-signed node cert endorsed_certificate = crypto::create_endorsed_cert( in.certificate_signing_request.value(), crypto::OpenSSL::to_x509_time_string(time.tv_sec), - config->node_cert_allowed_validity_period_days.value_or( - default_node_cert_validity_period_days), + config->node_cert_initial_validity_period_days.value_or( + default_node_cert_initial_validity_period_days), this->network.identity->priv_key, this->network.identity->cert); @@ -1182,8 +1187,7 @@ namespace ccf crypto::create_endorsed_cert( in.certificate_signing_request, in.node_cert_valid_from, - config->node_cert_allowed_validity_period_days.value_or( - default_node_cert_validity_period_days), + in.initial_node_cert_validity_period_days, this->network.identity->priv_key, this->network.identity->cert)); } diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index 845065e5ed3..31aaedc4aee 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -86,7 +86,8 @@ namespace ccf public_encryption_key, code_digest, node_info_network, - node_cert_valid_from) + node_cert_valid_from, + initial_node_cert_validity_period_days) DECLARE_JSON_OPTIONAL_FIELDS( CreateNetworkNodeToNode::In, node_cert, genesis_info) diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 9eb760020a0..8156857b3e8 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -153,7 +153,7 @@ function invalidateOtherOpenProposals(proposalIdToRetain) { // TODO: Only implement non-ACL-specific proposals for now // 1. [DONE] Rename proposal to `set_node_certificate_validity` -// 2. Pass `valid_to` to `transition_node_to_trusted` +// 2. Pass `valid_from` to `transition_node_to_trusted` // 3. Rename things and set initial default period to 24h // 4. Create set_all_nodes_certificate_validity proposal // 5. Add proposal to set max allowed node certificate validity @@ -755,6 +755,7 @@ const actions = new Map([ new Action( function (args) { checkEntityId(args.node_id, "node_id"); + checkType(args.valid_from, "string", "valid_from"); }, function (args) { const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( @@ -791,8 +792,8 @@ const actions = new Map([ // Note: CSR is only present from 2.x const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, - "211001100000Z", // TODO: Get argument from proposal paramters - serviceConfig.node_cert_allowed_validity_period_days + args.valid_from, + serviceConfig.node_cert_allowed_validity_period_days // TODO: What if this isn't set on the service? ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), @@ -924,7 +925,7 @@ const actions = new Map([ checkEntityId(args.node_id, "node_id"); checkType(args.valid_from, "string", "valid_from"); checkType(args.validity_period_days, "integer", "validity_period_days"); - checkBounds(args.validity_period_days, 0, null, "validity_period_days"); + checkBounds(args.validity_period_days, 1, null, "validity_period_days"); }, function (args) { const node = ccf.kv["public:ccf.gov.nodes.info"].get( diff --git a/tests/governance.py b/tests/governance.py index 3ff2053e300..a56150086f4 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -221,12 +221,12 @@ def post_proposal_request_raw(node, headers=None, expected_error_msg=None): @reqs.description("Update certificates of all nodes") def test_node_cert_renewal(network, args): - + primary, _ = network.find_primary() now = datetime.now().replace( microsecond=0 ) # Truncate microseconds which are not reflected in RFC5280 UTCTime - validity_period_allowed = args.initial_node_cert_validity_days - 1 - validity_period_forbidden = args.initial_node_cert_validity_days + 1 + validity_period_allowed = args.max_allowed_node_cert_validity_days - 1 + validity_period_forbidden = args.max_allowed_node_cert_validity_days + 1 test_vectors = [ (now, validity_period_allowed, None), @@ -249,8 +249,8 @@ def test_node_cert_renewal(network, args): try: network.consortium.set_node_certificate_validity( + primary, node, - node.node_id, valid_from=str(infra.crypto.datetime_as_UTCtime(valid_from)), validity_period_days=validity_period_days, ) @@ -262,32 +262,7 @@ def test_node_cert_renewal(network, args): expected_exception is None ), "Proposal should have not succeeded" - # Verify that node certificate has been renewed - node_cert_tls_after = node.get_tls_certificate_pem() - assert ( - node_cert_tls_before != node_cert_tls_after - ), "Node TLS certificate should be updated after renewal" - ( - cert_valid_from, - cert_valid_to, - ) = infra.crypto.get_validity_period_from_pem_cert(node_cert_tls_after) - # Note: CCF automatically substracts one second from validity period - expected_valid_to = valid_from + timedelta( - days=validity_period_days, seconds=-1 - ) - assert ( - cert_valid_from == valid_from - ), f"{cert_valid_from} != {valid_from}" - assert ( - cert_valid_to == expected_valid_to - ), f"{cert_valid_to} != {expected_valid_to}" - - assert ( - infra.crypto.compute_public_key_der_hash_hex_from_pem( - node_cert_tls_before - ) - == node.node_id - ) + node.verify_certificate_validity_period(validity_period_days) LOG.info( f"Certificate for node {node.local_node_id} has successfully been renewed" ) diff --git a/tests/infra/certs.py b/tests/infra/certs.py deleted file mode 100644 index 00e8ece65d5..00000000000 --- a/tests/infra/certs.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the Apache 2.0 License. - -from datetime import datetime, timedelta -from cryptography.hazmat.backends import default_backend -from cryptography.x509 import ( - load_pem_x509_certificate, -) -from pyasn1.type.useful import UTCTime - - -def get_validity_period_from_pem_cert(pem: str): - cert = load_pem_x509_certificate(pem.encode(), default_backend()) - return cert.not_valid_before, cert.not_valid_after - - -def datetime_as_UTCtime(datetime: datetime): - return UTCTime.fromDateTime(datetime) - - -def verify_certificate_validity_period( - pem: str, expected_validity_period_days: int, expected_valid_from=None -): - valid_from, valid_to = get_validity_period_from_pem_cert(pem) - - # By default, assume that certificate has been issued within this test run - expected_valid_from = expected_valid_from or ( - datetime.utcnow() - timedelta(hours=1) - ) - if valid_from < expected_valid_from: - raise ValueError( - f'Certificate is too old: valid from "{valid_from}", expected "{expected_valid_from}"' - ) - - # Note: CCF substracts one second from validity period since x509 - # specifies that validity dates are inclusive. - expected_valid_to = valid_from + timedelta( - days=expected_validity_period_days, seconds=-1 - ) - if valid_to != expected_valid_to: - raise ValueError( - f'Validity period for certiticate is not as expected: valid to "{valid_to}, expected to "{expected_valid_to}"' - ) - - return valid_from, valid_to diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 925d63cd4ee..3a535c665a5 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -301,12 +301,17 @@ def retire_node(self, remote_node, node_to_retire): ) assert r.body.json()["status"] == expected - def trust_node(self, remote_node, node_id, timeout=3): + def trust_node( + self, remote_node, node_id, valid_from, validity_period_days=None, timeout=3 + ): if not self._check_node_exists(remote_node, node_id, NodeStatus.PENDING): raise ValueError(f"Node {node_id} does not exist in state PENDING") proposal_body, careful_vote = self.make_proposal( - "transition_node_to_trusted", node_id + "transition_node_to_trusted", + node_id, + valid_from, + validity_period_days, ) proposal = self.get_any_active_member().propose(remote_node, proposal_body) self.vote_using_majority( @@ -535,9 +540,15 @@ def retire_code(self, remote_node, code_id): proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) - def set_node_certificate_validity(self, remote_node, *args, **kwargs): + def set_node_certificate_validity( + self, remote_node, node_to_renew, valid_from, validity_period_days + ): + LOG.error(validity_period_days) proposal_body, careful_vote = self.make_proposal( - "set_node_certificate_validity", *args, **kwargs + "set_node_certificate_validity", + node_to_renew.node_id, + valid_from, + validity_period_days, ) proposal = self.get_any_active_member().propose(remote_node, proposal_body) return self.vote_using_majority(remote_node, proposal, careful_vote) diff --git a/tests/infra/crypto.py b/tests/infra/crypto.py index 928f1276e1c..144b44af281 100644 --- a/tests/infra/crypto.py +++ b/tests/infra/crypto.py @@ -7,6 +7,7 @@ import secrets import datetime import hashlib +from pyasn1.type.useful import UTCTime from cryptography import x509 @@ -272,3 +273,12 @@ def check_key_pair_pem(private: str, public: str, password=None) -> bool: ) pub_der = pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) return prv_pub_der == pub_der + + +def get_validity_period_from_pem_cert(pem: str): + cert = load_pem_x509_certificate(pem.encode(), default_backend()) + return cert.not_valid_before, cert.not_valid_after + + +def datetime_as_UTCtime(datetime: datetime): + return UTCTime.fromDateTime(datetime) diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index b037b3868bd..1c451c84268 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -355,13 +355,13 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): help="Initial validity period (days) for certificates of nodes before the " "service is open by members", type=int, - default=365, # TODO: Should be 1 + default=1, # TODO: Should be 1 ) parser.add_argument( "--max-allowed-node-cert-validity-days", help="Maximum validity period (days) for certificates of trusted nodes", type=int, - default=365, # TODO: Set to something random and see if tests still work + default=123, # TODO: Set to something random and see if tests still work ) add(parser) diff --git a/tests/infra/network.py b/tests/infra/network.py index 487b128d838..15c9439032c 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -19,6 +19,7 @@ import http import pprint import functools +from datetime import datetime from loguru import logger as LOG @@ -632,13 +633,16 @@ def join_node( raise StartupSnapshotIsOld from e raise - def trust_node(self, node, args): + def trust_node(self, node, args, valid_from=None, validity_period_days=None): primary, _ = self.find_primary() try: if self.status is ServiceStatus.OPEN: self.consortium.trust_node( primary, node.node_id, + valid_from=valid_from + or str(infra.crypto.datetime_as_UTCtime(datetime.now())), + validity_period_days=validity_period_days, timeout=ceil(args.join_timer * 2 / 1000), ) # Here, quote verification has already been run when the node diff --git a/tests/infra/node.py b/tests/infra/node.py index 05984e9c6ab..fbb306cdff3 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -6,6 +6,7 @@ import infra.crypto import infra.remote import infra.remote_shim +from datetime import datetime, timedelta import infra.net import infra.path import ccf.clients @@ -492,6 +493,37 @@ def get_tls_certificate_pem(self, use_public_rpc_host=True): ) ) + def verify_certificate_validity_period(self, expected_validity_period_days): + node_tls_cert = self.get_tls_certificate_pem() + valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( + node_tls_cert + ) + + assert ( + infra.crypto.compute_public_key_der_hash_hex_from_pem(node_tls_cert) + == self.node_id + ) + + # Assume that certificate has been issued within this test run + expected_valid_from = datetime.utcnow() - timedelta(hours=1) + if valid_from < expected_valid_from: + raise ValueError( + f'Node certificate is too old: valid from "{valid_from}" older than expected "{expected_valid_from}"' + ) + + # Note: CCF substracts one second from validity period since x509 + # specifies that validity dates are inclusive. + expected_valid_to = valid_from + timedelta( + days=expected_validity_period_days, seconds=-1 + ) + if valid_to != expected_valid_to: + raise ValueError( + f'Validity period for certiticate is not as expected: valid to "{valid_to}, expected to "{expected_valid_to}"' + ) + LOG.info( + f"Certificate validity period for node {self.local_node_id} successfully verified: {valid_from} - {valid_to} (for {valid_to - valid_from})" + ) + def suspend(self): assert not self.suspended self.suspended = True diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index dfa92f63d73..87a773c74ef 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -3,7 +3,6 @@ import infra.network import infra.e2e_args import infra.proc -import infra.certs import infra.logging_app as app import infra.utils import infra.github @@ -171,9 +170,8 @@ def run_code_upgrade_from( # Note: validity period for 2.x node joining 1.x service is hardcoded # to 365 days since existing service is not capable of issuing endorsed # node certificate - infra.certs.verify_certificate_validity_period( - new_node.get_tls_certificate_pem(), - expected_validity_period_days=365, + new_node.verify_certificate_validity_period( + expected_validity_period_days=365 ) from_snapshot = not from_snapshot new_nodes.append(new_node) @@ -359,9 +357,8 @@ def run_ledger_compatibility_since_first(args, local_branch, use_snapshot): assert ( r.body.json()["ccf_version"] == expected_version ), f"Node version is not {expected_version}" - infra.certs.verify_certificate_validity_period( - node.get_tls_certificate_pem(), - expected_validity_period_days=args.max_allowed_node_cert_validity_days, + node.verify_certificate_validity_period( + args.max_allowed_node_cert_validity_days, ) # Rollover JWKS so that new primary must read historical CA bundle table diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 70d2fd3f876..731a21c0a9d 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -12,7 +12,6 @@ import ccf.ledger import json import infra.crypto -import infra.certs from loguru import logger as LOG @@ -42,9 +41,7 @@ def test_add_node(network, args): new_node = network.create_node("local://localhost") network.join_node(new_node, args.package, args, from_snapshot=False) # Verify self-signed node certificate validity period - infra.certs.verify_certificate_validity_period( - new_node.get_tls_certificate_pem(), args.initial_node_cert_validity_days - ) + new_node.verify_certificate_validity_period(args.initial_node_cert_validity_days) network.trust_node(new_node, args) with new_node.client() as c: s = c.get("/node/state") @@ -52,6 +49,10 @@ def test_add_node(network, args): assert ( s.body.json()["startup_seqno"] == 0 ), "Node started without snapshot but reports startup seqno != 0" + # Now that the node is trusted, verify endorsed certificate validity period + new_node.verify_certificate_validity_period( + args.max_allowed_node_cert_validity_days + ) assert new_node return network @@ -418,12 +419,7 @@ def test_learner_does_not_take_part(network, args): @reqs.description("Test node certificates validity period") def test_node_certificates_validity_period(network, args): for node in network.get_joined_nodes(): - valid_from, valid_to = infra.certs.verify_certificate_validity_period( - node.get_tls_certificate_pem(), args.max_allowed_node_cert_validity_days - ) - LOG.info( - f"Certificate validity period for node {node.local_node_id} successfully verified: {valid_from} - {valid_to}" - ) + node.verify_certificate_validity_period(args.initial_node_cert_validity_days) def run(args): @@ -438,31 +434,35 @@ def run(args): ) as network: network.start_and_join(args) - test_version(network, args) - - if args.consensus != "bft": - test_join_straddling_primary_replacement(network, args) - test_node_replacement(network, args) - test_add_node_from_backup(network, args) - test_add_node(network, args) - test_add_node_on_other_curve(network, args) - test_retire_backup(network, args) - test_add_as_many_pending_nodes(network, args) - test_add_node(network, args) - test_retire_primary(network, args) - - test_add_node_from_snapshot(network, args) - test_add_node_from_snapshot(network, args, from_backup=True) - test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) - - test_node_filter(network, args) - test_retiring_nodes_emit_at_most_one_signature(network, args) - else: - test_learner_catches_up(network, args) - # test_learner_does_not_take_part(network, args) - test_retire_backup(network, args) test_node_certificates_validity_period(network, args) + test_add_node(network, args) + + # test_version(network, args) + + # if args.consensus != "bft": + # test_join_straddling_primary_replacement(network, args) + # test_node_replacement(network, args) + # test_add_node_from_backup(network, args) + # test_add_node(network, args) + # test_add_node_on_other_curve(network, args) + # test_retire_backup(network, args) + # test_add_as_many_pending_nodes(network, args) + # test_add_node(network, args) + # test_retire_primary(network, args) + + # test_add_node_from_snapshot(network, args) + # test_add_node_from_snapshot(network, args, from_backup=True) + # test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) + + # test_node_filter(network, args) + # test_retiring_nodes_emit_at_most_one_signature(network, args) + # else: + # test_learner_catches_up(network, args) + # # test_learner_does_not_take_part(network, args) + # test_retire_backup(network, args) + # test_node_certificates_validity_period(network, args) + def run_join_old_snapshot(args): txs = app.LoggingTxs("user0") From be66f35e58da578aa4974b3912867cf3d5b906aa Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 15 Oct 2021 16:35:38 +0000 Subject: [PATCH 084/105] Sanitise certificate validity period early --- src/crypto/openssl/key_pair.cpp | 17 +++++++--- src/crypto/openssl/x509_time.h | 19 +++++++++++ src/crypto/test/crypto.cpp | 46 +++++++++++++++++++++++++++ src/node/config.h | 10 ++++-- src/node/rpc/node_frontend.h | 3 +- src/runtime_config/default/actions.js | 2 +- tests/lts_compatibility.py | 4 ++- 7 files changed, 91 insertions(+), 10 deletions(-) diff --git a/src/crypto/openssl/key_pair.cpp b/src/crypto/openssl/key_pair.cpp index 1f5bb8ae0ae..2542246bade 100644 --- a/src/crypto/openssl/key_pair.cpp +++ b/src/crypto/openssl/key_pair.cpp @@ -7,6 +7,7 @@ #include "crypto/openssl/public_key.h" #include "hash.h" #include "openssl_wrappers.h" +#include "x509_time.h" #define FMT_HEADER_ONLY #include @@ -260,11 +261,19 @@ namespace crypto // Note: 825-day validity range // https://support.apple.com/en-us/HT210176 - Unique_ASN1_TIME before(valid_from.value_or("20210311000000Z")); - Unique_ASN1_TIME after(valid_to.value_or("20230611235959Z")); + Unique_ASN1_TIME not_before(valid_from.value_or("20210311000000Z")); + Unique_ASN1_TIME not_after(valid_to.value_or("20230611235959Z")); + if (!validate_chronological_times(not_before, not_after)) + { + throw std::logic_error(fmt::format( + "Certificate cannot be created with not_before date {} > not_after " + "date {}", + to_x509_time_string(not_before), + to_x509_time_string(not_after))); + } - OpenSSL::CHECK1(X509_set1_notBefore(crt, before)); - OpenSSL::CHECK1(X509_set1_notAfter(crt, after)); + OpenSSL::CHECK1(X509_set1_notBefore(crt, not_before)); + OpenSSL::CHECK1(X509_set1_notAfter(crt, not_after)); X509_set_subject_name(crt, X509_REQ_get_subject_name(csr)); X509_set_pubkey(crt, req_pubkey); diff --git a/src/crypto/openssl/x509_time.h b/src/crypto/openssl/x509_time.h index 962e86ddd98..31bb11acdbd 100644 --- a/src/crypto/openssl/x509_time.h +++ b/src/crypto/openssl/x509_time.h @@ -16,6 +16,20 @@ namespace crypto /** Set of utilites functions for working with x509 time, as defined in RFC 5280 (https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5.1) */ + static inline bool validate_chronological_times( + const Unique_ASN1_TIME& time_before, + const Unique_ASN1_TIME& time_after, + const std::optional& allowed_diff_days = std::nullopt) + { + int diff_days = 0; + int diff_secs = 0; + CHECK1(ASN1_TIME_diff(&diff_days, &diff_secs, time_before, time_after)); + + return (diff_days > 0 || diff_secs > 0) && + (!allowed_diff_days.has_value() || + (unsigned int)diff_days <= allowed_diff_days.value()); + } + static inline Unique_ASN1_TIME from_time_t(const time_t& t) { return Unique_ASN1_TIME(ASN1_TIME_set(nullptr, t)); @@ -41,5 +55,10 @@ namespace crypto // https://www.openssl.org/docs/man1.1.1/man3/ASN1_UTCTIME_set.html return fmt::format("{:%y%m%d%H%M%SZ}", fmt::gmtime(time)); } + + static inline std::string to_x509_time_string(const Unique_ASN1_TIME& time) + { + return to_x509_time_string(to_time_t(time)); + } } } \ No newline at end of file diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index d30f98f17b3..d8f31846efc 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -644,11 +644,57 @@ TEST_CASE("ASN1 time") next_day_time.tm_mday++; auto next_year_time = time; next_year_time.tm_year++; + auto next_minute_time = time; + next_minute_time.tm_min++; auto current_time = crypto::OpenSSL::from_time_t(current_time_t); auto next_day = crypto::OpenSSL::from_time_t(std::mktime(&next_day_time)); auto next_year = crypto::OpenSSL::from_time_t(std::mktime(&next_year_time)); + std::string before = "211015154115Z"; + std::string after = "211016154114Z"; + REQUIRE(crypto::OpenSSL::validate_chronological_times(before, after)); + + INFO("Chronological time"); + { + struct TimeTest + { + struct Input + { + std::tm from; + std::tm to; + std::optional maximum_validity_period_days = std::nullopt; + }; + Input input; + + bool expected_verification_result; + }; + + std::vector test_vector{ + {{time, next_day_time}, true}, // Valid: Next day + {{time, time}, false}, // Invalid: Same date + {{next_day_time, time}, false}, // Invalid: to is before from + {{time, next_day_time, 100}, true}, // Valid: Next day within 100 days + {{time, next_year_time, 100}, + false}, // Valid: Next day not within 100 days + {{time, next_minute_time}, true}, // Valid: Next minute + {{next_minute_time, time}, false}, // Invalid: to is before from + {{time, next_minute_time, 1}, true} // Valid: Next min within 1 day + }; + + for (auto& data : test_vector) + { + auto* from = &data.input.from; + auto* to = &data.input.to; + REQUIRE( + crypto::OpenSSL::validate_chronological_times( + crypto::OpenSSL::from_time_t(std::mktime(from)), + crypto::OpenSSL::from_time_t(std::mktime(to)), + data.input.maximum_validity_period_days) == + data.expected_verification_result); + } + } + INFO("Adjust time"); { std::vector times = {time, next_day_time, next_day_time}; diff --git a/src/node/config.h b/src/node/config.h index 3b38f1c2388..48bb144591d 100644 --- a/src/node/config.h +++ b/src/node/config.h @@ -22,11 +22,17 @@ namespace ccf std::optional reconfiguration_type = std::nullopt; - std::optional node_cert_allowed_validity_period_days = std::nullopt; + /** + * Fields below are added in 2.x + */ + + size_t node_cert_allowed_validity_period_days = + default_node_cert_validity_period_days; // TODO: Only required if initial node join (when service is opening) uses // primary's host time - std::optional node_cert_initial_validity_period_days = std::nullopt; + size_t node_cert_initial_validity_period_days = + default_node_cert_initial_validity_period_days; bool operator==(const ServiceConfiguration& other) const { diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 96b4756ceda..b1a573a4306 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -275,8 +275,7 @@ namespace ccf endorsed_certificate = crypto::create_endorsed_cert( in.certificate_signing_request.value(), crypto::OpenSSL::to_x509_time_string(time.tv_sec), - config->node_cert_initial_validity_period_days.value_or( - default_node_cert_initial_validity_period_days), + config->node_cert_initial_validity_period_days, this->network.identity->priv_key, this->network.identity->cert); diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 8156857b3e8..b3939393320 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -793,7 +793,7 @@ const actions = new Map([ const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, args.valid_from, - serviceConfig.node_cert_allowed_validity_period_days // TODO: What if this isn't set on the service? + serviceConfig.node_cert_allowed_validity_period_days | 365 // TODO: This is necessary for the LTS compatibility test to work ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index 87a773c74ef..e039b1558f0 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -81,6 +81,7 @@ def test_new_service(network, args, install_path, binary_dir, library_dir, versi ) network.join_node(new_node, args.package, args) network.trust_node(new_node, args) + new_node.verify_certificate_validity_period(expected_validity_period_days=120) LOG.info("Apply transactions to new nodes only") issue_activity_on_live_service(network, args) @@ -204,6 +205,8 @@ def run_code_upgrade_from( primary, _ = network.wait_for_new_primary(primary) node.stop() + LOG.info("Service is now made of new nodes only") + # Rollover JWKS so that new primary must read historical CA bundle table # and retrieve new keys via auto refresh jwt_issuer.refresh_keys() @@ -214,7 +217,6 @@ def run_code_upgrade_from( else: time.sleep(3) - # From here onwards, service is only made of new nodes test_new_service( network, args, From 51e6ea88e4cbb3e519dd3ad5224abaeb2b661139 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Fri, 15 Oct 2021 17:01:50 +0000 Subject: [PATCH 085/105] Verify validity period for service cert --- tests/reconfiguration.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 731a21c0a9d..56409ac38a7 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -12,6 +12,7 @@ import ccf.ledger import json import infra.crypto +from datetime import datetime from loguru import logger as LOG @@ -422,6 +423,24 @@ def test_node_certificates_validity_period(network, args): node.verify_certificate_validity_period(args.initial_node_cert_validity_days) +@reqs.description("Test service certificate validity period") +def test_service_certificate_validity_period(network, args): + # TODO: See https://github.com/microsoft/CCF/issues/3090 + with open( + os.path.join(network.common_dir, "networkcert.pem"), "r", encoding="utf-8" + ) as service_cert: + valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( + service_cert.read() + ) + assert valid_from == datetime(year=2021, month=3, day=11) # 20210311000000Z + assert valid_to == datetime( + year=2023, month=6, day=11, hour=23, minute=59, second=59 + ) # 20230611235959Z + LOG.info( + f"Certificate validity period for service successfully verified: {valid_from} - {valid_to} (for {valid_to - valid_from})" + ) + + def run(args): txs = app.LoggingTxs("user0") with infra.network.network( @@ -434,9 +453,11 @@ def run(args): ) as network: network.start_and_join(args) + test_service_certificate_validity_period(network, args) + test_node_certificates_validity_period(network, args) - test_add_node(network, args) + # test_add_node(network, args) # test_version(network, args) From 01fd29646340f7eae3df0dcda23608db77225dbe Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 18 Oct 2021 09:54:13 +0000 Subject: [PATCH 086/105] Fix node addition --- src/runtime_config/default/actions.js | 4 ++-- tests/infra/network.py | 19 ++++++++++++++++++- tests/infra/node.py | 3 ++- tests/lts_compatibility.py | 2 +- tests/reconfiguration.py | 19 +++---------------- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index b3939393320..7c747c4930f 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -789,11 +789,11 @@ const actions = new Map([ nodeInfo.certificate_signing_request !== undefined && serviceConfig.consensus !== "BFT" ) { - // Note: CSR is only present from 2.x + // Note: CSR and node certificate validity config are only present from 2.x const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, args.valid_from, - serviceConfig.node_cert_allowed_validity_period_days | 365 // TODO: This is necessary for the LTS compatibility test to work + serviceConfig.node_cert_allowed_validity_period_days ?? 365 ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), diff --git a/tests/infra/network.py b/tests/infra/network.py index 15c9439032c..851a524a4b3 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -19,7 +19,7 @@ import http import pprint import functools -from datetime import datetime +from datetime import datetime, timedelta from loguru import logger as LOG @@ -1108,6 +1108,23 @@ def cert(self): ) return network_cert + def verify_service_certificate_validity_period(self): + # TODO: Hardcoded for now. See # TODO: See https://github.com/microsoft/CCF/issues/3090 + assert self.cert.not_valid_before == datetime( + year=2021, month=3, day=11 + ) # 20210311000000Z + assert self.cert.not_valid_after == datetime( + year=2023, month=6, day=11, hour=23, minute=59, second=59 + ) # 20230611235959Z + validity_period = ( + self.cert.not_valid_after + - self.cert.not_valid_before + + timedelta(seconds=1) + ) + LOG.info( + f"Certificate validity period for service successfully verified: {self.cert.not_valid_before} - {self.cert.not_valid_after} (for {validity_period})" + ) + @contextmanager def network( diff --git a/tests/infra/node.py b/tests/infra/node.py index fbb306cdff3..0ab833960c2 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -520,8 +520,9 @@ def verify_certificate_validity_period(self, expected_validity_period_days): raise ValueError( f'Validity period for certiticate is not as expected: valid to "{valid_to}, expected to "{expected_valid_to}"' ) + validity_period = valid_to - valid_from + timedelta(seconds=1) LOG.info( - f"Certificate validity period for node {self.local_node_id} successfully verified: {valid_from} - {valid_to} (for {valid_to - valid_from})" + f"Certificate validity period for node {self.local_node_id} successfully verified: {valid_from} - {valid_to} (for {validity_period})" ) def suspend(self): diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index e039b1558f0..138865dcdc3 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -81,7 +81,7 @@ def test_new_service(network, args, install_path, binary_dir, library_dir, versi ) network.join_node(new_node, args.package, args) network.trust_node(new_node, args) - new_node.verify_certificate_validity_period(expected_validity_period_days=120) + new_node.verify_certificate_validity_period(expected_validity_period_days=365) LOG.info("Apply transactions to new nodes only") issue_activity_on_live_service(network, args) diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 56409ac38a7..06f4de634e9 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -12,7 +12,7 @@ import ccf.ledger import json import infra.crypto -from datetime import datetime + from loguru import logger as LOG @@ -425,20 +425,7 @@ def test_node_certificates_validity_period(network, args): @reqs.description("Test service certificate validity period") def test_service_certificate_validity_period(network, args): - # TODO: See https://github.com/microsoft/CCF/issues/3090 - with open( - os.path.join(network.common_dir, "networkcert.pem"), "r", encoding="utf-8" - ) as service_cert: - valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( - service_cert.read() - ) - assert valid_from == datetime(year=2021, month=3, day=11) # 20210311000000Z - assert valid_to == datetime( - year=2023, month=6, day=11, hour=23, minute=59, second=59 - ) # 20230611235959Z - LOG.info( - f"Certificate validity period for service successfully verified: {valid_from} - {valid_to} (for {valid_to - valid_from})" - ) + network.verify_service_certificate_validity_period() def run(args): @@ -457,7 +444,7 @@ def run(args): test_node_certificates_validity_period(network, args) - # test_add_node(network, args) + test_add_node(network, args) # test_version(network, args) From 5e1a7fbc82853f217e8f2615273bbc73dd24413c Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 18 Oct 2021 10:45:37 +0000 Subject: [PATCH 087/105] Recovery --- tests/infra/e2e_args.py | 2 +- tests/recovery.py | 7 ++++--- tests/suite/test_suite.py | 3 --- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index 4ce832d46e5..b186133eb56 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -355,7 +355,7 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): help="Initial validity period (days) for certificates of nodes before the " "service is open by members", type=int, - default=1, # TODO: Should be 1 + default=128, # TODO: Should be 1 ) parser.add_argument( "--max-allowed-node-cert-validity-days", diff --git a/tests/recovery.py b/tests/recovery.py index 066fd9dc43d..7ee375ec6b9 100644 --- a/tests/recovery.py +++ b/tests/recovery.py @@ -7,8 +7,6 @@ import infra.checker import suite.test_requirements as reqs -import reconfiguration - from loguru import logger as LOG @@ -134,7 +132,10 @@ def run(args): recovered_network = test(network, args, from_snapshot=False) network = recovered_network - reconfiguration.test_node_certificates_validity_period(network, args) + for node in network.get_joined_nodes(): + node.verify_certificate_validity_period( + args.initial_node_cert_validity_days + ) LOG.success("Recovery complete on all nodes") diff --git a/tests/suite/test_suite.py b/tests/suite/test_suite.py index ee3d361385f..3411bb235f3 100644 --- a/tests/suite/test_suite.py +++ b/tests/suite/test_suite.py @@ -10,7 +10,6 @@ import membership import governance_history import jwt_test -import governance from inspect import signature, Parameter @@ -115,8 +114,6 @@ recovery.test, # jwt jwt_test.test_refresh_jwt_issuer, - # governance - governance.test_test_node_cert_renewal, # # # From d85971a62f3c45877ab235b0eb68eaaed0d28399 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 18 Oct 2021 13:10:36 +0000 Subject: [PATCH 088/105] Enable more tests --- src/crypto/mbedtls/key_pair.cpp | 8 +++- src/crypto/test/crypto.cpp | 6 +-- tests/infra/network.py | 6 ++- tests/infra/node.py | 4 +- tests/reconfiguration.py | 70 +++++++++++++++------------------ 5 files changed, 45 insertions(+), 49 deletions(-) diff --git a/src/crypto/mbedtls/key_pair.cpp b/src/crypto/mbedtls/key_pair.cpp index d13b7944a12..3e7c81c2a85 100644 --- a/src/crypto/mbedtls/key_pair.cpp +++ b/src/crypto/mbedtls/key_pair.cpp @@ -325,9 +325,13 @@ namespace crypto // Note: 825-day validity range // https://support.apple.com/en-us/HT210176 - // TODO: Use valid_from and valid_to + // Note: For the mbedtls implementation, we do not check that valid_from and + // valid_to are valid or chronological. See OpenSSL equivalent cal for a + // safer implementation. MCHK(mbedtls_x509write_crt_set_validity( - crt.get(), "20210311000000", "20230611235959")); + crt.get(), + valid_from.value_or("20210311000000").c_str(), + valid_to.value_or("20230611235959").c_str())); MCHK(mbedtls_x509write_crt_set_basic_constraints(crt.get(), ca ? 1 : 0, 0)); MCHK(mbedtls_x509write_crt_set_subject_key_identifier(crt.get())); diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index d8f31846efc..f2a064f1c69 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -634,7 +634,7 @@ TEST_CASE("AES-GCM convenience functions") REQUIRE(decrypted == contents); } -TEST_CASE("ASN1 time") +TEST_CASE("x509 time") { auto current_time_t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); @@ -651,10 +651,6 @@ TEST_CASE("ASN1 time") auto next_day = crypto::OpenSSL::from_time_t(std::mktime(&next_day_time)); auto next_year = crypto::OpenSSL::from_time_t(std::mktime(&next_year_time)); - std::string before = "211015154115Z"; - std::string after = "211016154114Z"; - REQUIRE(crypto::OpenSSL::validate_chronological_times(before, after)); - INFO("Chronological time"); { struct TimeTest diff --git a/tests/infra/network.py b/tests/infra/network.py index 1507e46ce91..bbeb16f3968 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -441,6 +441,7 @@ def start_and_join(self, args): ) self.status = ServiceStatus.OPEN LOG.info(f"Initial set of users added: {len(initial_users)}") + self.verify_service_certificate_validity_period() LOG.success("***** Network is now open *****") def start_in_recovery( @@ -519,6 +520,7 @@ def recover(self, args): self._wait_for_app_open(node) self.consortium.check_for_service(self.find_random_node(), ServiceStatus.OPEN) + self.verify_service_certificate_validity_period() LOG.success("***** Recovered network is now open *****") def ignore_errors_on_shutdown(self): @@ -1117,8 +1119,8 @@ def verify_service_certificate_validity_period(self): - self.cert.not_valid_before + timedelta(seconds=1) ) - LOG.info( - f"Certificate validity period for service successfully verified: {self.cert.not_valid_before} - {self.cert.not_valid_after} (for {validity_period})" + LOG.debug( + f"Certificate validity period for service: {self.cert.not_valid_before} - {self.cert.not_valid_after} (for {validity_period})" ) diff --git a/tests/infra/node.py b/tests/infra/node.py index 0ab833960c2..71d0e6a3535 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -508,7 +508,7 @@ def verify_certificate_validity_period(self, expected_validity_period_days): expected_valid_from = datetime.utcnow() - timedelta(hours=1) if valid_from < expected_valid_from: raise ValueError( - f'Node certificate is too old: valid from "{valid_from}" older than expected "{expected_valid_from}"' + f'Node {self.local_node_id} certificate is too old: valid from "{valid_from}" older than expected "{expected_valid_from}"' ) # Note: CCF substracts one second from validity period since x509 @@ -518,7 +518,7 @@ def verify_certificate_validity_period(self, expected_validity_period_days): ) if valid_to != expected_valid_to: raise ValueError( - f'Validity period for certiticate is not as expected: valid to "{valid_to}, expected to "{expected_valid_to}"' + f'Validity period for node {self.local_node_id} certiticate is not as expected: valid to "{valid_to}, expected to "{expected_valid_to}"' ) validity_period = valid_to - valid_from + timedelta(seconds=1) LOG.info( diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index ee74e57b767..c2ad13cf232 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -12,6 +12,7 @@ import ccf.ledger import json import infra.crypto +from datetime import datetime from loguru import logger as LOG @@ -295,7 +296,10 @@ def test_join_straddling_primary_replacement(network, args): "actions": [ { "name": "transition_node_to_trusted", - "args": {"node_id": new_node.node_id}, + "args": { + "node_id": new_node.node_id, + "valid_from": str(infra.crypto.datetime_as_UTCtime(datetime.now())), + }, }, { "name": "remove_node", @@ -424,11 +428,6 @@ def test_node_certificates_validity_period(network, args): node.verify_certificate_validity_period(args.initial_node_cert_validity_days) -@reqs.description("Test service certificate validity period") -def test_service_certificate_validity_period(network, args): - network.verify_service_certificate_validity_period() - - @reqs.description("Add a new node without a snapshot but with the historical ledger") def test_add_node_with_read_only_ledger(network, args): network.txs.issue(network, number_txs=10) @@ -453,38 +452,33 @@ def run(args): ) as network: network.start_and_join(args) - test_service_certificate_validity_period(network, args) - + test_version(network, args) + + if args.consensus != "bft": + test_join_straddling_primary_replacement(network, args) + test_node_replacement(network, args) + test_add_node_from_backup(network, args) + test_node_certificates_validity_period(network, args) + test_add_node(network, args) + test_add_node_on_other_curve(network, args) + test_retire_backup(network, args) + test_add_as_many_pending_nodes(network, args) + test_add_node(network, args) + test_retire_primary(network, args) + test_add_node_with_read_only_ledger(network, args) + + test_add_node_from_snapshot(network, args) + test_add_node_from_snapshot(network, args, from_backup=True) + test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) + + test_node_filter(network, args) + test_retiring_nodes_emit_at_most_one_signature(network, args) + else: + test_learner_catches_up(network, args) + # test_learner_does_not_take_part(network, args) + test_retire_backup(network, args) test_node_certificates_validity_period(network, args) - test_add_node(network, args) - - # test_version(network, args) - - # if args.consensus != "bft": - # test_join_straddling_primary_replacement(network, args) - # test_node_replacement(network, args) - # test_add_node_from_backup(network, args) - # test_add_node(network, args) - # test_add_node_on_other_curve(network, args) - # test_retire_backup(network, args) - # test_add_as_many_pending_nodes(network, args) - # test_add_node(network, args) - # test_retire_primary(network, args) - # test_add_node_with_read_only_ledger(network, args) - - # test_add_node_from_snapshot(network, args) - # test_add_node_from_snapshot(network, args, from_backup=True) - # test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) - - # test_node_filter(network, args) - # test_retiring_nodes_emit_at_most_one_signature(network, args) - # else: - # test_learner_catches_up(network, args) - # # test_learner_does_not_take_part(network, args) - # test_retire_backup(network, args) - # test_node_certificates_validity_period(network, args) - def run_join_old_snapshot(args): txs = app.LoggingTxs("user0") @@ -555,5 +549,5 @@ def run_join_old_snapshot(args): run(args) - # if args.consensus != "bft": - # run_join_old_snapshot(args) + if args.consensus != "bft": + run_join_old_snapshot(args) From a5e4b4d8e7c303df58902d21a6d95313c187174d Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 18 Oct 2021 15:09:17 +0000 Subject: [PATCH 089/105] Extract validity period from self-signed node certificate --- src/crypto/certs.h | 25 +++++--- src/crypto/mbedtls/verifier.cpp | 20 +++++++ src/crypto/mbedtls/verifier.h | 3 + src/crypto/openssl/key_pair.cpp | 4 +- src/crypto/openssl/openssl_wrappers.h | 8 +-- src/crypto/openssl/verifier.cpp | 8 +++ src/crypto/openssl/verifier.h | 3 + src/crypto/openssl/x509_time.h | 85 +++++++++++++-------------- src/crypto/test/crypto.cpp | 6 +- src/crypto/verifier.h | 5 +- src/node/config.h | 12 +--- src/node/node_state.h | 3 +- src/node/rpc/node_frontend.h | 24 ++------ 13 files changed, 115 insertions(+), 91 deletions(-) diff --git a/src/crypto/certs.h b/src/crypto/certs.h index f4645351497..4587f7299d4 100644 --- a/src/crypto/certs.h +++ b/src/crypto/certs.h @@ -35,17 +35,27 @@ namespace crypto static Pem create_endorsed_cert( const Pem& csr, const std::string& valid_from, - size_t validity_period_days, + const std::string& valid_to, const Pem& issuer_key_pair, const Pem& issuer_cert) { return make_key_pair(issuer_key_pair) - ->sign_csr( - issuer_cert, - csr, - false /* Not CA */, - valid_from, - compute_cert_valid_to_string(valid_from, validity_period_days)); + ->sign_csr(issuer_cert, csr, false /* Not CA */, valid_from, valid_to); + } + + static Pem create_endorsed_cert( + const Pem& csr, + const std::string& valid_from, + size_t validity_period_days, + const Pem& issuer_key_pair, + const Pem& issuer_cert) + { + return create_endorsed_cert( + csr, + valid_from, + compute_cert_valid_to_string(valid_from, validity_period_days), + issuer_key_pair, + issuer_cert); } static Pem create_endorsed_cert( @@ -63,5 +73,4 @@ namespace crypto issuer_key_pair, issuer_cert); } - } \ No newline at end of file diff --git a/src/crypto/mbedtls/verifier.cpp b/src/crypto/mbedtls/verifier.cpp index 27d0a2ee1a9..5a12cf86368 100644 --- a/src/crypto/mbedtls/verifier.cpp +++ b/src/crypto/mbedtls/verifier.cpp @@ -21,6 +21,19 @@ namespace crypto "-----BEGIN CERTIFICATE-----\n"; static constexpr auto PEM_CERTIFICATE_FOOTER = "-----END CERTIFICATE-----\n"; + static inline std::string to_x509_time_string(const mbedtls_x509_time& time) + { + // Returns ASN1 time string (YYYYMMDDHHMMSSZ) + return fmt::format( + "{:02}{:02}{:02}{:02}{:02}{:02}Z", + time.year, + time.mon, + time.day, + time.hour, + time.min, + time.sec); + } + MDType Verifier_mbedTLS::get_md_type(mbedtls_md_type_t mdt) const { switch (mdt) @@ -196,4 +209,11 @@ namespace crypto } return buf; } + + std::pair Verifier_mbedTLS::validity_period() const + { + return std::make_pair( + to_x509_time_string(cert->valid_from), + to_x509_time_string(cert->valid_to)); + } } \ No newline at end of file diff --git a/src/crypto/mbedtls/verifier.h b/src/crypto/mbedtls/verifier.h index f7ffab269aa..b82e188d686 100644 --- a/src/crypto/mbedtls/verifier.h +++ b/src/crypto/mbedtls/verifier.h @@ -29,5 +29,8 @@ namespace crypto virtual bool is_self_signed() const override; virtual std::string serial_number() const override; + + virtual std::pair validity_period() + const override; }; } diff --git a/src/crypto/openssl/key_pair.cpp b/src/crypto/openssl/key_pair.cpp index 2542246bade..aa4824fca39 100644 --- a/src/crypto/openssl/key_pair.cpp +++ b/src/crypto/openssl/key_pair.cpp @@ -261,8 +261,8 @@ namespace crypto // Note: 825-day validity range // https://support.apple.com/en-us/HT210176 - Unique_ASN1_TIME not_before(valid_from.value_or("20210311000000Z")); - Unique_ASN1_TIME not_after(valid_to.value_or("20230611235959Z")); + Unique_X509_TIME not_before(valid_from.value_or("20210311000000Z")); + Unique_X509_TIME not_after(valid_to.value_or("20230611235959Z")); if (!validate_chronological_times(not_before, not_after)) { throw std::logic_error(fmt::format( diff --git a/src/crypto/openssl/openssl_wrappers.h b/src/crypto/openssl/openssl_wrappers.h index 8996eecbf41..c7f47d5bc2f 100644 --- a/src/crypto/openssl/openssl_wrappers.h +++ b/src/crypto/openssl/openssl_wrappers.h @@ -261,22 +261,22 @@ namespace crypto } }; - class Unique_ASN1_TIME + class Unique_X509_TIME { std::unique_ptr p; public: - Unique_ASN1_TIME() : p(ASN1_TIME_new(), ASN1_TIME_free) + Unique_X509_TIME() : p(ASN1_TIME_new(), ASN1_TIME_free) { CHECKNULL(p.get()); } - Unique_ASN1_TIME(const std::string& s) : + Unique_X509_TIME(const std::string& s) : p(ASN1_TIME_new(), ASN1_TIME_free) { CHECK1(ASN1_TIME_set_string(*this, s.c_str())); CHECK1(ASN1_TIME_normalize(*this)); } - Unique_ASN1_TIME(ASN1_TIME* t) : p(t, ASN1_TIME_free) {} + Unique_X509_TIME(ASN1_TIME* t) : p(t, ASN1_TIME_free) {} operator ASN1_TIME*() const { return p.get(); diff --git a/src/crypto/openssl/verifier.cpp b/src/crypto/openssl/verifier.cpp index 4a3abbf67f4..a16152ed2c0 100644 --- a/src/crypto/openssl/verifier.cpp +++ b/src/crypto/openssl/verifier.cpp @@ -6,6 +6,7 @@ #include "crypto/openssl/openssl_wrappers.h" #include "public_key.h" #include "rsa_key_pair.h" +#include "x509_time.h" #include #include @@ -149,4 +150,11 @@ namespace crypto BIO_get_mem_ptr(mem, &bptr); return std::string(bptr->data, bptr->length); } + + std::pair Verifier_OpenSSL::validity_period() const + { + return std::make_pair( + to_x509_time_string(X509_get0_notBefore(cert)), + to_x509_time_string(X509_get0_notAfter(cert))); + } } diff --git a/src/crypto/openssl/verifier.h b/src/crypto/openssl/verifier.h index 83badb6f9b6..9ca37a43d14 100644 --- a/src/crypto/openssl/verifier.h +++ b/src/crypto/openssl/verifier.h @@ -32,5 +32,8 @@ namespace crypto virtual bool is_self_signed() const override; virtual std::string serial_number() const override; + + virtual std::pair validity_period() + const override; }; } diff --git a/src/crypto/openssl/x509_time.h b/src/crypto/openssl/x509_time.h index 31bb11acdbd..4fab55d16bc 100644 --- a/src/crypto/openssl/x509_time.h +++ b/src/crypto/openssl/x509_time.h @@ -7,58 +7,53 @@ #include #include -namespace crypto +namespace crypto::OpenSSL { - // TODO: ASN1_TIME or ASN1_GENERALIZEDTIME? + /** Set of utilites functions for working with x509 time, as defined in RFC + 5280 (https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5.1) */ - namespace OpenSSL + static inline bool validate_chronological_times( + const Unique_X509_TIME& time_before, + const Unique_X509_TIME& time_after, + const std::optional& allowed_diff_days = std::nullopt) { - /** Set of utilites functions for working with x509 time, as defined in RFC - 5280 (https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5.1) */ + int diff_days = 0; + int diff_secs = 0; + CHECK1(ASN1_TIME_diff(&diff_days, &diff_secs, time_before, time_after)); - static inline bool validate_chronological_times( - const Unique_ASN1_TIME& time_before, - const Unique_ASN1_TIME& time_after, - const std::optional& allowed_diff_days = std::nullopt) - { - int diff_days = 0; - int diff_secs = 0; - CHECK1(ASN1_TIME_diff(&diff_days, &diff_secs, time_before, time_after)); - - return (diff_days > 0 || diff_secs > 0) && - (!allowed_diff_days.has_value() || - (unsigned int)diff_days <= allowed_diff_days.value()); - } + return (diff_days > 0 || diff_secs > 0) && + (!allowed_diff_days.has_value() || + (unsigned int)diff_days <= allowed_diff_days.value()); + } - static inline Unique_ASN1_TIME from_time_t(const time_t& t) - { - return Unique_ASN1_TIME(ASN1_TIME_set(nullptr, t)); - } + static inline Unique_X509_TIME from_time_t(const time_t& t) + { + return Unique_X509_TIME(ASN1_TIME_set(nullptr, t)); + } - static inline time_t to_time_t(const Unique_ASN1_TIME& time) - { - tm tm_time; - CHECK1(ASN1_TIME_to_tm(time, &tm_time)); - return std::mktime(&tm_time); - } + static inline time_t to_time_t(const ASN1_TIME* time) + { + tm tm_time; + CHECK1(ASN1_TIME_to_tm(time, &tm_time)); + return std::mktime(&tm_time); + } - static inline Unique_ASN1_TIME adjust_time( - const Unique_ASN1_TIME& time, size_t offset_days, int64_t offset_secs = 0) - { - return Unique_ASN1_TIME( - ASN1_TIME_adj(nullptr, to_time_t(time), offset_days, offset_secs)); - } + static inline Unique_X509_TIME adjust_time( + const Unique_X509_TIME& time, size_t offset_days, int64_t offset_secs = 0) + { + return Unique_X509_TIME( + ASN1_TIME_adj(nullptr, to_time_t(time), offset_days, offset_secs)); + } - static inline std::string to_x509_time_string(const time_t& time) - { - // Returns ASN1 time string (YYYYMMDDHHMMSSZ) from time_t, as per - // https://www.openssl.org/docs/man1.1.1/man3/ASN1_UTCTIME_set.html - return fmt::format("{:%y%m%d%H%M%SZ}", fmt::gmtime(time)); - } + static inline std::string to_x509_time_string(const time_t& time) + { + // Returns ASN1 time string (YYYYMMDDHHMMSSZ) from time_t, as per + // https://www.openssl.org/docs/man1.1.1/man3/ASN1_UTCTIME_set.html + return fmt::format("{:%Y%m%d%H%M%SZ}", fmt::gmtime(time)); + } - static inline std::string to_x509_time_string(const Unique_ASN1_TIME& time) - { - return to_x509_time_string(to_time_t(time)); - } + static inline std::string to_x509_time_string(const ASN1_TIME* time) + { + return to_x509_time_string(to_time_t(time)); } -} \ No newline at end of file +} diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index f2a064f1c69..49087c7916b 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -444,6 +444,10 @@ void run_csr(bool corrupt_csr = false) S v(crt.raw()); REQUIRE(v.verify(content, signature)); + + auto [valid_from, valid_to] = v.validity_period(); + REQUIRE(valid_from == "20210311000000Z"); + REQUIRE(valid_to == "20230611235959Z"); } TEST_CASE("Create sign and verify certificates") @@ -719,7 +723,7 @@ TEST_CASE("x509 time") auto adjusted_time_t = crypto::OpenSSL::to_time_t(adjusted_time); auto x509_str = crypto::OpenSSL::to_x509_time_string(adjusted_time_t); - auto asn1_time = crypto::OpenSSL::Unique_ASN1_TIME(x509_str); + auto asn1_time = crypto::OpenSSL::Unique_X509_TIME(x509_str); auto converted_time_t = crypto::OpenSSL::to_time_t(asn1_time); REQUIRE(converted_time_t == adjusted_time_t); } diff --git a/src/crypto/verifier.h b/src/crypto/verifier.h index e4f8b8e246a..c82ae13b2ad 100644 --- a/src/crypto/verifier.h +++ b/src/crypto/verifier.h @@ -192,8 +192,11 @@ namespace crypto /** Indicates whether the certificate (held intenally) is self-signed */ virtual bool is_self_signed() const = 0; - /** The serial number of the certificate*/ + /** The serial number of the certificate */ virtual std::string serial_number() const = 0; + + /** The validity period of the certificate */ + virtual std::pair validity_period() const = 0; }; using VerifierPtr = std::shared_ptr; diff --git a/src/node/config.h b/src/node/config.h index 48bb144591d..e6ad9a8fd52 100644 --- a/src/node/config.h +++ b/src/node/config.h @@ -29,20 +29,13 @@ namespace ccf size_t node_cert_allowed_validity_period_days = default_node_cert_validity_period_days; - // TODO: Only required if initial node join (when service is opening) uses - // primary's host time - size_t node_cert_initial_validity_period_days = - default_node_cert_initial_validity_period_days; - bool operator==(const ServiceConfiguration& other) const { return recovery_threshold == other.recovery_threshold && consensus == other.consensus && reconfiguration_type == other.reconfiguration_type && node_cert_allowed_validity_period_days == - other.node_cert_allowed_validity_period_days && - node_cert_initial_validity_period_days == - other.node_cert_initial_validity_period_days; + other.node_cert_allowed_validity_period_days; } }; DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(ServiceConfiguration) @@ -51,8 +44,7 @@ namespace ccf DECLARE_JSON_OPTIONAL_FIELDS( ServiceConfiguration, reconfiguration_type, - node_cert_allowed_validity_period_days, - node_cert_initial_validity_period_days) + node_cert_allowed_validity_period_days) // The there is always only one active configuration, so this is a single // Value diff --git a/src/node/node_state.h b/src/node/node_state.h index cac99d28977..fd7b1fbcdca 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -1593,8 +1593,7 @@ namespace ccf config.genesis.recovery_threshold, network.consensus_type, reconf_type, - config.genesis.max_allowed_node_cert_validity_days, - config.initial_node_certificate_validity_period_days}; + config.genesis.max_allowed_node_cert_validity_days}; create_params.genesis_info = genesis_info; } diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index b1a573a4306..dec6f7f8d91 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -256,26 +256,14 @@ namespace ccf in.certificate_signing_request.has_value() && this->network.consensus_type == ConsensusType::CFT) { - ::timespec time; - ccf::ApiResult result = get_untrusted_host_time_v1(time); - if (result != ccf::ApiResult::OK) - { - return ccf::make_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Unable to get time: {}", ccf::api_result_to_str(result))); - } - - // TODO: Joining node while service is opening - // Should the validity period be: - // 1. [this node's host time, this node's host time + initial validity - // period?] - // 2. validity period extracted from self-signed node cert + // For a pre-open service, extract the validity period of self-signed + // node certificate and use it verbatim in endorsed certificate + auto [valid_from, valid_to] = + crypto::make_verifier(node_der)->validity_period(); endorsed_certificate = crypto::create_endorsed_cert( in.certificate_signing_request.value(), - crypto::OpenSSL::to_x509_time_string(time.tv_sec), - config->node_cert_initial_validity_period_days, + valid_from, + valid_to, this->network.identity->priv_key, this->network.identity->cert); From a913e577692909ffde01f9af322c503a125d4b3d Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 18 Oct 2021 16:01:50 +0000 Subject: [PATCH 090/105] Tighten end-to-end test --- tests/code_update.py | 1 - tests/governance.py | 6 +++- tests/infra/consortium.py | 4 ++- tests/infra/e2e_args.py | 4 +-- tests/infra/network.py | 9 +++-- tests/infra/node.py | 67 ++++++++++++++++++++++++-------------- tests/lts_compatibility.py | 2 +- tests/reconfiguration.py | 15 ++++----- tests/recovery.py | 4 +-- 9 files changed, 68 insertions(+), 44 deletions(-) diff --git a/tests/code_update.py b/tests/code_update.py index 88f5a6c7b5d..84efcd86349 100644 --- a/tests/code_update.py +++ b/tests/code_update.py @@ -130,7 +130,6 @@ def test_update_all_nodes(network, args): new_node = network.create_node("local://localhost") network.join_node(new_node, replacement_package, args) network.trust_node(new_node, args) - assert new_node LOG.info("Retire original nodes running old code") for node in old_nodes: diff --git a/tests/governance.py b/tests/governance.py index a56150086f4..cb742aa7f07 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -262,7 +262,11 @@ def test_node_cert_renewal(network, args): expected_exception is None ), "Proposal should have not succeeded" - node.verify_certificate_validity_period(validity_period_days) + node_cert_tls_after = node.get_tls_certificate_pem() + assert ( + node_cert_tls_before != node_cert_tls_after + ), f"Node {node.local_node_id} certificate was not renewed" + node.verify_certificate_validity_period() LOG.info( f"Certificate for node {node.local_node_id} has successfully been renewed" ) diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 0dcf35f1776..a1e87daf0b1 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -551,7 +551,9 @@ def set_node_certificate_validity( validity_period_days, ) proposal = self.get_any_active_member().propose(remote_node, proposal_body) - return self.vote_using_majority(remote_node, proposal, careful_vote) + r = self.vote_using_majority(remote_node, proposal, careful_vote) + node_to_renew.set_certificate_validity_period(valid_from, validity_period_days) + return r def check_for_service(self, remote_node, status): """ diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index b186133eb56..dd556b2a32c 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -355,13 +355,13 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): help="Initial validity period (days) for certificates of nodes before the " "service is open by members", type=int, - default=128, # TODO: Should be 1 + default=1, ) parser.add_argument( "--max-allowed-node-cert-validity-days", help="Maximum validity period (days) for certificates of trusted nodes", type=int, - default=123, # TODO: Set to something random and see if tests still work + default=365, ) add(parser) diff --git a/tests/infra/network.py b/tests/infra/network.py index bbeb16f3968..fc1a6a94d2c 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -635,11 +635,13 @@ def trust_node(self, node, args, valid_from=None, validity_period_days=None): primary, _ = self.find_primary() try: if self.status is ServiceStatus.OPEN: + valid_from = valid_from or str( + infra.crypto.datetime_as_UTCtime(datetime.now()) + ) self.consortium.trust_node( primary, node.node_id, - valid_from=valid_from - or str(infra.crypto.datetime_as_UTCtime(datetime.now())), + valid_from=valid_from, validity_period_days=validity_period_days, timeout=ceil(args.join_timer * 2 / 1000), ) @@ -653,6 +655,9 @@ def trust_node(self, node, args, valid_from=None, validity_period_days=None): raise node.network_state = infra.node.NodeNetworkState.joined + node.set_certificate_validity_period( + valid_from, validity_period_days or args.max_allowed_node_cert_validity_days + ) self.wait_for_all_nodes_to_commit(primary=primary) def retire_node(self, remote_node, node_to_retire): diff --git a/tests/infra/node.py b/tests/infra/node.py index 71d0e6a3535..f6ff0dba120 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -110,6 +110,8 @@ def __init__( else None ) self.consensus = None + self.certificate_valid_from = None + self.certificate_validity_days = None if os.getenv("CONTAINER_NODES"): self.remote_shim = infra.remote_shim.DockerShim @@ -313,6 +315,7 @@ def _start( ) self._read_ports() + self.certificate_validity_days = kwargs.get("initial_node_cert_validity_days") LOG.info(f"Node {self.local_node_id} started: {self.node_id}") def _read_ports(self): @@ -493,50 +496,64 @@ def get_tls_certificate_pem(self, use_public_rpc_host=True): ) ) - def verify_certificate_validity_period(self, expected_validity_period_days): - node_tls_cert = self.get_tls_certificate_pem() - valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( - node_tls_cert - ) + def suspend(self): + assert not self.suspended + self.suspended = True + self.remote.suspend() + LOG.info(f"Node {self.local_node_id} suspended...") + def resume(self): + assert self.suspended + self.suspended = False + self.remote.resume() + LOG.info(f"Node {self.local_node_id} has resumed from suspension.") + + def set_certificate_validity_period(self, valid_from, validity_period_days): + self.certificate_valid_from = valid_from + self.certificate_validity_days = validity_period_days + + def verify_certificate_validity_period(self, expected_validity_period=None): + node_tls_cert = self.get_tls_certificate_pem() assert ( infra.crypto.compute_public_key_der_hash_hex_from_pem(node_tls_cert) == self.node_id ) - # Assume that certificate has been issued within this test run - expected_valid_from = datetime.utcnow() - timedelta(hours=1) - if valid_from < expected_valid_from: - raise ValueError( - f'Node {self.local_node_id} certificate is too old: valid from "{valid_from}" older than expected "{expected_valid_from}"' - ) + valid_from, valid_to = infra.crypto.get_validity_period_from_pem_cert( + node_tls_cert + ) + + if self.certificate_valid_from is None: + # If the node certificate has not been renewed, assume that certificate has + # been issued within this test run + expected_valid_from = datetime.utcnow() - timedelta(hours=1) + if valid_from < datetime.utcnow() - timedelta(hours=1): + raise ValueError( + f'Node {self.local_node_id} certificate is too old: valid from "{valid_from}" older than expected "{expected_valid_from}"' + ) + else: + if ( + infra.crypto.datetime_as_UTCtime(valid_from) + != self.certificate_valid_from + ): + raise ValueError( + f'Validity period for node {self.local_node_id} certificate is not as expected: valid from "{valid_from}", but expected "{self.certificate_valid_from}"' + ) # Note: CCF substracts one second from validity period since x509 # specifies that validity dates are inclusive. expected_valid_to = valid_from + timedelta( - days=expected_validity_period_days, seconds=-1 + days=self.certificate_validity_days, seconds=-1 ) if valid_to != expected_valid_to: raise ValueError( - f'Validity period for node {self.local_node_id} certiticate is not as expected: valid to "{valid_to}, expected to "{expected_valid_to}"' + f'Validity period for node {self.local_node_id} certiticate is not as expected: valid to "{valid_to} but expected "{expected_valid_to}"' ) validity_period = valid_to - valid_from + timedelta(seconds=1) LOG.info( f"Certificate validity period for node {self.local_node_id} successfully verified: {valid_from} - {valid_to} (for {validity_period})" ) - def suspend(self): - assert not self.suspended - self.suspended = True - self.remote.suspend() - LOG.info(f"Node {self.local_node_id} suspended...") - - def resume(self): - assert self.suspended - self.suspended = False - self.remote.resume() - LOG.info(f"Node {self.local_node_id} has resumed from suspension.") - @contextmanager def node( diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index e6cf822b74c..d30bea8d1d0 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -171,7 +171,7 @@ def run_code_upgrade_from( ) network.trust_node(new_node, args) # Note: validity period for 2.x node joining 1.x service is hardcoded - # to 365 days since existing service is not capable of issuing endorsed + # to 365 days since existing service does not issue endorsed # node certificate new_node.verify_certificate_validity_period( expected_validity_period_days=365 diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index c2ad13cf232..688f341444f 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -42,8 +42,10 @@ def count_nodes(configs, network): def test_add_node(network, args): new_node = network.create_node("local://localhost") network.join_node(new_node, args.package, args, from_snapshot=False) + # Verify self-signed node certificate validity period - new_node.verify_certificate_validity_period(args.initial_node_cert_validity_days) + new_node.verify_certificate_validity_period() + network.trust_node(new_node, args) with new_node.client() as c: s = c.get("/node/state") @@ -51,11 +53,10 @@ def test_add_node(network, args): assert ( s.body.json()["startup_seqno"] == 0 ), "Node started without snapshot but reports startup seqno != 0" + # Now that the node is trusted, verify endorsed certificate validity period - new_node.verify_certificate_validity_period( - args.max_allowed_node_cert_validity_days - ) - assert new_node + new_node.verify_certificate_validity_period() + return network @@ -223,7 +224,6 @@ def get_nodes(status): assert all(info["status"] == "Trusted" for info in trusted_after), trusted_after assert all(info["status"] == "Pending" for info in pending_after), pending_after assert all(info["status"] == "Retired" for info in retired_after), retired_after - assert new_node return network @@ -425,7 +425,7 @@ def test_learner_does_not_take_part(network, args): @reqs.description("Test node certificates validity period") def test_node_certificates_validity_period(network, args): for node in network.get_joined_nodes(): - node.verify_certificate_validity_period(args.initial_node_cert_validity_days) + node.verify_certificate_validity_period() @reqs.description("Add a new node without a snapshot but with the historical ledger") @@ -458,7 +458,6 @@ def run(args): test_join_straddling_primary_replacement(network, args) test_node_replacement(network, args) test_add_node_from_backup(network, args) - test_node_certificates_validity_period(network, args) test_add_node(network, args) test_add_node_on_other_curve(network, args) test_retire_backup(network, args) diff --git a/tests/recovery.py b/tests/recovery.py index 7ee375ec6b9..9f2d5b93878 100644 --- a/tests/recovery.py +++ b/tests/recovery.py @@ -133,9 +133,7 @@ def run(args): network = recovered_network for node in network.get_joined_nodes(): - node.verify_certificate_validity_period( - args.initial_node_cert_validity_days - ) + node.verify_certificate_validity_period() LOG.success("Recovery complete on all nodes") From b538bdd83216c9b867f2bc729a5f93392724d6ae Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Mon, 18 Oct 2021 16:27:02 +0000 Subject: [PATCH 091/105] Fix LTS test --- src/runtime_config/default/actions.js | 4 +++- tests/infra/node.py | 21 +++++++++++++++------ tests/lts_compatibility.py | 10 +++++----- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 7c747c4930f..484642e0892 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -790,10 +790,12 @@ const actions = new Map([ serviceConfig.consensus !== "BFT" ) { // Note: CSR and node certificate validity config are only present from 2.x + const default_validity_period_days = 365; const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, args.valid_from, - serviceConfig.node_cert_allowed_validity_period_days ?? 365 + serviceConfig.node_cert_allowed_validity_period_days ?? + default_validity_period_days ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), diff --git a/tests/infra/node.py b/tests/infra/node.py index f6ff0dba120..49dd6807745 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -24,6 +24,11 @@ BASE_NODE_CLIENT_HOST = "127.100.0.0" +# When a 2.x node joins a 1.x service, the node has to self-endorse +# its certificate, using a default value for the validity period +# hardcoded in CCF. +DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS = 365 + class NodeNetworkState(Enum): stopped = auto() @@ -512,7 +517,9 @@ def set_certificate_validity_period(self, valid_from, validity_period_days): self.certificate_valid_from = valid_from self.certificate_validity_days = validity_period_days - def verify_certificate_validity_period(self, expected_validity_period=None): + def verify_certificate_validity_period( + self, expected_validity_period_days=None, ignore_proposal_valid_from=False + ): node_tls_cert = self.get_tls_certificate_pem() assert ( infra.crypto.compute_public_key_der_hash_hex_from_pem(node_tls_cert) @@ -523,11 +530,11 @@ def verify_certificate_validity_period(self, expected_validity_period=None): node_tls_cert ) - if self.certificate_valid_from is None: + if ignore_proposal_valid_from or self.certificate_valid_from is None: # If the node certificate has not been renewed, assume that certificate has # been issued within this test run expected_valid_from = datetime.utcnow() - timedelta(hours=1) - if valid_from < datetime.utcnow() - timedelta(hours=1): + if valid_from < expected_valid_from: raise ValueError( f'Node {self.local_node_id} certificate is too old: valid from "{valid_from}" older than expected "{expected_valid_from}"' ) @@ -540,15 +547,17 @@ def verify_certificate_validity_period(self, expected_validity_period=None): f'Validity period for node {self.local_node_id} certificate is not as expected: valid from "{valid_from}", but expected "{self.certificate_valid_from}"' ) - # Note: CCF substracts one second from validity period since x509 - # specifies that validity dates are inclusive. + # Note: CCF substracts one second from validity period since x509 specifies + # that validity dates are inclusive. expected_valid_to = valid_from + timedelta( - days=self.certificate_validity_days, seconds=-1 + days=expected_validity_period_days or self.certificate_validity_days, + seconds=-1, ) if valid_to != expected_valid_to: raise ValueError( f'Validity period for node {self.local_node_id} certiticate is not as expected: valid to "{valid_to} but expected "{expected_valid_to}"' ) + validity_period = valid_to - valid_from + timedelta(seconds=1) LOG.info( f"Certificate validity period for node {self.local_node_id} successfully verified: {valid_from} - {valid_to} (for {validity_period})" diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index d30bea8d1d0..225c1f3e11e 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -81,7 +81,9 @@ def test_new_service(network, args, install_path, binary_dir, library_dir, versi ) network.join_node(new_node, args.package, args) network.trust_node(new_node, args) - new_node.verify_certificate_validity_period(expected_validity_period_days=365) + new_node.verify_certificate_validity_period( + expected_validity_period_days=infra.node.DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS + ) LOG.info("Apply transactions to new nodes only") issue_activity_on_live_service(network, args) @@ -170,11 +172,9 @@ def run_code_upgrade_from( new_node, args.package, args, from_snapshot=from_snapshot ) network.trust_node(new_node, args) - # Note: validity period for 2.x node joining 1.x service is hardcoded - # to 365 days since existing service does not issue endorsed - # node certificate new_node.verify_certificate_validity_period( - expected_validity_period_days=365 + expected_validity_period_days=infra.node.DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS, + ignore_proposal_valid_from=True, ) from_snapshot = not from_snapshot new_nodes.append(new_node) From 5b55bd215b92684c783171d299ac5c6672d6cc85 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 19 Oct 2021 09:40:03 +0000 Subject: [PATCH 092/105] Fix governance test --- src/node/config.h | 3 +-- src/runtime_config/default/actions.js | 12 ++++++------ tests/infra/consortium.py | 1 - 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/node/config.h b/src/node/config.h index e6ad9a8fd52..fd935a77054 100644 --- a/src/node/config.h +++ b/src/node/config.h @@ -26,8 +26,7 @@ namespace ccf * Fields below are added in 2.x */ - size_t node_cert_allowed_validity_period_days = - default_node_cert_validity_period_days; + std::optional node_cert_allowed_validity_period_days = std::nullopt; bool operator==(const ServiceConfiguration& other) const { diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 484642e0892..b8819c988a3 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -947,7 +947,6 @@ const actions = new Map([ ); } - // TODO: Do we still need a service-wide configuration value? const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( getSingletonKvKey() ); @@ -956,12 +955,13 @@ const actions = new Map([ } const serviceConfig = ccf.bufToJsonCompatible(rawConfig); - if ( - args.validity_period_days > - serviceConfig.node_cert_allowed_validity_period_days - ) { + const max_validity_period = + serviceConfig.node_cert_allowed_validity_period_days ?? + default_validity_period_days; + + if (args.validity_period_days > max_validity_period) { throw new Error( - `Validity period ${args.validity_period_days} (days) must be less than or equal to service node certificate maximum validity period ${serviceConfig.node_cert_allowed_validity_period_days} (days)` + `Validity period ${args.validity_period_days} (days) must be less than or equal to service node certificate maximum validity period ${max_validity_period} (days)` ); } diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index a1e87daf0b1..e665881e5ee 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -543,7 +543,6 @@ def retire_code(self, remote_node, code_id): def set_node_certificate_validity( self, remote_node, node_to_renew, valid_from, validity_period_days ): - LOG.error(validity_period_days) proposal_body, careful_vote = self.make_proposal( "set_node_certificate_validity", node_to_renew.node_id, From 15137a68eb3b0c62eb7d9d1474ca43cfe533308f Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 19 Oct 2021 10:52:56 +0000 Subject: [PATCH 093/105] Check validity period in trust node proposal if it is set --- src/runtime_config/default/actions.js | 22 +++++++- tests/governance.py | 72 +++++++++++++-------------- tests/infra/e2e_args.py | 5 +- tests/reconfiguration.py | 28 ++++++++++- tests/suite/test_suite.py | 3 ++ 5 files changed, 88 insertions(+), 42 deletions(-) diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index b8819c988a3..ad65724c724 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -756,6 +756,13 @@ const actions = new Map([ function (args) { checkEntityId(args.node_id, "node_id"); checkType(args.valid_from, "string", "valid_from"); + if (args.validity_period_days !== undefined) { + checkType( + args.validity_period_days, + "integer", + "validity_period_days" + ); + } }, function (args) { const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( @@ -791,11 +798,22 @@ const actions = new Map([ ) { // Note: CSR and node certificate validity config are only present from 2.x const default_validity_period_days = 365; + const max_allowed_cert_validity_period_days = + serviceConfig.node_cert_allowed_validity_period_days ?? + default_validity_period_days; + if ( + args.validity_period_days !== undefined && + args.validity_period_days > max_allowed_cert_validity_period_days + ) { + throw new Error( + `Validity period ${args.validity_period_days} is not allowed: max allowed is ${max_allowed_cert_validity_period_days}` + ); + } + const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, args.valid_from, - serviceConfig.node_cert_allowed_validity_period_days ?? - default_validity_period_days + args.validity_period_days ?? max_allowed_cert_validity_period_days ); ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( ccf.strToBuf(args.node_id), diff --git a/tests/governance.py b/tests/governance.py index cb742aa7f07..88ae829e5a7 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -281,15 +281,15 @@ def gov(args): ) as network: network.start_and_join(args) network.consortium.set_authenticate_session(args.authenticate_session) - # test_create_endpoint(network, args) - # test_consensus_status(network, args) - # test_node_ids(network, args) - # test_member_data(network, args) - # test_quote(network, args) - # test_user(network, args) - # test_no_quote(network, args) - # test_ack_state_digest_update(network, args) - # test_invalid_client_signature(network, args) + test_create_endpoint(network, args) + test_consensus_status(network, args) + test_node_ids(network, args) + test_member_data(network, args) + test_quote(network, args) + test_user(network, args) + test_no_quote(network, args) + test_ack_state_digest_update(network, args) + test_invalid_client_signature(network, args) test_node_cert_renewal(network, args) @@ -323,32 +323,32 @@ def js_gov(args): authenticate_session=True, ) - # cr.add( - # "session_noauth", - # gov, - # package="samples/apps/logging/liblogging", - # nodes=infra.e2e_args.max_nodes(cr.args, f=0), - # initial_user_count=3, - # authenticate_session=False, - # ) - - # cr.add( - # "js", - # js_gov, - # package="samples/apps/logging/liblogging", - # nodes=infra.e2e_args.max_nodes(cr.args, f=0), - # initial_user_count=3, - # authenticate_session=True, - # ) - - # cr.add( - # "history", - # governance_history.run, - # package="samples/apps/logging/liblogging", - # nodes=infra.e2e_args.max_nodes(cr.args, f=0), - # # Higher snapshot interval as snapshots trigger new ledger chunks, which - # # may result in latest chunk being partially written - # snapshot_tx_interval=10000, - # ) + cr.add( + "session_noauth", + gov, + package="samples/apps/logging/liblogging", + nodes=infra.e2e_args.max_nodes(cr.args, f=0), + initial_user_count=3, + authenticate_session=False, + ) + + cr.add( + "js", + js_gov, + package="samples/apps/logging/liblogging", + nodes=infra.e2e_args.max_nodes(cr.args, f=0), + initial_user_count=3, + authenticate_session=True, + ) + + cr.add( + "history", + governance_history.run, + package="samples/apps/logging/liblogging", + nodes=infra.e2e_args.max_nodes(cr.args, f=0), + # Higher snapshot interval as snapshots trigger new ledger chunks, which + # may result in latest chunk being partially written + snapshot_tx_interval=10000, + ) cr.run(2) diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index dd556b2a32c..1f979237f9f 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -352,14 +352,13 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): ) parser.add_argument( "--initial-node-cert-validity-days", - help="Initial validity period (days) for certificates of nodes before the " - "service is open by members", + help="Initial validity period in days for certificates of nodes before the first certificate renewal", type=int, default=1, ) parser.add_argument( "--max-allowed-node-cert-validity-days", - help="Maximum validity period (days) for certificates of trusted nodes", + help="Maximum allowed validity period in days for certificates of trusted nodes", type=int, default=365, ) diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 688f341444f..8ac8524ab78 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -46,7 +46,11 @@ def test_add_node(network, args): # Verify self-signed node certificate validity period new_node.verify_certificate_validity_period() - network.trust_node(new_node, args) + network.trust_node( + new_node, + args, + validity_period_days=args.max_allowed_node_cert_validity_days // 2, + ) with new_node.client() as c: s = c.get("/node/state") assert s.body.json()["node_id"] == new_node.node_id @@ -60,6 +64,27 @@ def test_add_node(network, args): return network +@reqs.description("Adding a node with an invalid certificate validity period") +def test_add_node_invalid_validity_period(network, args): + new_node = network.create_node("local://localhost") + network.join_node(new_node, args.package, args) + try: + network.trust_node( + new_node, + args, + validity_period_days=args.max_allowed_node_cert_validity_days + 1, + ) + except infra.proposal.ProposalNotAccepted: + LOG.info( + "As expected, not could not be trusted since its certificate validity period is invalid" + ) + else: + raise Exception( + "Node should not be trusted if its certificate validity period is invalid" + ) + return network + + def test_add_node_on_other_curve(network, args): original_curve = args.curve_id args.curve_id = ( @@ -460,6 +485,7 @@ def run(args): test_add_node_from_backup(network, args) test_add_node(network, args) test_add_node_on_other_curve(network, args) + test_add_node_invalid_validity_period(network, args) test_retire_backup(network, args) test_add_as_many_pending_nodes(network, args) test_add_node(network, args) diff --git a/tests/suite/test_suite.py b/tests/suite/test_suite.py index 3411bb235f3..4e9549d20fd 100644 --- a/tests/suite/test_suite.py +++ b/tests/suite/test_suite.py @@ -10,6 +10,7 @@ import membership import governance_history import jwt_test +import governance from inspect import signature, Parameter @@ -114,6 +115,8 @@ recovery.test, # jwt jwt_test.test_refresh_jwt_issuer, + # governance + governance.test_node_cert_renewal, # # # From af0ec7b167e6985aa7ac4dee9b93c93a583d1f3f Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 19 Oct 2021 15:08:45 +0000 Subject: [PATCH 094/105] Add proposal to renew all certificates --- python/ccf/proposal_generator.py | 14 ++++ src/runtime_config/default/actions.js | 101 +++++++++++++++++--------- tests/governance.py | 53 ++++++++++---- tests/infra/consortium.py | 12 ++- tests/infra/crypto.py | 2 +- tests/infra/network.py | 2 +- tests/infra/node.py | 7 +- tests/lts_compatibility.py | 12 ++- tests/reconfiguration.py | 4 +- 9 files changed, 145 insertions(+), 62 deletions(-) diff --git a/python/ccf/proposal_generator.py b/python/ccf/proposal_generator.py index 117948586d7..1d8cee0b636 100644 --- a/python/ccf/proposal_generator.py +++ b/python/ccf/proposal_generator.py @@ -345,6 +345,20 @@ def set_node_certificate_validity( ) +@cli_proposal +def set_all_nodes_certificate_validity( + valid_from: str, validity_period_days: int, **kwargs +): + return build_proposal( + "set_all_nodes_certificate_validity", + { + "valid_from": valid_from, + "validity_period_days": validity_period_days, + }, + **kwargs, + ) + + if __name__ == "__main__": parser = argparse.ArgumentParser() diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index ad65724c724..55a9b7b5f5e 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -151,12 +151,44 @@ function invalidateOtherOpenProposals(proposalIdToRetain) { }); } -// TODO: Only implement non-ACL-specific proposals for now -// 1. [DONE] Rename proposal to `set_node_certificate_validity` -// 2. Pass `valid_from` to `transition_node_to_trusted` -// 3. Rename things and set initial default period to 24h -// 4. Create set_all_nodes_certificate_validity proposal -// 5. Add proposal to set max allowed node certificate validity +function setNodeCertificateValidityPeriod( + nodeId, + nodeInfo, + validFrom, + validityPeriodDays +) { + if (nodeInfo.certificate_signing_request === undefined) { + throw new Error(`Node ${nodeId} has no certificate signing request`); + } + + const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( + getSingletonKvKey() + ); + if (rawConfig === undefined) { + throw new Error("Service configuration could not be found"); + } + const serviceConfig = ccf.bufToJsonCompatible(rawConfig); + + const max_validity_period = + serviceConfig.node_cert_allowed_validity_period_days ?? + default_validity_period_days; + + if (validityPeriodDays > max_validity_period) { + throw new Error( + `Validity period ${validityPeriodDays} (days) must be less than or equal to service node certificate maximum validity period ${max_validity_period} (days)` + ); + } + + const endorsed_node_cert = ccf.network.generateEndorsedCertificate( + nodeInfo.certificate_signing_request, + validFrom, + validityPeriodDays + ); + ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( + ccf.strToBuf(nodeId), + ccf.strToBuf(endorsed_node_cert) + ); +} const actions = new Map([ [ @@ -959,39 +991,36 @@ const actions = new Map([ throw new Error(`Node ${args.node_id} is not trusted`); } - if (nodeInfo.certificate_signing_request === undefined) { - throw new Error( - `Node ${args.node_id} has no certificate signing request` - ); - } - - const rawConfig = ccf.kv["public:ccf.gov.service.config"].get( - getSingletonKvKey() - ); - if (rawConfig === undefined) { - throw new Error("Service configuration could not be found"); - } - const serviceConfig = ccf.bufToJsonCompatible(rawConfig); - - const max_validity_period = - serviceConfig.node_cert_allowed_validity_period_days ?? - default_validity_period_days; - - if (args.validity_period_days > max_validity_period) { - throw new Error( - `Validity period ${args.validity_period_days} (days) must be less than or equal to service node certificate maximum validity period ${max_validity_period} (days)` - ); - } - - const endorsed_node_cert = ccf.network.generateEndorsedCertificate( - nodeInfo.certificate_signing_request, + setNodeCertificateValidityPeriod( + args.node_id, + nodeInfo, args.valid_from, args.validity_period_days ); - ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set( - ccf.strToBuf(args.node_id), - ccf.strToBuf(endorsed_node_cert) - ); + } + ), + ], + [ + "set_all_nodes_certificate_validity", + new Action( + function (args) { + checkType(args.valid_from, "string", "valid_from"); + checkType(args.validity_period_days, "integer", "validity_period_days"); + checkBounds(args.validity_period_days, 1, null, "validity_period_days"); + }, + function (args) { + ccf.kv["public:ccf.gov.nodes.info"].forEach((v, k) => { + const nodeId = ccf.bufToStr(k); + const nodeInfo = ccf.bufToJsonCompatible(v); + if (nodeInfo.status === "Trusted") { + setNodeCertificateValidityPeriod( + nodeId, + nodeInfo, + args.valid_from, + args.validity_period_days + ); + } + }); } ), ], diff --git a/tests/governance.py b/tests/governance.py index 88ae829e5a7..814efc3c182 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -219,8 +219,8 @@ def post_proposal_request_raw(node, headers=None, expected_error_msg=None): ) -@reqs.description("Update certificates of all nodes") -def test_node_cert_renewal(network, args): +@reqs.description("Update certificates of all nodes, one by one") +def test_each_node_cert_renewal(network, args): primary, _ = network.find_primary() now = datetime.now().replace( microsecond=0 @@ -248,12 +248,16 @@ def test_node_cert_renewal(network, args): ) try: + valid_from_x509 = str(infra.crypto.datetime_to_X509time(valid_from)) network.consortium.set_node_certificate_validity( primary, node, - valid_from=str(infra.crypto.datetime_as_UTCtime(valid_from)), + valid_from=valid_from_x509, validity_period_days=validity_period_days, ) + node.set_certificate_validity_period( + valid_from_x509, validity_period_days + ) except Exception as e: assert isinstance(e, expected_exception) continue @@ -275,22 +279,43 @@ def test_node_cert_renewal(network, args): c.get("/node/network/nodes") +@reqs.description("Update certificates of all nodes, one by one") +def test_all_nodes_cert_renewal(network, args): + primary, _ = network.find_primary() + now = datetime.now().replace( + microsecond=0 + ) # Truncate microseconds which are not reflected in RFC5280 UTCTime + + valid_from = str(infra.crypto.datetime_to_X509time(now)) + validity_period_days = args.max_allowed_node_cert_validity_days + + network.consortium.set_all_nodes_certificate_validity( + primary, + valid_from=valid_from, + validity_period_days=validity_period_days, + ) + + for node in network.get_joined_nodes(): + node.set_certificate_validity_period(valid_from, validity_period_days) + + def gov(args): with infra.network.network( args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb ) as network: network.start_and_join(args) - network.consortium.set_authenticate_session(args.authenticate_session) - test_create_endpoint(network, args) - test_consensus_status(network, args) - test_node_ids(network, args) - test_member_data(network, args) - test_quote(network, args) - test_user(network, args) - test_no_quote(network, args) - test_ack_state_digest_update(network, args) - test_invalid_client_signature(network, args) - test_node_cert_renewal(network, args) + # network.consortium.set_authenticate_session(args.authenticate_session) + # test_create_endpoint(network, args) + # test_consensus_status(network, args) + # test_node_ids(network, args) + # test_member_data(network, args) + # test_quote(network, args) + # test_user(network, args) + # test_no_quote(network, args) + # test_ack_state_digest_update(network, args) + # test_invalid_client_signature(network, args) + test_each_node_cert_renewal(network, args) + test_all_nodes_cert_renewal(network, args) def js_gov(args): diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index e665881e5ee..987aaaea51d 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -550,8 +550,18 @@ def set_node_certificate_validity( validity_period_days, ) proposal = self.get_any_active_member().propose(remote_node, proposal_body) + return self.vote_using_majority(remote_node, proposal, careful_vote) + + def set_all_nodes_certificate_validity( + self, remote_node, valid_from, validity_period_days + ): + proposal_body, careful_vote = self.make_proposal( + "set_all_nodes_certificate_validity", + valid_from, + validity_period_days, + ) + proposal = self.get_any_active_member().propose(remote_node, proposal_body) r = self.vote_using_majority(remote_node, proposal, careful_vote) - node_to_renew.set_certificate_validity_period(valid_from, validity_period_days) return r def check_for_service(self, remote_node, status): diff --git a/tests/infra/crypto.py b/tests/infra/crypto.py index 144b44af281..bc835b40604 100644 --- a/tests/infra/crypto.py +++ b/tests/infra/crypto.py @@ -280,5 +280,5 @@ def get_validity_period_from_pem_cert(pem: str): return cert.not_valid_before, cert.not_valid_after -def datetime_as_UTCtime(datetime: datetime): +def datetime_to_X509time(datetime: datetime): return UTCTime.fromDateTime(datetime) diff --git a/tests/infra/network.py b/tests/infra/network.py index fc1a6a94d2c..6d1ffa95244 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -636,7 +636,7 @@ def trust_node(self, node, args, valid_from=None, validity_period_days=None): try: if self.status is ServiceStatus.OPEN: valid_from = valid_from or str( - infra.crypto.datetime_as_UTCtime(datetime.now()) + infra.crypto.datetime_to_X509time(datetime.now()) ) self.consortium.trust_node( primary, diff --git a/tests/infra/node.py b/tests/infra/node.py index 49dd6807745..295da0114b2 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -24,11 +24,6 @@ BASE_NODE_CLIENT_HOST = "127.100.0.0" -# When a 2.x node joins a 1.x service, the node has to self-endorse -# its certificate, using a default value for the validity period -# hardcoded in CCF. -DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS = 365 - class NodeNetworkState(Enum): stopped = auto() @@ -540,7 +535,7 @@ def verify_certificate_validity_period( ) else: if ( - infra.crypto.datetime_as_UTCtime(valid_from) + infra.crypto.datetime_to_X509time(valid_from) != self.certificate_valid_from ): raise ValueError( diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index 225c1f3e11e..2d96b22d9f2 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -29,6 +29,11 @@ LOCAL_CHECKOUT_DIRECTORY = "." +# When a 2.x node joins a 1.x service, the node has to self-endorse +# its certificate, using a default value for the validity period +# hardcoded in CCF. +DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS = 365 + def issue_activity_on_live_service(network, args): log_capture = [] @@ -82,7 +87,7 @@ def test_new_service(network, args, install_path, binary_dir, library_dir, versi network.join_node(new_node, args.package, args) network.trust_node(new_node, args) new_node.verify_certificate_validity_period( - expected_validity_period_days=infra.node.DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS + expected_validity_period_days=DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS ) LOG.info("Apply transactions to new nodes only") @@ -172,8 +177,11 @@ def run_code_upgrade_from( new_node, args.package, args, from_snapshot=from_snapshot ) network.trust_node(new_node, args) + # For 2.x nodes joining a 1.x service before the constitution is update, + # the node certificate validity period is set by the joining node itself + # as [node startup time, node startup time + 365 days] new_node.verify_certificate_validity_period( - expected_validity_period_days=infra.node.DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS, + expected_validity_period_days=DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS, ignore_proposal_valid_from=True, ) from_snapshot = not from_snapshot diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 8ac8524ab78..2b085a1b99a 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -323,7 +323,9 @@ def test_join_straddling_primary_replacement(network, args): "name": "transition_node_to_trusted", "args": { "node_id": new_node.node_id, - "valid_from": str(infra.crypto.datetime_as_UTCtime(datetime.now())), + "valid_from": str( + infra.crypto.datetime_to_X509time(datetime.now()) + ), }, }, { From 6e796e4dbe243f3b49381fcbb44021f7aef6975e Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 19 Oct 2021 15:17:00 +0000 Subject: [PATCH 095/105] Changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 535fffd0cbc..1f5655dccc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `get_metrics_v1` API to `BaseEndpointRegistry` for applications that do not make use of builtins and want to version or customise metrics output. - Slow ledger IO operations will now be logged at level FAIL. The threshold over which logging will activate can be adjusted by the `--io-logging-threshold` CLI argument to cchost (#3067). - Snapshot files now include receipt of evidence transaction. Nodes can now join or recover a service from a standalone snapshot file. 2.x nodes can still make use of snapshots created by a 1.x node, as long as the ledger suffix containing the proof of evidence is also specified at start-up (#2998). +- Nodes certificates validity period is no longer hardcoded and can instead be set by operators and renewed by members (#2924): + - The new `--initial-node-cert-validity-days` (defaults to 1 day) CLI argument to cchost lets operators set the initial validity period for the node certificate (valid from the current system time). + - The new `--max-allowed-node-cert-validity-days` (defaults to 1 year) CLI argument to cchost sets the maximum validity period allowed for node certificates. + - The new `set_node_certificate_validity` proposal action allows members to renew a node certificate (or `set_all_nodes_certificate_validity` equivalent action to renew _all_ trusted nodes certificates). + - The existing `transition_node_to_trusted` proposal action now requires a new `valid_from` argument (and optional `validity_period_days`, which defaults to the value of ``--max-allowed-node-cert-validity-days`). ### Fixed From c5d4a1d6f817fa75954725d27aa53fc3a5ec9313 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 19 Oct 2021 16:01:31 +0000 Subject: [PATCH 096/105] Docs --- CHANGELOG.md | 2 +- doc/governance/common_member_operations.rst | 31 ++++++++++++++++++++- doc/governance/proposals.rst | 4 +-- doc/operations/certificates.rst | 15 ++++++++++ doc/operations/index.rst | 7 +++++ tests/test_python_cli.sh | 2 +- 6 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 doc/operations/certificates.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f5655dccc5..6176ae6f31b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Snapshot files now include receipt of evidence transaction. Nodes can now join or recover a service from a standalone snapshot file. 2.x nodes can still make use of snapshots created by a 1.x node, as long as the ledger suffix containing the proof of evidence is also specified at start-up (#2998). - Nodes certificates validity period is no longer hardcoded and can instead be set by operators and renewed by members (#2924): - The new `--initial-node-cert-validity-days` (defaults to 1 day) CLI argument to cchost lets operators set the initial validity period for the node certificate (valid from the current system time). - - The new `--max-allowed-node-cert-validity-days` (defaults to 1 year) CLI argument to cchost sets the maximum validity period allowed for node certificates. + - The new `--max-allowed-node-cert-validity-days` (defaults to 365 days) CLI argument to cchost sets the maximum validity period allowed for node certificates. - The new `set_node_certificate_validity` proposal action allows members to renew a node certificate (or `set_all_nodes_certificate_validity` equivalent action to renew _all_ trusted nodes certificates). - The existing `transition_node_to_trusted` proposal action now requires a new `valid_from` argument (and optional `validity_period_days`, which defaults to the value of ``--max-allowed-node-cert-validity-days`). diff --git a/doc/governance/common_member_operations.rst b/doc/governance/common_member_operations.rst index e6bcbd0ac23..c093ad9f259 100644 --- a/doc/governance/common_member_operations.rst +++ b/doc/governance/common_member_operations.rst @@ -136,4 +136,33 @@ The number of member shares required to restore the private ledger (``recovery_t "state": "Accepted" } -.. note:: The new recovery threshold has to be in the range between 1 and the current number of active recovery members. \ No newline at end of file +.. note:: The new recovery threshold has to be in the range between 1 and the current number of active recovery members. + +Renewing Node Certificate +------------------------- + +.. note:: Renewing the certificate of a node does not change the identity (public key) of that node but only its validity period. + +To renew the soon-to-be-expired certificate of a node, members should issue a ``set_node_certificate_validity`` proposal, specifying the date at which the validity period of the renewed certificate should start (``valid_from``), as well as its validity period in days (``validity_period_days``). + +The ``valid_from`` date argument should be a ASN1 UTCTime string, i.e. ``"YYMMDDhhmmssZ"``. The ``validity_period_days`` should be less than the validity period set by operators (see :ref:`operations/certificates:Node Certificates`). + +A sample proposal is: + +.. code-block:: bash + + $ cat set_node_certificate_validity.json + { + "actions": [ + { + "name": "set_node_certificate_validity", + "args": { + "node_id": "86c0ccfab4b869abbc779937c51158c9dd2a130d58323643a3119e83b33dcf5c" + "valid_from": "211019154318Z", + "validity_period_days": 365 + } + } + ] + } + +.. tip:: All currently trusted nodes certificates can be renewed at once using the ``set_all_nodes_certificate_validity`` proposal (same arguments minus ``node_id``). \ No newline at end of file diff --git a/doc/governance/proposals.rst b/doc/governance/proposals.rst index 1fc4553db29..9f8bea66d41 100644 --- a/doc/governance/proposals.rst +++ b/doc/governance/proposals.rst @@ -69,12 +69,12 @@ Some of these subcommands require additional arguments, such as the node ID or u .. code-block:: bash - $ python -m ccf.proposal_generator transition_node_to_trusted 6d566123a899afaea977c5fc0f7a2a9fef33f2946fbc4abefbc3e10ee597343f + $ python -m ccf.proposal_generator transition_node_to_trusted 6d566123a899afaea977c5fc0f7a2a9fef33f2946fbc4abefbc3e10ee597343f 211019154318Z SUCCESS | Writing proposal to ./trust_node_proposal.json SUCCESS | Wrote vote to ./trust_node_vote_for.json $ cat trust_node_proposal.json - {"actions": [{"name": "transition_node_to_trusted", "args": {"node_id": "6d566123a899afaea977c5fc0f7a2a9fef33f2946fbc4abefbc3e10ee597343f"}}]} + {"actions": [{"name": "transition_node_to_trusted", "args": {"node_id": "6d566123a899afaea977c5fc0f7a2a9fef33f2946fbc4abefbc3e10ee597343f", "valid_from": "211019154318Z"}}]} $ python -m ccf.proposal_generator --pretty-print --proposal-output-file add_pedro.json --vote-output-file vote_for_pedro.json set_user pedro_cert.pem SUCCESS | Writing proposal to ./add_pedro.json diff --git a/doc/operations/certificates.rst b/doc/operations/certificates.rst new file mode 100644 index 00000000000..28f8de48b3f --- /dev/null +++ b/doc/operations/certificates.rst @@ -0,0 +1,15 @@ +Certificates +============ + +Since 2.x releases, the validity period of certificates is no longer hardcoded. This page describes how the validity period can instead be set by operators, and renewed by members. + +.. note:: The granularity for the validity period of nodes certificates is one day. + +Node Certificates +----------------- + +At startup, operators can set the validity period for a node using the ``--initial-node-cert-validity-days`` CLI argument. The default value is set to 1 day and it is expected that members will issue a proposal to renew the certificate before it expires, when the service is open. Initial nodes certificates are valid from the current system time when the ``cchost`` executable is launched. + +The ``--max-allowed-node-cert-validity-days`` CLI argument (defaults to 365 days) can be used to set the maximum allowed validity period for nodes certificates when they are renewed by members. It is used as the default value for the validity period when a node certificate is renewed but the validity period is omitted. + +.. tip:: Once a node certificate has expired, clients will no longer trust the node serving their request. It is expected that operators and members will monitor the certificate validity dates with regards to current time and renew the node certificate before expiration. See :ref:`governance/common_member_operations:Renewing Node Certificate` for more details. \ No newline at end of file diff --git a/doc/operations/index.rst b/doc/operations/index.rst index f0730e018ec..405a4c78bd0 100644 --- a/doc/operations/index.rst +++ b/doc/operations/index.rst @@ -26,6 +26,13 @@ This section describes how :term:`Operators` manage the different nodes constitu --- + :fa:`stamp` :doc:`certificates` + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Set and renew nodes and service x509 certificates. + + --- + :fa:`helicopter` :doc:`recovery` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/test_python_cli.sh b/tests/test_python_cli.sh index e485f4f2ea9..69972ff6192 100755 --- a/tests/test_python_cli.sh +++ b/tests/test_python_cli.sh @@ -27,7 +27,7 @@ python -m ccf.proposal_generator transition_service_to_open --help python -m ccf.proposal_generator transition_service_to_open python -m ccf.proposal_generator transition_node_to_trusted --help -python -m ccf.proposal_generator transition_node_to_trusted 42 +python -m ccf.proposal_generator transition_node_to_trusted 42 211019154318Z python -m ccf.proposal_generator add_node_code --help python -m ccf.proposal_generator add_node_code 1234abcd From 720aa047143a1e3f39daa2f8cc48db2cad189574 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Tue, 19 Oct 2021 16:27:45 +0000 Subject: [PATCH 097/105] Validity period is optional for all proposals --- python/ccf/proposal_generator.py | 31 ++++++----------- src/runtime_config/default/actions.js | 49 +++++++++++++++++++++++---- tests/governance.py | 30 ++++++++-------- tests/lts_compatibility.py | 6 ++++ tests/suite/test_suite.py | 2 +- 5 files changed, 75 insertions(+), 43 deletions(-) diff --git a/python/ccf/proposal_generator.py b/python/ccf/proposal_generator.py index 1d8cee0b636..bddb2269314 100644 --- a/python/ccf/proposal_generator.py +++ b/python/ccf/proposal_generator.py @@ -227,7 +227,7 @@ def refresh_js_app_bytecode_cache(**kwargs): @cli_proposal def transition_node_to_trusted( - node_id: str, valid_from: str, validity_period_days=None, **kwargs + node_id: str, valid_from: str, validity_period_days: Optional[int] = None, **kwargs ): args = {"node_id": node_id, "valid_from": valid_from} if validity_period_days is not None: @@ -332,31 +332,22 @@ def set_jwt_public_signing_keys(issuer: str, jwks_path: str, **kwargs): @cli_proposal def set_node_certificate_validity( - node_id: str, valid_from: str, validity_period_days: int, **kwargs + node_id: str, valid_from: str, validity_period_days: Optional[int] = None, **kwargs ): - return build_proposal( - "set_node_certificate_validity", - { - "node_id": node_id, - "valid_from": valid_from, - "validity_period_days": validity_period_days, - }, - **kwargs, - ) + args = {"node_id": node_id, "valid_from": valid_from} + if validity_period_days is not None: + args["validity_period_days"] = validity_period_days + return build_proposal("set_node_certificate_validity", args, **kwargs) @cli_proposal def set_all_nodes_certificate_validity( - valid_from: str, validity_period_days: int, **kwargs + valid_from: str, validity_period_days: Optional[int] = None, **kwargs ): - return build_proposal( - "set_all_nodes_certificate_validity", - { - "valid_from": valid_from, - "validity_period_days": validity_period_days, - }, - **kwargs, - ) + args = {"valid_from": valid_from} + if validity_period_days is not None: + args["validity_period_days"] = validity_period_days + return build_proposal("set_all_nodes_certificate_validity", args, **kwargs) if __name__ == "__main__": diff --git a/src/runtime_config/default/actions.js b/src/runtime_config/default/actions.js index 55a9b7b5f5e..c161a3245eb 100644 --- a/src/runtime_config/default/actions.js +++ b/src/runtime_config/default/actions.js @@ -169,16 +169,23 @@ function setNodeCertificateValidityPeriod( } const serviceConfig = ccf.bufToJsonCompatible(rawConfig); - const max_validity_period = + const default_validity_period_days = 365; + const max_allowed_cert_validity_period_days = serviceConfig.node_cert_allowed_validity_period_days ?? default_validity_period_days; - if (validityPeriodDays > max_validity_period) { + if ( + validityPeriodDays !== undefined && + validityPeriodDays > max_allowed_cert_validity_period_days + ) { throw new Error( - `Validity period ${validityPeriodDays} (days) must be less than or equal to service node certificate maximum validity period ${max_validity_period} (days)` + `Validity period ${validityPeriodDayss} (days) is not allowed: service max allowed is ${max_allowed_cert_validity_period_days} (days)` ); } + validityPeriodDays = + validityPeriodDays ?? max_allowed_cert_validity_period_days; + const endorsed_node_cert = ccf.network.generateEndorsedCertificate( nodeInfo.certificate_signing_request, validFrom, @@ -794,6 +801,12 @@ const actions = new Map([ "integer", "validity_period_days" ); + checkBounds( + args.validity_period_days, + 1, + null, + "validity_period_days" + ); } }, function (args) { @@ -976,8 +989,19 @@ const actions = new Map([ function (args) { checkEntityId(args.node_id, "node_id"); checkType(args.valid_from, "string", "valid_from"); - checkType(args.validity_period_days, "integer", "validity_period_days"); - checkBounds(args.validity_period_days, 1, null, "validity_period_days"); + if (args.validity_period_days !== undefined) { + checkType( + args.validity_period_days, + "integer", + "validity_period_days" + ); + checkBounds( + args.validity_period_days, + 1, + null, + "validity_period_days" + ); + } }, function (args) { const node = ccf.kv["public:ccf.gov.nodes.info"].get( @@ -1005,8 +1029,19 @@ const actions = new Map([ new Action( function (args) { checkType(args.valid_from, "string", "valid_from"); - checkType(args.validity_period_days, "integer", "validity_period_days"); - checkBounds(args.validity_period_days, 1, null, "validity_period_days"); + if (args.validity_period_days !== undefined) { + checkType( + args.validity_period_days, + "integer", + "validity_period_days" + ); + checkBounds( + args.validity_period_days, + 1, + null, + "validity_period_days" + ); + } }, function (args) { ccf.kv["public:ccf.gov.nodes.info"].forEach((v, k) => { diff --git a/tests/governance.py b/tests/governance.py index 814efc3c182..c35aeb0507c 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -230,6 +230,7 @@ def test_each_node_cert_renewal(network, args): test_vectors = [ (now, validity_period_allowed, None), + (now, None, None), # Omit validity period (deduced from service configuration) (now, -1, infra.proposal.ProposalNotCreated), (now, validity_period_forbidden, infra.proposal.ProposalNotAccepted), ] @@ -256,7 +257,9 @@ def test_each_node_cert_renewal(network, args): validity_period_days=validity_period_days, ) node.set_certificate_validity_period( - valid_from_x509, validity_period_days + valid_from_x509, + validity_period_days + or args.max_allowed_node_cert_validity_days, ) except Exception as e: assert isinstance(e, expected_exception) @@ -282,11 +285,8 @@ def test_each_node_cert_renewal(network, args): @reqs.description("Update certificates of all nodes, one by one") def test_all_nodes_cert_renewal(network, args): primary, _ = network.find_primary() - now = datetime.now().replace( - microsecond=0 - ) # Truncate microseconds which are not reflected in RFC5280 UTCTime - valid_from = str(infra.crypto.datetime_to_X509time(now)) + valid_from = str(infra.crypto.datetime_to_X509time(datetime.now())) validity_period_days = args.max_allowed_node_cert_validity_days network.consortium.set_all_nodes_certificate_validity( @@ -304,16 +304,16 @@ def gov(args): args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb ) as network: network.start_and_join(args) - # network.consortium.set_authenticate_session(args.authenticate_session) - # test_create_endpoint(network, args) - # test_consensus_status(network, args) - # test_node_ids(network, args) - # test_member_data(network, args) - # test_quote(network, args) - # test_user(network, args) - # test_no_quote(network, args) - # test_ack_state_digest_update(network, args) - # test_invalid_client_signature(network, args) + network.consortium.set_authenticate_session(args.authenticate_session) + test_create_endpoint(network, args) + test_consensus_status(network, args) + test_node_ids(network, args) + test_member_data(network, args) + test_quote(network, args) + test_user(network, args) + test_no_quote(network, args) + test_ack_state_digest_update(network, args) + test_invalid_client_signature(network, args) test_each_node_cert_renewal(network, args) test_all_nodes_cert_renewal(network, args) diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index 2d96b22d9f2..8cf65341211 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -13,7 +13,9 @@ import os import json import time +from datetime import datetime from e2e_logging import test_random_receipts +from governance import test_all_nodes_cert_renewal from loguru import logger as LOG @@ -90,6 +92,10 @@ def test_new_service(network, args, install_path, binary_dir, library_dir, versi expected_validity_period_days=DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS ) + # 2.x nodes will not have an endorsed certificate in the ledger yet + # so renew their certificate to record it + test_all_nodes_cert_renewal(network, args) + LOG.info("Apply transactions to new nodes only") issue_activity_on_live_service(network, args) test_random_receipts(network, args, lts=True) diff --git a/tests/suite/test_suite.py b/tests/suite/test_suite.py index 4e9549d20fd..791f98eb092 100644 --- a/tests/suite/test_suite.py +++ b/tests/suite/test_suite.py @@ -116,7 +116,7 @@ # jwt jwt_test.test_refresh_jwt_issuer, # governance - governance.test_node_cert_renewal, + governance.test_each_node_cert_renewal, # # # From 5876879cd370bdfd579a7ec8a77c9a50a9e20a9f Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 20 Oct 2021 10:22:22 +0000 Subject: [PATCH 098/105] Return network object in test --- src/crypto/mbedtls/key_pair.cpp | 2 +- tests/governance.py | 2 ++ tests/reconfiguration.py | 28 +++++++++++++++------------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/crypto/mbedtls/key_pair.cpp b/src/crypto/mbedtls/key_pair.cpp index 3e7c81c2a85..aee504ce7e5 100644 --- a/src/crypto/mbedtls/key_pair.cpp +++ b/src/crypto/mbedtls/key_pair.cpp @@ -326,7 +326,7 @@ namespace crypto // Note: 825-day validity range // https://support.apple.com/en-us/HT210176 // Note: For the mbedtls implementation, we do not check that valid_from and - // valid_to are valid or chronological. See OpenSSL equivalent cal for a + // valid_to are valid or chronological. See OpenSSL equivalent call for a // safer implementation. MCHK(mbedtls_x509write_crt_set_validity( crt.get(), diff --git a/tests/governance.py b/tests/governance.py index c35aeb0507c..d4b3b9cca9a 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -281,6 +281,8 @@ def test_each_node_cert_renewal(network, args): # Long-connected client is still connected after certificate renewal c.get("/node/network/nodes") + return network + @reqs.description("Update certificates of all nodes, one by one") def test_all_nodes_cert_renewal(network, args): diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 2b085a1b99a..100a2be4d80 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -453,6 +453,7 @@ def test_learner_does_not_take_part(network, args): def test_node_certificates_validity_period(network, args): for node in network.get_joined_nodes(): node.verify_certificate_validity_period() + return network @reqs.description("Add a new node without a snapshot but with the historical ledger") @@ -465,6 +466,7 @@ def test_add_node_with_read_only_ledger(network, args): new_node, args.package, args, from_snapshot=False, copy_ledger_read_only=True ) network.trust_node(new_node, args) + return network def run(args): @@ -492,19 +494,19 @@ def run(args): test_add_as_many_pending_nodes(network, args) test_add_node(network, args) test_retire_primary(network, args) - test_add_node_with_read_only_ledger(network, args) + # test_add_node_with_read_only_ledger(network, args) - test_add_node_from_snapshot(network, args) - test_add_node_from_snapshot(network, args, from_backup=True) - test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) + # test_add_node_from_snapshot(network, args) + # test_add_node_from_snapshot(network, args, from_backup=True) + # test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) - test_node_filter(network, args) - test_retiring_nodes_emit_at_most_one_signature(network, args) - else: - test_learner_catches_up(network, args) - # test_learner_does_not_take_part(network, args) - test_retire_backup(network, args) - test_node_certificates_validity_period(network, args) + # test_node_filter(network, args) + # test_retiring_nodes_emit_at_most_one_signature(network, args) + # else: + # test_learner_catches_up(network, args) + # # test_learner_does_not_take_part(network, args) + # test_retire_backup(network, args) + # test_node_certificates_validity_period(network, args) def run_join_old_snapshot(args): @@ -576,5 +578,5 @@ def run_join_old_snapshot(args): run(args) - if args.consensus != "bft": - run_join_old_snapshot(args) + # if args.consensus != "bft": + # run_join_old_snapshot(args) From 5945d57b453ef0218b709c9d0a3aecfbcceb1510 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 20 Oct 2021 10:43:07 +0000 Subject: [PATCH 099/105] . --- tests/governance.py | 4 +--- tests/infra/network.py | 2 +- tests/lts_compatibility.py | 1 - tests/reconfiguration.py | 26 +++++++++++++------------- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/tests/governance.py b/tests/governance.py index d4b3b9cca9a..d1c7cc9f776 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -222,9 +222,7 @@ def post_proposal_request_raw(node, headers=None, expected_error_msg=None): @reqs.description("Update certificates of all nodes, one by one") def test_each_node_cert_renewal(network, args): primary, _ = network.find_primary() - now = datetime.now().replace( - microsecond=0 - ) # Truncate microseconds which are not reflected in RFC5280 UTCTime + now = datetime.now() validity_period_allowed = args.max_allowed_node_cert_validity_days - 1 validity_period_forbidden = args.max_allowed_node_cert_validity_days + 1 diff --git a/tests/infra/network.py b/tests/infra/network.py index 6d1ffa95244..d1e2a2919fa 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -1112,7 +1112,7 @@ def cert(self): return network_cert def verify_service_certificate_validity_period(self): - # TODO: Hardcoded for now. See # TODO: See https://github.com/microsoft/CCF/issues/3090 + # See https://github.com/microsoft/CCF/issues/3090 assert self.cert.not_valid_before == datetime( year=2021, month=3, day=11 ) # 20210311000000Z diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index 8cf65341211..6cca79296a0 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -13,7 +13,6 @@ import os import json import time -from datetime import datetime from e2e_logging import test_random_receipts from governance import test_all_nodes_cert_renewal diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 100a2be4d80..637223061b9 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -494,19 +494,19 @@ def run(args): test_add_as_many_pending_nodes(network, args) test_add_node(network, args) test_retire_primary(network, args) - # test_add_node_with_read_only_ledger(network, args) + test_add_node_with_read_only_ledger(network, args) - # test_add_node_from_snapshot(network, args) - # test_add_node_from_snapshot(network, args, from_backup=True) - # test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) + test_add_node_from_snapshot(network, args) + test_add_node_from_snapshot(network, args, from_backup=True) + test_add_node_from_snapshot(network, args, copy_ledger_read_only=False) - # test_node_filter(network, args) - # test_retiring_nodes_emit_at_most_one_signature(network, args) - # else: - # test_learner_catches_up(network, args) - # # test_learner_does_not_take_part(network, args) - # test_retire_backup(network, args) - # test_node_certificates_validity_period(network, args) + test_node_filter(network, args) + test_retiring_nodes_emit_at_most_one_signature(network, args) + else: + test_learner_catches_up(network, args) + # test_learner_does_not_take_part(network, args) + test_retire_backup(network, args) + test_node_certificates_validity_period(network, args) def run_join_old_snapshot(args): @@ -578,5 +578,5 @@ def run_join_old_snapshot(args): run(args) - # if args.consensus != "bft": - # run_join_old_snapshot(args) + if args.consensus != "bft": + run_join_old_snapshot(args) From 810c1212e09abf906f34943a8c817bfff52e479f Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 20 Oct 2021 12:18:22 +0000 Subject: [PATCH 100/105] . --- .daily_canary | 2 +- tests/governance.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.daily_canary b/.daily_canary index a7e5bf505f7..9c558e357c4 100644 --- a/.daily_canary +++ b/.daily_canary @@ -1 +1 @@ -DaILY +. diff --git a/tests/governance.py b/tests/governance.py index d1c7cc9f776..49ec974a098 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -14,7 +14,7 @@ import json import requests import infra.crypto -from datetime import datetime, timedelta +from datetime import datetime import governance_js from infra.runner import ConcurrentRunner import governance_history From 00d4470d7d4ae6cc05d41c35b64055dd5fa059e7 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 20 Oct 2021 12:34:34 +0000 Subject: [PATCH 101/105] Fix reconfiguration test --- tests/reconfiguration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/reconfiguration.py b/tests/reconfiguration.py index 637223061b9..bd81fb24465 100644 --- a/tests/reconfiguration.py +++ b/tests/reconfiguration.py @@ -489,7 +489,6 @@ def run(args): test_add_node_from_backup(network, args) test_add_node(network, args) test_add_node_on_other_curve(network, args) - test_add_node_invalid_validity_period(network, args) test_retire_backup(network, args) test_add_as_many_pending_nodes(network, args) test_add_node(network, args) @@ -507,6 +506,7 @@ def run(args): # test_learner_does_not_take_part(network, args) test_retire_backup(network, args) test_node_certificates_validity_period(network, args) + test_add_node_invalid_validity_period(network, args) def run_join_old_snapshot(args): From 21ce7f3933b6f269907763058b2c6a9ee33d64e6 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 20 Oct 2021 13:24:27 +0000 Subject: [PATCH 102/105] Cycle nodes in lts compatibility --- tests/lts_compatibility.py | 52 +++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index 6cca79296a0..54e17a746b7 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -70,7 +70,15 @@ def replace_constitution_fragment(args, fragment_name): return args.constitution -def test_new_service(network, args, install_path, binary_dir, library_dir, version): +def test_new_service( + network, + args, + install_path, + binary_dir, + library_dir, + version, + cycle_existing_nodes=False, +): LOG.info("Update constitution") primary, _ = network.find_primary() new_constitution = get_new_constitution_for_install(args, install_path) @@ -78,21 +86,29 @@ def test_new_service(network, args, install_path, binary_dir, library_dir, versi # Note: Changes to constitution between versions should be tested here - LOG.info("Add node to new service") - new_node = network.create_node( - "local://localhost", - binary_dir=binary_dir, - library_dir=library_dir, - version=version, - ) - network.join_node(new_node, args.package, args) - network.trust_node(new_node, args) - new_node.verify_certificate_validity_period( - expected_validity_period_days=DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS - ) + LOG.info(f"Add node to new service [cycle nodes: {cycle_existing_nodes}]") + nodes_to_cycle = network.get_joined_nodes() if cycle_existing_nodes else [] + nodes_to_add_count = len(nodes_to_cycle) if cycle_existing_nodes else 1 + + for _ in range(0, nodes_to_add_count): + new_node = network.create_node( + "local://localhost", + binary_dir=binary_dir, + library_dir=library_dir, + version=version, + ) + network.join_node(new_node, args.package, args) + network.trust_node(new_node, args) + new_node.verify_certificate_validity_period( + expected_validity_period_days=DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS + ) + + for node in nodes_to_cycle: + network.retire_node(primary, node) + if primary == node: + primary, _ = network.wait_for_new_primary(primary) + node.stop() - # 2.x nodes will not have an endorsed certificate in the ledger yet - # so renew their certificate to record it test_all_nodes_cert_renewal(network, args) LOG.info("Apply transactions to new nodes only") @@ -215,6 +231,7 @@ def run_code_upgrade_from( ) primary, _ = network.find_primary() network.consortium.retire_code(primary, old_code_id) + for node in old_nodes: network.retire_node(primary, node) if primary == node: @@ -233,6 +250,10 @@ def run_code_upgrade_from( else: time.sleep(3) + # Code update from 1.x to 2.x requires cycling the freshly-added 2.x nodes + # once. This is because 2.x nodes will not have an endorsed certificate + # recorded in the store and thus will not be able to have their certificate + # refreshed, etc. test_new_service( network, args, @@ -240,6 +261,7 @@ def run_code_upgrade_from( to_binary_dir, to_library_dir, to_version, + cycle_existing_nodes=True, ) # Check that the ledger can be parsed From 84c9d06cacb1ed4f1eaeff84353bedd6443f5fac Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 20 Oct 2021 14:17:26 +0000 Subject: [PATCH 103/105] Add image --- doc/img/node_cert_renewal.svg | 1 + doc/operations/certificates.rst | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 doc/img/node_cert_renewal.svg diff --git a/doc/img/node_cert_renewal.svg b/doc/img/node_cert_renewal.svg new file mode 100644 index 00000000000..59d5bac4645 --- /dev/null +++ b/doc/img/node_cert_renewal.svg @@ -0,0 +1 @@ +Node 0(primary)Node 1Node 2Initial certificate validity period (24h default, cchostoption)Members open service+ set_all_nodes_certificates_validityNode 3joins after service openMembers trust new node 3transition_node_to_trustedPost service open validity periodNew joinerValidity period/!\Members should issue new set_node_certificate_validityproposal before certificate expiry \ No newline at end of file diff --git a/doc/operations/certificates.rst b/doc/operations/certificates.rst index 28f8de48b3f..aaef72fdd11 100644 --- a/doc/operations/certificates.rst +++ b/doc/operations/certificates.rst @@ -12,4 +12,8 @@ At startup, operators can set the validity period for a node using the ``--initi The ``--max-allowed-node-cert-validity-days`` CLI argument (defaults to 365 days) can be used to set the maximum allowed validity period for nodes certificates when they are renewed by members. It is used as the default value for the validity period when a node certificate is renewed but the validity period is omitted. -.. tip:: Once a node certificate has expired, clients will no longer trust the node serving their request. It is expected that operators and members will monitor the certificate validity dates with regards to current time and renew the node certificate before expiration. See :ref:`governance/common_member_operations:Renewing Node Certificate` for more details. \ No newline at end of file +.. tip:: Once a node certificate has expired, clients will no longer trust the node serving their request. It is expected that operators and members will monitor the certificate validity dates with regards to current time and renew the node certificate before expiration. See :ref:`governance/common_member_operations:Renewing Node Certificate` for more details. + +The procedure that operators and members should follow is summarised in the following example. A 3-node service is started by operators and the initial certificate validity period is set by ``--initial-node-cert-validity-days`` (blue). Before these certificates expire, the service is open by members who renew the certificate for each node, via the ``set_all_nodes_certificate_validity`` proposal action, either standalone or bundled with the existing ``transition_service_to_open`` action (green). When a new node (3) joins the service, members should set the validity period for its certificate when submitting the ``transition_node_to_trusted`` proposal (pale orange). Finally, operators and members should issue a new proposal to renew soon-to-expire node certificates (red). + +.. image:: ../img/node_cert_renewal.svg \ No newline at end of file From 47cba5476a3cf3595de93aaaba0eefb7347b778e Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 20 Oct 2021 15:22:24 +0000 Subject: [PATCH 104/105] Fix lts ledger compatibility --- src/node/rpc/node_frontend.h | 1 - tests/infra/node.py | 2 +- tests/lts_compatibility.py | 21 ++++++++++----------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index dec6f7f8d91..2fbdba6adba 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -1164,7 +1164,6 @@ namespace ccf g.set_constitution(in.genesis_info->constitution); } - auto config = ctx.tx.ro(this->network.config)->get(); if (!in.node_cert.has_value()) { auto endorsed_certificates = diff --git a/tests/infra/node.py b/tests/infra/node.py index 295da0114b2..72deae25106 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -539,7 +539,7 @@ def verify_certificate_validity_period( != self.certificate_valid_from ): raise ValueError( - f'Validity period for node {self.local_node_id} certificate is not as expected: valid from "{valid_from}", but expected "{self.certificate_valid_from}"' + f'Validity period for node {self.local_node_id} certificate is not as expected: valid from "{infra.crypto.datetime_to_X509time(valid_from)}", but expected "{self.certificate_valid_from}"' ) # Note: CCF substracts one second from validity period since x509 specifies diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index 54e17a746b7..79cb9be253d 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -399,9 +399,7 @@ def run_ledger_compatibility_since_first(args, local_branch, use_snapshot): assert ( r.body.json()["ccf_version"] == expected_version ), f"Node version is not {expected_version}" - node.verify_certificate_validity_period( - args.max_allowed_node_cert_validity_days, - ) + node.verify_certificate_validity_period() # Rollover JWKS so that new primary must read historical CA bundle table # and retrieve new keys via auto refresh @@ -413,14 +411,15 @@ def run_ledger_compatibility_since_first(args, local_branch, use_snapshot): else: time.sleep(3) - test_new_service( - network, - args, - install_path, - binary_dir, - library_dir, - version, - ) + if idx > 0: + test_new_service( + network, + args, + install_path, + binary_dir, + library_dir, + version, + ) snapshot_dir = ( network.get_committed_snapshots(primary) if use_snapshot else None From 1070ceb3ab642c4e4d77dad89bfdfbdc88d712d2 Mon Sep 17 00:00:00 2001 From: Julien Maffre Date: Wed, 20 Oct 2021 15:29:30 +0000 Subject: [PATCH 105/105] mypy --- python/ccf/proposal_generator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/ccf/proposal_generator.py b/python/ccf/proposal_generator.py index bddb2269314..db6ee7245c6 100644 --- a/python/ccf/proposal_generator.py +++ b/python/ccf/proposal_generator.py @@ -231,7 +231,7 @@ def transition_node_to_trusted( ): args = {"node_id": node_id, "valid_from": valid_from} if validity_period_days is not None: - args["validity_period_days"] = validity_period_days + args["validity_period_days"] = validity_period_days # type: ignore return build_proposal("transition_node_to_trusted", args, **kwargs) @@ -336,7 +336,7 @@ def set_node_certificate_validity( ): args = {"node_id": node_id, "valid_from": valid_from} if validity_period_days is not None: - args["validity_period_days"] = validity_period_days + args["validity_period_days"] = validity_period_days # type: ignore return build_proposal("set_node_certificate_validity", args, **kwargs) @@ -346,7 +346,7 @@ def set_all_nodes_certificate_validity( ): args = {"valid_from": valid_from} if validity_period_days is not None: - args["validity_period_days"] = validity_period_days + args["validity_period_days"] = validity_period_days # type: ignore return build_proposal("set_all_nodes_certificate_validity", args, **kwargs)