diff --git a/CMakeLists.txt b/CMakeLists.txt index e7bd75b..b90f520 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.14) -project(everest-evse_security VERSION 0.5 +project(everest-evse_security VERSION 0.6 DESCRIPTION "Implementation of EVSE related security operations" LANGUAGES CXX C ) diff --git a/include/evse_security/crypto/interface/crypto_supplier.hpp b/include/evse_security/crypto/interface/crypto_supplier.hpp index ca263db..d8119c4 100644 --- a/include/evse_security/crypto/interface/crypto_supplier.hpp +++ b/include/evse_security/crypto/interface/crypto_supplier.hpp @@ -64,8 +64,8 @@ class AbstractCryptoSupplier { const std::optional file_path); /// @brief Checks if the private key is consistent with the provided handle - static bool x509_check_private_key(X509Handle* handle, std::string private_key, - std::optional password); + static KeyValidationResult x509_check_private_key(X509Handle* handle, std::string private_key, + std::optional password); /// @brief Verifies the signature with the certificate handle public key against the data static bool x509_verify_signature(X509Handle* handle, const std::vector& signature, diff --git a/include/evse_security/crypto/interface/crypto_types.hpp b/include/evse_security/crypto/interface/crypto_types.hpp index ba51065..6e0305b 100644 --- a/include/evse_security/crypto/interface/crypto_types.hpp +++ b/include/evse_security/crypto/interface/crypto_types.hpp @@ -18,6 +18,13 @@ enum class CryptoKeyType { RSA_7680, // Protection lifetime: >2031 }; +enum class KeyValidationResult { + Valid, + KeyLoadFailure, // The key could not be loaded, might be an password or invalid string + Invalid, // The key is not linked to the specified certificate + Unknown, // Unknown error, not related to provider validation +}; + struct KeyGenerationInfo { CryptoKeyType key_type; diff --git a/include/evse_security/crypto/openssl/openssl_supplier.hpp b/include/evse_security/crypto/openssl/openssl_supplier.hpp index 5c553ec..3c091c3 100644 --- a/include/evse_security/crypto/openssl/openssl_supplier.hpp +++ b/include/evse_security/crypto/openssl/openssl_supplier.hpp @@ -36,8 +36,8 @@ class OpenSSLSupplier : public AbstractCryptoSupplier { bool allow_future_certificates, const std::optional dir_path, const std::optional file_path); - static bool x509_check_private_key(X509Handle* handle, std::string private_key, - std::optional password); + static KeyValidationResult x509_check_private_key(X509Handle* handle, std::string private_key, + std::optional password); static bool x509_verify_signature(X509Handle* handle, const std::vector& signature, const std::vector& data); diff --git a/include/evse_security/crypto/openssl/openssl_tpm.hpp b/include/evse_security/crypto/openssl/openssl_tpm.hpp index f0c5524..80853e7 100644 --- a/include/evse_security/crypto/openssl/openssl_tpm.hpp +++ b/include/evse_security/crypto/openssl/openssl_tpm.hpp @@ -9,6 +9,8 @@ #include #include +#include + // opaque types (from OpenSSL) struct ossl_lib_ctx_st; // OpenSSL OSSL_LIB_CTX; struct ossl_provider_st; // OpenSSL OSSL_PROVIDER @@ -25,7 +27,7 @@ bool is_tpm_key_string(const std::string& private_key_pem); /// @param private_key_file_pem filename of the PEM file /// @return true when file starts "-----BEGIN TSS2 PRIVATE KEY-----" /// @note works irrespective of OpenSSL version -bool is_tpm_key_filename(const std::string& private_key_file_pem); +bool is_tpm_key_file(const fs::path& private_key_file_pem); /// @brief Manage the loading and configuring of OpenSSL providers /// diff --git a/lib/evse_security/crypto/interface/crypto_supplier.cpp b/lib/evse_security/crypto/interface/crypto_supplier.cpp index de3d912..3c1dea5 100644 --- a/lib/evse_security/crypto/interface/crypto_supplier.cpp +++ b/lib/evse_security/crypto/interface/crypto_supplier.cpp @@ -81,9 +81,9 @@ CertificateValidationResult AbstractCryptoSupplier::x509_verify_certificate_chai default_crypto_supplier_usage_error() return CertificateValidationResult::Unknown; } -bool AbstractCryptoSupplier::x509_check_private_key(X509Handle* handle, std::string private_key, - std::optional password) { - default_crypto_supplier_usage_error() return false; +KeyValidationResult AbstractCryptoSupplier::x509_check_private_key(X509Handle* handle, std::string private_key, + std::optional password) { + default_crypto_supplier_usage_error() return KeyValidationResult::Unknown; } bool AbstractCryptoSupplier::x509_verify_signature(X509Handle* handle, const std::vector& signature, diff --git a/lib/evse_security/crypto/openssl/openssl_supplier.cpp b/lib/evse_security/crypto/openssl/openssl_supplier.cpp index 641134e..543721c 100644 --- a/lib/evse_security/crypto/openssl/openssl_supplier.cpp +++ b/lib/evse_security/crypto/openssl/openssl_supplier.cpp @@ -615,12 +615,13 @@ CertificateValidationResult OpenSSLSupplier::x509_verify_certificate_chain(X509H return CertificateValidationResult::Valid; } -bool OpenSSLSupplier::x509_check_private_key(X509Handle* handle, std::string private_key, - std::optional password) { +KeyValidationResult OpenSSLSupplier::x509_check_private_key(X509Handle* handle, std::string private_key, + std::optional password) { X509* x509 = get(handle); - if (x509 == nullptr) - return false; + if (x509 == nullptr) { + return KeyValidationResult::Unknown; + } OpenSSLProvider provider; @@ -630,7 +631,7 @@ bool OpenSSLSupplier::x509_check_private_key(X509Handle* handle, std::string pri } else { provider.set_global_mode(OpenSSLProvider::mode_t::default_provider); } - EVLOG_info << "TPM Key: " << tpm_key; + EVLOG_debug << "TPM Key: " << tpm_key; BIO_ptr bio(BIO_new_mem_buf(private_key.c_str(), -1)); // Passing password string since if NULL is provided, the password CB will be called @@ -642,11 +643,14 @@ bool OpenSSLSupplier::x509_check_private_key(X509Handle* handle, std::string pri << " Password configured correctly?"; ERR_print_errors_fp(stderr); - bResult = false; + return KeyValidationResult::KeyLoadFailure; } - bResult = bResult && X509_check_private_key(x509, evp_pkey.get()) == 1; - return bResult; + if (X509_check_private_key(x509, evp_pkey.get()) == 1) { + return KeyValidationResult::Valid; + } else { + return KeyValidationResult::Invalid; + } } bool OpenSSLSupplier::x509_verify_signature(X509Handle* handle, const std::vector& signature, diff --git a/lib/evse_security/crypto/openssl/openssl_tpm.cpp b/lib/evse_security/crypto/openssl/openssl_tpm.cpp index 7252d96..e9b8fdd 100644 --- a/lib/evse_security/crypto/openssl/openssl_tpm.cpp +++ b/lib/evse_security/crypto/openssl/openssl_tpm.cpp @@ -29,12 +29,17 @@ bool is_tpm_key_string(const std::string& private_key_pem) { return private_key_pem.find("-----BEGIN TSS2 PRIVATE KEY-----") != std::string::npos; } -bool is_tpm_key_filename(const std::string& private_key_file_pem) { - std::ifstream key_file(private_key_file_pem); - std::string line; - std::getline(key_file, line); - key_file.close(); - return line == "-----BEGIN TSS2 PRIVATE KEY-----"; +bool is_tpm_key_file(const fs::path& private_key_file_pem) { + if (fs::is_regular_file(private_key_file_pem)) { + std::ifstream key_file(private_key_file_pem); + std::string line; + std::getline(key_file, line); + key_file.close(); + + return line.find("-----BEGIN TSS2 PRIVATE KEY-----") != std::string::npos; + } + + return false; } #ifdef USING_OPENSSL_3_TPM diff --git a/lib/evse_security/evse_security.cpp b/lib/evse_security/evse_security.cpp index f7da9fd..888d5f1 100644 --- a/lib/evse_security/evse_security.cpp +++ b/lib/evse_security/evse_security.cpp @@ -94,7 +94,30 @@ static bool is_keyfile(const fs::path& file_path) { /// @brief Searches for the private key linked to the provided certificate static fs::path get_private_key_path_of_certificate(const X509Wrapper& certificate, const fs::path& key_path_directory, const std::optional password) { - // TODO(ioan): Before iterating the whole dir check by the filename first 'key_path'.key + // Before iterating the whole dir check by the filename first 'key_path'.key/.tkey + if (certificate.get_file().has_value()) { + // Check normal keyfile & tpm filename + for (const auto& extension : {KEY_EXTENSION, TPM_KEY_EXTENSION}) { + fs::path potential_keyfile = certificate.get_file().value(); + potential_keyfile.replace_extension(extension); + + if (fs::exists(potential_keyfile)) { + try { + fsstd::ifstream file(potential_keyfile, std::ios::binary); + std::string private_key((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + if (KeyValidationResult::Valid == + CryptoSupplier::x509_check_private_key(certificate.get(), private_key, password)) { + EVLOG_debug << "Key found for certificate at path: " << potential_keyfile; + return potential_keyfile; + } + } catch (const std::exception& e) { + EVLOG_debug << "Could not load or verify private key at: " << potential_keyfile << ": " << e.what(); + } + } + } + } + for (const auto& entry : fs::recursive_directory_iterator(key_path_directory)) { if (fs::is_regular_file(entry)) { auto key_file_path = entry.path(); @@ -103,7 +126,8 @@ static fs::path get_private_key_path_of_certificate(const X509Wrapper& certifica fsstd::ifstream file(key_file_path, std::ios::binary); std::string private_key((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - if (CryptoSupplier::x509_check_private_key(certificate.get(), private_key, password)) { + if (KeyValidationResult::Valid == + CryptoSupplier::x509_check_private_key(certificate.get(), private_key, password)) { EVLOG_debug << "Key found for certificate at path: " << key_file_path; return key_file_path; } @@ -130,13 +154,44 @@ static std::set get_certificate_path_of_key(const fs::path& key, const fsstd::ifstream file(key, std::ios::binary); std::string private_key((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + // Before iterating all bundles, check by certificates from key filename + fs::path cert_filename = key; + cert_filename.replace_extension(PEM_EXTENSION); + + if (fs::exists(cert_filename)) { + try { + std::set bundles; + X509CertificateBundle certificate_bundles(cert_filename, EncodingFormat::PEM); + + certificate_bundles.for_each_chain( + [&](const fs::path& bundle, const std::vector& certificates) { + for (const auto& certificate : certificates) { + if (KeyValidationResult::Valid == + CryptoSupplier::x509_check_private_key(certificate.get(), private_key, password)) { + bundles.emplace(bundle); + } + } + + // Continue iterating + return true; + }); + + if (bundles.empty() == false) { + return bundles; + } + } catch (const CertificateLoadException& e) { + EVLOG_debug << "Could not load certificate bundle at: " << certificate_path_directory << ": " << e.what(); + } + } + try { std::set bundles; X509CertificateBundle certificate_bundles(certificate_path_directory, EncodingFormat::PEM); certificate_bundles.for_each_chain([&](const fs::path& bundle, const std::vector& certificates) { for (const auto& certificate : certificates) { - if (CryptoSupplier::x509_check_private_key(certificate.get(), private_key, password)) { + if (KeyValidationResult::Valid == + CryptoSupplier::x509_check_private_key(certificate.get(), private_key, password)) { bundles.emplace(bundle); } } @@ -149,7 +204,7 @@ static std::set get_certificate_path_of_key(const fs::path& key, const return bundles; } } catch (const CertificateLoadException& e) { - throw e; + EVLOG_debug << "Could not load certificate bundle at: " << certificate_path_directory << ": " << e.what(); } std::string error = "Could not find certificate for given private key: "; @@ -408,7 +463,8 @@ InstallCertificateResult EvseSecurity::update_leaf_certificate(const std::string cert_path = this->directories.secc_leaf_cert_directory; key_path = this->directories.secc_leaf_key_directory; } else { - throw std::runtime_error("Attempt to update leaf certificate for non CSMS/V2G certificate!"); + EVLOG_error << "Attempt to update leaf certificate for non CSMS/V2G certificate!"; + return InstallCertificateResult::WriteError; } try { @@ -456,11 +512,16 @@ InstallCertificateResult EvseSecurity::update_leaf_certificate(const std::string if (filesystem_utils::write_to_file(file_path, str_cert, std::ios::out) && filesystem_utils::write_to_file(chain_file_path, str_chain_cert, std::ios::out)) { - // Remove from managed certificate keys, it is safe, no need to delete - if (managed_csr.find(private_key_path) != managed_csr.end()) { - managed_csr.erase(managed_csr.find(private_key_path)); + // Remove from managed certificate keys, the CSR is fulfilled, no need to delete the key + // since it is not orphaned any more + auto it = managed_csr.find(private_key_path); + if (it != managed_csr.end()) { + managed_csr.erase(it); } + // TODO(ioan): properly rename key path here for fast retrieval + // @see 'get_private_key_path_of_certificate' and 'get_certificate_path_of_key' + return InstallCertificateResult::Accepted; } else { return InstallCertificateResult::WriteError; @@ -782,7 +843,7 @@ std::string EvseSecurity::generate_certificate_signing_request(LeafCertificateTy fs::path key_path; - EVLOG_info << "generate_certificate_signing_request: create filename"; + EVLOG_info << "Generating CSR: " << conversions::leaf_certificate_type_to_string(certificate_type); // Make a difference between normal and tpm keys for identification const auto file_name = @@ -794,7 +855,7 @@ std::string EvseSecurity::generate_certificate_signing_request(LeafCertificateTy } else if (certificate_type == LeafCertificateType::V2G) { key_path = this->directories.secc_leaf_key_directory / file_name; } else { - EVLOG_error << "generate_certificate_signing_request: create filename failed"; + EVLOG_error << "Generate CSR: create filename failed"; throw std::runtime_error("Attempt to generate CSR for MF certificate"); } @@ -824,15 +885,16 @@ std::string EvseSecurity::generate_certificate_signing_request(LeafCertificateTy info.key_info.private_key_pass = private_key_password; } - EVLOG_info << "generate_certificate_signing_request: start"; + EVLOG_debug << "Generate CSR start"; + if (false == CryptoSupplier::x509_generate_csr(info, csr)) { - throw std::runtime_error("Failed to generate certificate signing request!"); + EVLOG_error << "Failed to generate certificate signing request!"; + } else { + // Add the key to the managed CRS that we will delete if we can't find a certificate pair within the time + managed_csr.emplace(key_path, std::chrono::steady_clock::now()); } - // Add the key to the managed CRS that we will delete if we can't find a certificate pair within the time - managed_csr.emplace(key_path, std::chrono::steady_clock::now()); - - EVLOG_debug << csr; + EVLOG_debug << "Generated CSR end. CSR: " << csr; return csr; } @@ -880,15 +942,59 @@ GetKeyPairResult EvseSecurity::get_key_pair_internal(LeafCertificateType certifi return result; } - fs::path key_file; - fs::path certificate_file; - fs::path chain_file; + std::optional latest_valid; + std::optional found_private_key_path; + + // 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()) { + 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); - auto certificate = std::move(leaf_certificates.get_latest_valid_certificate()); - auto private_key_path = get_private_key_path_of_certificate(certificate, key_dir, this->private_key_password); + // Copy to latest valid + latest_valid = chain.at(0); + found_private_key_path = priv_key_path; + + // We found, break + return false; + } catch (const NoPrivateKeyException& e) { + } + } + + return true; + }, + [](const std::vector& a, const std::vector& b) { + // Order from newest to oldest + if (not a.empty() && not b.empty()) { + return a.at(0).get_valid_to() > b.at(0).get_valid_to(); + } else { + return false; + } + }); + + if (latest_valid.has_value() == false) { + EVLOG_warning << "Could not find valid certificate"; + result.status = GetKeyPairStatus::NotFoundValid; + return result; + } + + if (found_private_key_path.has_value() == false) { + EVLOG_warning << "Could not find private key for the valid certificate"; + result.status = GetKeyPairStatus::PrivateKeyNotFound; + return result; + } // Key path doesn't change - key_file = private_key_path; + fs::path key_file = found_private_key_path.value(); + auto& certificate = latest_valid.value(); + + // Paths to search + fs::path certificate_file; + fs::path chain_file; X509CertificateBundle leaf_directory(cert_dir, EncodingFormat::PEM); @@ -933,14 +1039,6 @@ GetKeyPairResult EvseSecurity::get_key_pair_internal(LeafCertificateType certifi result.pair = {key_file, chain_file, certificate_file, this->private_key_password}; result.status = GetKeyPairStatus::Accepted; - return result; - } catch (const NoPrivateKeyException& e) { - EVLOG_warning << "Could not find private key for the selected certificate: (" << e.what() << ")"; - result.status = GetKeyPairStatus::PrivateKeyNotFound; - return result; - } catch (const NoCertificateValidException& e) { - EVLOG_warning << "Could not find valid cerificate"; - result.status = GetKeyPairStatus::NotFoundValid; return result; } catch (const CertificateLoadException& e) { EVLOG_warning << "Leaf certificate load exception"; @@ -1238,6 +1336,9 @@ void EvseSecurity::garbage_collect() { // Delete certificates first, give the option to cleanup the dangling keys afterwards std::set invalid_certificate_files; + // Private keys that are linked to the skipped certificates and that will not be deleted regardless + std::set protected_private_keys; + // Order by latest valid, and keep newest with a safety limit for (auto const& [cert_dir, key_dir] : leaf_paths) { X509CertificateBundle expired_certs(cert_dir, EncodingFormat::PEM); @@ -1249,8 +1350,8 @@ void EvseSecurity::garbage_collect() { // Order by expiry date, and keep even expired certificates with a minimum of 10 certificates expired_certs.for_each_chain_ordered( - [this, &invalid_certificate_files, &skipped, &key_directory](const fs::path& file, - const std::vector& chain) { + [this, &invalid_certificate_files, &skipped, &key_directory, + &protected_private_keys](const fs::path& file, const std::vector& chain) { // By default delete all empty if (chain.size() <= 0) { invalid_certificate_files.emplace(file); @@ -1271,6 +1372,20 @@ void EvseSecurity::garbage_collect() { } } } + } else { + // Add to protected certificate list + try { + fs::path key_file = get_private_key_path_of_certificate(chain[0], key_directory, + this->private_key_password); + protected_private_keys.emplace(key_file); + + // Erase all protected keys from the managed CRSs + auto it = managed_csr.find(key_file); + if (it != managed_csr.end()) { + managed_csr.erase(it); + } + } catch (NoPrivateKeyException& e) { + } } return true; @@ -1299,18 +1414,31 @@ void EvseSecurity::garbage_collect() { // at a further invocation after the GC timer will elapse a few times. This behavior // was added so that if we have a reset and the CSMS sends us a CSR response while we were // down it should still be processed when we boot up and NOT delete the CSRs - for (auto const& [cert_dir, key_dir] : leaf_paths) { + for (auto const& [cert_dir, keys_dir] : leaf_paths) { fs::path cert_path = cert_dir; - fs::path key_path = key_dir; + fs::path key_path = keys_dir; for (const auto& key_entry : fs::recursive_directory_iterator(key_path)) { auto key_file_path = key_entry.path(); - if (is_keyfile(key_entry)) { - // No certificate pair is found, and it is also unmanaged, add it again to give it the - // chance to be fulfilled by the CSMS - if (managed_csr.find(key_file_path) == managed_csr.end()) { - managed_csr.emplace(key_file_path, std::chrono::steady_clock::now()); + // Skip protected keys + if (protected_private_keys.find(key_file_path) != protected_private_keys.end()) { + continue; + } + + if (is_keyfile(key_file_path)) { + try { + // Check if we have found any matching certificate + get_certificate_path_of_key(key_file_path, keys_dir, this->private_key_password); + } catch (const NoCertificateValidException& e) { + // If we did not found, add to the potential delete list + EVLOG_debug << "Could not find matching certificate for key: " << key_file_path + << " adding to potential deletes"; + + // Give a chance to be fulfilled by the CSMS + if (managed_csr.find(key_file_path) == managed_csr.end()) { + managed_csr.emplace(key_file_path, std::chrono::steady_clock::now()); + } } } } diff --git a/tests/openssl_supplier_test.cpp b/tests/openssl_supplier_test.cpp index 5289a86..c8fd659 100644 --- a/tests/openssl_supplier_test.cpp +++ b/tests/openssl_supplier_test.cpp @@ -80,7 +80,7 @@ TEST_F(OpenSSLSupplierTest, x509_check_private_key) { auto cert = res_leaf[0].get(); auto key = getFile("pki/server_priv.pem"); auto res = OpenSSLSupplier::x509_check_private_key(cert, key, std::nullopt); - ASSERT_TRUE(res); + ASSERT_TRUE(res == KeyValidationResult::Valid); } TEST_F(OpenSSLSupplierTest, x509_verify_certificate_chain) { diff --git a/tests/tests.cpp b/tests/tests.cpp index e34b6eb..55a7e53 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -22,6 +22,7 @@ #if USING_OPENSSL_3 // provider management has changed - ensure tests still work #ifndef USING_TPM2 + #include #else @@ -790,8 +791,8 @@ TEST_F(EvseSecurityTestsExpired, verify_expired_leaf_deletion) { // List of date sorted certificates std::vector sorted; - std::vector sorded_should_delete; - std::vector sorded_should_keep; + std::vector sorted_should_delete; + std::vector sorted_should_keep; // Ensure that we have GEN_CERTIFICATES + 2 (CPO_CERT_CHAIN.pem + SECC_LEAF.pem) { @@ -816,9 +817,9 @@ TEST_F(EvseSecurityTestsExpired, verify_expired_leaf_deletion) { for (const auto& cert : sorted) { if (++skipped > DEFAULT_MINIMUM_CERTIFICATE_ENTRIES) { - sorded_should_delete.push_back(cert.get_file().value()); + sorted_should_delete.push_back(cert.get_file().value()); } else { - sorded_should_keep.push_back(cert.get_file().value()); + sorted_should_keep.push_back(cert.get_file().value()); } } @@ -836,12 +837,32 @@ TEST_F(EvseSecurityTestsExpired, verify_expired_leaf_deletion) { ASSERT_EQ(full_certs.get_certificate_chains_count(), DEFAULT_MINIMUM_CERTIFICATE_ENTRIES); // Ensure that we only have the newest ones - for (const auto& deleted : sorded_should_delete) { + for (const auto& deleted : sorted_should_delete) { ASSERT_FALSE(fs::exists(deleted)); } - for (const auto& deleted : sorded_should_keep) { - ASSERT_TRUE(fs::exists(deleted)); + for (const auto& not_deleted : sorted_should_keep) { + fs::path key_file = not_deleted; + key_file.replace_extension(".key"); + + ASSERT_TRUE(fs::exists(not_deleted)); + + // Ignore the CPO chain that does not have a properly + if (not_deleted.string().find("CPO_CERT_CHAIN") != std::string::npos) { + key_file = "certs/client/cso/SECC_LEAF.key"; + } + + // Check their respective keys exist + EVLOG_info << key_file; + ASSERT_TRUE(fs::exists(key_file)); + + X509Wrapper cert = X509CertificateBundle(not_deleted, EncodingFormat::PEM).split().at(0); + + fsstd::ifstream file(key_file, std::ios::binary); + std::string private_key((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + ASSERT_EQ(KeyValidationResult::Valid, CryptoSupplier::x509_check_private_key( + cert.get(), private_key, evse_security->private_key_password)); } } } @@ -896,4 +917,4 @@ TEST_F(EvseSecurityTests, verify_expired_csr_deletion) { } // namespace evse_security -// FIXME(piet): Add more tests for getRootCertificateHashData (incl. V2GCertificateChain etc.) + // FIXME(piet): Add more tests for getRootCertificateHashData (incl. V2GCertificateChain etc.) \ No newline at end of file