diff --git a/include/evse_security/certificate/x509_bundle.hpp b/include/evse_security/certificate/x509_bundle.hpp index 5d21e18..c3ef4bc 100644 --- a/include/evse_security/certificate/x509_bundle.hpp +++ b/include/evse_security/certificate/x509_bundle.hpp @@ -2,6 +2,7 @@ // Copyright Pionix GmbH and Contributors to EVerest #pragma once +#include #include #include @@ -66,6 +67,9 @@ class X509CertificateBundle { /// @return Contained certificate count int get_certificate_count() const; + /// @return Contained certificate chains count + int get_certificate_chains_count() const; + fs::path get_path() const { return path; } @@ -83,6 +87,28 @@ class X509CertificateBundle { } } + /// @brief Same as 'for_each_chain' but it also uses a predicate for ordering + template void for_each_chain_ordered(function func, ordering order) { + struct Chain { + const fs::path* path; + const std::vector* certificates; + }; + + std::vector ordered; + + for (auto& [path, certs] : certificates) { + ordered.push_back(Chain{&path, &certs}); + } + + std::sort(std::begin(ordered), std::end(ordered), + [&order](Chain& a, Chain& b) { return order(*a.certificates, *b.certificates); }); + + for (const auto& chain : ordered) { + if (!func(*chain.path, *chain.certificates)) + break; + } + } + public: /// @brief Splits the certificate (chain) into single certificates /// @return vector containing single certificates diff --git a/include/evse_security/certificate/x509_wrapper.hpp b/include/evse_security/certificate/x509_wrapper.hpp index aa4a7f6..d57501b 100644 --- a/include/evse_security/certificate/x509_wrapper.hpp +++ b/include/evse_security/certificate/x509_wrapper.hpp @@ -24,6 +24,7 @@ enum class X509CertificateSource { const fs::path PEM_EXTENSION = ".pem"; const fs::path DER_EXTENSION = ".der"; const fs::path KEY_EXTENSION = ".key"; +const fs::path TPM_KEY_EXTENSION = ".tkey"; /// @brief Convenience wrapper around openssl X509 certificate class X509Wrapper { diff --git a/include/evse_security/evse_security.hpp b/include/evse_security/evse_security.hpp index 029779f..a8112ba 100644 --- a/include/evse_security/evse_security.hpp +++ b/include/evse_security/evse_security.hpp @@ -6,6 +6,12 @@ #include #include +#include + +#ifdef BUILD_TESTING_EVSE_SECURITY +#include +#endif + namespace evse_security { struct LinkPaths { @@ -31,6 +37,16 @@ struct FilePaths { LinkPaths links; }; +// Unchangeable security limit for certificate deletion, a min entry count will be always kept (newest) +static constexpr std::size_t DEFAULT_MINIMUM_CERTIFICATE_ENTRIES = 10; +// 50 MB default limit for filesystem usage +static constexpr std::uintmax_t DEFAULT_MAX_FILESYSTEM_SIZE = 1024 * 1024 * 50; +// Default maximum 2000 certificate entries +static constexpr std::uintmax_t DEFAULT_MAX_CERTIFICATE_ENTRIES = 2000; + +// Expiry for CSRs that did not receive a response CSR, 10 minutes or reboot +static std::chrono::seconds DEFAULT_CSR_EXPIRY(10 * 60); + /// @brief This class holds filesystem paths to CA bundle file locations and directories for leaf certificates class EvseSecurity { @@ -40,7 +56,10 @@ class EvseSecurity { /// directories are specified. /// @param file_paths specifies the certificate and key storage locations on the filesystem /// @param private_key_password optional password for encrypted private keys - EvseSecurity(const FilePaths& file_paths, const std::optional& private_key_password = std::nullopt); + EvseSecurity(const FilePaths& file_paths, const std::optional& private_key_password = std::nullopt, + const std::optional& max_fs_usage_bytes = std::nullopt, + const std::optional& max_fs_certificate_store_entries = std::nullopt, + const std::optional& csr_expiry = std::nullopt); /// @brief Destructor ~EvseSecurity(); @@ -146,6 +165,11 @@ class EvseSecurity { /// @return day count until the leaf certificate expires int get_leaf_expiry_days_count(LeafCertificateType certificate_type); + /// @brief Collects and deletes unfulfilled CSR private keys. If also deleting the expired + /// certificates, make sure the system clock is properly set for detecting expired certificates + /// @param delete_expired if the expired certificates should be deleted + void garbage_collect(bool delete_expired_certificates); + /// @brief Verifies the file at the given \p path using the provided \p signing_certificate and \p signature /// @param path /// @param signing_certificate @@ -155,14 +179,40 @@ class EvseSecurity { const std::string signature); private: + /// @brief Determines if the total filesize of certificates is > than the max_filesystem_usage bytes + bool is_filesystem_full(); + +private: + // TODO(ioan): implement library thread-safety + std::mutex security_mutex; + // why not reusing the FilePaths here directly (storage duplication) std::map ca_bundle_path_map; DirectoryPaths directories; LinkPaths links; + // CSRs that were generated and require an expiry time + std::map> managed_csr; + + // Maximum filesystem usage + std::uintmax_t max_fs_usage_bytes; + // Maximum filesystem certificate entries + std::uintmax_t max_fs_certificate_store_entries; + // Default csr expiry in seconds + std::chrono::seconds csr_expiry; + // FIXME(piet): map passwords to encrypted private key files // is there only one password for all private keys? - std::optional private_key_password; // used to decrypt encrypted private keys; + std::optional private_key_password; // used to decrypt encrypted private keys + +private: +// Define here all tests that require internal function usage +#ifdef BUILD_TESTING_EVSE_SECURITY + FRIEND_TEST(EvseSecurityTests, verify_full_filesystem_install_reject); + FRIEND_TEST(EvseSecurityTests, verify_full_filesystem); + FRIEND_TEST(EvseSecurityTests, verify_expired_csr_deletion); + FRIEND_TEST(EvseSecurityTests, verify_expired_leaf_deletion); +#endif }; } // namespace evse_security diff --git a/lib/evse_security/certificate/x509_bundle.cpp b/lib/evse_security/certificate/x509_bundle.cpp index 5485ec1..996497f 100644 --- a/lib/evse_security/certificate/x509_bundle.cpp +++ b/lib/evse_security/certificate/x509_bundle.cpp @@ -87,6 +87,10 @@ int X509CertificateBundle::get_certificate_count() const { return count; } +int X509CertificateBundle::get_certificate_chains_count() const { + return certificates.size(); +} + void X509CertificateBundle::add_certificates(const std::string& data, const EncodingFormat encoding, const std::optional& path) { auto loaded = CryptoSupplier::load_certificates(data, encoding); diff --git a/lib/evse_security/evse_security.cpp b/lib/evse_security/evse_security.cpp index 510b149..8f1366a 100644 --- a/lib/evse_security/evse_security.cpp +++ b/lib/evse_security/evse_security.cpp @@ -74,13 +74,27 @@ static CertificateType get_certificate_type(const CaCertificateType ca_certifica } } -static fs::path get_private_key_path(const X509Wrapper& certificate, const fs::path& key_path, - const std::optional password) { +static bool is_keyfile(const fs::path& file_path) { + if (fs::is_regular_file(file_path)) { + if (file_path.has_extension()) { + auto extension = file_path.extension(); + if (extension == KEY_EXTENSION || extension == TPM_KEY_EXTENSION) { + return true; + } + } + } + + return false; +} + +/// @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 - for (const auto& entry : fs::recursive_directory_iterator(key_path)) { + for (const auto& entry : fs::recursive_directory_iterator(key_path_directory)) { if (fs::is_regular_file(entry)) { auto key_file_path = entry.path(); - if (key_file_path.extension() == KEY_EXTENSION) { + if (is_keyfile(key_file_path)) { try { fsstd::ifstream file(key_file_path, std::ios::binary); std::string private_key((std::istreambuf_iterator(file)), std::istreambuf_iterator()); @@ -95,19 +109,61 @@ static fs::path get_private_key_path(const X509Wrapper& certificate, const fs::p } } } + std::string error = "Could not find private key for given certificate: "; error += certificate.get_file().value_or("N/A"); error += " key path: "; - error += key_path; + error += key_path_directory; throw NoPrivateKeyException(error); } +/// @brief Searches for the certificate linked to the provided key +/// @return The files where the certificates were found, more than one can be returned in case it is +/// present in a bundle too +static std::set get_certificate_path_of_key(const fs::path& key, const fs::path& certificate_path_directory, + const std::optional password) { + fsstd::ifstream file(key, std::ios::binary); + std::string private_key((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + 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)) { + bundles.emplace(bundle); + } + } + + // Continue iterating + return true; + }); + + if (bundles.empty() == false) { + return bundles; + } + } catch (const CertificateLoadException& e) { + throw e; + } + + std::string error = "Could not find certificate for given private key: "; + error += key; + error += " certificates path: "; + error += certificate_path_directory; + + throw NoCertificateValidException(error); +} + X509CertificateBundle get_leaf_certificates(const fs::path& cert_dir) { return X509CertificateBundle(cert_dir, EncodingFormat::PEM); } -EvseSecurity::EvseSecurity(const FilePaths& file_paths, const std::optional& private_key_password) : +EvseSecurity::EvseSecurity(const FilePaths& file_paths, const std::optional& private_key_password, + const std::optional& max_fs_usage_bytes, + const std::optional& max_fs_certificate_store_entries, + const std::optional& csr_expiry) : private_key_password(private_key_password) { std::vector dirs = { @@ -144,8 +200,23 @@ EvseSecurity::EvseSecurity(const FilePaths& file_paths, const std::optionaldirectories = file_paths.directories; this->links = file_paths.links; + + this->max_fs_usage_bytes = max_fs_usage_bytes.value_or(DEFAULT_MAX_FILESYSTEM_SIZE); + this->max_fs_certificate_store_entries = max_fs_certificate_store_entries.value_or(DEFAULT_MAX_CERTIFICATE_ENTRIES); + this->csr_expiry = csr_expiry.value_or(DEFAULT_CSR_EXPIRY); } EvseSecurity::~EvseSecurity() { @@ -155,7 +226,11 @@ InstallCertificateResult EvseSecurity::install_ca_certificate(const std::string& CaCertificateType certificate_type) { EVLOG_debug << "Installing ca certificate: " << conversions::ca_certificate_type_to_string(certificate_type); - // TODO(piet): Check CertificateStoreMaxEntries + if (is_filesystem_full()) { + EVLOG_error << "Filesystem full, can't install new CA certificate!"; + return InstallCertificateResult::CertificateStoreMaxLengthExceeded; + } + try { X509Wrapper new_cert(certificate, EncodingFormat::PEM); @@ -300,8 +375,16 @@ DeleteCertificateResult EvseSecurity::delete_certificate(const CertificateHashDa InstallCertificateResult EvseSecurity::update_leaf_certificate(const std::string& certificate_chain, LeafCertificateType certificate_type) { + + // TODO(piet): Check CertificateStoreMaxEntries too + if (is_filesystem_full()) { + EVLOG_error << "Filesystem full, can't install new CA certificate!"; + return InstallCertificateResult::CertificateStoreMaxLengthExceeded; + } + fs::path cert_path; fs::path key_path; + if (certificate_type == LeafCertificateType::CSMS) { cert_path = this->directories.csms_leaf_cert_directory; key_path = this->directories.csms_leaf_key_directory; @@ -325,27 +408,36 @@ InstallCertificateResult EvseSecurity::update_leaf_certificate(const std::string const auto& leaf_certificate = _certificate_chain[0]; // Check if a private key belongs to the provided certificate + fs::path private_key_path; + try { - const auto private_key_path = get_private_key_path(leaf_certificate, key_path, this->private_key_password); + private_key_path = + get_private_key_path_of_certificate(leaf_certificate, key_path, this->private_key_password); } catch (const NoPrivateKeyException& e) { EVLOG_warning << "Provided certificate does not belong to any private key"; return InstallCertificateResult::WriteError; } // Write certificate to file - const auto file_name = - std::string("SECC_LEAF_") + filesystem_utils::get_random_file_name(PEM_EXTENSION.string()); + std::string extra_filename = filesystem_utils::get_random_file_name(PEM_EXTENSION.string()); + + const auto file_name = std::string("SECC_LEAF_") + extra_filename; const auto file_path = cert_path / file_name; std::string str_cert = leaf_certificate.get_export_string(); // Also write chain to file - const auto chain_file_name = - std::string("CPO_CERT_CHAIN_") + filesystem_utils::get_random_file_name(PEM_EXTENSION.string()); + const auto chain_file_name = std::string("CPO_CERT_CHAIN_") + extra_filename; const auto chain_file_path = cert_path / chain_file_name; std::string str_chain_cert = chain_certificate.to_export_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)); + } + return InstallCertificateResult::Accepted; } else { return InstallCertificateResult::WriteError; @@ -578,7 +670,11 @@ std::string EvseSecurity::generate_certificate_signing_request(LeafCertificateTy const std::string& common, bool use_tpm) { fs::path key_path; - const auto file_name = std::string("SECC_LEAF_") + filesystem_utils::get_random_file_name(KEY_EXTENSION.string()); + // Make a difference between normal and tpm keys for identification + const auto file_name = + std::string("SECC_LEAF_") + + filesystem_utils::get_random_file_name(use_tpm ? TPM_KEY_EXTENSION.string() : KEY_EXTENSION.string()); + if (certificate_type == LeafCertificateType::CSMS) { key_path = this->directories.csms_leaf_key_directory / file_name; } else if (certificate_type == LeafCertificateType::V2G) { @@ -607,6 +703,9 @@ std::string EvseSecurity::generate_certificate_signing_request(LeafCertificateTy throw std::runtime_error("Failed to generate certificate signing request!"); } + // 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; return csr; } @@ -654,7 +753,7 @@ GetKeyPairResult EvseSecurity::get_key_pair(LeafCertificateType certificate_type fs::path chain_file; auto certificate = std::move(leaf_certificates.get_latest_valid_certificate()); - auto private_key_path = get_private_key_path(certificate, key_dir, this->private_key_password); + auto private_key_path = get_private_key_path_of_certificate(certificate, key_dir, this->private_key_password); // Key path doesn't change key_file = private_key_path; @@ -843,6 +942,7 @@ int EvseSecurity::get_leaf_expiry_days_count(LeafCertificateType certificate_typ EVLOG_error << "Could not obtain leaf expiry certificate: " << e.what(); } } + return 0; } @@ -921,4 +1021,183 @@ InstallCertificateResult EvseSecurity::verify_certificate(const std::string& cer } } +void EvseSecurity::garbage_collect(bool delete_expired_certificates) { + // Only garbage collect if we are full + if (is_filesystem_full() == false) { + EVLOG_debug << "Garbage collect postponed, filesystem is not full"; + return; + } + + EVLOG_debug << "Starting garbage collect!"; + + std::vector> leaf_paths; + + leaf_paths.push_back( + std::make_pair(this->directories.csms_leaf_cert_directory, this->directories.csms_leaf_key_directory)); + leaf_paths.push_back( + std::make_pair(this->directories.secc_leaf_cert_directory, this->directories.secc_leaf_key_directory)); + + // Delete certificates first, give the option to cleanup the dangling keys afterwards + if (delete_expired_certificates) { + EVLOG_debug << "Expired certificates deletion requested!"; + + std::set invalid_certificate_files; + + // 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); + + // Only handle if we have more than the minimum certificates entry + if (expired_certs.get_certificate_chains_count() > DEFAULT_MINIMUM_CERTIFICATE_ENTRIES) { + int skipped = 0; + + // Order by expiry date, and keep even expired certificates with a minimum of 10 certificates + expired_certs.for_each_chain_ordered( + [&invalid_certificate_files, &skipped](const fs::path& file, + const std::vector& chain) { + // By default delete all empty + if (chain.size() <= 0) { + invalid_certificate_files.emplace(file); + } + + if (skipped++ > DEFAULT_MINIMUM_CERTIFICATE_ENTRIES) { + // If the chain contains the first expired (leafs are the first) + if (chain.size()) { + if (chain[0].is_expired()) { + invalid_certificate_files.emplace(file); + } + } + } + + return true; + }, + [](const std::vector& a, const std::vector& b) { + // Order from newest to oldest (newest DEFAULT_MINIMUM_CERTIFICATE_ENTRIES) are kept + // even if they are expired + if (a.size() && b.size()) { + return a.at(0).get_valid_to() > b.at(0).get_valid_to(); + } else { + return false; + } + }); + } + } + + for (const auto& expired_certificate_file : invalid_certificate_files) { + EVLOG_debug << "Deleted expired certificate file: " << expired_certificate_file; + filesystem_utils::delete_file(expired_certificate_file); + } + } + + // Delete all private keys that were lost from the managed_csr since at a + // reboot or other error the 'managed_csr' map can be emptied + std::set unpaired_key_files; + + // Populate private key files for csms/secc + for (auto const& [cert_dir, key_dir] : leaf_paths) { + fs::path cert_path = cert_dir; + fs::path key_path = key_dir; + + for (const auto& key_entry : fs::recursive_directory_iterator(key_path)) { + auto key_file_path = key_entry.path(); + + if (is_keyfile(key_entry)) { + bool any_found = false; + + try { + any_found = + (false == + get_certificate_path_of_key(key_file_path, cert_path, this->private_key_password).empty()); + } catch (const std::exception& e) { + } + + // No certificate pair is found, and it is also unmanaged, add to delete list + if (any_found == false && (managed_csr.find(key_file_path) == managed_csr.end())) { + unpaired_key_files.emplace(key_file_path); + } + } + } + } + + // If they are not managed (probably lost on a reboot) and not having any pair certificate, delete them + for (const auto& unmanaged_key_file : unpaired_key_files) { + EVLOG_debug << "Deleted unmanaged keyfile: " << unmanaged_key_file; + filesystem_utils::delete_file(unmanaged_key_file); + } + + // Delete all managed private keys of a CSR that we did not had a response to + auto now_timepoint = std::chrono::steady_clock::now(); + + for (auto it = managed_csr.begin(); it != managed_csr.end();) { + std::chrono::seconds elapsed = std::chrono::duration_cast(now_timepoint - it->second); + + if (elapsed > csr_expiry) { + EVLOG_debug << "Found expired csr key, deleting: " << it->first; + filesystem_utils::delete_file(it->first); + + it = managed_csr.erase(it); + } else { + ++it; + } + } +} + +bool EvseSecurity::is_filesystem_full() { + std::set unique_paths; + + // Collect all bundles + for (auto const& [certificate_type, ca_bundle_path] : ca_bundle_path_map) { + if (fs::is_regular_file(ca_bundle_path)) { + unique_paths.emplace(ca_bundle_path); + } else if (fs::is_directory(ca_bundle_path)) { + for (const auto& entry : fs::recursive_directory_iterator(ca_bundle_path)) { + if (fs::is_regular_file(entry)) { + unique_paths.emplace(entry); + } + } + } + } + + // Collect all key/leafs + std::vector key_pairs; + + key_pairs.push_back(directories.csms_leaf_cert_directory); + key_pairs.push_back(directories.csms_leaf_key_directory); + key_pairs.push_back(directories.secc_leaf_cert_directory); + key_pairs.push_back(directories.secc_leaf_key_directory); + + for (auto const& directory : key_pairs) { + if (fs::is_regular_file(directory)) { + unique_paths.emplace(directory); + } else if (fs::is_directory(directory)) { + for (const auto& entry : fs::recursive_directory_iterator(directory)) { + if (fs::is_regular_file(entry)) { + unique_paths.emplace(entry); + } + } + } + } + + uintmax_t total_entries = unique_paths.size(); + EVLOG_debug << "Total entries used: " << total_entries; + + if (total_entries > max_fs_certificate_store_entries) { + EVLOG_warning << "Exceeded maximum entries: " << total_entries; + return true; + } + + uintmax_t total_size_bytes = 0; + for (const auto& path : unique_paths) { + total_size_bytes = fs::file_size(path); + } + + EVLOG_debug << "Total bytes used: " << total_size_bytes; + if (total_size_bytes >= max_fs_usage_bytes) { + EVLOG_warning << "Exceeded maximum byte size: " << total_size_bytes; + return true; + } + + return false; +} + } // namespace evse_security diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5faf998..5bfe4cc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,6 +21,8 @@ if(LIBEVSE_CRYPTO_SUPPLIER_OPENSSL) add_compile_definitions(LIBEVSE_CRYPTO_SUPPLIER_OPENSSL) endif() +add_compile_definitions(BUILD_TESTING_EVSE_SECURITY) + add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME}) install( diff --git a/tests/tests.cpp b/tests/tests.cpp index b303fab..40adab9 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -114,6 +115,40 @@ class EvseSecurityTests : public ::testing::Test { } }; +TEST_F(EvseSecurityTests, verify_expired_leaf_deletion) { + // Copy many expired certificates + std::set existing; + + for (int i = 0; i < 30; i++) { + std::string key_filename = std::string("certs/client/cso/SECC_LEAF_EXPIRED_") + std::to_string(i) + ".key"; + std::string cert_filename = std::string("certs/client/cso/SECC_LEAF_EXPIRED_") + std::to_string(i) + ".pem"; + + existing.emplace(key_filename); + existing.emplace(cert_filename); + + std::filesystem::copy("expired_leaf/SECC_LEAF_EXPIRED.key", key_filename); + std::filesystem::copy("expired_leaf/SECC_LEAF_EXPIRED.pem", cert_filename); + } + + // Check that the FS is not full + ASSERT_FALSE(evse_security->is_filesystem_full()); + + // Fill the disk + evse_security->max_fs_certificate_store_entries = 20; + + // Garbage collect + evse_security->garbage_collect(true); + + // Assert the files/keys do not exist any more + std::size_t existing_count = 0; + for (const auto& path : existing) { + existing_count += fs::exists(path) ? 1 : 0; + } + + // Only 10 should be kept (key + certificate) + ASSERT_EQ(existing_count, 20); +} + TEST_F(EvseSecurityTests, verify_basics) { // Check that we have the default provider ASSERT_TRUE(check_openssl_providers({PROVIDER_DEFAULT})); @@ -706,6 +741,63 @@ TEST_F(EvseSecurityTests, expired_leaf_cert_rejected) { ASSERT_TRUE(result_client == InstallCertificateResult::Expired); } +TEST_F(EvseSecurityTests, verify_full_filesystem) { + ASSERT_EQ(evse_security->is_filesystem_full(), false); + + evse_security->max_fs_usage_bytes = 1; + ASSERT_EQ(evse_security->is_filesystem_full(), true); +} + +TEST_F(EvseSecurityTests, verify_full_filesystem_install_reject) { + evse_security->max_fs_usage_bytes = 1; + ASSERT_EQ(evse_security->is_filesystem_full(), true); + + // Must have a rejection + const auto new_root_ca_1 = + read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA1.pem")); + const auto result = this->evse_security->install_ca_certificate(new_root_ca_1, CaCertificateType::CSMS); + ASSERT_TRUE(result == InstallCertificateResult::CertificateStoreMaxLengthExceeded); +} + +TEST_F(EvseSecurityTests, verify_expired_csr_deletion) { + // Generate a CSR + std::string csr = + evse_security->generate_certificate_signing_request(LeafCertificateType::CSMS, "DE", "Pionix", "NA"); + fs::path csr_key_path = evse_security->managed_csr.begin()->first; + + // Simulate a full fs else no deletion will take place + evse_security->max_fs_usage_bytes = 1; + + ASSERT_EQ(evse_security->managed_csr.size(), 1); + ASSERT_TRUE(fs::exists(csr_key_path)); + + // Check that is is NOT deleted + evse_security->garbage_collect(false); + ASSERT_TRUE(fs::exists(csr_key_path)); + + // Sleep 1 second AND it must be deleted + evse_security->csr_expiry = std::chrono::seconds(0); + std::this_thread::sleep_for(std::chrono::seconds(1)); + evse_security->garbage_collect(false); + + ASSERT_FALSE(fs::exists(csr_key_path)); + ASSERT_EQ(evse_security->managed_csr.size(), 0); + + // Delete unmanaged CSRs + csr = evse_security->generate_certificate_signing_request(LeafCertificateType::CSMS, "DE", "Pionix", "NA"); + csr_key_path = evse_security->managed_csr.begin()->first; + + // Remove from managed (simulate a reboot/reinit) + evse_security->managed_csr.clear(); + + ASSERT_EQ(evse_security->managed_csr.size(), 0); + ASSERT_TRUE(fs::exists(csr_key_path)); + + // Garbage collect should delete the unmanaged key + evse_security->garbage_collect(false); + ASSERT_FALSE(fs::exists(csr_key_path)); +} + } // namespace evse_security // FIXME(piet): Add more tests for getRootCertificateHashData (incl. V2GCertificateChain etc.)