From 482f28de08af576d5e089d61d6414e92de9a8333 Mon Sep 17 00:00:00 2001 From: AssemblyJohn Date: Tue, 15 Oct 2024 11:49:01 +0300 Subject: [PATCH 1/8] Added interface changes for supporting trusted CA keys (multiple roots) Signed-off-by: AssemblyJohn --- include/evse_security/evse_security.hpp | 18 ++++++++++++++++++ include/evse_security/evse_types.hpp | 16 ++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/include/evse_security/evse_security.hpp b/include/evse_security/evse_security.hpp index e1a7507..5df71d3 100644 --- a/include/evse_security/evse_security.hpp +++ b/include/evse_security/evse_security.hpp @@ -197,6 +197,24 @@ class EvseSecurity { GetCertificateInfoResult get_leaf_certificate_info(LeafCertificateType certificate_type, EncodingFormat encoding, bool include_ocsp = false); + /// @brief Finds the latest valid leafs, for each root certificate that is present on the filesystem, and + /// returns all the newest valid leafs that are present for different roots. This is required, because + /// a query parameter when requesting the leaf is not advisable during the TLS handshake + /// Existing filesystem: + /// ROOT_V2G_Hubject->SUB_CA1->SUB_CA2->Leaf_Invalid_A + /// ROOT_V2G_Hubject->SUB_CA1->SUB_CA2->Leaf_Valid_A + /// ROOT_V2G_Hubject->SUB_CA1->SUB_CA2->Leaf_Valid_B + /// ROOT_V2G_OtherProvider->SUB_CA_O1->SUB_CA_O2->Leav_Valid_A + /// will return: + /// ROOT_V2G_Hubject->SUB_CA1->SUB_CA2->Leaf_Valid_B + + /// ROOT_V2G_OtherProvider->SUB_CA_O1->SUB_CA_O2->Leav_Valid_A + /// @param certificate_type type of leaf certificate that we start the search from + /// @param encoding specifies PEM or DER format + /// @param include_ocsp if OCSP data should be included + /// @return contains response result, with info related to the full certificate chains info and response status + GetCertificateFullInfoResult get_all_valid_certificates_info(LeafCertificateType certificate_type, + EncodingFormat encoding, bool include_ocsp = false); + /// @brief Checks and updates the symlinks for the V2G leaf certificates and keys to the most recent valid one /// @return true if one of the links was updated bool update_certificate_links(LeafCertificateType certificate_type); diff --git a/include/evse_security/evse_types.hpp b/include/evse_security/evse_types.hpp index a038e48..34236cf 100644 --- a/include/evse_security/evse_types.hpp +++ b/include/evse_security/evse_types.hpp @@ -143,10 +143,13 @@ struct CertificateOCSP { }; struct CertificateInfo { - fs::path key; ///< The path of the PEM or DER encoded private key - std::optional certificate; ///< The path of the PEM or DER encoded certificate chain if found - std::optional certificate_single; ///< The path of the PEM or DER encoded certificate if found - int certificate_count; ///< The count of certificates, if the chain is available, or 1 if single + fs::path key; ///< The path of the PEM or DER encoded private key + std::optional certificate_root; ///< The PEM of root certificate used by the leaf, has a value only + /// when using 'get_all_valid_certificates_info' + std::optional certificate; ///< The path of the PEM or DER encoded certificate chain if found + std::optional certificate_single; ///< The path of the PEM or DER encoded single certificate if found + int leaf_certificate_count; ///< The count of certificates, if the chain is available, or 1 if single + /// (the root is not taken into account because of the OCSP cache) std::optional password; ///< Specifies the password for the private key if encrypted std::vector ocsp; ///< The ordered list of OCSP certificate data based on the chain file order }; @@ -156,6 +159,11 @@ struct GetCertificateInfoResult { std::optional info; }; +struct GetCertificateFullInfoResult { + GetCertificateInfoStatus status; + std::vector info; +}; + struct GetCertificateSignRequestResult { GetCertificateSignRequestStatus status; std::optional csr; From 96f77aaaf06fcf1b208956e115dbfb8351eec0dd Mon Sep 17 00:00:00 2001 From: AssemblyJohn Date: Wed, 16 Oct 2024 11:40:59 +0300 Subject: [PATCH 2/8] Added get full certs implementation Signed-off-by: AssemblyJohn --- .../certificate/x509_hierarchy.hpp | 3 + include/evse_security/evse_security.hpp | 13 + include/evse_security/evse_types.hpp | 2 +- .../certificate/x509_hierarchy.cpp | 22 ++ lib/evse_security/evse_security.cpp | 284 ++++++++++++------ 5 files changed, 231 insertions(+), 93 deletions(-) diff --git a/include/evse_security/certificate/x509_hierarchy.hpp b/include/evse_security/certificate/x509_hierarchy.hpp index ab88e8c..b9a86ca 100644 --- a/include/evse_security/certificate/x509_hierarchy.hpp +++ b/include/evse_security/certificate/x509_hierarchy.hpp @@ -57,6 +57,9 @@ class X509CertificateHierarchy { /// @brief returns true if we contain a certificate with the following hash bool contains_certificate_hash(const CertificateHashData& hash); + /// @brief Searches for the root of the provided leaf, throwing a NoCertificateFound if not found + X509Wrapper find_certificate_root(const X509Wrapper& leaf); + /// @brief Searches for the provided hash, throwing a NoCertificateFound if not found X509Wrapper find_certificate(const CertificateHashData& hash); diff --git a/include/evse_security/evse_security.hpp b/include/evse_security/evse_security.hpp index 5df71d3..45ed8d3 100644 --- a/include/evse_security/evse_security.hpp +++ b/include/evse_security/evse_security.hpp @@ -272,8 +272,21 @@ class EvseSecurity { // Internal versions of the functions do not lock the mutex CertificateValidationResult verify_certificate_internal(const std::string& certificate_chain, LeafCertificateType certificate_type); + GetCertificateInfoResult get_leaf_certificate_info_internal(LeafCertificateType certificate_type, EncodingFormat encoding, bool include_ocsp = false); + + /// @brief Retrieves information related to leaf certificates + /// @param include_ocsp if OCSP information should be included + /// @param include_root if the root certificate of the leaf should be included in the returned list + /// @param include_all_valid if true, all valid leafs will be included, sorted in order, with the newest being + /// first. If false, only the newest one will be returned + GetCertificateFullInfoResult get_full_leaf_certificate_info_internal(LeafCertificateType certificate_type, + EncodingFormat encoding, + bool include_ocsp = false, + bool include_root = false, + bool include_all_valid = false); + GetCertificateInfoResult get_ca_certificate_info_internal(CaCertificateType certificate_type); std::optional retrieve_ocsp_cache_internal(const CertificateHashData& certificate_hash_data); bool is_ca_certificate_installed_internal(CaCertificateType certificate_type); diff --git a/include/evse_security/evse_types.hpp b/include/evse_security/evse_types.hpp index 34236cf..d6d6ed9 100644 --- a/include/evse_security/evse_types.hpp +++ b/include/evse_security/evse_types.hpp @@ -148,7 +148,7 @@ struct CertificateInfo { /// when using 'get_all_valid_certificates_info' std::optional certificate; ///< The path of the PEM or DER encoded certificate chain if found std::optional certificate_single; ///< The path of the PEM or DER encoded single certificate if found - int leaf_certificate_count; ///< The count of certificates, if the chain is available, or 1 if single + int certificate_count; ///< The count of certificates, if the chain is available, or 1 if single /// (the root is not taken into account because of the OCSP cache) std::optional password; ///< Specifies the password for the private key if encrypted std::vector ocsp; ///< The ordered list of OCSP certificate data based on the chain file order diff --git a/lib/evse_security/certificate/x509_hierarchy.cpp b/lib/evse_security/certificate/x509_hierarchy.cpp index 97f044a..3bf4c05 100644 --- a/lib/evse_security/certificate/x509_hierarchy.cpp +++ b/lib/evse_security/certificate/x509_hierarchy.cpp @@ -79,6 +79,28 @@ bool X509CertificateHierarchy::contains_certificate_hash(const CertificateHashDa return contains; } +X509Wrapper X509CertificateHierarchy::find_certificate_root(const X509Wrapper& leaf) { + const X509Wrapper* root_ptr = nullptr; + + for (const auto& root : hierarchy) { + if (root.state.is_selfsigned) { + for_each_descendant( + [&](const X509Node& node, int depth) { + // If we found our matching certificate, we also found the root + if (node.certificate == leaf) { + root_ptr = &root.certificate; + } + }, + root, 1); + } + } + + if (root_ptr) + return *root_ptr; + + throw NoCertificateFound("Could not find a certificate root for leaf: " + leaf.get_common_name()); +} + X509Wrapper X509CertificateHierarchy::find_certificate(const CertificateHashData& hash) { X509Wrapper* certificate = nullptr; diff --git a/lib/evse_security/evse_security.cpp b/lib/evse_security/evse_security.cpp index 9a2e34d..cfb825b 100644 --- a/lib/evse_security/evse_security.cpp +++ b/lib/evse_security/evse_security.cpp @@ -1086,6 +1086,42 @@ GetCertificateSignRequestResult EvseSecurity::generate_certificate_signing_reque return generate_certificate_signing_request(certificate_type, country, organization, common, false); } +GetCertificateFullInfoResult EvseSecurity::get_all_valid_certificates_info(LeafCertificateType certificate_type, + EncodingFormat encoding, bool include_ocsp) { + std::lock_guard guard(EvseSecurity::security_mutex); + + GetCertificateFullInfoResult result = + get_full_leaf_certificate_info_internal(certificate_type, encoding, include_ocsp, true, true); + + // If we failed, simply return the result + if (result.status != GetCertificateInfoStatus::Accepted) { + return result; + } + + GetCertificateFullInfoResult filtered_results; + filtered_results.status = result.status; + + // Filter the certificates to return only the ones that have a unique + // root, and from those that have a unique root, return only the newest + std::set unique_roots; + + // The newest are the first, that's how 'get_leaf_certificate_info_internal' + // returns them + for (const auto& chain : result.info) { + const std::string& root = chain.certificate_root.value(); + + // If we don't contain the unique root yet, it is the newest leaf for that root + if (unique_roots.find(root) == unique_roots.end()) { + filtered_results.info.push_back(chain); + + // Add it to the roots list, adding only unique roots + unique_roots.insert(root); + } + } + + return filtered_results; +} + GetCertificateInfoResult EvseSecurity::get_leaf_certificate_info(LeafCertificateType certificate_type, EncodingFormat encoding, bool include_ocsp) { std::lock_guard guard(EvseSecurity::security_mutex); @@ -1095,11 +1131,26 @@ GetCertificateInfoResult EvseSecurity::get_leaf_certificate_info(LeafCertificate GetCertificateInfoResult EvseSecurity::get_leaf_certificate_info_internal(LeafCertificateType certificate_type, EncodingFormat encoding, bool include_ocsp) { + GetCertificateFullInfoResult result = + get_full_leaf_certificate_info_internal(certificate_type, encoding, include_ocsp, false, false); + GetCertificateInfoResult internal_result; + + internal_result.status = result.status; + if (!result.info.empty()) { + internal_result.info = std::move(result.info.at(0)); + } + + return internal_result; +} + +GetCertificateFullInfoResult EvseSecurity::get_full_leaf_certificate_info_internal(LeafCertificateType certificate_type, + EncodingFormat encoding, + bool include_ocsp, bool include_root, + bool include_all_valid) { EVLOG_info << "Requesting leaf certificate info: " << conversions::leaf_certificate_type_to_string(certificate_type); - GetCertificateInfoResult result; - result.info = std::nullopt; + GetCertificateFullInfoResult result; fs::path key_dir; fs::path cert_dir; @@ -1131,26 +1182,43 @@ GetCertificateInfoResult EvseSecurity::get_leaf_certificate_info_internal(LeafCe return result; } - std::optional latest_valid; - std::optional found_private_key_path; + struct KeyPairInternal { + X509Wrapper certificate; + fs::path certificate_key; + }; + + std::vector valid_leafs; + + bool any_valid_certificate = false; + bool any_valid_key = false; // Iterate all certificates from newest to the oldest leaf_certificates.for_each_chain_ordered( [&](const fs::path& file, const std::vector& chain) { // Search for the first valid where we can find a key if (not chain.empty() && chain.at(0).is_valid()) { + any_valid_certificate = true; + try { // Search for the private key auto priv_key_path = get_private_key_path_of_certificate(chain.at(0), key_dir, this->private_key_password); + // Found at least one valid key + any_valid_key = true; + // Copy to latest valid - latest_valid = chain.at(0); - found_private_key_path = priv_key_path; + KeyPairInternal key_pair{chain.at(0), priv_key_path}; + valid_leafs.emplace_back(std::move(key_pair)); // We found, break EVLOG_info << "Found valid leaf: [" << chain.at(0).get_file().value() << "]"; - return false; + + // Collect all if we don't include valid only + if (include_all_valid == false) { + EVLOG_info << "Not requiring all valid leafs, returning"; + return false; + } } catch (const NoPrivateKeyException& e) { } } @@ -1166,122 +1234,154 @@ GetCertificateInfoResult EvseSecurity::get_leaf_certificate_info_internal(LeafCe } }); - if (latest_valid.has_value() == false) { + if (!any_valid_certificate) { EVLOG_warning << "Could not find valid certificate"; result.status = GetCertificateInfoStatus::NotFoundValid; return result; } - if (found_private_key_path.has_value() == false) { + if (!any_valid_key) { EVLOG_warning << "Could not find private key for the valid certificate"; result.status = GetCertificateInfoStatus::PrivateKeyNotFound; return result; } - // Key path doesn't change - fs::path key_file = found_private_key_path.value(); - auto& certificate = latest_valid.value(); - - // Paths to search - std::optional certificate_file; - std::optional chain_file; + for (const auto& valid_leaf : valid_leafs) { + // Key path doesn't change + fs::path key_file = valid_leaf.certificate_key; + auto& certificate = valid_leaf.certificate; + + // Paths to search + std::optional certificate_file; + std::optional chain_file; + + X509CertificateBundle leaf_directory(cert_dir, EncodingFormat::PEM); + + const std::vector* leaf_fullchain = nullptr; + const std::vector* leaf_single = nullptr; + int chain_len = 1; // Defaults to 1, single certificate + + // We are searching for both the full leaf bundle, containing the leaf and the cso1/2 and the single leaf + // without the cso1/2 + leaf_directory.for_each_chain( + [&](const std::filesystem::path& path, const std::vector& chain) { + // If we contain the latest valid, we found our generated bundle + bool leaf_found = (std::find(chain.begin(), chain.end(), certificate) != chain.end()); + + if (leaf_found) { + if (chain.size() > 1) { + leaf_fullchain = &chain; + chain_len = chain.size(); + } else if (chain.size() == 1) { + leaf_single = &chain; + } + } - X509CertificateBundle leaf_directory(cert_dir, EncodingFormat::PEM); + // Found both, break + if (leaf_fullchain != nullptr && leaf_single != nullptr) + return false; - const std::vector* leaf_fullchain = nullptr; - const std::vector* leaf_single = nullptr; - int chain_len = 1; // Defaults to 1, single certificate + return true; + }); - // We are searching for both the full leaf bundle, containing the leaf and the cso1/2 and the single leaf - // without the cso1/2 - leaf_directory.for_each_chain([&](const std::filesystem::path& path, const std::vector& chain) { - // If we contain the latest valid, we found our generated bundle - bool bFound = (std::find(chain.begin(), chain.end(), certificate) != chain.end()); + std::vector certificate_ocsp{}; + std::optional leafs_root = std::nullopt; - if (bFound) { - if (chain.size() > 1) { - leaf_fullchain = &chain; - chain_len = chain.size(); - } else if (chain.size() == 1) { - leaf_single = &chain; - } + // None were found + if (leaf_single == nullptr && leaf_fullchain == nullptr) { + EVLOG_error << "Could not find any leaf certificate for:" + << conversions::leaf_certificate_type_to_string(certificate_type); + // Move onto next valid leaf, and attempt a search there + continue; } - // Found both, break - if (leaf_fullchain != nullptr && leaf_single != nullptr) - return false; - - return true; - }); - - std::vector certificate_ocsp{}; - - // None were found - if (leaf_single == nullptr && leaf_fullchain == nullptr) { - EVLOG_error << "Could not find any leaf certificate for:" - << conversions::leaf_certificate_type_to_string(certificate_type); + if (leaf_fullchain != nullptr) { + chain_file = leaf_fullchain->at(0).get_file(); + EVLOG_debug << "Leaf fullchain: [" << chain_file.value_or("INVALID") << "]"; + } else { + EVLOG_debug << conversions::leaf_certificate_type_to_string(certificate_type) + << " leaf requires full bundle, but full bundle not found at path: " << cert_dir; + } - result.status = GetCertificateInfoStatus::NotFound; - return result; - } + if (leaf_single != nullptr) { + certificate_file = leaf_single->at(0).get_file(); + EVLOG_debug << "Leaf single: [" << certificate_file.value_or("INVALID") << "]"; + } else { + EVLOG_debug << conversions::leaf_certificate_type_to_string(certificate_type) + << " single leaf not found at path: " << cert_dir; + } - if (leaf_fullchain != nullptr) { - chain_file = leaf_fullchain->at(0).get_file(); - EVLOG_debug << "Leaf fullchain: [" << chain_file.value_or("INVALID") << "]"; - } else { - EVLOG_warning << conversions::leaf_certificate_type_to_string(certificate_type) - << " leaf requires full bundle, but full bundle not found at path: " << cert_dir; - } + // Both require the hierarchy build + if (include_ocsp || include_root) { + X509CertificateBundle root_bundle(root_dir, EncodingFormat::PEM); // Required for hierarchy - if (leaf_single != nullptr) { - certificate_file = leaf_single->at(0).get_file(); - EVLOG_debug << "Leaf single: [" << certificate_file.value_or("INVALID") << "]"; - } else { - EVLOG_warning << conversions::leaf_certificate_type_to_string(certificate_type) - << " single leaf not found at path: " << cert_dir; - } + // The hierarchy is required for both roots and the OCSP cache + auto hierarchy = + std::move(X509CertificateHierarchy::build_hierarchy(root_bundle.split(), leaf_directory.split())); + EVLOG_debug << "Hierarchy for root/OCSP data: \n" << hierarchy.to_debug_string(); - // Include OCSP data if possible - if (include_ocsp && (leaf_fullchain != nullptr || leaf_single != nullptr)) { - X509CertificateBundle root_bundle(root_dir, EncodingFormat::PEM); // Required for hierarchy + // Include OCSP data if possible + if (include_ocsp) { + // Search for OCSP data for each certificate + if (leaf_fullchain != nullptr) { + for (const auto& chain_certif : *leaf_fullchain) { + try { + CertificateHashData hash = hierarchy.get_certificate_hash(chain_certif); + std::optional data = retrieve_ocsp_cache_internal(hash); - auto hierarchy = - std::move(X509CertificateHierarchy::build_hierarchy(root_bundle.split(), leaf_directory.split())); - EVLOG_debug << "Hierarchy for OCSP data: \n" << hierarchy.to_debug_string(); + certificate_ocsp.push_back({hash, data}); + } catch (const NoCertificateFound& e) { + // Always add to preserve file order + certificate_ocsp.push_back({{}, std::nullopt}); + } + } + } else { + try { + CertificateHashData hash = hierarchy.get_certificate_hash(leaf_single->at(0)); + certificate_ocsp.push_back({hash, retrieve_ocsp_cache_internal(hash)}); + } catch (const NoCertificateFound& e) { + } + } + } - // Search for OCSP data for each certificate - if (leaf_fullchain != nullptr) { - for (const auto& chain_certif : *leaf_fullchain) { + // Include root data if possible + if (include_root) { + // Search for the root of any of the leafs + // present either in the chain or single try { - CertificateHashData hash = hierarchy.get_certificate_hash(chain_certif); - std::optional data = retrieve_ocsp_cache_internal(hash); + X509Wrapper leafs_root_cert = hierarchy.find_certificate_root( + leaf_fullchain != nullptr ? leaf_fullchain->at(0) : leaf_single->at(0)); - certificate_ocsp.push_back({hash, data}); + // Append the root + leafs_root = leafs_root_cert.get_export_string(); } catch (const NoCertificateFound& e) { - // Always add to preserve file order - certificate_ocsp.push_back({{}, std::nullopt}); + EVLOG_warning << "Root required for [" + << conversions::leaf_certificate_type_to_string(certificate_type) + << "] leaf certificate, but no root could be found"; } } - } else { - try { - CertificateHashData hash = hierarchy.get_certificate_hash(leaf_single->at(0)); - certificate_ocsp.push_back({hash, retrieve_ocsp_cache_internal(hash)}); - } catch (const NoCertificateFound& e) { - } } - } - CertificateInfo info; + CertificateInfo info; - info.key = key_file; - info.certificate = chain_file; - info.certificate_single = certificate_file; - info.certificate_count = chain_len; - info.password = this->private_key_password; - info.ocsp = certificate_ocsp; + info.key = key_file; + info.certificate = chain_file; + info.certificate_single = certificate_file; + info.certificate_count = chain_len; + info.password = this->private_key_password; - result.info = info; - result.status = GetCertificateInfoStatus::Accepted; + if (include_ocsp) { + info.ocsp = certificate_ocsp; + } + + if (include_root && leafs_root.has_value()) { + info.certificate_root = leafs_root.value(); + } + + // Add it to the returned result list + result.info.push_back(info); + result.status = GetCertificateInfoStatus::Accepted; + } // End valid leaf iteration return result; } catch (const CertificateLoadException& e) { From e83621342a238b23b8cd9abfc4f1cb1887e441b9 Mon Sep 17 00:00:00 2001 From: AssemblyJohn Date: Thu, 17 Oct 2024 12:56:09 +0300 Subject: [PATCH 3/8] Add tests for testing multi-root return Signed-off-by: AssemblyJohn --- tests/tests.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/tests.cpp b/tests/tests.cpp index c31c016..00a7ebc 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -173,6 +173,10 @@ class EvseSecurityTestsExpired : public EvseSecurityTests { } }; +TEST_F(EvseSecurityTests, verify_multi_root_leaf_retrieval) { + +} + TEST_F(EvseSecurityTests, verify_basics) { const char* bundle_path = "certs/ca/v2g/V2G_CA_BUNDLE.pem"; From 39bc6894d920ce616195455d1c7f433ad7e00b48 Mon Sep 17 00:00:00 2001 From: AssemblyJohn Date: Thu, 17 Oct 2024 15:07:03 +0300 Subject: [PATCH 4/8] Tests implementation with alternate root Signed-off-by: AssemblyJohn --- lib/evse_security/evse_security.cpp | 5 +++++ tests/configs/seccLeafCert_Alternate.cnf | 15 +++++++++++++++ tests/configs/v2gRootCACert_Alternate.cnf | 15 +++++++++++++++ tests/generate_test_certs.sh | 7 ++++++- tests/tests.cpp | 4 ++++ 5 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/configs/seccLeafCert_Alternate.cnf create mode 100644 tests/configs/v2gRootCACert_Alternate.cnf diff --git a/lib/evse_security/evse_security.cpp b/lib/evse_security/evse_security.cpp index cfb825b..1420a90 100644 --- a/lib/evse_security/evse_security.cpp +++ b/lib/evse_security/evse_security.cpp @@ -1108,6 +1108,11 @@ GetCertificateFullInfoResult EvseSecurity::get_all_valid_certificates_info(LeafC // The newest are the first, that's how 'get_leaf_certificate_info_internal' // returns them for (const auto& chain : result.info) { + // Ignore non-root items + if(!chain.certificate_root.has_value()) { + continue; + } + const std::string& root = chain.certificate_root.value(); // If we don't contain the unique root yet, it is the newest leaf for that root diff --git a/tests/configs/seccLeafCert_Alternate.cnf b/tests/configs/seccLeafCert_Alternate.cnf new file mode 100644 index 0000000..ca696df --- /dev/null +++ b/tests/configs/seccLeafCert_Alternate.cnf @@ -0,0 +1,15 @@ +[req] +prompt = no +distinguished_name = ca_dn + +[ca_dn] +commonName = SECCGridSyncCert +organizationName = GridSync +countryName = DE +domainComponent = CPO + +[ext] +basicConstraints = critical,CA:false +keyUsage = critical,digitalSignature,keyAgreement +subjectKeyIdentifier = hash + diff --git a/tests/configs/v2gRootCACert_Alternate.cnf b/tests/configs/v2gRootCACert_Alternate.cnf new file mode 100644 index 0000000..480594f --- /dev/null +++ b/tests/configs/v2gRootCACert_Alternate.cnf @@ -0,0 +1,15 @@ +[req] +prompt = no +distinguished_name = ca_dn + +[ca_dn] +commonName = V2GRootGridSyncCA +organizationName = GridSync +countryName = DE +domainComponent = V2G + +[ext] +basicConstraints = critical,CA:true +keyUsage = critical,keyCertSign,cRLSign +subjectKeyIdentifier = hash + diff --git a/tests/generate_test_certs.sh b/tests/generate_test_certs.sh index 00cd0e0..43214a2 100755 --- a/tests/generate_test_certs.sh +++ b/tests/generate_test_certs.sh @@ -74,8 +74,13 @@ create_certificate SECC_LEAF "${CLIENT_CSO_PATH}" seccLeafCert.cnf 12348 "${CA_C # Invalid self-signed CSMS cert create_certificate INVALID_CSMS "${CLIENT_INVALID_PATH}" v2gRootCACert.cnf 12345 +# V2G alternate root CA +create_certificate V2G_ROOT_GRIDSYNC_CA "${CA_V2G_PATH}" v2gRootCACert_Alternate.cnf 12345 +# Alternate chargepoint leaf +create_certificate SECC_LEAF_GRIDSYNC "${CLIENT_CSMS_PATH}" seccLeafCert_Alternate.cnf 12348 "${CA_V2G_PATH}/V2G_ROOT_GRIDSYNC_CA.pem" "${CA_V2G_PATH}/V2G_ROOT_GRIDSYNC_CA.key" + # create cert chain bundles in the V2G root ca and chargepoint leaf dirs -cat "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" "$CA_V2G_PATH/V2G_ROOT_CA.pem" > "$CA_V2G_PATH/V2G_CA_BUNDLE.pem" +cat "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" "$CA_V2G_PATH/V2G_ROOT_CA.pem" "$CA_V2G_PATH/V2G_ROOT_GRIDSYNC_CA.pem" > "$CA_V2G_PATH/V2G_CA_BUNDLE.pem" cat "$CLIENT_CSO_PATH/SECC_LEAF.pem" "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" > "$CLIENT_CSO_PATH/CPO_CERT_CHAIN.pem" cp "$CLIENT_CSO_PATH/SECC_LEAF.key" "$CLIENT_CSMS_PATH/CSMS_LEAF.key" diff --git a/tests/tests.cpp b/tests/tests.cpp index 00a7ebc..5f6fb87 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -174,7 +174,11 @@ class EvseSecurityTestsExpired : public EvseSecurityTests { }; TEST_F(EvseSecurityTests, verify_multi_root_leaf_retrieval) { + auto result = this->evse_security->get_all_valid_certificates_info(LeafCertificateType::CSMS, EncodingFormat::PEM, false); + ASSERT_EQ(result.status, GetCertificateInfoStatus::Accepted); + + std::cout << "Result size: " << result.info.size(); } TEST_F(EvseSecurityTests, verify_basics) { From 1ccc6b927d5edf57fedd0296ffd52ce6e505e250 Mon Sep 17 00:00:00 2001 From: AssemblyJohn Date: Thu, 17 Oct 2024 15:26:45 +0300 Subject: [PATCH 5/8] Test code fixes Signed-off-by: AssemblyJohn --- lib/evse_security/evse_security.cpp | 2 +- tests/CMakeLists.txt | 1 + tests/generate_test_certs.sh | 7 +- tests/generate_test_certs_multi.sh | 100 ++++++++++++++++++++++++++++ tests/tests.cpp | 45 +++++++++---- 5 files changed, 136 insertions(+), 19 deletions(-) create mode 100755 tests/generate_test_certs_multi.sh diff --git a/lib/evse_security/evse_security.cpp b/lib/evse_security/evse_security.cpp index 1420a90..6a790a6 100644 --- a/lib/evse_security/evse_security.cpp +++ b/lib/evse_security/evse_security.cpp @@ -1109,7 +1109,7 @@ GetCertificateFullInfoResult EvseSecurity::get_all_valid_certificates_info(LeafC // returns them for (const auto& chain : result.info) { // Ignore non-root items - if(!chain.certificate_root.has_value()) { + if (!chain.certificate_root.has_value()) { continue; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 43e4489..9443eb1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -34,6 +34,7 @@ add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME}) install( PROGRAMS "${CMAKE_CURRENT_SOURCE_DIR}/generate_test_certs.sh" + PROGRAMS "${CMAKE_CURRENT_SOURCE_DIR}/generate_test_certs_multi.sh" DESTINATION "${CMAKE_BINARY_DIR}/tests" ) diff --git a/tests/generate_test_certs.sh b/tests/generate_test_certs.sh index 43214a2..00cd0e0 100755 --- a/tests/generate_test_certs.sh +++ b/tests/generate_test_certs.sh @@ -74,13 +74,8 @@ create_certificate SECC_LEAF "${CLIENT_CSO_PATH}" seccLeafCert.cnf 12348 "${CA_C # Invalid self-signed CSMS cert create_certificate INVALID_CSMS "${CLIENT_INVALID_PATH}" v2gRootCACert.cnf 12345 -# V2G alternate root CA -create_certificate V2G_ROOT_GRIDSYNC_CA "${CA_V2G_PATH}" v2gRootCACert_Alternate.cnf 12345 -# Alternate chargepoint leaf -create_certificate SECC_LEAF_GRIDSYNC "${CLIENT_CSMS_PATH}" seccLeafCert_Alternate.cnf 12348 "${CA_V2G_PATH}/V2G_ROOT_GRIDSYNC_CA.pem" "${CA_V2G_PATH}/V2G_ROOT_GRIDSYNC_CA.key" - # create cert chain bundles in the V2G root ca and chargepoint leaf dirs -cat "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" "$CA_V2G_PATH/V2G_ROOT_CA.pem" "$CA_V2G_PATH/V2G_ROOT_GRIDSYNC_CA.pem" > "$CA_V2G_PATH/V2G_CA_BUNDLE.pem" +cat "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" "$CA_V2G_PATH/V2G_ROOT_CA.pem" > "$CA_V2G_PATH/V2G_CA_BUNDLE.pem" cat "$CLIENT_CSO_PATH/SECC_LEAF.pem" "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" > "$CLIENT_CSO_PATH/CPO_CERT_CHAIN.pem" cp "$CLIENT_CSO_PATH/SECC_LEAF.key" "$CLIENT_CSMS_PATH/CSMS_LEAF.key" diff --git a/tests/generate_test_certs_multi.sh b/tests/generate_test_certs_multi.sh new file mode 100755 index 0000000..43214a2 --- /dev/null +++ b/tests/generate_test_certs_multi.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +CERT_PATH="./certs" +CSR_PATH="./csr" + +EC_CURVE=prime256v1 +SYMMETRIC_CIPHER=-aes-128-cbc +password="123456" + +CA_CSMS_PATH="$CERT_PATH/ca/csms" +CA_CSO_PATH="$CERT_PATH/ca/cso" +CA_V2G_PATH="$CERT_PATH/ca/v2g" +CA_MO_PATH="$CERT_PATH/ca/mo" +CA_INVALID_PATH="$CERT_PATH/ca/invalid" + +CLIENT_CSMS_PATH="$CERT_PATH/client/csms" +CLIENT_CSO_PATH="$CERT_PATH/client/cso" +CLIENT_V2G_PATH="$CERT_PATH/client/v2g" +CLIENT_INVALID_PATH="$CERT_PATH/client/invalid" +VALIDITY=3650 + +TO_BE_INSTALLED_PATH="$CERT_PATH/to_be_installed" + +mkdir -p "$CERT_PATH" +mkdir -p "$CSR_PATH" +mkdir -p "$CA_CSMS_PATH" +mkdir -p "$CA_CSO_PATH" +mkdir -p "$CA_V2G_PATH" +mkdir -p "$CA_MO_PATH" +mkdir -p "$CLIENT_CSMS_PATH" +mkdir -p "$CLIENT_CSO_PATH" +mkdir -p "$CLIENT_V2G_PATH" +mkdir -p "$CLIENT_INVALID_PATH" +mkdir -p "$TO_BE_INSTALLED_PATH" + +function create_certificate() { + # Args: + # $1: name of the certificate (without the .pem extension) + # $2: directory to install the certificate and private key into + # $3: openssl config file for the certificate + # $4: serial number for the certificate + # $5: CA certificate file. If this is missing, we will create a self-signed certificate. + # $6: CA private key file. Likewise omit this to create a self-signed certificate. + + local name="$1" + local install_dir="$2" + local config="$3" + local serial_num="$4" + local signed_by_cert="$5" + local signed_by_key="$6" + + openssl ecparam -genkey -name "$EC_CURVE" | openssl ec "$SYMMETRIC_CIPHER" -passout pass:"$password" -out "${install_dir}/${name}.key" + + if [ -z $signed_by_cert ] + then + openssl req -new -key "${install_dir}/${name}.key" -passin pass:"$password" -config "configs/${config}" -out "${CSR_PATH}/${name}.csr" + openssl x509 -req -in "${CSR_PATH}/${name}.csr" -extfile "configs/${config}" -extensions ext -signkey "${install_dir}/${name}.key" -passin pass:"$password" $SHA -set_serial "${serial_num}" -out "${install_dir}/${name}.pem" -days "$VALIDITY" + else + openssl req -new -key "${install_dir}/${name}.key" -passin pass:"$password" -config "configs/${config}" -out "${CSR_PATH}/${name}.csr" + openssl x509 -req -in "${CSR_PATH}/${name}.csr" -extfile "configs/${config}" -extensions ext -CA "${signed_by_cert}" -CAkey "${signed_by_key}" -passin pass:"$password" -set_serial "${serial_num}" -out "${install_dir}/${name}.pem" -days "$VALIDITY" + fi +} + +# V2G root CA +create_certificate V2G_ROOT_CA "${CA_V2G_PATH}" v2gRootCACert.cnf 12345 +# Second V2G root CA +create_certificate V2G_ROOT_CA_NEW "${CA_V2G_PATH}" v2gRootCACert.cnf 12349 +# Sub-CA 1 +create_certificate CPO_SUB_CA1 "${CA_CSMS_PATH}" cpoSubCA1Cert.cnf 12346 "${CA_V2G_PATH}/V2G_ROOT_CA.pem" "${CA_V2G_PATH}/V2G_ROOT_CA.key" +# Sub-CA 2 +create_certificate CPO_SUB_CA2 "${CA_CSMS_PATH}" cpoSubCA2Cert.cnf 12347 "${CA_CSMS_PATH}/CPO_SUB_CA1.pem" "${CA_CSMS_PATH}/CPO_SUB_CA1.key" +# Chargepoint leaf +create_certificate SECC_LEAF "${CLIENT_CSO_PATH}" seccLeafCert.cnf 12348 "${CA_CSMS_PATH}/CPO_SUB_CA2.pem" "${CA_CSMS_PATH}/CPO_SUB_CA2.key" +# Invalid self-signed CSMS cert +create_certificate INVALID_CSMS "${CLIENT_INVALID_PATH}" v2gRootCACert.cnf 12345 + +# V2G alternate root CA +create_certificate V2G_ROOT_GRIDSYNC_CA "${CA_V2G_PATH}" v2gRootCACert_Alternate.cnf 12345 +# Alternate chargepoint leaf +create_certificate SECC_LEAF_GRIDSYNC "${CLIENT_CSMS_PATH}" seccLeafCert_Alternate.cnf 12348 "${CA_V2G_PATH}/V2G_ROOT_GRIDSYNC_CA.pem" "${CA_V2G_PATH}/V2G_ROOT_GRIDSYNC_CA.key" + +# create cert chain bundles in the V2G root ca and chargepoint leaf dirs +cat "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" "$CA_V2G_PATH/V2G_ROOT_CA.pem" "$CA_V2G_PATH/V2G_ROOT_GRIDSYNC_CA.pem" > "$CA_V2G_PATH/V2G_CA_BUNDLE.pem" +cat "$CLIENT_CSO_PATH/SECC_LEAF.pem" "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" > "$CLIENT_CSO_PATH/CPO_CERT_CHAIN.pem" + +cp "$CLIENT_CSO_PATH/SECC_LEAF.key" "$CLIENT_CSMS_PATH/CSMS_LEAF.key" + +# assume CSO and CSMS are same authority +cp -r $CA_CSMS_PATH/* $CA_CSO_PATH +cp "$CLIENT_CSO_PATH/SECC_LEAF.pem" "$CLIENT_CSMS_PATH/CSMS_LEAF.pem" + +# empty MO bundle +touch "$CA_MO_PATH/MO_CA_BUNDLE.pem" + +# Create certificates used for installation tests +create_certificate INSTALL_TEST_ROOT_CA1 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21234 +create_certificate INSTALL_TEST_ROOT_CA2 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21235 +create_certificate INSTALL_TEST_ROOT_CA3 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21236 +create_certificate INSTALL_TEST_ROOT_CA3_SUBCA1 "${TO_BE_INSTALLED_PATH}" install_test_subca1.cnf 21237 "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3.pem" "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3.key" +create_certificate INSTALL_TEST_ROOT_CA3_SUBCA2 "${TO_BE_INSTALLED_PATH}" install_test_subca2.cnf 21238 "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3_SUBCA1.pem" "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3_SUBCA1.key" diff --git a/tests/tests.cpp b/tests/tests.cpp index 5f6fb87..d84f455 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -50,10 +50,6 @@ bool equal_certificate_strings(const std::string& cert1, const std::string& cert return true; } -void install_certs() { - std::system("./generate_test_certs.sh"); -} - namespace evse_security { class EvseSecurityTests : public ::testing::Test { @@ -86,6 +82,18 @@ class EvseSecurityTests : public ::testing::Test { fs::remove_all("certs"); fs::remove_all("csr"); } + + virtual void install_certs() { + std::system("./generate_test_certs.sh"); + } +}; + + +class EvseSecurityTestsMulti : public EvseSecurityTests { +protected: + void install_certs() override { + std::system("./generate_test_certs_multi.sh"); + } }; class EvseSecurityTestsExpired : public EvseSecurityTests { @@ -173,14 +181,6 @@ class EvseSecurityTestsExpired : public EvseSecurityTests { } }; -TEST_F(EvseSecurityTests, verify_multi_root_leaf_retrieval) { - auto result = this->evse_security->get_all_valid_certificates_info(LeafCertificateType::CSMS, EncodingFormat::PEM, false); - - ASSERT_EQ(result.status, GetCertificateInfoStatus::Accepted); - - std::cout << "Result size: " << result.info.size(); -} - TEST_F(EvseSecurityTests, verify_basics) { const char* bundle_path = "certs/ca/v2g/V2G_CA_BUNDLE.pem"; @@ -280,6 +280,27 @@ TEST_F(EvseSecurityTests, verify_certificate_counts) { ASSERT_EQ(this->evse_security->get_count_of_installed_certificates({CertificateType::MORootCertificate}), 0); } +TEST_F(EvseSecurityTestsMulti, verify_multi_root_leaf_retrieval) { + auto result = + this->evse_security->get_all_valid_certificates_info(LeafCertificateType::CSMS, EncodingFormat::PEM, false); + + ASSERT_EQ(result.status, GetCertificateInfoStatus::Accepted); + + // We have 2 leafs + ASSERT_EQ(result.info.size(), 2); + + ASSERT_EQ(fs::path("certs/client/csms/CSMS_LEAF.pem"), result.info[0].certificate_single.value()); + ASSERT_EQ(fs::path("certs/client/csms/SECC_LEAF_GRIDSYNC.pem"), result.info[1].certificate_single.value()); + + ASSERT_TRUE(result.info[0].certificate_root.has_value()); + ASSERT_TRUE(result.info[1].certificate_root.has_value()); + + ASSERT_TRUE(equal_certificate_strings(result.info[0].certificate_root.value(), + read_file_to_string("certs/ca/v2g/V2G_ROOT_CA.pem"))); + ASSERT_TRUE(equal_certificate_strings(result.info[1].certificate_root.value(), + read_file_to_string("certs/ca/v2g/V2G_ROOT_GRIDSYNC_CA.pem"))); +} + TEST_F(EvseSecurityTests, verify_normal_keygen) { KeyGenerationInfo info; KeyHandle_ptr key; From f4fb53c48ff14af3ddbba21029b7eefd2f40f525 Mon Sep 17 00:00:00 2001 From: AssemblyJohn Date: Thu, 17 Oct 2024 15:28:39 +0300 Subject: [PATCH 6/8] Format Signed-off-by: AssemblyJohn --- tests/tests.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/tests.cpp b/tests/tests.cpp index d84f455..c42d607 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -88,12 +88,11 @@ class EvseSecurityTests : public ::testing::Test { } }; - class EvseSecurityTestsMulti : public EvseSecurityTests { -protected: +protected: void install_certs() override { std::system("./generate_test_certs_multi.sh"); - } + } }; class EvseSecurityTestsExpired : public EvseSecurityTests { From 1ee16ba6a0ae94998d1737e95e5f0596685c7cab Mon Sep 17 00:00:00 2001 From: AssemblyJohn Date: Mon, 21 Oct 2024 11:56:22 +0300 Subject: [PATCH 7/8] Implemented comments Signed-off-by: AssemblyJohn --- lib/evse_security/evse_security.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/evse_security/evse_security.cpp b/lib/evse_security/evse_security.cpp index 6a790a6..b60af48 100644 --- a/lib/evse_security/evse_security.cpp +++ b/lib/evse_security/evse_security.cpp @@ -1321,8 +1321,7 @@ GetCertificateFullInfoResult EvseSecurity::get_full_leaf_certificate_info_intern X509CertificateBundle root_bundle(root_dir, EncodingFormat::PEM); // Required for hierarchy // The hierarchy is required for both roots and the OCSP cache - auto hierarchy = - std::move(X509CertificateHierarchy::build_hierarchy(root_bundle.split(), leaf_directory.split())); + auto hierarchy = X509CertificateHierarchy::build_hierarchy(root_bundle.split(), leaf_directory.split()); EVLOG_debug << "Hierarchy for root/OCSP data: \n" << hierarchy.to_debug_string(); // Include OCSP data if possible From 3add8ddc949c12211e19aa37d834fa5f28e4f3f2 Mon Sep 17 00:00:00 2001 From: AssemblyJohn Date: Mon, 21 Oct 2024 12:04:02 +0300 Subject: [PATCH 8/8] Documentation update related to library limitations Signed-off-by: AssemblyJohn --- README.md | 6 ++++++ include/evse_security/certificate/x509_hierarchy.hpp | 1 + include/evse_security/evse_security.hpp | 1 + 3 files changed, 8 insertions(+) diff --git a/README.md b/README.md index f65ba0a..9565f9c 100644 --- a/README.md +++ b/README.md @@ -104,3 +104,9 @@ Defaults: - Minimum certificates kept: 10 - Maximum storage space: 50 MB - Maximum certificate entries: 2000 + +## Limitations + +Based on information from [ssl](https://www.ssl.com/article/what-are-root-certificates-and-why-do-they-matter/), self-signed roots are possible, but not supported in our library at the moment. + +Cross-signed certificate chains (see [ssl](https://www.ssl.com/blogs/ssl-com-legacy-cross-signed-root-certificate-expiring-on-september-11-2023/)), required for seamless root transitions are not supported at the moment. \ No newline at end of file diff --git a/include/evse_security/certificate/x509_hierarchy.hpp b/include/evse_security/certificate/x509_hierarchy.hpp index b9a86ca..d3bc6cf 100644 --- a/include/evse_security/certificate/x509_hierarchy.hpp +++ b/include/evse_security/certificate/x509_hierarchy.hpp @@ -37,6 +37,7 @@ struct X509Node { /// @brief Utility class that is able to build a immutable certificate hierarchy /// with a list of self-signed root certificates and their respective sub-certificates +/// Note: non self-signed roots and cross-signed certificates are not supported now class X509CertificateHierarchy { public: const std::vector& get_hierarchy() const { diff --git a/include/evse_security/evse_security.hpp b/include/evse_security/evse_security.hpp index 45ed8d3..1d3b3e8 100644 --- a/include/evse_security/evse_security.hpp +++ b/include/evse_security/evse_security.hpp @@ -208,6 +208,7 @@ class EvseSecurity { /// will return: /// ROOT_V2G_Hubject->SUB_CA1->SUB_CA2->Leaf_Valid_B + /// ROOT_V2G_OtherProvider->SUB_CA_O1->SUB_CA_O2->Leav_Valid_A + /// Note: non self-signed roots and cross-signed certificates are not supported /// @param certificate_type type of leaf certificate that we start the search from /// @param encoding specifies PEM or DER format /// @param include_ocsp if OCSP data should be included