diff --git a/include/evse_utilities.hpp b/include/evse_utilities.hpp new file mode 100644 index 0000000..d5ea0bd --- /dev/null +++ b/include/evse_utilities.hpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef EVSE_UTILITIES_HPP +#define EVSE_UTILITIES_HPP + +#include +#include +#include + +namespace evse_security { + +class EvseUtils { +public: + static bool delete_file(const std::filesystem::path& file_path) { + if (std::filesystem::is_regular_file(file_path)) + return std::filesystem::remove(file_path); + + return false; + } + + static bool read_from_file(const std::filesystem::path& file_path, std::string& out_data) { + if (std::filesystem::is_regular_file(file_path)) { + std::ifstream file(file_path, std::ios::binary); + + if (file.is_open()) { + out_data = std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + return true; + } + } + + return false; + } + + static bool write_to_file(const std::filesystem::path& file_path, const std::string& data, + std::ios::openmode mode) { + try { + std::ofstream fs(file_path, mode | std::ios::binary); + if (!fs.is_open()) { + // EVLOG_error << "Error opening file: " << file_path; + return false; + } + fs.write(data.c_str(), data.size()); + + if (!fs) { + // EVLOG_error << "Error writing to file: " << file_path; + return false; + } + return true; + } catch (const std::exception& e) { + // EVLOG_error << "Unknown error occurred while writing to file: " << file_path; + return false; + } + + return true; + } + + static std::string get_random_file_name(const std::string& extension) { + char path[] = "XXXXXX"; + mktemp(path); + + return std::string(path) + extension; + } +}; + +} // namespace evse_security + +#endif \ No newline at end of file diff --git a/include/sec_types.hpp b/include/sec_types.hpp new file mode 100644 index 0000000..71e8b0e --- /dev/null +++ b/include/sec_types.hpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef EVSE_SEC_TYPES_HPP +#define EVSE_SEC_TYPES_HPP + +#include +#include + +template <> class std::default_delete { +public: + void operator()(X509* ptr) const { + ::X509_free(ptr); + } +}; + +template <> class std::default_delete { +public: + void operator()(X509_STORE* ptr) const { + ::X509_STORE_free(ptr); + } +}; + +template <> class std::default_delete { +public: + void operator()(X509_STORE_CTX* ptr) const { + ::X509_STORE_CTX_free(ptr); + } +}; + +template <> class std::default_delete { +public: + void operator()(X509_REQ* ptr) const { + ::X509_REQ_free(ptr); + } +}; + +template <> class std::default_delete { +public: + void operator()(EVP_PKEY* ptr) const { + ::EVP_PKEY_free(ptr); + } +}; + +template <> class std::default_delete { +public: + void operator()(BIO* ptr) const { + ::BIO_free(ptr); + } +}; + +namespace evse_security { + +using X509_ptr = std::unique_ptr; +using X509_STORE_ptr = std::unique_ptr; +using X509_STORE_CTX_ptr = std::unique_ptr; +using X509_REQ_ptr = std::unique_ptr; +using EVP_PKEY_ptr = std::unique_ptr; +using BIO_ptr = std::unique_ptr; + +} // namespace evse_security + +#endif \ No newline at end of file diff --git a/include/x509_bundle.hpp b/include/x509_bundle.hpp new file mode 100644 index 0000000..bd93391 --- /dev/null +++ b/include/x509_bundle.hpp @@ -0,0 +1,95 @@ + +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef X509_BUNDLE_HPP +#define X509_BUNDLE_HPP + +#include + +namespace evse_security { + +/// @brief X509 certificate bundle, used for holding multiple X509Wrappers. Supports +/// operations like add/delete importing/exporting certificates +class X509CertificateBundle { +public: + X509CertificateBundle(const std::filesystem::path& path, const EncodingFormat encoding); + X509CertificateBundle(const std::string& certificate, const EncodingFormat encoding); + +public: + /// @brief Gets if this certificate bundle comes from a single certificate bundle file + /// @return + bool is_using_bundle_file() { + return (source == X509CertificateSource::FILE); + } + + /// @brief Gets if this certificate bundle comes from an entire directory + /// @return + bool is_using_directory() { + return (source == X509CertificateSource::DIRECTORY); + } + +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); + /// @brief If we already have the certificate + bool contains_certificate(const CertificateHashData& certificate_hash); + + /// @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); + + /// @brief Deletes a single certificate in the chain. Only in memory, use @ref export_certificates to filesystem + /// export + /// @return true if the certificate was found and deleted, false otherwise + bool delete_certificate(const X509Wrapper& certificate); + bool delete_certificate(const CertificateHashData& data); + + /// @brief Deletes all certificates. Only in memory, use @ref export_certificates to filesystem export + void delete_all_certificates(); + + /// @brief Returns a full exportable representation of a certificate bundle file in PEM format + std::string to_export_string() const; + + /// @brief Exports the full certificate chain either as individual files if it is using a directory + /// or as a bundle if it uses a bundle file, at the initially provided path. Also deletes/adds the updated + /// certificates + /// @return true on success, false otherwise + bool export_certificates(); + + /// @brief Syncs the file structure with the certificate store adding certificates that are not found on the + /// storage and deleting the certificates that are not contained in this bundle + bool sync_to_certificate_store(); + +public: + static std::vector load_certificates(const std::string& data, const EncodingFormat encoding); + + static bool is_certificate_file(const std::filesystem::path& file) { + return std::filesystem::is_regular_file(file) && + ((file.extension() == PEM_EXTENSION) || (file.extension() == DER_EXTENSION)); + } + +private: + /// @brief Adds to our certificate list the certificates found in the file + /// @return number of added certificates + void add_certifcates(const std::string& data, const EncodingFormat encoding, + const std::optional& path); + +private: + // Certificates in this chain, can only be loaded either from a bundle or a dir folder, never combined + std::vector certificates; + // Relevant bundle or directory for this certificate chain + std::filesystem::path path; + // Source from where we created the certificates. If 'string' the 'export' functions will not work + X509CertificateSource source; +}; + +} // namespace evse_security + +#endif // X509_BUNDLE_HPP diff --git a/include/x509_wrapper.hpp b/include/x509_wrapper.hpp index 93843ae..1776b8c 100644 --- a/include/x509_wrapper.hpp +++ b/include/x509_wrapper.hpp @@ -11,6 +11,7 @@ #include #include +#include #include namespace evse_security { @@ -20,19 +21,33 @@ class CertificateLoadException : public std::runtime_error { using std::runtime_error::runtime_error; }; -/// @brief Convenience wrapper around openssl X509 certificate. Can contain multiple certificates -class X509Wrapper { - using X509_ptr = std::unique_ptr; +enum class X509CertificateSource { + // Built from a certificate file + FILE, + // Built from a directory of certificates + DIRECTORY, + // Build from a raw string + STRING +}; +const std::filesystem::path PEM_EXTENSION = ".pem"; +const std::filesystem::path DER_EXTENSION = ".der"; +const std::filesystem::path KEY_EXTENSION = ".key"; + +/// @brief Convenience wrapper around openssl X509 certificate +class X509Wrapper { public: - // Constructors - X509Wrapper(const std::string& certificate, const EncodingFormat encoding); - X509Wrapper(const std::filesystem::path& path, const EncodingFormat encoding); + X509Wrapper(const std::filesystem::path& file, const EncodingFormat encoding); + X509Wrapper(const std::string& data, const EncodingFormat encoding); - /// @brief Since it implies ownership full transfer, must be very carefull with this that's why it's explicit - /// If another object owns the x509 will destroy it and if another one tries to use the dead reference - /// it will crash the program + /// @brief Since it implies ownership full transfer, must be very careful with this that's why it's explicit + /// If another object owns the x509 will destroy it and if another one tries to use the dead reference will crash + /// the program explicit X509Wrapper(X509* x509); + explicit X509Wrapper(X509_ptr&& x509); + + X509Wrapper(X509* x509, const std::filesystem::path& file); + X509Wrapper(X509_ptr&& x509, const std::filesystem::path& file); X509Wrapper(const X509Wrapper& other); X509Wrapper(X509Wrapper&& other) = default; @@ -41,16 +56,14 @@ class X509Wrapper { /// @brief Gets raw X509 pointer /// @return - X509* get() const; + inline X509* get() const { + return x509.get(); + } /// @brief Resets raw X509 pointer to given \p x509 /// @param x509 void reset(X509* x509); - /// @brief Splits the certificate (chain) into single certificates - /// @return vector containing single certificates - std::vector split(); - /// @brief Gets valid_in /// @return seconds until certificate is valid; if > 0 cert is not yet valid int get_valid_in() const; @@ -59,13 +72,9 @@ class X509Wrapper { /// @return seconds until certificate is expired; if < 0 cert has expired int get_valid_to() const; - /// @brief Gets str - /// @result raw certificate string - std::string get_str() const; - - /// @brief Gets optional path of certificate + /// @brief Gets optional file of certificate /// @result - std::optional get_path() const; + std::optional get_file() const; /// @brief Gets CN of certificate /// @result @@ -91,25 +100,34 @@ class X509Wrapper { /// @return std::string get_responder_url() const; - /// @brief Gets the bin64 string representation of this certificate + /// @brief Gets the export string representation for this certificate /// @return - std::string to_base64_string() const; + std::string get_export_string() const; - /// @brief Gets if this certificate file is containing multiple certificates - /// @return - bool is_bundle() { - return x509.size() > 1; +public: + X509Wrapper& operator=(X509Wrapper&& other) = default; + + /// @return true if the two certificates are the same + bool operator==(const X509Wrapper& other) const { + return get_issuer_name_hash() == other.get_issuer_name_hash() && + get_issuer_key_hash() == other.get_issuer_key_hash() && get_serial_number() == other.get_serial_number(); + } + + bool operator==(const CertificateHashData& other) const { + return get_issuer_name_hash() == other.issuer_name_hash && get_issuer_key_hash() == other.issuer_key_hash && + get_serial_number() == other.serial_number; } private: - void load_certificate(const std::string& data, const EncodingFormat encoding); void update_validity(); private: - std::vector x509; - int valid_in; // seconds; if > 0 cert is not yet valid - int valid_to; // seconds; if < 0 cert has expired - std::optional path; + X509_ptr x509; // X509 wrapper object + std::int64_t valid_in; // seconds; if > 0 cert is not yet valid + std::int64_t valid_to; // seconds; if < 0 cert has expired + + // Relevant file in which this certificate resides + std::optional file; }; } // namespace evse_security diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 68e3bf4..17b9c03 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(evse_security evse_security.cpp x509_wrapper.cpp types.cpp + x509_bundle.cpp ) target_include_directories(evse_security diff --git a/lib/evse_security.cpp b/lib/evse_security.cpp index e339a4e..35a38a1 100644 --- a/lib/evse_security.cpp +++ b/lib/evse_security.cpp @@ -11,120 +11,11 @@ #include #include +#include +#include #include namespace evse_security { -const std::filesystem::path PEM_EXTENSION = ".pem"; -const std::filesystem::path DER_EXTENSION = ".der"; -const std::filesystem::path KEY_EXTENSION = ".key"; - -using X509_STORE_ptr = std::unique_ptr; -using X509_STORE_CTX_ptr = std::unique_ptr; -using X509_REQ_ptr = std::unique_ptr; -using EVP_PKEY_ptr = std::unique_ptr; -using BIO_ptr = std::unique_ptr; - -static std::filesystem::path get_private_key_path(const X509Wrapper& certificate, const std::filesystem::path& key_path, - const std::optional password) { - for (const auto& entry : std::filesystem::recursive_directory_iterator(key_path)) { - if (std::filesystem::is_regular_file(entry)) { - auto key_file_path = entry.path(); - if (key_file_path.extension() == KEY_EXTENSION) { - try { - 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), ::BIO_free); - EVP_PKEY_ptr evp_pkey( - PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, (void*)password.value_or("").c_str()), - EVP_PKEY_free); - if (X509_check_private_key(certificate.get(), evp_pkey.get())) { - return key_path; - } - } catch (const std::exception& e) { - EVLOG_debug << "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(); }); - - return *latest_certificate; -} - -static bool write_to_file(const std::filesystem::path& file_path, const std::string& data, std::ios::openmode mode) { - try { - std::ofstream fs(file_path, mode | std::ios::binary); - if (!fs.is_open()) { - EVLOG_error << "Error opening file: " << file_path; - return false; - } - fs.write(data.c_str(), data.size()); - - if (!fs) { - EVLOG_error << "Error writing to file: " << file_path; - return false; - } - return true; - } catch (const std::exception& e) { - EVLOG_error << "Unknown error occured while writing to file: " << file_path; - return false; - } - return true; -} - -static bool delete_certificate_from_bundle(const std::string& certificate, - const std::filesystem::path& ca_bundle_path) { - if (!std::filesystem::exists(ca_bundle_path)) { - return false; - } - // Read the content of the file - std::ifstream in_file(ca_bundle_path); - if (!in_file) { - EVLOG_error << "Error opening file: " << ca_bundle_path; - return false; - } - - std::string file_content((std::istreambuf_iterator(in_file)), std::istreambuf_iterator()); - in_file.close(); - - size_t pos = file_content.find(certificate); - if (pos == std::string::npos) { - // cert is not part of bundle - return true; - } - - file_content.erase(pos, certificate.length()); - - std::ofstream out_file(ca_bundle_path); - if (!out_file) { - EVLOG_error << "Error opening file for writing: " << ca_bundle_path; - return false; - } - out_file << file_content; - out_file.close(); - return true; -} - static InstallCertificateResult to_install_certificate_result(const int ec) { switch (ec) { case X509_V_ERR_CERT_HAS_EXPIRED: @@ -179,11 +70,50 @@ static CertificateType get_certificate_type(const CaCertificateType ca_certifica } } -static std::string get_random_file_name(const std::string& extension) { - char path[] = "XXXXXX"; - mkstemp(path); +static std::filesystem::path get_private_key_path(const X509Wrapper& certificate, const std::filesystem::path& key_path, + const std::optional password) { + for (const auto& entry : std::filesystem::recursive_directory_iterator(key_path)) { + if (std::filesystem::is_regular_file(entry)) { + auto key_file_path = entry.path(); + if (key_file_path.extension() == KEY_EXTENSION) { + try { + 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 (X509_check_private_key(certificate.get(), evp_pkey.get())) { + return key_path; + } + } catch (const std::exception& e) { + EVLOG_debug << "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(); }); - return std::string(path) + extension; + return *latest_certificate; } std::vector get_leaf_certificates(const std::filesystem::path& cert_dir) { @@ -223,27 +153,6 @@ std::vector get_leaf_certificates(std::vector -get_ca_certificates(const std::map& ca_bundle_path_map) { - // x509 wrapper specific - std::vector ca_certificates; - for (auto const& [certificate_type, ca_bundle_path] : ca_bundle_path_map) { - try { - X509Wrapper ca_bundle(ca_bundle_path, EncodingFormat::PEM); - const auto certificates_of_bundle = ca_bundle.split(); - - if (certificates_of_bundle.size() > 0) { - ca_certificates.reserve(ca_certificates.size() + certificates_of_bundle.size()); - std::move(std::begin(certificates_of_bundle), std::end(certificates_of_bundle), - std::back_inserter(ca_certificates)); - } - } catch (const CertificateLoadException& e) { - EVLOG_info << "Could not load ca bundle from file: " << ca_bundle_path; - } - } - return ca_certificates; -} - EvseSecurity::EvseSecurity(const FilePaths& file_paths, const std::optional& private_key_password) : private_key_password(private_key_password) { @@ -287,11 +196,12 @@ EvseSecurity::~EvseSecurity() { InstallCertificateResult EvseSecurity::install_ca_certificate(const std::string& certificate, CaCertificateType certificate_type) { // TODO(piet): Check CertificateStoreMaxEntries + // TODO:(ioanbogdan) Check if cert is already installed try { X509Wrapper cert(certificate, EncodingFormat::PEM); const auto ca_bundle_path = this->ca_bundle_path_map.at(certificate_type); - if (write_to_file(ca_bundle_path, certificate, std::ios::app)) { + if (EvseUtils::write_to_file(ca_bundle_path, certificate, std::ios::app)) { return InstallCertificateResult::Accepted; } else { return InstallCertificateResult::WriteError; @@ -302,43 +212,41 @@ InstallCertificateResult EvseSecurity::install_ca_certificate(const std::string& } DeleteCertificateResult EvseSecurity::delete_certificate(const CertificateHashData& certificate_hash_data) { - // your code for cmd delete_certificate goes here auto response = DeleteCertificateResult::NotFound; - const auto ca_certificates = get_ca_certificates(ca_bundle_path_map); bool found_certificate = false; bool failed_to_write = false; - for (const auto& ca_certificate : ca_certificates) { - // write a compare function for CertificateHashData and X509 wrapper ! - if (ca_certificate.get_issuer_name_hash() == certificate_hash_data.issuer_name_hash and - ca_certificate.get_issuer_key_hash() == certificate_hash_data.issuer_key_hash and - ca_certificate.get_serial_number() == certificate_hash_data.serial_number and - ca_certificate.get_path().has_value()) { - // cert could be present in multiple ca bundles - found_certificate = true; - if (!delete_certificate_from_bundle(ca_certificate.get_str(), ca_certificate.get_path().value())) { - failed_to_write = true; + for (auto const& [certificate_type, ca_bundle_path] : ca_bundle_path_map) { + try { + X509CertificateBundle ca_bundle(ca_bundle_path, EncodingFormat::PEM); + + if (ca_bundle.delete_certificate(certificate_hash_data)) { + found_certificate = true; + if (!ca_bundle.export_certificates()) { + failed_to_write = true; + } } + + } catch (const CertificateLoadException& e) { + EVLOG_info << "Could not load ca bundle from file: " << ca_bundle_path; } } - const auto leaf_certificates = - get_leaf_certificates({directories.secc_leaf_cert_directory, directories.csms_leaf_cert_directory}); - for (const auto& leaf_certificate : leaf_certificates) { - if (leaf_certificate.get_issuer_name_hash() == certificate_hash_data.issuer_name_hash and - leaf_certificate.get_issuer_key_hash() == certificate_hash_data.issuer_key_hash and - leaf_certificate.get_serial_number() == certificate_hash_data.serial_number and - leaf_certificate.get_path().has_value()) { - // cert could be present in multiple ca bundles - found_certificate = true; - try { - std::filesystem::remove(leaf_certificate.get_path().value()); - } catch (const std::filesystem::filesystem_error& e) { - // don't use liblog! unnecessary dependency - EVLOG_error << "Error removing leaf certificate: " << e.what(); - failed_to_write = true; + for (const auto& leaf_certificate_path : + {directories.secc_leaf_cert_directory, directories.csms_leaf_cert_directory}) { + try { + X509CertificateBundle leaf_bundle(leaf_certificate_path, EncodingFormat::PEM); + + if (leaf_bundle.delete_certificate(certificate_hash_data)) { + found_certificate = true; + if (!leaf_bundle.export_certificates()) { + failed_to_write = true; + EVLOG_error << "Error removing leaf certificate: " << certificate_hash_data.issuer_name_hash; + } } + } catch (const CertificateLoadException& e) { + EVLOG_info << "Could not load ca bundle from file: " << leaf_certificate_path; } } @@ -365,7 +273,7 @@ InstallCertificateResult EvseSecurity::update_leaf_certificate(const std::string } try { - X509Wrapper certificate(certificate_chain, EncodingFormat::PEM); + X509CertificateBundle certificate(certificate_chain, EncodingFormat::PEM); std::vector _certificate_chain = certificate.split(); if (_certificate_chain.empty()) { return InstallCertificateResult::InvalidFormat; @@ -386,11 +294,11 @@ InstallCertificateResult EvseSecurity::update_leaf_certificate(const std::string } // write certificate to file - const auto file_name = get_random_file_name(PEM_EXTENSION.string()); + const auto file_name = EvseUtils::get_random_file_name(PEM_EXTENSION.string()); const auto file_path = cert_path / file_name; - std::string str_cert = leaf_certificate.get_str(); + std::string str_cert = leaf_certificate.get_export_string(); - if (write_to_file(file_path, str_cert, std::ios::out)) { + if (EvseUtils::write_to_file(file_path, str_cert, std::ios::out)) { return InstallCertificateResult::Accepted; } else { return InstallCertificateResult::WriteError; @@ -418,7 +326,7 @@ EvseSecurity::get_installed_certificates(const std::vector& cer for (const auto& ca_certificate_type : ca_certificate_types) { auto ca_bundle_path = this->ca_bundle_path_map.at(ca_certificate_type); try { - X509Wrapper ca_bundle(ca_bundle_path, EncodingFormat::PEM); + X509CertificateBundle ca_bundle(ca_bundle_path, EncodingFormat::PEM); auto certificates_of_bundle = ca_bundle.split(); CertificateHashDataChain certificate_hash_data_chain; @@ -442,6 +350,7 @@ EvseSecurity::get_installed_certificates(const std::vector& cer // retrieve v2g certificate chain if (std::find(certificate_types.begin(), certificate_types.end(), CertificateType::V2GCertificateChain) != certificate_types.end()) { + const auto secc_key_pair = this->get_key_pair(LeafCertificateType::V2G, EncodingFormat::PEM); if (secc_key_pair.status == GetKeyPairStatus::Accepted) { X509Wrapper cert(secc_key_pair.pair.value().certificate, EncodingFormat::PEM); @@ -451,9 +360,10 @@ EvseSecurity::get_installed_certificates(const std::vector& cer certificate_hash_data_chain.certificate_type = CertificateType::V2GCertificateChain; const auto ca_bundle_path = this->ca_bundle_path_map.at(CaCertificateType::V2G); - X509Wrapper ca_bundle(ca_bundle_path, EncodingFormat::PEM); + X509CertificateBundle ca_bundle(ca_bundle_path, EncodingFormat::PEM); const auto certificates_of_bundle = ca_bundle.split(); std::vector child_certificate_hash_data; + bool keep_searching = true; while (keep_searching) { keep_searching = false; @@ -468,6 +378,7 @@ EvseSecurity::get_installed_certificates(const std::vector& cer } } } + certificate_hash_data_chain.child_certificate_hash_data = child_certificate_hash_data; certificate_chains.push_back(certificate_hash_data_chain); } @@ -487,7 +398,7 @@ OCSPRequestDataList EvseSecurity::get_ocsp_request_data() { OCSPRequestDataList response; std::vector ocsp_request_data_list; - X509Wrapper ca_bundle(this->ca_bundle_path_map.at(CaCertificateType::V2G), EncodingFormat::PEM); + X509CertificateBundle ca_bundle(this->ca_bundle_path_map.at(CaCertificateType::V2G), EncodingFormat::PEM); const auto certificates_of_bundle = ca_bundle.split(); for (const auto& certificate : certificates_of_bundle) { std::string responder_url = certificate.get_responder_url(); @@ -505,22 +416,20 @@ OCSPRequestDataList EvseSecurity::get_ocsp_request_data() { void EvseSecurity::update_ocsp_cache(const CertificateHashData& certificate_hash_data, const std::string& ocsp_response) { const auto ca_bundle_path = this->ca_bundle_path_map.at(CaCertificateType::V2G); - X509Wrapper ca_bundle(ca_bundle_path, EncodingFormat::PEM); + X509CertificateBundle ca_bundle(ca_bundle_path, EncodingFormat::PEM); const auto certificates_of_bundle = ca_bundle.split(); for (const auto& cert : certificates_of_bundle) { - if (cert.get_issuer_name_hash() == certificate_hash_data.issuer_name_hash && - cert.get_issuer_key_hash() == certificate_hash_data.issuer_key_hash && - cert.get_serial_number() == certificate_hash_data.serial_number) { + if (cert == certificate_hash_data) { EVLOG_info << "Writing OCSP Response to filesystem"; - if (!cert.get_path().has_value()) { + if (!cert.get_file().has_value()) { continue; } - const auto ocsp_path = cert.get_path().value().parent_path() / "ocsp"; + const auto ocsp_path = cert.get_file().value().parent_path() / "ocsp"; if (!std::filesystem::exists(ocsp_path)) { std::filesystem::create_directories(ocsp_path); } - const auto ocsp_file_path = ocsp_path / cert.get_path().value().filename().replace_extension(".ocsp.der"); + const auto ocsp_file_path = ocsp_path / cert.get_file().value().filename().replace_extension(".ocsp.der"); std::ofstream fs(ocsp_file_path.c_str()); fs << ocsp_response; fs.close(); @@ -546,7 +455,7 @@ std::string EvseSecurity::generate_certificate_signing_request(LeafCertificateTy std::filesystem::path key_path; - const auto file_name = get_random_file_name(KEY_EXTENSION.string()); + const auto file_name = 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) { @@ -556,13 +465,13 @@ std::string EvseSecurity::generate_certificate_signing_request(LeafCertificateTy } // csr req - X509_REQ_ptr x509ReqPtr(X509_REQ_new(), X509_REQ_free); - EVP_PKEY_ptr evpKey(EVP_PKEY_new(), EVP_PKEY_free); + X509_REQ_ptr x509ReqPtr(X509_REQ_new()); + EVP_PKEY_ptr evpKey(EVP_PKEY_new()); EC_KEY* ecKey = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); X509_NAME* x509Name = X509_REQ_get_subject_name(x509ReqPtr.get()); - BIO_ptr prkey(BIO_new_file(key_path.c_str(), "w"), ::BIO_free); - BIO_ptr bio(BIO_new(BIO_s_mem()), ::BIO_free); + BIO_ptr prkey(BIO_new_file(key_path.c_str(), "w")); + BIO_ptr bio(BIO_new(BIO_s_mem())); // generate ec key pair EC_KEY_generate_key(ecKey); @@ -639,7 +548,7 @@ GetKeyPairResult EvseSecurity::get_key_pair(LeafCertificateType certificate_type const auto certificate = get_latest_valid_certificate(certificates); const auto private_key_path = get_private_key_path(certificate, key_dir, this->private_key_password); - result.pair = {private_key_path.string(), certificate.get_path().value(), this->private_key_password}; + result.pair = {private_key_path.string(), certificate.get_file().value(), this->private_key_password}; result.status = GetKeyPairStatus::Accepted; return result; @@ -656,7 +565,7 @@ 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_path().value().c_str(); + return verify_file.get_file().value().string(); } int EvseSecurity::get_leaf_expiry_days_count(LeafCertificateType certificate_type) { @@ -671,7 +580,7 @@ int EvseSecurity::get_leaf_expiry_days_count(LeafCertificateType certificate_typ InstallCertificateResult EvseSecurity::verify_certificate(const std::string& certificate_chain, LeafCertificateType certificate_type) { try { - X509Wrapper certificate(certificate_chain, EncodingFormat::PEM); + X509CertificateBundle certificate(certificate_chain, EncodingFormat::PEM); std::vector _certificate_chain = certificate.split(); if (_certificate_chain.empty()) { return InstallCertificateResult::InvalidFormat; @@ -679,8 +588,8 @@ InstallCertificateResult EvseSecurity::verify_certificate(const std::string& cer const auto leaf_certificate = _certificate_chain.at(0); - X509_STORE_ptr store_ptr(X509_STORE_new(), ::X509_STORE_free); - X509_STORE_CTX_ptr store_ctx_ptr(X509_STORE_CTX_new(), ::X509_STORE_CTX_free); + X509_STORE_ptr store_ptr(X509_STORE_new()); + X509_STORE_CTX_ptr store_ctx_ptr(X509_STORE_CTX_new()); for (size_t i = 1; i < _certificate_chain.size(); i++) { X509_STORE_add_cert(store_ptr.get(), _certificate_chain[i].get()); diff --git a/lib/x509_bundle.cpp b/lib/x509_bundle.cpp new file mode 100644 index 0000000..87a9147 --- /dev/null +++ b/lib/x509_bundle.cpp @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include +#include + +#include +#include +#include + +namespace evse_security { + +/// @brief Loads all certificates from the string data that can contain multiple cetifs +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()))); + + if (!bio) { + throw CertificateLoadException("Failed to create BIO from data"); + } + + std::vector certificates; + + if (encoding == EncodingFormat::PEM) { + STACK_OF(X509_INFO)* allcerts = PEM_X509_INFO_read_bio(bio.get(), nullptr, nullptr, nullptr); + + if (allcerts) { + for (int i = 0; i < sk_X509_INFO_num(allcerts); i++) { + X509_INFO* xi = sk_X509_INFO_value(allcerts, i); + + if (xi && xi->x509) { + // Transfer owneship + certificates.emplace_back(xi->x509); + xi->x509 = nullptr; + } + } + + sk_X509_INFO_pop_free(allcerts, X509_INFO_free); + } else { + throw CertificateLoadException("Certificate (PEM) parsing error"); + } + } else if (encoding == EncodingFormat::DER) { + X509* x509 = d2i_X509_bio(bio.get(), nullptr); + + if (x509) { + certificates.emplace_back(x509); + } else { + throw CertificateLoadException("Certificate (DER) parsing error"); + } + } else { + throw CertificateLoadException("Unsupported encoding format"); + } + + return certificates; +} + +X509CertificateBundle::X509CertificateBundle(const std::string& certificate, const EncodingFormat encoding) { + source = X509CertificateSource::STRING; + add_certifcates(certificate, encoding, std::nullopt); +} + +X509CertificateBundle::X509CertificateBundle(const std::filesystem::path& path, const EncodingFormat encoding) { + this->path = path; + + if (std::filesystem::is_directory(path)) { + source = X509CertificateSource::DIRECTORY; + + // Iterate directory + 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)) + add_certifcates(certificate, encoding, entry.path()); + } + } + } else if (is_certificate_file(path)) { + source = X509CertificateSource::FILE; + + std::string certificate; + if (EvseUtils::read_from_file(path, certificate)) + add_certifcates(certificate, encoding, path); + } + + if (certificates.size() <= 0) { + throw CertificateLoadException("Failed to read X509 from BIO"); + } +} + +void X509CertificateBundle::add_certifcates(const std::string& data, const EncodingFormat encoding, + const std::optional& path) { + auto loaded = load_certificates(data, encoding); + + // 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!"); + } + + for (auto& x509 : loaded) { + if (path.has_value()) + certificates.emplace_back(std::move(x509), path.value()); + else + certificates.emplace_back(std::move(x509)); + } +} + +std::vector X509CertificateBundle::split() { + return certificates; +} + +bool X509CertificateBundle::contains_certificate(const X509Wrapper& certificate) { + for (const auto& certif : certificates) { + if (certif == certificate) + return true; + } + + return false; +} + +bool X509CertificateBundle::contains_certificate(const CertificateHashData& certificate_hash) { + for (const auto& certif : certificates) { + if (certif == certificate_hash) + return true; + } + + return false; +} + +bool X509CertificateBundle::delete_certificate(const X509Wrapper& certificate) { + return delete_certificate(certificate.get_certificate_hash_data()); +} + +bool X509CertificateBundle::delete_certificate(const CertificateHashData& data) { + for (auto it = certificates.begin(); it != certificates.end(); ++it) { + if (*it == data) { + certificates.erase(it); + return true; + } + } + + return false; +} + +void X509CertificateBundle::delete_all_certificates() { + certificates.clear(); +} + +bool X509CertificateBundle::update_certificate(X509Wrapper& certificate) { + for (int i = 0; i < certificates.size(); ++i) { + if (certificates[i] == certificate) { + certificates.at(i) = std::move(certificate); + return true; + } + } + + return false; +} + +bool X509CertificateBundle::export_certificates() { + if (source == X509CertificateSource::STRING) { + return false; + } + + // Add/delete certifs + if (!sync_to_certificate_store()) + return false; + + if (source == X509CertificateSource::DIRECTORY) { + bool exported_all = true; + + // Write updated certificates + for (auto& certificate : certificates) { + if (certificate.get_file().has_value()) { + if (!EvseUtils::write_to_file(certificate.get_file().value(), certificate.get_export_string(), + std::ios::trunc)) { + exported_all = false; + } + } else { + exported_all = false; + } + } + + return exported_all; + } else if (source == X509CertificateSource::FILE) { + // We're using a single file, no need to check for deleted certificates + return EvseUtils::write_to_file(path, to_export_string(), std::ios::trunc); + } + + return false; +} + +bool X509CertificateBundle::sync_to_certificate_store() { + if (source == X509CertificateSource::STRING) + 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); + } + } + } + + 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 + if (!EvseUtils::delete_file(fs_certif.get_file().value())) + success = false; + } + } + + // 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 + if (!EvseUtils::write_to_file(certif.get_file().value(), certif.get_export_string(), std::ios::trunc)) + success = false; + } + } + + return success; + } else if (source == X509CertificateSource::FILE) { + // Delete source file if we're empty + if (certificates.empty()) { + return EvseUtils::delete_file(path); + } + } + + return true; +} + +std::string X509CertificateBundle::to_export_string() const { + std::string export_string; + + for (auto& certificate : certificates) { + export_string += certificate.get_export_string(); + } + + return export_string; +} + +} // namespace evse_security \ No newline at end of file diff --git a/lib/x509_wrapper.cpp b/lib/x509_wrapper.cpp index 6deef44..c6e4932 100644 --- a/lib/x509_wrapper.cpp +++ b/lib/x509_wrapper.cpp @@ -6,18 +6,20 @@ #include #include +#include #include #include +#include #include namespace evse_security { -using BIO_ptr = std::unique_ptr; +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()), ::BIO_free); + BIO_ptr bio_write(BIO_new(BIO_s_mem())); int rc = PEM_write_bio_X509(bio_write.get(), x509); @@ -32,138 +34,100 @@ std::string x509_to_string(X509* x509) { return {}; } -void X509Wrapper::update_validity() { - // For valid_in and valid_to - ASN1_TIME* notBefore = X509_get_notBefore(x509[0].get()); - ASN1_TIME* notAfter = X509_get_notAfter(x509[0].get()); - int day, sec; - ASN1_TIME_diff(&day, &sec, notBefore, nullptr); - this->valid_in = day * 86400 + sec; // Convert days to seconds - ASN1_TIME_diff(&day, &sec, nullptr, notAfter); - this->valid_to = day * 86400 + sec; // Convert days to seconds -} - -// Load a certificate from a string using the specified encoding. -void X509Wrapper::load_certificate(const std::string& data, const EncodingFormat encoding) { - BIO_ptr bio(BIO_new_mem_buf(data.data(), static_cast(data.size())), ::BIO_free); - - if (!bio) { - throw CertificateLoadException("Failed to create BIO from data"); +X509Wrapper::X509Wrapper(const std::filesystem::path& file, const EncodingFormat encoding) { + if (std::filesystem::is_regular_file(file) == false) { + throw CertificateLoadException("X509Wrapper can only load from files!"); } - if (encoding == EncodingFormat::PEM) { - STACK_OF(X509_INFO)* allcerts = PEM_X509_INFO_read_bio(bio.get(), nullptr, nullptr, nullptr); - - if (allcerts) { - for (int i = 0; i < sk_X509_INFO_num(allcerts); i++) { - X509_INFO* xi = sk_X509_INFO_value(allcerts, i); - - if (xi && xi->x509) { - // Transfer owneship - x509.emplace_back(xi->x509, ::X509_free); - xi->x509 = nullptr; - } - } + std::ifstream read(file, std::ios::binary); + std::string certificate((std::istreambuf_iterator(read)), std::istreambuf_iterator()); - sk_X509_INFO_pop_free(allcerts, X509_INFO_free); - } else { - throw CertificateLoadException("Unsupported encoding format"); - } - } else if (encoding == EncodingFormat::DER) { - x509.emplace_back(d2i_X509_bio(bio.get(), nullptr), ::X509_free); - } else { - throw CertificateLoadException("Unsupported encoding format"); + auto loaded = X509CertificateBundle::load_certificates(certificate, encoding); + if (loaded.size() != 1) { + throw CertificateLoadException("X509Wrapper can only load a single certificate!"); } - if (!x509.size()) { - throw CertificateLoadException("Failed to read X509 from BIO"); + this->file = file; + x509 = std::move(loaded[0]); + update_validity(); +} + +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!"); } + x509 = std::move(loaded[0]); update_validity(); } -X509Wrapper::X509Wrapper(const std::string& certificate, const EncodingFormat encoding) { - load_certificate(certificate, encoding); +X509Wrapper::X509Wrapper(X509* x509) : x509(x509) { + update_validity(); } -X509Wrapper::X509Wrapper(const std::filesystem::path& path, const EncodingFormat encoding) { - // TODO: Directory support - if (std::filesystem::is_directory(path)) { +X509Wrapper::X509Wrapper(X509_ptr&& x509) : x509(std::move(x509)) { + update_validity(); +} - } else { - std::ifstream file(path, std::ios::binary); - std::string certificate((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - load_certificate(certificate, encoding); +X509Wrapper::X509Wrapper(X509* x509, const std::filesystem::path& file) : x509(x509), file(file) { + if (std::filesystem::is_regular_file(file) == false) { + throw CertificateLoadException("X509Wrapper can only load from files!"); } - this->path = path; -} - -X509Wrapper::X509Wrapper(X509* x509) { - this->x509.emplace_back(x509, ::X509_free); update_validity(); } -X509Wrapper::X509Wrapper(const X509Wrapper& other) { - for (const auto& cert : other.x509) { - X509* dup_cert = X509_dup(cert.get()); - x509.emplace_back(dup_cert, ::X509_free); +X509Wrapper::X509Wrapper(X509_ptr&& x509, const std::filesystem::path& file) : x509(std::move(x509)), file(file) { + if (std::filesystem::is_regular_file(file) == false) { + throw CertificateLoadException("X509Wrapper can only load from files!"); } - if (other.path) { - path = other.path.value(); - } + update_validity(); +} - valid_in = other.valid_in; - valid_to = other.valid_to; +X509Wrapper::X509Wrapper(const X509Wrapper& other) : + x509(X509_dup(other.x509.get())), file(other.file), valid_in(other.valid_in), valid_to(other.valid_to) { } X509Wrapper::~X509Wrapper() { } -X509* X509Wrapper::get() const { - return x509[0].get(); -} +void X509Wrapper::update_validity() { + // For valid_in and valid_to + ASN1_TIME* notBefore = X509_get_notBefore(get()); + ASN1_TIME* notAfter = X509_get_notAfter(get()); -void X509Wrapper::reset(X509* _x509) { - x509[0].reset(_x509); + int day, sec; + ASN1_TIME_diff(&day, &sec, notBefore, nullptr); + valid_in = std::chrono::duration_cast(ossl_days_to_seconds(day)).count() + + sec; // Convert days to seconds + ASN1_TIME_diff(&day, &sec, nullptr, notAfter); + valid_to = std::chrono::duration_cast(ossl_days_to_seconds(day)).count() + + sec; // Convert days to seconds } -std::vector X509Wrapper::split() { - std::vector certificates; - - for (const auto& cert : x509) { - // Duplicates since a X509Wrapper requires exclusive ownership - X509* dup_cert = X509_dup(cert.get()); - certificates.emplace_back(dup_cert); - - if (this->path.has_value()) { - certificates.back().path = this->path.value(); - } - } - - return certificates; +void X509Wrapper::reset(X509* _x509) { + x509.reset(_x509); } int X509Wrapper::get_valid_in() const { - return this->valid_in; + return valid_in; } /// \brief Gets valid_in int X509Wrapper::get_valid_to() const { - return this->valid_to; -} - -std::string X509Wrapper::get_str() const { - return this->to_base64_string(); + return valid_to; } -std::optional X509Wrapper::get_path() const { - return this->path; +std::optional X509Wrapper::get_file() const { + return this->file; } std::string X509Wrapper::get_common_name() const { - X509_NAME* subject = X509_get_subject_name(this->x509[0].get()); + const X509* x509 = get(); + + X509_NAME* subject = X509_get_subject_name(x509); int nid = OBJ_txt2nid("CN"); int index = X509_NAME_get_index_by_NID(subject, nid, -1); @@ -185,7 +149,7 @@ std::string X509Wrapper::get_common_name() const { std::string X509Wrapper::get_issuer_name_hash() const { unsigned char md[SHA256_DIGEST_LENGTH]; - X509_NAME* name = X509_get_issuer_name(this->x509[0].get()); + X509_NAME* name = X509_get_issuer_name(get()); X509_NAME_digest(name, EVP_sha256(), md, NULL); std::stringstream ss; @@ -196,7 +160,7 @@ std::string X509Wrapper::get_issuer_name_hash() const { } std::string X509Wrapper::get_serial_number() const { - ASN1_INTEGER* serial_asn1 = X509_get_serialNumber(this->x509[0].get()); + ASN1_INTEGER* serial_asn1 = X509_get_serialNumber(get()); if (serial_asn1 == nullptr) { ERR_print_errors_fp(stderr); return ""; @@ -226,7 +190,7 @@ std::string X509Wrapper::get_serial_number() const { std::string X509Wrapper::get_issuer_key_hash() const { unsigned char tmphash[SHA256_DIGEST_LENGTH]; - X509_pubkey_digest(this->x509[0].get(), EVP_sha256(), tmphash, NULL); + X509_pubkey_digest(get(), EVP_sha256(), tmphash, NULL); std::stringstream ss; for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { ss << std::setw(2) << std::setfill('0') << std::hex << (int)tmphash[i]; @@ -244,7 +208,7 @@ CertificateHashData X509Wrapper::get_certificate_hash_data() const { } std::string X509Wrapper::get_responder_url() const { - const auto ocsp = X509_get1_ocsp(this->x509[0].get()); + const auto ocsp = X509_get1_ocsp(get()); std::string responder_url; for (int i = 0; i < sk_OPENSSL_STRING_num(ocsp); i++) { responder_url.append(sk_OPENSSL_STRING_value(ocsp, i)); @@ -257,8 +221,8 @@ std::string X509Wrapper::get_responder_url() const { return responder_url; } -std::string X509Wrapper::to_base64_string() const { - return x509_to_string(x509[0].get()); +std::string X509Wrapper::get_export_string() const { + return x509_to_string(get()); } } // namespace evse_security diff --git a/tests/tests.cpp b/tests/tests.cpp index 691d43b..88aa0f5 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -11,6 +11,7 @@ #include #include +#include #include std::string read_file_to_string(const std::filesystem::path filepath) { @@ -86,22 +87,39 @@ TEST_F(EvseSecurityTests, verify_basics) { ASSERT_TRUE(certificate_strings.size() == 3); - X509Wrapper bundle(std::filesystem::path(bundle_path), EncodingFormat::PEM); - ASSERT_TRUE(bundle.is_bundle()); + X509CertificateBundle bundle(std::filesystem::path(bundle_path), EncodingFormat::PEM); + ASSERT_TRUE(bundle.is_using_bundle_file()); auto certificates = bundle.split(); ASSERT_TRUE(certificates.size() == 3); - ASSERT_TRUE(certificates[0].get_certificate_hash_data() == bundle.get_certificate_hash_data()); - - ASSERT_TRUE(equal_certificate_strings(bundle.get_str(), certificates[0].get_str())); - ASSERT_TRUE(equal_certificate_strings(bundle.get_str(), certificate_strings[0])); for (int i = 0; i < certificate_strings.size(); ++i) { X509Wrapper cert(certificate_strings[i], EncodingFormat::PEM); ASSERT_TRUE(certificates[i].get_certificate_hash_data() == cert.get_certificate_hash_data()); - ASSERT_TRUE(equal_certificate_strings(cert.get_str(), certificate_strings[i])); + ASSERT_TRUE(equal_certificate_strings(cert.get_export_string(), certificate_strings[i])); + } +} + +TEST_F(EvseSecurityTests, verify_bundle_management) { + const char* directory_path = "certs/ca/csms/"; + X509CertificateBundle bundle(std::filesystem::path(directory_path), EncodingFormat::PEM); + ASSERT_TRUE(bundle.split().size() == 2); + bundle.delete_certificate(bundle.split()[0].get_certificate_hash_data()); + bundle.sync_to_certificate_store(); + + int items = 0; + for (const auto& entry : std::filesystem::recursive_directory_iterator(directory_path)) { + if (X509CertificateBundle::is_certificate_file(entry)) { + items++; + } } + 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 @@ -109,7 +127,7 @@ TEST_F(EvseSecurityTests, verify_chargepoint_cert_01) { const auto client_certificate = read_file_to_string(std::filesystem::path("certs/client/csms/CSMS_LEAF.pem")); std::cout << client_certificate << std::endl; const auto result = this->evse_security->update_leaf_certificate(client_certificate, LeafCertificateType::CSMS); - ASSERT_TRUE(result == InstallCertificateResult::Success); + ASSERT_TRUE(result == InstallCertificateResult::Accepted); } /// \brief test verifyChargepointCertificate with invalid cert @@ -122,7 +140,7 @@ TEST_F(EvseSecurityTests, verify_chargepoint_cert_02) { TEST_F(EvseSecurityTests, verify_v2g_cert_01) { const auto client_certificate = read_file_to_string(std::filesystem::path("certs/client/cso/SECC_LEAF.pem")); const auto result = this->evse_security->update_leaf_certificate(client_certificate, LeafCertificateType::V2G); - ASSERT_TRUE(result == InstallCertificateResult::Success); + ASSERT_TRUE(result == InstallCertificateResult::Accepted); } /// \brief test verifyV2GChargingStationCertificate with invalid cert @@ -136,7 +154,7 @@ TEST_F(EvseSecurityTests, verify_v2g_cert_02) { TEST_F(EvseSecurityTests, install_root_ca_01) { const auto v2g_root_ca = read_file_to_string(std::filesystem::path("certs/ca/v2g/V2G_ROOT_CA_NEW.pem")); const auto result = this->evse_security->install_ca_certificate(v2g_root_ca, CaCertificateType::V2G); - ASSERT_TRUE(result == InstallCertificateResult::Success); + ASSERT_TRUE(result == InstallCertificateResult::Accepted); } TEST_F(EvseSecurityTests, install_root_ca_02) {