From 59499a354926a99f28a9cfbe8debc9b7932cc9d1 Mon Sep 17 00:00:00 2001 From: ioanbogdan Date: Mon, 16 Oct 2023 10:21:27 +0300 Subject: [PATCH] Update for v2g module interoperability: - install certificate check if already contained - last valid certificate retrieval move in bundle helper - added certificate directory helper - added support for the case when the V2G root doesn't contain the full chain Signed-off-by: ioanbogdan --- include/evse_security.hpp | 13 --- include/evse_utilities.hpp | 20 +++- include/x509_bundle.hpp | 92 +++++++++++++++- include/x509_wrapper.hpp | 5 + lib/evse_security.cpp | 217 ++++++++++++++++++++++--------------- lib/x509_bundle.cpp | 95 ++++++++++------ lib/x509_wrapper.cpp | 16 ++- tests/tests.cpp | 11 +- 8 files changed, 316 insertions(+), 153 deletions(-) diff --git a/include/evse_security.hpp b/include/evse_security.hpp index 5d224ba..9c0ee26 100644 --- a/include/evse_security.hpp +++ b/include/evse_security.hpp @@ -132,19 +132,6 @@ class EvseSecurity { std::optional private_key_password; // used to decrypt encrypted private keys; }; -/// @brief Custom exception that is thrown when no private key could be found for a selected certificate -class NoPrivateKeyException : public std::runtime_error { -public: - using std::runtime_error::runtime_error; -}; - -/// @brief Custom exception that is thrown when no valid certificate could be found for the specified filesystem -/// locations -class NoCertificateValidException : public std::runtime_error { -public: - using std::runtime_error::runtime_error; -}; - } // namespace evse_security #endif // EVSE_SECURITY_HPP diff --git a/include/evse_utilities.hpp b/include/evse_utilities.hpp index ad34e79..f172f1b 100644 --- a/include/evse_utilities.hpp +++ b/include/evse_utilities.hpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include namespace evse_security { @@ -16,6 +18,7 @@ class EvseUtils { if (std::filesystem::is_regular_file(file_path)) return std::filesystem::remove(file_path); + EVLOG_error << "Error deleting file: " << file_path; return false; } @@ -29,6 +32,7 @@ class EvseUtils { } } + EVLOG_error << "Error reading file: " << file_path; return false; } @@ -56,10 +60,20 @@ class EvseUtils { } static std::string get_random_file_name(const std::string& extension) { - char path[] = "XXXXXX"; - mktemp(path); + static std::random_device rd; + static std::mt19937 generator(rd()); + static std::uniform_int_distribution distribution(1, std::numeric_limits::max()); - return std::string(path) + extension; + static int increment = 0; + + std::ostringstream buff; + + auto now = std::chrono::system_clock::now(); + std::time_t time = std::chrono::system_clock::to_time_t(now); + buff << std::put_time(std::gmtime(&time), "%m_%d_%Y_%H_%M_%S_") << std::to_string(++increment) << "_" + << distribution(generator) << extension; + + return buff.str(); } }; diff --git a/include/x509_bundle.hpp b/include/x509_bundle.hpp index bd93391..ed80b67 100644 --- a/include/x509_bundle.hpp +++ b/include/x509_bundle.hpp @@ -8,42 +8,86 @@ namespace evse_security { +/// @brief Custom exception that is thrown when no private key could be found for a selected certificate +class NoPrivateKeyException : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +/// @brief Custom exception that is thrown when no valid certificate could be found for the specified filesystem +/// locations +class NoCertificateValidException : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + /// @brief X509 certificate bundle, used for holding multiple X509Wrappers. Supports -/// operations like add/delete importing/exporting certificates +/// operations like add/delete importing/exporting certificates. Can use either a +/// directory with multiple certificates or a single file with one or more certificates +/// in it. A directory that contains certificate bundle files will not work, the entry +/// will be ignored class X509CertificateBundle { public: X509CertificateBundle(const std::filesystem::path& path, const EncodingFormat encoding); X509CertificateBundle(const std::string& certificate, const EncodingFormat encoding); + X509CertificateBundle(X509CertificateBundle&& other) = default; + X509CertificateBundle(const X509CertificateBundle& other) = delete; + public: /// @brief Gets if this certificate bundle comes from a single certificate bundle file /// @return - bool is_using_bundle_file() { + bool is_using_bundle_file() const { return (source == X509CertificateSource::FILE); } /// @brief Gets if this certificate bundle comes from an entire directory /// @return - bool is_using_directory() { + bool is_using_directory() const { return (source == X509CertificateSource::DIRECTORY); } + /// @return True if multiple certificates are contained within + bool is_bundle() const { + return (get_certificate_count() > 1); + } + + bool empty() const { + return certificates.empty(); + } + + /// @return Contained certificate count + int get_certificate_count() const { + return certificates.size(); + } + + std::filesystem::path get_path() const { + return path; + } + + X509CertificateSource get_source() const { + return source; + } + public: /// @brief Splits the certificate (chain) into single certificates /// @return vector containing single certificates std::vector split(); /// @brief If we already have the certificate - bool contains_certificate(const X509Wrapper& certificate); + bool contains_certificate(const X509Wrapper& certificate) const; /// @brief If we already have the certificate - bool contains_certificate(const CertificateHashData& certificate_hash); + bool contains_certificate(const CertificateHashData& certificate_hash) const; + + /// @brief Updates a single certificate in the chain. Only in memory, use @ref export_certificates to filesystem + void add_certificate(X509Wrapper&& certificate); /// @brief Updates a single certificate in the chain. Only in memory, use @ref export_certificates to filesystem /// export /// @param certificate certificate to update /// @return true if the certificate was found and updated, false otherwise. If true is returned the provided /// certificate is invalidated - bool update_certificate(X509Wrapper& certificate); + bool update_certificate(X509Wrapper&& certificate); /// @brief Deletes a single certificate in the chain. Only in memory, use @ref export_certificates to filesystem /// export @@ -68,7 +112,21 @@ class X509CertificateBundle { bool sync_to_certificate_store(); public: + const X509Wrapper& get_at(int index) { + return certificates.at(index); + } + + /// @brief returns the latest valid certificate within this bundle + X509Wrapper get_latest_valid_certificate(); + +public: + X509CertificateBundle& operator=(X509CertificateBundle&& other) = default; + +public: + /// @brief Loads all certificates from the string data that can contain multiple cetifs static std::vector load_certificates(const std::string& data, const EncodingFormat encoding); + /// @brief Returns the latest valid certif that we might contain + static X509Wrapper get_latest_valid_certificate(const std::vector& certificates); static bool is_certificate_file(const std::filesystem::path& file) { return std::filesystem::is_regular_file(file) && @@ -90,6 +148,28 @@ class X509CertificateBundle { X509CertificateSource source; }; +/// @brief Unlike the bundle, this is loaded only from a directory that can contain a +/// combination of bundle files and single files. Used for easier operations on the +/// directory structures. All bundles will use individual files instead of directories +class X509CertificateDirectory { +public: + X509CertificateDirectory(const std::filesystem::path& directory); + +public: + /// @brief Iterates through all the contained bundles, while the provided function + /// returns true + template void for_each(function func) { + for (const auto& bundle : bundles) { + if (!func(bundle)) + break; + } + } + +private: + std::vector bundles; + std::filesystem::path directory; +}; + } // namespace evse_security #endif // X509_BUNDLE_HPP diff --git a/include/x509_wrapper.hpp b/include/x509_wrapper.hpp index 1776b8c..915247d 100644 --- a/include/x509_wrapper.hpp +++ b/include/x509_wrapper.hpp @@ -34,6 +34,8 @@ const std::filesystem::path PEM_EXTENSION = ".pem"; const std::filesystem::path DER_EXTENSION = ".der"; const std::filesystem::path KEY_EXTENSION = ".key"; +using ossl_days_to_seconds = std::chrono::duration>; + /// @brief Convenience wrapper around openssl X509 certificate class X509Wrapper { public: @@ -104,6 +106,9 @@ class X509Wrapper { /// @return std::string get_export_string() const; + /// @brief If the certificate is within the validity date + bool is_valid() const; + public: X509Wrapper& operator=(X509Wrapper&& other) = default; diff --git a/lib/evse_security.cpp b/lib/evse_security.cpp index 35a38a1..e231e43 100644 --- a/lib/evse_security.cpp +++ b/lib/evse_security.cpp @@ -72,6 +72,7 @@ static CertificateType get_certificate_type(const CaCertificateType ca_certifica static std::filesystem::path get_private_key_path(const X509Wrapper& certificate, const std::filesystem::path& key_path, const std::optional password) { + // TODO(ioan): Before iterating the whole dir check by the filename first 'key_path'.key for (const auto& entry : std::filesystem::recursive_directory_iterator(key_path)) { if (std::filesystem::is_regular_file(entry)) { auto key_file_path = entry.path(); @@ -80,77 +81,39 @@ static std::filesystem::path get_private_key_path(const X509Wrapper& certificate std::ifstream file(key_file_path, std::ios::binary); std::string private_key((std::istreambuf_iterator(file)), std::istreambuf_iterator()); BIO_ptr bio(BIO_new_mem_buf(private_key.c_str(), -1)); + EVP_PKEY_ptr evp_pkey( PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, (void*)password.value_or("").c_str())); + + if (!evp_pkey) { + EVLOG_warning << "Invalid evp_pkey: " << key_path + << " error: " << ERR_error_string(ERR_get_error(), NULL) + << " Password configured correctly?"; + + // Next entry if the key was null + continue; + } + if (X509_check_private_key(certificate.get(), evp_pkey.get())) { - return key_path; + EVLOG_info << "Key found for certificate at path: " << key_file_path; + return key_file_path; } } catch (const std::exception& e) { - EVLOG_debug << "Could not load or verify private key at: " << key_file_path << ": " << e.what(); + EVLOG_info << "Could not load or verify private key at: " << key_file_path << ": " << e.what(); } } } } - throw NoPrivateKeyException("Could not find private key for given certificate"); -} - -static X509Wrapper get_latest_valid_certificate(const std::vector& certificates) { - // Filter certificates with valid_in > 0 - std::vector valid_certificates; - for (const auto& cert : certificates) { - if (cert.get_valid_in() >= 0) { - valid_certificates.push_back(cert); - } - } - - if (valid_certificates.empty()) { - // No valid certificates found - throw NoCertificateValidException("No valid certificates available."); - } - - // Find the certificate with the latest valid_in - auto latest_certificate = std::max_element( - valid_certificates.begin(), valid_certificates.end(), - [](const X509Wrapper& cert1, const X509Wrapper& cert2) { return cert1.get_valid_in() < cert2.get_valid_in(); }); + 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; - return *latest_certificate; + throw NoPrivateKeyException(error); } -std::vector get_leaf_certificates(const std::filesystem::path& cert_dir) { - std::vector certificates; - - for (const auto& entry : std::filesystem::recursive_directory_iterator(cert_dir)) { - if (std::filesystem::is_regular_file(entry)) { - const auto cert_path = entry.path(); - try { - if (cert_path.extension() == PEM_EXTENSION) { - certificates.emplace_back(cert_path, EncodingFormat::PEM); - } else if (cert_path.extension() == DER_EXTENSION) { - certificates.emplace_back(cert_path, EncodingFormat::DER); - } else { - // Ignore other file formats - } - } catch (const CertificateLoadException& e) { - EVLOG_debug << "Could not load client certificate from specified directory: " << cert_path.string(); - } - } - } - return certificates; -} - -std::vector get_leaf_certificates(std::vector paths) { - std::vector certificates; - - for (const auto& path : paths) { - auto certifs = get_leaf_certificates(path); - - if (certifs.size() > 0) { - certificates.reserve(certificates.size() + certifs.size()); - std::move(std::begin(certifs), std::end(certifs), std::back_inserter(certificates)); - } - } - - return certificates; +X509CertificateBundle get_leaf_certificates(const std::filesystem::path& cert_dir) { + return X509CertificateBundle(cert_dir, EncodingFormat::PEM); } EvseSecurity::EvseSecurity(const FilePaths& file_paths, const std::optional& private_key_password) : @@ -195,18 +158,45 @@ EvseSecurity::~EvseSecurity() { InstallCertificateResult EvseSecurity::install_ca_certificate(const std::string& certificate, CaCertificateType certificate_type) { + EVLOG_info << "Installing ca certificate: " << conversions::ca_certificate_type_to_string(certificate_type); // TODO(piet): Check CertificateStoreMaxEntries - // TODO:(ioanbogdan) Check if cert is already installed - try { - X509Wrapper cert(certificate, EncodingFormat::PEM); + X509Wrapper new_cert(certificate, EncodingFormat::PEM); + + // Load existing const auto ca_bundle_path = this->ca_bundle_path_map.at(certificate_type); - if (EvseUtils::write_to_file(ca_bundle_path, certificate, std::ios::app)) { - return InstallCertificateResult::Accepted; + + // TODO: (ioan) check for directories too + // Ensure file exists + if (!std::filesystem::exists(ca_bundle_path)) { + std::ofstream file(ca_bundle_path); + } + + X509CertificateBundle existing_certs(ca_bundle_path, EncodingFormat::PEM); + + // Check if cert is already installed + if (existing_certs.contains_certificate(new_cert) == false) { + existing_certs.add_certificate(std::move(new_cert)); + + if (existing_certs.export_certificates()) { + return InstallCertificateResult::Accepted; + } else { + return InstallCertificateResult::WriteError; + } } else { - return InstallCertificateResult::WriteError; + // Else, simply update it + if (existing_certs.update_certificate(std::move(new_cert))) { + if (existing_certs.export_certificates()) { + return InstallCertificateResult::Accepted; + } else { + return InstallCertificateResult::WriteError; + } + } else { + return InstallCertificateResult::WriteError; + } } } catch (const CertificateLoadException& e) { + EVLOG_error << "Certificate load error: " << e.what(); return InstallCertificateResult::InvalidFormat; } } @@ -273,8 +263,8 @@ InstallCertificateResult EvseSecurity::update_leaf_certificate(const std::string } try { - X509CertificateBundle certificate(certificate_chain, EncodingFormat::PEM); - std::vector _certificate_chain = certificate.split(); + X509CertificateBundle chain_certificate(certificate_chain, EncodingFormat::PEM); + std::vector _certificate_chain = chain_certificate.split(); if (_certificate_chain.empty()) { return InstallCertificateResult::InvalidFormat; } @@ -283,9 +273,10 @@ InstallCertificateResult EvseSecurity::update_leaf_certificate(const std::string return result; } + // First certificate is always the leaf as per the spec const auto& leaf_certificate = _certificate_chain[0]; - // check if a private key belongs to the provided certificate + // Check if a private key belongs to the provided certificate try { const auto private_key_path = get_private_key_path(leaf_certificate, key_path, this->private_key_password); } catch (const NoPrivateKeyException& e) { @@ -293,12 +284,19 @@ InstallCertificateResult EvseSecurity::update_leaf_certificate(const std::string return InstallCertificateResult::WriteError; } - // write certificate to file - const auto file_name = EvseUtils::get_random_file_name(PEM_EXTENSION.string()); + // Write certificate to file + const auto file_name = std::string("SECC_LEAF_") + EvseUtils::get_random_file_name(PEM_EXTENSION.string()); const auto file_path = cert_path / file_name; std::string str_cert = leaf_certificate.get_export_string(); - if (EvseUtils::write_to_file(file_path, str_cert, std::ios::out)) { + // Also write chain to file + const auto chain_file_name = + std::string("CPO_CERT_CHAIN_") + EvseUtils::get_random_file_name(PEM_EXTENSION.string()); + const auto chain_file_path = cert_path / chain_file_name; + std::string str_chain_cert = chain_certificate.to_export_string(); + + if (EvseUtils::write_to_file(file_path, str_cert, std::ios::out) && + EvseUtils::write_to_file(chain_file_path, str_chain_cert, std::ios::out)) { return InstallCertificateResult::Accepted; } else { return InstallCertificateResult::WriteError; @@ -330,17 +328,18 @@ EvseSecurity::get_installed_certificates(const std::vector& cer auto certificates_of_bundle = ca_bundle.split(); CertificateHashDataChain certificate_hash_data_chain; - std::vector child_certificate_hash_data; + certificate_hash_data_chain.certificate_type = + get_certificate_type(ca_certificate_type); // We always know type + for (int i = 0; i < certificates_of_bundle.size(); i++) { CertificateHashData certificate_hash_data = certificates_of_bundle[i].get_certificate_hash_data(); if (i == 0) { certificate_hash_data_chain.certificate_hash_data = certificate_hash_data; - certificate_hash_data_chain.certificate_type = get_certificate_type(ca_certificate_type); } else { - child_certificate_hash_data.push_back(certificate_hash_data); + certificate_hash_data_chain.child_certificate_hash_data.push_back(certificate_hash_data); } } - certificate_hash_data_chain.child_certificate_hash_data = child_certificate_hash_data; + certificate_chains.push_back(certificate_hash_data_chain); } catch (const CertificateLoadException& e) { EVLOG_warning << "Could not load CA bundle file at: " << ca_bundle_path << " error: " << e.what(); @@ -359,6 +358,9 @@ EvseSecurity::get_installed_certificates(const std::vector& cer certificate_hash_data_chain.certificate_hash_data = certificate_hash_data; certificate_hash_data_chain.certificate_type = CertificateType::V2GCertificateChain; + // TODO (ioan): as per V2GCertificateChain: OCPP 2.0.1 part 2 spec 2 (3.36): + // V2G certificate chain (excluding the V2GRootCertificate) + // Exclude V2G root when returning the V2GCertificateChain const auto ca_bundle_path = this->ca_bundle_path_map.at(CaCertificateType::V2G); X509CertificateBundle ca_bundle(ca_bundle_path, EncodingFormat::PEM); const auto certificates_of_bundle = ca_bundle.split(); @@ -455,7 +457,7 @@ std::string EvseSecurity::generate_certificate_signing_request(LeafCertificateTy std::filesystem::path key_path; - const auto file_name = EvseUtils::get_random_file_name(KEY_EXTENSION.string()); + const auto file_name = std::string("SECC_LEAF_") + EvseUtils::get_random_file_name(KEY_EXTENSION.string()); if (certificate_type == LeafCertificateType::CSMS) { key_path = this->directories.csms_leaf_key_directory / file_name; } else if (certificate_type == LeafCertificateType::V2G) { @@ -517,6 +519,8 @@ std::string EvseSecurity::generate_certificate_signing_request(LeafCertificateTy } GetKeyPairResult EvseSecurity::get_key_pair(LeafCertificateType certificate_type, EncodingFormat encoding) { + EVLOG_info << "Requesting key/pair: " << conversions::leaf_certificate_type_to_string(certificate_type); + GetKeyPairResult result; result.pair = std::nullopt; @@ -535,9 +539,9 @@ GetKeyPairResult EvseSecurity::get_key_pair(LeafCertificateType certificate_type return result; } - const auto certificates = get_leaf_certificates(cert_dir); + auto leaf_certificates = std::move(get_leaf_certificates(cert_dir)); - if (certificates.empty()) { + if (leaf_certificates.empty()) { EVLOG_warning << "Could not find any key pair"; result.status = GetKeyPairStatus::NotFound; return result; @@ -545,15 +549,47 @@ GetKeyPairResult EvseSecurity::get_key_pair(LeafCertificateType certificate_type // choose appropriate cert (valid_from / valid_to) try { - const auto certificate = get_latest_valid_certificate(certificates); - const auto private_key_path = get_private_key_path(certificate, key_dir, this->private_key_password); + std::filesystem::path key_file; + std::filesystem::path certificate_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); + + // Key path doesn't change + key_file = private_key_path; + + // We are searching for the full leaf bundle, containing both the leaf and the cso1/2 + X509CertificateDirectory leaf_directory(cert_dir); + const X509CertificateBundle* leaf_fullchain = nullptr; - result.pair = {private_key_path.string(), certificate.get_file().value(), this->private_key_password}; + // Collect the correct bundle + leaf_directory.for_each([&certificate, &leaf_fullchain](const X509CertificateBundle& bundle) { + // If we contain the latest valid, we found our generated bundle + if (bundle.is_bundle() && bundle.contains_certificate(certificate)) { + leaf_fullchain = &bundle; + return false; + } + + return true; + }); + + if (leaf_fullchain != nullptr) { + certificate_file = leaf_fullchain->get_path(); + } else { + EVLOG_warning << "V2G leaf requires full bundle, but full bundle not found at path: " << cert_dir; + } + + // If chain is not found, set single leaf certificate file + if (certificate_file.empty()) { + certificate_file = certificate.get_file().value(); + } + + result.pair = {key_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"; + EVLOG_warning << "Could not find private key for the selected certificate: (" << e.what() << ")"; result.status = GetKeyPairStatus::PrivateKeyNotFound; return result; } catch (const NoCertificateValidException& e) { @@ -564,15 +600,24 @@ GetKeyPairResult EvseSecurity::get_key_pair(LeafCertificateType certificate_type } std::string EvseSecurity::get_verify_file(CaCertificateType certificate_type) { - X509Wrapper verify_file(this->ca_bundle_path_map.at(certificate_type), EncodingFormat::PEM); - return verify_file.get_file().value().string(); + EVLOG_info << "Requesting certificate file: " << conversions::ca_certificate_type_to_string(certificate_type); + + // Support bundle files, in case the certificates contain + // multiple entries (should be 3) as per the specification + X509CertificateBundle verify_file(this->ca_bundle_path_map.at(certificate_type), EncodingFormat::PEM); + return verify_file.get_path().string(); } int EvseSecurity::get_leaf_expiry_days_count(LeafCertificateType certificate_type) { + EVLOG_info << "Requesting certificate expiry: " << conversions::leaf_certificate_type_to_string(certificate_type); + const auto key_pair = this->get_key_pair(certificate_type, EncodingFormat::PEM); if (key_pair.status == GetKeyPairStatus::Accepted) { - X509Wrapper cert(key_pair.pair.value().certificate, EncodingFormat::PEM); - return cert.get_valid_to() / 86400; + // In case it is a bundle, we know the leaf is always the first + X509CertificateBundle cert(key_pair.pair.value().certificate, EncodingFormat::PEM); + + int64_t seconds = cert.get_at(0).get_valid_to(); + return std::chrono::duration_cast(std::chrono::seconds(seconds)).count(); } return 0; } diff --git a/lib/x509_bundle.cpp b/lib/x509_bundle.cpp index 87a9147..253df06 100644 --- a/lib/x509_bundle.cpp +++ b/lib/x509_bundle.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest +#include #include #include @@ -10,7 +11,28 @@ namespace evse_security { -/// @brief Loads all certificates from the string data that can contain multiple cetifs +X509Wrapper X509CertificateBundle::get_latest_valid_certificate(const std::vector& certificates) { + // Filter certificates with valid_in > 0 + std::vector valid_certificates; + for (const auto& cert : certificates) { + if (cert.is_valid()) { + valid_certificates.push_back(cert); + } + } + + if (valid_certificates.empty()) { + // No valid certificates found + throw NoCertificateValidException("No valid certificates available."); + } + + // Find the certificate with the latest valid_in + auto latest_certificate = std::max_element( + valid_certificates.begin(), valid_certificates.end(), + [](const X509Wrapper& cert1, const X509Wrapper& cert2) { return cert1.get_valid_in() < cert2.get_valid_in(); }); + + return *latest_certificate; +} + std::vector X509CertificateBundle::load_certificates(const std::string& data, const EncodingFormat encoding) { BIO_ptr bio(BIO_new_mem_buf(data.data(), static_cast(data.size()))); @@ -78,10 +100,11 @@ X509CertificateBundle::X509CertificateBundle(const std::filesystem::path& path, std::string certificate; if (EvseUtils::read_from_file(path, certificate)) add_certifcates(certificate, encoding, path); - } + } else { + std::string error = "Failed to create certificate info from path: "; + error += path; - if (certificates.size() <= 0) { - throw CertificateLoadException("Failed to read X509 from BIO"); + throw CertificateLoadException(error); } } @@ -91,7 +114,8 @@ void X509CertificateBundle::add_certifcates(const std::string& data, const Encod // If we are using a directory we can't load certificate bundles if (is_using_directory() && loaded.size() > 1) { - throw CertificateLoadException("Failed to read single certificate from directory file!"); + EVLOG_warning << "Failed to read single certificate from directory file, ignoring entry!"; + return; } for (auto& x509 : loaded) { @@ -106,7 +130,7 @@ std::vector X509CertificateBundle::split() { return certificates; } -bool X509CertificateBundle::contains_certificate(const X509Wrapper& certificate) { +bool X509CertificateBundle::contains_certificate(const X509Wrapper& certificate) const { for (const auto& certif : certificates) { if (certif == certificate) return true; @@ -115,7 +139,7 @@ bool X509CertificateBundle::contains_certificate(const X509Wrapper& certificate) return false; } -bool X509CertificateBundle::contains_certificate(const CertificateHashData& certificate_hash) { +bool X509CertificateBundle::contains_certificate(const CertificateHashData& certificate_hash) const { for (const auto& certif : certificates) { if (certif == certificate_hash) return true; @@ -143,7 +167,11 @@ void X509CertificateBundle::delete_all_certificates() { certificates.clear(); } -bool X509CertificateBundle::update_certificate(X509Wrapper& certificate) { +void X509CertificateBundle::add_certificate(X509Wrapper&& certificate) { + certificates.push_back(certificate); +} + +bool X509CertificateBundle::update_certificate(X509Wrapper&& certificate) { for (int i = 0; i < certificates.size(); ++i) { if (certificates[i] == certificate) { certificates.at(i) = std::move(certificate); @@ -156,12 +184,15 @@ bool X509CertificateBundle::update_certificate(X509Wrapper& certificate) { bool X509CertificateBundle::export_certificates() { if (source == X509CertificateSource::STRING) { + EVLOG_error << "Export for string is invalid!"; return false; } // Add/delete certifs - if (!sync_to_certificate_store()) + if (!sync_to_certificate_store()) { + EVLOG_error << "Sync to certificate store failed!"; return false; + } if (source == X509CertificateSource::DIRECTORY) { bool exported_all = true; @@ -188,36 +219,20 @@ bool X509CertificateBundle::export_certificates() { } bool X509CertificateBundle::sync_to_certificate_store() { - if (source == X509CertificateSource::STRING) + if (source == X509CertificateSource::STRING) { + EVLOG_error << "Sync for string is invalid!"; return false; + } if (source == X509CertificateSource::DIRECTORY) { - // Delete inexistent certificates - std::vector fs_certificates; - - for (const auto& entry : std::filesystem::recursive_directory_iterator(path)) { - if (is_certificate_file(entry)) { - std::string certificate; - if (EvseUtils::read_from_file(entry.path(), certificate)) { - auto certifs = load_certificates(certificate, EncodingFormat::PEM); - - if (certifs.size() > 1) { - throw CertificateLoadException("Failed to read single certificate from directory file!"); - } - - // Emplace all filesystem certificates - for (auto& x509 : certifs) - fs_certificates.emplace_back(std::move(x509), entry); - } - } - } + // Get existing certificates from filesystem + std::vector fs_certificates = X509CertificateBundle(path, EncodingFormat::PEM).split(); bool success = true; - // Delete filesystem certificates missing from our list for (const auto& fs_certif : fs_certificates) { if (std::find(certificates.begin(), certificates.end(), fs_certif) == certificates.end()) { - // fs certif not existing in our certificate list + // fs certif not existing in our certificate list, delete if (!EvseUtils::delete_file(fs_certif.get_file().value())) success = false; } @@ -226,7 +241,7 @@ bool X509CertificateBundle::sync_to_certificate_store() { // Add the certificates that are not existing in the filesystem for (const auto& certif : certificates) { if (std::find(fs_certificates.begin(), fs_certificates.end(), certif) == fs_certificates.end()) { - // certif not existing in fs certificates write it out + // Certif not existing in fs certificates write it out if (!EvseUtils::write_to_file(certif.get_file().value(), certif.get_export_string(), std::ios::trunc)) success = false; } @@ -243,6 +258,10 @@ bool X509CertificateBundle::sync_to_certificate_store() { return true; } +X509Wrapper X509CertificateBundle::get_latest_valid_certificate() { + return get_latest_valid_certificate(certificates); +} + std::string X509CertificateBundle::to_export_string() const { std::string export_string; @@ -253,4 +272,16 @@ std::string X509CertificateBundle::to_export_string() const { return export_string; } +X509CertificateDirectory::X509CertificateDirectory(const std::filesystem::path& directory) { + if (std::filesystem::is_directory(directory)) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) { + if (X509CertificateBundle::is_certificate_file(entry)) { + bundles.emplace_back(entry, EncodingFormat::PEM); + } + } + } else { + throw CertificateLoadException("X509CertificateDirectory can only load from directories!"); + } +} + } // namespace evse_security \ No newline at end of file diff --git a/lib/x509_wrapper.cpp b/lib/x509_wrapper.cpp index c6e4932..fcde47e 100644 --- a/lib/x509_wrapper.cpp +++ b/lib/x509_wrapper.cpp @@ -15,8 +15,6 @@ namespace evse_security { -using ossl_days_to_seconds = std::chrono::duration>; - std::string x509_to_string(X509* x509) { if (x509) { BIO_ptr bio_write(BIO_new(BIO_s_mem())); @@ -44,7 +42,10 @@ X509Wrapper::X509Wrapper(const std::filesystem::path& file, const EncodingFormat auto loaded = X509CertificateBundle::load_certificates(certificate, encoding); if (loaded.size() != 1) { - throw CertificateLoadException("X509Wrapper can only load a single certificate!"); + std::string error = "X509Wrapper can only load a single certificate! Loaded: "; + error += std::to_string(loaded.size()); + + throw CertificateLoadException(error); } this->file = file; @@ -55,7 +56,10 @@ X509Wrapper::X509Wrapper(const std::filesystem::path& file, const EncodingFormat X509Wrapper::X509Wrapper(const std::string& data, const EncodingFormat encoding) { auto loaded = X509CertificateBundle::load_certificates(data, encoding); if (loaded.size() != 1) { - throw CertificateLoadException("X509Wrapper can only load a single certificate!"); + std::string error = "X509Wrapper can only load a single certificate! Loaded: "; + error += std::to_string(loaded.size()); + + throw CertificateLoadException(error); } x509 = std::move(loaded[0]); @@ -120,6 +124,10 @@ int X509Wrapper::get_valid_to() const { return valid_to; } +bool X509Wrapper::is_valid() const { + return (get_valid_in() >= 0); +} + std::optional X509Wrapper::get_file() const { return this->file; } diff --git a/tests/tests.cpp b/tests/tests.cpp index 23a18ae..d4797b7 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -115,13 +115,6 @@ TEST_F(EvseSecurityTests, verify_bundle_management) { } } ASSERT_TRUE(items == 1); - - EXPECT_THROW( - { - // We don't support directory/bundle combination - X509CertificateBundle bundle_throw(std::filesystem::path("certs/ca/v2g/"), EncodingFormat::PEM); - }, - CertificateLoadException); } /// \brief test verifyChargepointCertificate with valid cert @@ -212,7 +205,7 @@ TEST_F(EvseSecurityTests, get_installed_certificates_and_delete_secc_leaf) { // ASSERT_EQ(r.status, GetInstalledCertificatesStatus::Accepted); ASSERT_EQ(r.status, GetInstalledCertificatesStatus::Accepted); - ASSERT_EQ(r.certificate_hash_data_chain.size(), 4); + ASSERT_EQ(r.certificate_hash_data_chain.size(), 5); bool found_v2g_chain = false; CertificateHashData secc_leaf_data; @@ -231,7 +224,7 @@ TEST_F(EvseSecurityTests, get_installed_certificates_and_delete_secc_leaf) { const auto get_certs_response = this->evse_security->get_installed_certificates(certificate_types); // ASSERT_EQ(r.status, GetInstalledCertificatesStatus::Accepted); - ASSERT_EQ(get_certs_response.certificate_hash_data_chain.size(), 3); + ASSERT_EQ(get_certs_response.certificate_hash_data_chain.size(), 4); delete_response = this->evse_security->delete_certificate(secc_leaf_data); ASSERT_EQ(delete_response, DeleteCertificateResult::NotFound);