From 68d5937e394a4271781d390b7f9c8ae00b8aba14 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 18 Mar 2024 17:59:32 +0000 Subject: [PATCH] Service cert subject name is configurable (#5993) --- .daily_canary | 2 +- doc/host_config_schema/cchost_config.json | 5 +++ include/ccf/crypto/verifier.h | 2 ++ include/ccf/node/startup_config.h | 2 ++ samples/config/start_config.json | 3 +- src/common/configuration.h | 1 + src/crypto/openssl/key_pair.cpp | 11 ++++--- src/crypto/verifier.cpp | 5 +++ src/host/configuration.h | 4 ++- src/host/main.cpp | 2 ++ src/node/historical_queries_utils.cpp | 4 +-- src/node/identity.h | 38 +++++++---------------- src/node/node_state.h | 5 +++ src/node/rpc/serialization.h | 4 +-- src/node/rpc/test/frontend_test_infra.h | 5 ++- tests/config.jinja | 3 +- tests/e2e_operations.py | 26 ++++++++++++++++ tests/infra/remote.py | 2 ++ tests/tls_report.csv | 2 +- 19 files changed, 85 insertions(+), 41 deletions(-) diff --git a/.daily_canary b/.daily_canary index 1abc907b99dd..5687a161f08c 100644 --- a/.daily_canary +++ b/.daily_canary @@ -2,4 +2,4 @@ (- -) (= =) | Y & +--? ( V ) / . \ | +---=---' /--x-m- /--n-n---xXx--/--yY------>>>----<<<>>]]{{}}---||-/\---.. -2024!--- \ No newline at end of file +2024___ diff --git a/doc/host_config_schema/cchost_config.json b/doc/host_config_schema/cchost_config.json index 5fc5a63ef259..ffe1a9f6731a 100644 --- a/doc/host_config_schema/cchost_config.json +++ b/doc/host_config_schema/cchost_config.json @@ -276,6 +276,11 @@ "description": "Initial validity period (days) for service certificate", "minimum": 1 }, + "service_subject_name": { + "type": "string", + "default": "CN=CCF Service", + "description": "Subject name to include in service certificate. Can only be set once on service start." + }, "members": { "type": "array", "items": { diff --git a/include/ccf/crypto/verifier.h b/include/ccf/crypto/verifier.h index b4594c670336..ed74d3b3623b 100644 --- a/include/ccf/crypto/verifier.h +++ b/include/ccf/crypto/verifier.h @@ -259,4 +259,6 @@ namespace crypto const std::vector& der); crypto::Pem public_key_pem_from_cert(const std::vector& der); + + std::string get_subject_name(const Pem& cert); } diff --git a/include/ccf/node/startup_config.h b/include/ccf/node/startup_config.h index a84f3360209c..5de7d20d8c2b 100644 --- a/include/ccf/node/startup_config.h +++ b/include/ccf/node/startup_config.h @@ -83,6 +83,8 @@ struct StartupConfig : CCFConfig // Only if starting or recovering size_t initial_service_certificate_validity_days = 1; + std::string service_subject_name = "CN=CCF Service"; + nlohmann::json service_data = nullptr; nlohmann::json node_data = nullptr; diff --git a/samples/config/start_config.json b/samples/config/start_config.json index eaae939eccec..bf2a53064270 100644 --- a/samples/config/start_config.json +++ b/samples/config/start_config.json @@ -50,7 +50,8 @@ "recovery_threshold": 1, "maximum_node_certificate_validity_days": 365 }, - "initial_service_certificate_validity_days": 1 + "initial_service_certificate_validity_days": 1, + "service_subject_name": "CN=A Sample CCF Service" } }, "ledger": { diff --git a/src/common/configuration.h b/src/common/configuration.h index 2b4958a79727..181c32fc70b5 100644 --- a/src/common/configuration.h +++ b/src/common/configuration.h @@ -115,6 +115,7 @@ DECLARE_JSON_REQUIRED_FIELDS( startup_host_time, snapshot_tx_interval, initial_service_certificate_validity_days, + service_subject_name, service_data, node_data, start, diff --git a/src/crypto/openssl/key_pair.cpp b/src/crypto/openssl/key_pair.cpp index 26d11248e3c0..bb3608c1124d 100644 --- a/src/crypto/openssl/key_pair.cpp +++ b/src/crypto/openssl/key_pair.cpp @@ -379,14 +379,17 @@ namespace crypto X509V3_set_ctx_nodb(&v3ctx); X509V3_set_ctx(&v3ctx, icrt ? icrt : crt, NULL, csr, NULL, 0); + std::string constraints = "critical,CA:FALSE"; + if (ca) + { + constraints = "critical,CA:TRUE,pathlen:0"; + } + // Add basic constraints X509_EXTENSION* ext = NULL; OpenSSL::CHECKNULL( ext = X509V3_EXT_conf_nid( - NULL, - &v3ctx, - NID_basic_constraints, - ca ? "critical,CA:TRUE,pathlen:0" : "critical,CA:FALSE")); + NULL, &v3ctx, NID_basic_constraints, constraints.c_str())); OpenSSL::CHECK1(X509_add_ext(crt, ext, -1)); X509_EXTENSION_free(ext); diff --git a/src/crypto/verifier.cpp b/src/crypto/verifier.cpp index 856dea6006f6..6e81ebfe543b 100644 --- a/src/crypto/verifier.cpp +++ b/src/crypto/verifier.cpp @@ -49,4 +49,9 @@ namespace crypto { return make_unique_verifier(der)->public_key_pem(); } + + std::string get_subject_name(const Pem& cert) + { + return make_verifier(cert)->subject(); + } } diff --git a/src/host/configuration.h b/src/host/configuration.h index 824feb0e0e52..f0f5297e738a 100644 --- a/src/host/configuration.h +++ b/src/host/configuration.h @@ -141,6 +141,7 @@ namespace host std::vector constitution_files = {}; ccf::ServiceConfiguration service_configuration; size_t initial_service_certificate_validity_days = 1; + std::string service_subject_name = "CN=CCF Service"; bool operator==(const Start&) const = default; }; @@ -205,7 +206,8 @@ namespace host DECLARE_JSON_OPTIONAL_FIELDS( CCHostConfig::Command::Start, service_configuration, - initial_service_certificate_validity_days); + initial_service_certificate_validity_days, + service_subject_name); DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(CCHostConfig::Command::Join); DECLARE_JSON_REQUIRED_FIELDS(CCHostConfig::Command::Join, target_rpc_address); diff --git a/src/host/main.cpp b/src/host/main.cpp index b229417d785f..724db95b28a8 100644 --- a/src/host/main.cpp +++ b/src/host/main.cpp @@ -633,6 +633,8 @@ int main(int argc, char** argv) recovery_threshold; startup_config.initial_service_certificate_validity_days = config.command.start.initial_service_certificate_validity_days; + startup_config.service_subject_name = + config.command.start.service_subject_name; LOG_INFO_FMT( "Creating new node: new network (with {} initial member(s) and {} " "member(s) required for recovery)", diff --git a/src/node/historical_queries_utils.cpp b/src/node/historical_queries_utils.cpp index c8aff9be5858..6daf3801837d 100644 --- a/src/node/historical_queries_utils.cpp +++ b/src/node/historical_queries_utils.cpp @@ -122,12 +122,12 @@ namespace ccf auto ncv = crypto::make_unique_verifier(network_identity->cert); auto endorsement = create_endorsed_cert( hpubkey, - ReplicatedNetworkIdentity::subject_name, + crypto::get_subject_name(opt_psi->cert), {}, ncv->validity_period(), network_identity->priv_key, network_identity->cert, - true); + true /* CA */); service_endorsement_cache[hpubkey] = {endorsement}; receipt.service_endorsements = {endorsement}; } diff --git a/src/node/identity.h b/src/node/identity.h index 77eb431dbd4b..28402706e23a 100644 --- a/src/node/identity.h +++ b/src/node/identity.h @@ -22,16 +22,20 @@ namespace ccf { crypto::Pem priv_key; crypto::Pem cert; - std::optional type; + std::optional type = IdentityType::REPLICATED; + std::string subject_name = "CN=CCF Service"; bool operator==(const NetworkIdentity& other) const { return cert == other.cert && priv_key == other.priv_key && - type == other.type; + type == other.type && subject_name == other.subject_name; } - NetworkIdentity() : type(IdentityType::REPLICATED) {} - NetworkIdentity(IdentityType type) : type(type) {} + NetworkIdentity(const std::string& subject_name_) : + type(IdentityType::REPLICATED), + subject_name(subject_name_) + {} + NetworkIdentity() = default; virtual crypto::Pem issue_certificate( const std::string& valid_from, size_t validity_period_days) @@ -47,15 +51,14 @@ namespace ccf class ReplicatedNetworkIdentity : public NetworkIdentity { public: - static constexpr auto subject_name = "CN=CCF Network"; - - ReplicatedNetworkIdentity() : NetworkIdentity(IdentityType::REPLICATED) {} + ReplicatedNetworkIdentity() = default; ReplicatedNetworkIdentity( + const std::string& subject_name_, crypto::CurveID curve_id, const std::string& valid_from, size_t validity_period_days) : - NetworkIdentity(IdentityType::REPLICATED) + NetworkIdentity(subject_name_) { auto identity_key_pair = std::make_shared(curve_id); @@ -70,7 +73,7 @@ namespace ccf } ReplicatedNetworkIdentity(const NetworkIdentity& other) : - NetworkIdentity(IdentityType::REPLICATED) + NetworkIdentity(other.subject_name) { if (type != other.type) { @@ -104,21 +107,4 @@ namespace ccf OPENSSL_cleanse(priv_key.data(), priv_key.size()); } }; - - class SplitNetworkIdentity : public NetworkIdentity - { - public: - SplitNetworkIdentity() : NetworkIdentity(IdentityType::SPLIT) {} - - SplitNetworkIdentity(const NetworkIdentity& other) : - NetworkIdentity(IdentityType::SPLIT) - { - if (type != other.type) - { - throw std::runtime_error("invalid identity type conversion"); - } - priv_key = {}; - cert = other.cert; - } - }; } \ No newline at end of file diff --git a/src/node/node_state.h b/src/node/node_state.h index 6be6c443e43a..d067f41706a1 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -493,6 +493,7 @@ namespace ccf case StartType::Start: { network.identity = std::make_unique( + config.service_subject_name, curve_id, config.startup_host_time, config.initial_service_certificate_validity_days); @@ -525,7 +526,11 @@ namespace ccf "identity"); } + crypto::Pem previous_service_identity_cert( + config.recover.previous_service_identity.value()); + network.identity = std::make_unique( + crypto::get_subject_name(previous_service_identity_cert), curve_id, config.startup_host_time, config.initial_service_certificate_validity_days); diff --git a/src/node/rpc/serialization.h b/src/node/rpc/serialization.h index e3a9ea00b115..89d0e36065a9 100644 --- a/src/node/rpc/serialization.h +++ b/src/node/rpc/serialization.h @@ -44,10 +44,8 @@ namespace ccf {ccf::IdentityType::SPLIT, "Split"}}) DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(NetworkIdentity) DECLARE_JSON_REQUIRED_FIELDS(NetworkIdentity, cert, priv_key) - DECLARE_JSON_OPTIONAL_FIELDS(NetworkIdentity, type) + DECLARE_JSON_OPTIONAL_FIELDS(NetworkIdentity, type, subject_name) DECLARE_JSON_TYPE_WITH_BASE(ReplicatedNetworkIdentity, NetworkIdentity) - DECLARE_JSON_TYPE_WITH_BASE(SplitNetworkIdentity, NetworkIdentity) - DECLARE_JSON_REQUIRED_FIELDS(SplitNetworkIdentity, cert, type) DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS( JoinNetworkNodeToNode::Out::NetworkInfo) diff --git a/src/node/rpc/test/frontend_test_infra.h b/src/node/rpc/test/frontend_test_infra.h index 9b3ae51b7bfd..bd46a2260af9 100644 --- a/src/node/rpc/test/frontend_test_infra.h +++ b/src/node/rpc/test/frontend_test_infra.h @@ -123,7 +123,10 @@ std::unique_ptr make_test_network_ident() const auto valid_from = ds::to_x509_time_string(std::chrono::system_clock::now() - 24h); return std::make_unique( - crypto::service_identity_curve_choice, valid_from, 2); + "CN=CCF test network", + crypto::service_identity_curve_choice, + valid_from, + 2); } void init_network(NetworkState& network) diff --git a/tests/config.jinja b/tests/config.jinja index 176ecbe28a31..a440b21a43f6 100644 --- a/tests/config.jinja +++ b/tests/config.jinja @@ -38,7 +38,8 @@ "maximum_node_certificate_validity_days": {{ maximum_node_certificate_validity_days }}, "maximum_service_certificate_validity_days": {{ maximum_service_certificate_validity_days }} }, - "initial_service_certificate_validity_days": {{ initial_service_cert_validity_days }} + "initial_service_certificate_validity_days": {{ initial_service_cert_validity_days }}, + "service_subject_name": {{ service_subject_name|tojson }} }, "join": { diff --git a/tests/e2e_operations.py b/tests/e2e_operations.py index 8ce9e150d1c9..08e9968d5de6 100644 --- a/tests/e2e_operations.py +++ b/tests/e2e_operations.py @@ -20,6 +20,8 @@ import time import http import infra.snp as snp +from cryptography import x509 +from cryptography.hazmat.backends import default_backend from loguru import logger as LOG @@ -564,6 +566,29 @@ def run_max_uncommitted_tx_count(args): assert r.status_code == http.HTTPStatus.OK.value, r +def run_service_subject_name_check(args): + with infra.network.network( + args.nodes, + args.binary_dir, + args.debug_nodes, + args.perf_nodes, + pdb=args.pdb, + ) as network: + network.start_and_open(args, service_subject_name="CN=This test service") + # Check service_cert.pem + with open(network.cert_path, "rb") as cert_file: + cert = x509.load_pem_x509_certificate(cert_file.read(), default_backend()) + assert cert.subject.rfc4514_string() == "CN=This test service", cert + # Check /node/service endpoint + primary, _ = network.find_primary() + with primary.client() as c: + r = c.get("/node/network") + assert r.status_code == http.HTTPStatus.OK.value, r + cert_pem = r.body.json()["service_certificate"] + cert = x509.load_pem_x509_certificate(cert_pem.encode(), default_backend()) + assert cert.subject.rfc4514_string() == "CN=This test service", cert + + def run(args): run_max_uncommitted_tx_count(args) run_file_operations(args) @@ -573,3 +598,4 @@ def run(args): run_pid_file_check(args) run_preopen_readiness_check(args) run_sighup_check(args) + run_service_subject_name_check(args) diff --git a/tests/infra/remote.py b/tests/infra/remote.py index 3d3af6a67967..d8b320edabab 100644 --- a/tests/infra/remote.py +++ b/tests/infra/remote.py @@ -621,6 +621,7 @@ def __init__( max_uncommitted_tx_count=0, snp_security_policy_file=None, snp_uvm_endorsements_file=None, + service_subject_name="CN=CCF Test Service", **kwargs, ): """ @@ -813,6 +814,7 @@ def __init__( max_uncommitted_tx_count=max_uncommitted_tx_count, snp_security_policy_file=snp_security_policy_file, snp_uvm_endorsements_file=snp_uvm_endorsements_file, + service_subject_name=service_subject_name, **kwargs, ) diff --git a/tests/tls_report.csv b/tests/tls_report.csv index a0395e2dfa4a..e25d2d798ae1 100644 --- a/tests/tls_report.csv +++ b/tests/tls_report.csv @@ -37,7 +37,7 @@ "banner_reverseproxy","","INFO","--","","CWE-200" "banner_server","","INFO","No Server banner line in header, interesting!","","" "cert","","INFO","----------","","" -"cert_caIssuers","","INFO","CCF Network","","" +"cert_caIssuers","","INFO","CCF Test Service","","" "cert_certificatePolicies_EV","","INFO","no","","" "cert_chain_of_trust","","CRITICAL","failed (chain incomplete).","","" "cert_commonName","","OK","CCF Node","",""