diff --git a/include/evse_security/certificate/x509_hierarchy.hpp b/include/evse_security/certificate/x509_hierarchy.hpp index 57f85ca..ab88e8c 100644 --- a/include/evse_security/certificate/x509_hierarchy.hpp +++ b/include/evse_security/certificate/x509_hierarchy.hpp @@ -2,6 +2,7 @@ // Copyright Pionix GmbH and Contributors to EVerest #pragma once +#include #include #include @@ -59,6 +60,10 @@ class X509CertificateHierarchy { /// @brief Searches for the provided hash, throwing a NoCertificateFound if not found X509Wrapper find_certificate(const CertificateHashData& hash); + /// @brief Searches for all the certificates with the provided hash, throwing a NoCertificateFound + // if none were found. Can be useful when we have SUB-CAs in multiple bundles + std::vector find_certificates_multi(const CertificateHashData& hash); + public: std::string to_debug_string(); @@ -110,6 +115,19 @@ class X509CertificateHierarchy { /// hierarchy can be incomplete, in case orphan certificates are present in the list static X509CertificateHierarchy build_hierarchy(std::vector& certificates); + template static X509CertificateHierarchy build_hierarchy(Args... certificates) { + X509CertificateHierarchy ordered; + + (std::for_each(certificates.begin(), certificates.end(), + [&ordered](X509Wrapper& cert) { ordered.insert(std::move(cert)); }), + ...); // Fold expr + + // Prune the tree + ordered.prune(); + + return ordered; + } + private: /// @brief Inserts the certificate in the hierarchy. If it is not a root /// and a parent is not found, it will be inserted as a temporary orphan diff --git a/include/evse_security/certificate/x509_wrapper.hpp b/include/evse_security/certificate/x509_wrapper.hpp index d57501b..4cdbd64 100644 --- a/include/evse_security/certificate/x509_wrapper.hpp +++ b/include/evse_security/certificate/x509_wrapper.hpp @@ -21,11 +21,6 @@ enum class X509CertificateSource { STRING }; -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 { public: diff --git a/include/evse_security/crypto/interface/crypto_supplier.hpp b/include/evse_security/crypto/interface/crypto_supplier.hpp index d8119c4..0ae3a08 100644 --- a/include/evse_security/crypto/interface/crypto_supplier.hpp +++ b/include/evse_security/crypto/interface/crypto_supplier.hpp @@ -68,15 +68,20 @@ class AbstractCryptoSupplier { std::optional password); /// @brief Verifies the signature with the certificate handle public key against the data - static bool x509_verify_signature(X509Handle* handle, const std::vector& signature, - const std::vector& data); + static bool x509_verify_signature(X509Handle* handle, const std::vector& signature, + const std::vector& data); /// @brief Generates a certificate signing request with the provided parameters static bool x509_generate_csr(const CertificateSigningRequestInfo& generation_info, std::string& out_csr); public: // Digesting/decoding utils - static bool digest_file_sha256(const fs::path& path, std::vector& out_digest); - static bool decode_base64_signature(const std::string& signature, std::vector& out_decoded); + static bool digest_file_sha256(const fs::path& path, std::vector& out_digest); + + static bool base64_decode_to_bytes(const std::string& base64_string, std::vector& out_decoded); + static bool base64_decode_to_string(const std::string& base64_string, std::string& out_decoded); + + static bool base64_encode_from_bytes(const std::vector& bytes, std::string& out_encoded); + static bool base64_encode_from_string(const std::string& string, std::string& out_encoded); }; } // namespace evse_security \ No newline at end of file diff --git a/include/evse_security/crypto/openssl/openssl_supplier.hpp b/include/evse_security/crypto/openssl/openssl_supplier.hpp index 3c091c3..3224f51 100644 --- a/include/evse_security/crypto/openssl/openssl_supplier.hpp +++ b/include/evse_security/crypto/openssl/openssl_supplier.hpp @@ -38,14 +38,19 @@ class OpenSSLSupplier : public AbstractCryptoSupplier { const std::optional file_path); static KeyValidationResult x509_check_private_key(X509Handle* handle, std::string private_key, std::optional password); - static bool x509_verify_signature(X509Handle* handle, const std::vector& signature, - const std::vector& data); + static bool x509_verify_signature(X509Handle* handle, const std::vector& signature, + const std::vector& data); static bool x509_generate_csr(const CertificateSigningRequestInfo& csr_info, std::string& out_csr); public: - static bool digest_file_sha256(const fs::path& path, std::vector& out_digest); - static bool decode_base64_signature(const std::string& signature, std::vector& out_decoded); + static bool digest_file_sha256(const fs::path& path, std::vector& out_digest); + + static bool base64_decode_to_bytes(const std::string& base64_string, std::vector& out_decoded); + static bool base64_decode_to_string(const std::string& base64_string, std::string& out_decoded); + + static bool base64_encode_from_bytes(const std::vector& bytes, std::string& out_encoded); + static bool base64_encode_from_string(const std::string& string, std::string& out_encoded); }; } // namespace evse_security \ No newline at end of file diff --git a/include/evse_security/evse_security.hpp b/include/evse_security/evse_security.hpp index de7a010..ad87902 100644 --- a/include/evse_security/evse_security.hpp +++ b/include/evse_security/evse_security.hpp @@ -142,6 +142,12 @@ class EvseSecurity { /// @param ocsp_response the actual OCSP data void update_ocsp_cache(const CertificateHashData& certificate_hash_data, const std::string& ocsp_response); + // TODO: Switch to path + /// @brief Retrieves from the OCSP cache for the given \p certificate_hash_data + /// @param certificate_hash_data identifies the certificate for which the \p ocsp_response is specified + /// @return the actual OCSP data or an empty value + std::optional retrieve_ocsp_cache(const CertificateHashData& certificate_hash_data); + /// @brief Indicates if a CA certificate for the given \p certificate_type is installed on the filesystem /// Supports both CA certificate bundles and directories /// @param certificate_type @@ -181,8 +187,10 @@ class EvseSecurity { /// the leaf including any possible SUBCAs /// @param certificate_type type of the leaf certificate /// @param encoding specifies PEM or DER format + /// @param include_ocsp if OCSP data should be included /// @return contains response result - GetKeyPairResult get_key_pair(LeafCertificateType certificate_type, EncodingFormat encoding); + GetCertificateInfoResult get_leaf_certificate_info(LeafCertificateType certificate_type, EncodingFormat encoding, + bool include_ocsp = false); /// @brief Checks and updates the symlinks for the V2G leaf certificates and keys to the most recent valid one /// @return true if one of the links was updated @@ -214,11 +222,33 @@ class EvseSecurity { static bool verify_file_signature(const fs::path& path, const std::string& signing_certificate, const std::string signature); + /// @brief Decodes the base64 encoded string to the raw byte representation + /// @param base64_string base64 encoded string + /// @return decoded byte vector + static std::vector base64_decode_to_bytes(const std::string& base64_string); + + /// @brief Decodes the base64 encoded string to string representation + /// @param base64_string base64 encoded string + /// @return decoded string array + static std::string base64_decode_to_string(const std::string& base64_string); + + /// @brief Encodes the raw bytes to a base64 string + /// @param decoded_bytes raw byte array + /// @return encoded base64 string + static std::string base64_encode_from_bytes(const std::vector& bytes); + + /// @brief Encodes the string containing raw bytes to a base64 string + /// @param decoded_bytes string containing raw bytes + /// @return encoded base64 string + static std::string base64_encode_from_string(const std::string& string); + private: // Internal versions of the functions do not lock the mutex CertificateValidationResult verify_certificate_internal(const std::string& certificate_chain, LeafCertificateType certificate_type); - GetKeyPairResult get_key_pair_internal(LeafCertificateType certificate_type, EncodingFormat encoding); + GetCertificateInfoResult get_leaf_certificate_info_internal(LeafCertificateType certificate_type, + EncodingFormat encoding, bool include_ocsp = false); + std::optional retrieve_ocsp_cache_internal(const CertificateHashData& certificate_hash_data); bool is_ca_certificate_installed_internal(CaCertificateType certificate_type); /// @brief Determines if the total filesize of certificates is > than the max_filesystem_usage bytes @@ -258,6 +288,7 @@ class EvseSecurity { 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_ocsp_garbage_collect); FRIEND_TEST(EvseSecurityTestsExpired, verify_expired_leaf_deletion); #endif }; diff --git a/include/evse_security/evse_types.hpp b/include/evse_security/evse_types.hpp index dd4c9d5..cb8ee86 100644 --- a/include/evse_security/evse_types.hpp +++ b/include/evse_security/evse_types.hpp @@ -10,6 +10,12 @@ namespace evse_security { +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"; +const fs::path CERT_HASH_EXTENSION = ".hash"; + enum class EncodingFormat { DER, PEM, @@ -76,7 +82,7 @@ enum class GetInstalledCertificatesStatus { NotFound, }; -enum class GetKeyPairStatus { +enum class GetCertificateInfoStatus { Accepted, Rejected, NotFound, @@ -123,17 +129,25 @@ struct OCSPRequestData { struct OCSPRequestDataList { std::vector ocsp_request_data_list; ///< A list of OCSP request data }; -struct KeyPair { - fs::path key; ///< The path of the PEM or DER encoded private key - fs::path certificate; ///< The path of the PEM or DER encoded certificate chain - fs::path certificate_single; ///< The path of the PEM or DER encoded certificate - std::optional password; ///< Specifies the password for the private key if encrypted + +struct CertificateOCSP { + CertificateHashData hash; ///< Hash of the certificate for which the OCSP data is held + std::optional ocsp_path; ///< Path to the file in which the certificate OCSP data is held }; -struct GetKeyPairResult { - GetKeyPairStatus status; - std::optional pair; + +struct CertificateInfo { + fs::path key; ///< The path of the PEM or DER encoded private key + std::optional certificate; ///< The path of the PEM or DER encoded certificate chain if found + std::optional certificate_single; ///< The path of the PEM or DER encoded certificate if found + int certificate_count; ///< The count of certificates, if the chain is available, or 1 if single + std::optional password; ///< Specifies the password for the private key if encrypted + std::vector ocsp; ///< The ordered list of OCSP certificate data based on the chain file order }; +struct GetCertificateInfoResult { + GetCertificateInfoStatus status; + std::optional info; +}; namespace conversions { std::string encoding_format_to_string(EncodingFormat e); std::string ca_certificate_type_to_string(CaCertificateType e); @@ -141,10 +155,11 @@ std::string leaf_certificate_type_to_string(LeafCertificateType e); std::string leaf_certificate_type_to_filename(LeafCertificateType e); std::string certificate_type_to_string(CertificateType e); std::string hash_algorithm_to_string(HashAlgorithm e); +HashAlgorithm string_to_hash_algorithm(const std::string& s); std::string install_certificate_result_to_string(InstallCertificateResult e); std::string delete_certificate_result_to_string(DeleteCertificateResult e); std::string get_installed_certificates_status_to_string(GetInstalledCertificatesStatus e); -std::string get_key_pair_status_to_string(GetKeyPairStatus e); +std::string get_certificate_info_status_to_string(GetCertificateInfoStatus e); } // namespace conversions } // namespace evse_security diff --git a/include/evse_security/utils/evse_filesystem.hpp b/include/evse_security/utils/evse_filesystem.hpp index aa7c99b..67b9afb 100644 --- a/include/evse_security/utils/evse_filesystem.hpp +++ b/include/evse_security/utils/evse_filesystem.hpp @@ -6,6 +6,8 @@ #include +struct CertificateHashData; + namespace evse_security::filesystem_utils { bool is_subdirectory(const fs::path& base, const fs::path& subdir); @@ -21,8 +23,16 @@ bool write_to_file(const fs::path& file_path, const std::string& data, std::ios: /// returns false, this function will also immediately return /// @return True if the file was properly opened false otherwise bool process_file(const fs::path& file_path, size_t buffer_size, - std::function&& func); + std::function&& func); std::string get_random_file_name(const std::string& extension); +/// @brief Attempts to read a certificate hash from a file. The extension is taken into account +/// @return True if we could read, false otherwise +bool read_hash_from_file(const fs::path& file_path, CertificateHashData& out_hash); + +/// @brief Attempts to write a certificate hash to a file +/// @return True if we could write, false otherwise +bool write_hash_to_file(const fs::path& file_path, const CertificateHashData& hash); + } // namespace evse_security::filesystem_utils diff --git a/lib/evse_security/certificate/x509_bundle.cpp b/lib/evse_security/certificate/x509_bundle.cpp index 0889812..1e8a43c 100644 --- a/lib/evse_security/certificate/x509_bundle.cpp +++ b/lib/evse_security/certificate/x509_bundle.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -44,6 +45,20 @@ X509CertificateBundle::X509CertificateBundle(const fs::path& path, const Encodin hierarchy_invalidated(true) { this->path = path; + // In case the path is missing, create it + if (fs::exists(path) == false) { + if (path.has_extension()) { + if (path.extension() == PEM_EXTENSION) { + // Create file if we have an PEM extension + std::ofstream new_file(path.c_str()); + new_file.close(); + } + } else { + // Else create a directory + fs::create_directories(path); + } + } + if (fs::is_directory(path)) { source = X509CertificateSource::DIRECTORY; diff --git a/lib/evse_security/certificate/x509_hierarchy.cpp b/lib/evse_security/certificate/x509_hierarchy.cpp index 6f6823d..e0d52c4 100644 --- a/lib/evse_security/certificate/x509_hierarchy.cpp +++ b/lib/evse_security/certificate/x509_hierarchy.cpp @@ -97,6 +97,20 @@ X509Wrapper X509CertificateHierarchy::find_certificate(const CertificateHashData throw NoCertificateFound("Could not find a certificate for hash: " + hash.issuer_name_hash); } +std::vector X509CertificateHierarchy::find_certificates_multi(const CertificateHashData& hash) { + std::vector certificates; + + for_each([&](X509Node& node) { + if (node.hash == hash) { + certificates.push_back(node.certificate); + } + + return true; + }); + + return certificates; +} + std::string X509CertificateHierarchy::to_debug_string() { std::stringstream str; diff --git a/lib/evse_security/crypto/interface/crypto_supplier.cpp b/lib/evse_security/crypto/interface/crypto_supplier.cpp index 3c1dea5..e389055 100644 --- a/lib/evse_security/crypto/interface/crypto_supplier.cpp +++ b/lib/evse_security/crypto/interface/crypto_supplier.cpp @@ -86,8 +86,8 @@ KeyValidationResult AbstractCryptoSupplier::x509_check_private_key(X509Handle* h default_crypto_supplier_usage_error() return KeyValidationResult::Unknown; } -bool AbstractCryptoSupplier::x509_verify_signature(X509Handle* handle, const std::vector& signature, - const std::vector& data) { +bool AbstractCryptoSupplier::x509_verify_signature(X509Handle* handle, const std::vector& signature, + const std::vector& data) { default_crypto_supplier_usage_error() return false; } @@ -95,12 +95,25 @@ bool AbstractCryptoSupplier::x509_generate_csr(const CertificateSigningRequestIn default_crypto_supplier_usage_error() return false; } -bool AbstractCryptoSupplier::digest_file_sha256(const fs::path& path, std::vector& out_digest) { +bool AbstractCryptoSupplier::digest_file_sha256(const fs::path& path, std::vector& out_digest) { default_crypto_supplier_usage_error() return false; } -bool AbstractCryptoSupplier::decode_base64_signature(const std::string& signature, - std::vector& out_decoded) { +bool AbstractCryptoSupplier::base64_decode_to_bytes(const std::string& base64_string, + std::vector& out_decoded) { + default_crypto_supplier_usage_error() return false; +} + +bool AbstractCryptoSupplier::base64_decode_to_string(const std::string& base64_string, std::string& out_decoded) { + default_crypto_supplier_usage_error() return false; +} + +bool AbstractCryptoSupplier::base64_encode_from_bytes(const std::vector& bytes, + std::string& out_encoded) { + default_crypto_supplier_usage_error() return false; +} + +bool AbstractCryptoSupplier::base64_encode_from_string(const std::string& string, std::string& out_encoded) { default_crypto_supplier_usage_error() return false; } diff --git a/lib/evse_security/crypto/openssl/openssl_supplier.cpp b/lib/evse_security/crypto/openssl/openssl_supplier.cpp index 543721c..4ef430d 100644 --- a/lib/evse_security/crypto/openssl/openssl_supplier.cpp +++ b/lib/evse_security/crypto/openssl/openssl_supplier.cpp @@ -653,8 +653,8 @@ KeyValidationResult OpenSSLSupplier::x509_check_private_key(X509Handle* handle, } } -bool OpenSSLSupplier::x509_verify_signature(X509Handle* handle, const std::vector& signature, - const std::vector& data) { +bool OpenSSLSupplier::x509_verify_signature(X509Handle* handle, const std::vector& signature, + const std::vector& data) { OpenSSLProvider provider; provider.set_global_mode(OpenSSLProvider::mode_t::default_provider); // extract public key @@ -802,7 +802,7 @@ bool OpenSSLSupplier::x509_generate_csr(const CertificateSigningRequestInfo& csr return true; } -bool OpenSSLSupplier::digest_file_sha256(const fs::path& path, std::vector& out_digest) { +bool OpenSSLSupplier::digest_file_sha256(const fs::path& path, std::vector& out_digest) { EVP_MD_CTX_ptr md_context_ptr(EVP_MD_CTX_create()); if (!md_context_ptr.get()) { EVLOG_error << "Could not create EVP_MD_CTX"; @@ -818,11 +818,11 @@ bool OpenSSLSupplier::digest_file_sha256(const fs::path& path, std::vector bool { + path, BUFSIZ, [&](const std::uint8_t* bytes, std::size_t read, bool last_chunk) -> bool { if (read > 0) { if (EVP_DigestUpdate(md_context_ptr.get(), bytes, read) == 0) { EVLOG_error << "Error during EVP_DigestUpdate"; @@ -854,8 +854,7 @@ bool OpenSSLSupplier::digest_file_sha256(const fs::path& path, std::vector& out_decoded) { - // decode base64 encoded signature +template static bool base64_decode(const std::string& base64_string, T& out_decoded) { EVP_ENCODE_CTX_ptr base64_decode_context_ptr(EVP_ENCODE_CTX_new()); if (!base64_decode_context_ptr.get()) { EVLOG_error << "Error during EVP_ENCODE_CTX_new"; @@ -868,28 +867,83 @@ bool OpenSSLSupplier::decode_base64_signature(const std::string& signature, std: return false; } - const unsigned char* signature_str = reinterpret_cast(signature.data()); - int base64_length = signature.size(); - std::byte signature_out[base64_length]; + const unsigned char* encoded_str = reinterpret_cast(base64_string.data()); + int base64_length = base64_string.size(); - int signature_out_length; - if (EVP_DecodeUpdate(base64_decode_context_ptr.get(), reinterpret_cast(signature_out), - &signature_out_length, signature_str, base64_length) < 0) { + std::uint8_t decoded_out[base64_length]; + + int decoded_out_length; + if (EVP_DecodeUpdate(base64_decode_context_ptr.get(), reinterpret_cast(decoded_out), + &decoded_out_length, encoded_str, base64_length) < 0) { EVLOG_error << "Error during DecodeUpdate"; return false; } int decode_final_out; - if (EVP_DecodeFinal(base64_decode_context_ptr.get(), reinterpret_cast(signature_out), + if (EVP_DecodeFinal(base64_decode_context_ptr.get(), reinterpret_cast(decoded_out), &decode_final_out) < 0) { EVLOG_error << "Error during EVP_DecodeFinal"; return false; } out_decoded.clear(); - out_decoded.insert(std::end(out_decoded), signature_out, signature_out + signature_out_length); + out_decoded.insert(std::end(out_decoded), decoded_out, decoded_out + decoded_out_length); + + return true; +} + +static bool base64_encode(const unsigned char* bytes_str, int bytes_size, std::string& out_encoded) { + EVP_ENCODE_CTX_ptr base64_encode_context_ptr(EVP_ENCODE_CTX_new()); + if (!base64_encode_context_ptr.get()) { + EVLOG_error << "Error during EVP_ENCODE_CTX_new"; + return false; + } + + EVP_EncodeInit(base64_encode_context_ptr.get()); + // evp_encode_ctx_set_flags(base64_encode_context_ptr.get(), EVP_ENCODE_CTX_NO_NEWLINES); // Of course it's not + // public + + if (!base64_encode_context_ptr.get()) { + EVLOG_error << "Error during EVP_EncodeInit"; + return false; + } + + int base64_length = ((bytes_size / 3) * 4) + 2; + // If it causes issues, replace with 'alloca' on different platform + char base64_out[base64_length + 66]; // + 66 bytes for final block + int full_len = 0; + + int base64_out_length; + if (EVP_EncodeUpdate(base64_encode_context_ptr.get(), reinterpret_cast(base64_out), + &base64_out_length, bytes_str, bytes_size) < 0) { + EVLOG_error << "Error during EVP_EncodeUpdate"; + return false; + } + full_len += base64_out_length; + + EVP_EncodeFinal(base64_encode_context_ptr.get(), reinterpret_cast(base64_out) + base64_out_length, + &base64_out_length); + full_len += base64_out_length; + + out_encoded.assign(base64_out, full_len); return true; } +bool OpenSSLSupplier::base64_decode_to_bytes(const std::string& base64_string, std::vector& out_decoded) { + return base64_decode>(base64_string, out_decoded); +} + +bool OpenSSLSupplier::base64_decode_to_string(const std::string& base64_string, std::string& out_decoded) { + return base64_decode(base64_string, out_decoded); +} + +bool OpenSSLSupplier::base64_encode_from_bytes(const std::vector& bytes, std::string& out_encoded) { + return base64_encode(reinterpret_cast(bytes.data()), bytes.size(), out_encoded); +} + +bool OpenSSLSupplier::base64_encode_from_string(const std::string& string, std::string& out_encoded) { + return base64_encode(reinterpret_cast(string.data()), string.size(), out_encoded); +} + } // namespace evse_security diff --git a/lib/evse_security/evse_security.cpp b/lib/evse_security/evse_security.cpp index 888d5f1..4345339 100644 --- a/lib/evse_security/evse_security.cpp +++ b/lib/evse_security/evse_security.cpp @@ -226,6 +226,7 @@ EvseSecurity::EvseSecurity(const FilePaths& file_paths, const std::optional& csr_expiry, const std::optional& garbage_collect_time) : private_key_password(private_key_password) { + static_assert(sizeof(std::uint8_t) == 1, "uint8_t not equal to 1 byte!"); std::vector dirs = { file_paths.directories.csms_leaf_cert_directory, @@ -397,12 +398,8 @@ DeleteCertificateResult EvseSecurity::delete_certificate(const CertificateHashDa X509CertificateBundle root_bundle(ca_bundle_path_map[load], EncodingFormat::PEM); X509CertificateBundle leaf_bundle(leaf_certificate_path, EncodingFormat::PEM); - auto full_list = root_bundle.split(); - for (X509Wrapper& certif : leaf_bundle.split()) { - full_list.push_back(std::move(certif)); - } - - X509CertificateHierarchy hierarchy = X509CertificateHierarchy::build_hierarchy(full_list); + X509CertificateHierarchy hierarchy = + std::move(X509CertificateHierarchy::build_hierarchy(root_bundle.split(), leaf_bundle.split())); EVLOG_debug << "Delete hierarchy:(" << leaf_certificate_path.string() << ")\n" << hierarchy.to_debug_string(); @@ -585,14 +582,15 @@ EvseSecurity::get_installed_certificates(const std::vector& cer certificate_types.end()) { // Internal since we already acquired the lock - const auto secc_key_pair = this->get_key_pair_internal(LeafCertificateType::V2G, EncodingFormat::PEM); - if (secc_key_pair.status == GetKeyPairStatus::Accepted) { + const auto secc_key_pair = + this->get_leaf_certificate_info_internal(LeafCertificateType::V2G, EncodingFormat::PEM); + if (secc_key_pair.status == GetCertificateInfoStatus::Accepted) { fs::path certificate_path; - if (secc_key_pair.pair.value().certificate.empty() == false) - certificate_path = secc_key_pair.pair.value().certificate; + if (secc_key_pair.info.value().certificate.has_value() == false) + certificate_path = secc_key_pair.info.value().certificate.value(); else - certificate_path = secc_key_pair.pair.value().certificate_single; + certificate_path = secc_key_pair.info.value().certificate_single.value(); // Leaf V2G chain X509CertificateBundle leaf_bundle(certificate_path, EncodingFormat::PEM); @@ -687,15 +685,16 @@ OCSPRequestDataList EvseSecurity::get_v2g_ocsp_request_data() { std::lock_guard guard(EvseSecurity::security_mutex); try { - const auto secc_key_pair = this->get_key_pair_internal(LeafCertificateType::V2G, EncodingFormat::PEM); + const auto secc_key_pair = + this->get_leaf_certificate_info_internal(LeafCertificateType::V2G, EncodingFormat::PEM); - if (secc_key_pair.status != GetKeyPairStatus::Accepted or !secc_key_pair.pair.has_value()) { + if (secc_key_pair.status != GetCertificateInfoStatus::Accepted or !secc_key_pair.info.has_value()) { EVLOG_error << "Could not get key pair, for v2g ocsp request!"; return OCSPRequestDataList(); } - std::vector chain = - std::move(X509CertificateBundle(secc_key_pair.pair.value().certificate, EncodingFormat::PEM).split()); + std::vector chain = std::move( + X509CertificateBundle(secc_key_pair.info.value().certificate.value(), EncodingFormat::PEM).split()); return get_ocsp_request_data_internal(this->ca_bundle_path_map.at(CaCertificateType::V2G), chain); } catch (const CertificateLoadException& e) { EVLOG_error << "Could not get v2g ocsp cache, certificate load failure: " << e.what(); @@ -726,10 +725,9 @@ OCSPRequestDataList get_ocsp_request_data_internal(fs::path& root_path, std::vec try { std::vector full_hierarchy = X509CertificateBundle(root_path, EncodingFormat::PEM).split(); - std::move(std::begin(leaf_chain), std::end(leaf_chain), std::back_inserter(full_hierarchy)); // Build the full hierarchy - auto hierarchy = X509CertificateHierarchy::build_hierarchy(full_hierarchy); + auto hierarchy = std::move(X509CertificateHierarchy::build_hierarchy(full_hierarchy, leaf_chain)); // Search for the first valid root, and collect all the chain for (auto& root : hierarchy.get_hierarchy()) { @@ -744,8 +742,18 @@ OCSPRequestDataList get_ocsp_request_data_internal(fs::path& root_path, std::vec if (!responder_url.empty()) { try { auto certificate_hash_data = hierarchy.get_certificate_hash(certificate); - OCSPRequestData ocsp_request_data = {certificate_hash_data, responder_url}; - ocsp_request_data_list.push_back(ocsp_request_data); + + // Do not insert duplicate hashes, in case we have multiple SUBCAs in different bundles + auto it = + std::find_if(std::begin(ocsp_request_data_list), std::end(ocsp_request_data_list), + [&certificate_hash_data](const OCSPRequestData& existing_data) { + return existing_data.certificate_hash_data == certificate_hash_data; + }); + + if (it == ocsp_request_data_list.end()) { + OCSPRequestData ocsp_request_data = {certificate_hash_data, responder_url}; + ocsp_request_data_list.push_back(ocsp_request_data); + } } catch (const NoCertificateFound& e) { EVLOG_error << "Could not find hash for certificate: " << certificate.get_common_name() << " with error: " << e.what(); @@ -775,32 +783,139 @@ void EvseSecurity::update_ocsp_cache(const CertificateHashData& certificate_hash const std::string& ocsp_response) { std::lock_guard guard(EvseSecurity::security_mutex); + EVLOG_info << "Updating OCSP cache"; + + // TODO(ioan): shouldn't we also do this for the MO? const auto ca_bundle_path = this->ca_bundle_path_map.at(CaCertificateType::V2G); + auto leaf_cert_dir = this->directories.secc_leaf_cert_directory; // V2G leafs try { X509CertificateBundle ca_bundle(ca_bundle_path, EncodingFormat::PEM); - const auto certificates_of_bundle = ca_bundle.split(); + X509CertificateBundle leaf_bundle(leaf_cert_dir, EncodingFormat::PEM); + + auto certificate_hierarchy = + std::move(X509CertificateHierarchy::build_hierarchy(ca_bundle.split(), leaf_bundle.split())); - for (const auto& cert : certificates_of_bundle) { - if (cert == certificate_hash_data) { + // If we already have the hash, over-write, else create a new one + try { + // Find the certificates, can me multiple if we have SUBcas in multiple bundles + std::vector certs = certificate_hierarchy.find_certificates_multi(certificate_hash_data); + + for (auto& cert : certs) { EVLOG_debug << "Writing OCSP Response to filesystem"; - if (!cert.get_file().has_value()) { - continue; + if (cert.get_file().has_value()) { + const auto ocsp_path = cert.get_file().value().parent_path() / "ocsp"; + + if (false == fs::exists(ocsp_path)) { + fs::create_directories(ocsp_path); + } else { + // Iterate existing hashes + for (const auto& hash_entry : fs::directory_iterator(ocsp_path)) { + if (hash_entry.is_regular_file()) { + CertificateHashData read_hash; + + if (filesystem_utils::read_hash_from_file(hash_entry.path(), read_hash) && + read_hash == certificate_hash_data) { + EVLOG_debug << "OCSP certificate hash already found, over-writing!"; + + // Over-write the data file and return + fs::path ocsp_path = hash_entry.path(); + ocsp_path.replace_extension(DER_EXTENSION); + + // Discard previous content + std::ofstream fs(ocsp_path.c_str(), std::ios::trunc); + fs << ocsp_response; + fs.close(); + + return; + } + } + } + } + + // Randomize filename, since multiple certificates can be stored in same bundle + const auto name = filesystem_utils::get_random_file_name("_ocsp"); + + const auto ocsp_file_path = (ocsp_path / name) += DER_EXTENSION; + const auto hash_file_path = (ocsp_path / name) += CERT_HASH_EXTENSION; + + // Write out OCSP data + try { + std::ofstream fs(ocsp_file_path.c_str()); + fs << ocsp_response; + fs.close(); + } catch (const std::exception& e) { + EVLOG_error << "Could not write OCSP certificate data!"; + } + + if (false == filesystem_utils::write_hash_to_file(hash_file_path, certificate_hash_data)) { + EVLOG_error << "Could not write OCSP certificate hash!"; + } + + EVLOG_debug << "OCSP certificate hash not found, written at path: " << ocsp_file_path; + } else { + EVLOG_error << "Could not find OCSP cache patch directory!"; } + } + } catch (const NoCertificateFound& e) { + EVLOG_error << "Could not find any certificate for ocsp cache update: " << e.what(); + } + } catch (const CertificateLoadException& e) { + EVLOG_error << "Could not update ocsp cache, certificate load failure: " << e.what(); + } +} + +std::optional EvseSecurity::retrieve_ocsp_cache(const CertificateHashData& certificate_hash_data) { + std::lock_guard guard(EvseSecurity::security_mutex); + + return retrieve_ocsp_cache_internal(certificate_hash_data); +} + +std::optional EvseSecurity::retrieve_ocsp_cache_internal(const CertificateHashData& certificate_hash_data) { + // TODO(ioan): shouldn't we also do this for the MO? + const auto ca_bundle_path = this->ca_bundle_path_map.at(CaCertificateType::V2G); + const auto leaf_path = this->directories.secc_leaf_key_directory; + + try { + X509CertificateBundle ca_bundle(ca_bundle_path, EncodingFormat::PEM); + X509CertificateBundle leaf_bundle(leaf_path, EncodingFormat::PEM); + + auto certificate_hierarchy = + std::move(X509CertificateHierarchy::build_hierarchy(ca_bundle.split(), leaf_bundle.split())); + + try { + // Find the certificate + X509Wrapper cert = certificate_hierarchy.find_certificate(certificate_hash_data); + + EVLOG_debug << "Reading OCSP Response from filesystem"; + + if (cert.get_file().has_value()) { const auto ocsp_path = cert.get_file().value().parent_path() / "ocsp"; - if (!fs::exists(ocsp_path)) { - fs::create_directories(ocsp_path); + + // Search through the OCSP directory and see if we can find any related certificate hash data + for (const auto& ocsp_entry : fs::directory_iterator(ocsp_path)) { + if (ocsp_entry.is_regular_file()) { + CertificateHashData read_hash; + + if (filesystem_utils::read_hash_from_file(ocsp_entry.path(), read_hash) && + (read_hash == certificate_hash_data)) { + fs::path replaced_ext = ocsp_entry.path(); + replaced_ext.replace_extension(DER_EXTENSION); + + // Return the data file's path + return std::make_optional(replaced_ext); + } + } } - 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(); } + } catch (const NoCertificateFound& e) { + EVLOG_error << "Could not find any certificate for ocsp cache retrieve: " << e.what(); } } catch (const CertificateLoadException& e) { - EVLOG_error << "Could not update ocsp cache, certificate load failure: " << e.what(); + EVLOG_error << "Could not retrieve ocsp cache, certificate load failure: " << e.what(); } + + return std::nullopt; } bool EvseSecurity::is_ca_certificate_installed(CaCertificateType certificate_type) { @@ -905,40 +1020,48 @@ std::string EvseSecurity::generate_certificate_signing_request(LeafCertificateTy return generate_certificate_signing_request(certificate_type, country, organization, common, false); } -GetKeyPairResult EvseSecurity::get_key_pair(LeafCertificateType certificate_type, EncodingFormat encoding) { +GetCertificateInfoResult EvseSecurity::get_leaf_certificate_info(LeafCertificateType certificate_type, + EncodingFormat encoding, bool include_ocsp) { std::lock_guard guard(EvseSecurity::security_mutex); - return get_key_pair_internal(certificate_type, encoding); + return get_leaf_certificate_info_internal(certificate_type, encoding, include_ocsp); } -GetKeyPairResult EvseSecurity::get_key_pair_internal(LeafCertificateType certificate_type, EncodingFormat encoding) { - EVLOG_info << "Requesting key/pair: " << conversions::leaf_certificate_type_to_string(certificate_type); +GetCertificateInfoResult EvseSecurity::get_leaf_certificate_info_internal(LeafCertificateType certificate_type, + EncodingFormat encoding, bool include_ocsp) { + EVLOG_info << "Requesting leaf certificate info: " + << conversions::leaf_certificate_type_to_string(certificate_type); - GetKeyPairResult result; - result.pair = std::nullopt; + GetCertificateInfoResult result; + result.info = std::nullopt; fs::path key_dir; fs::path cert_dir; + CaCertificateType root_type; if (certificate_type == LeafCertificateType::CSMS) { key_dir = this->directories.csms_leaf_key_directory; cert_dir = this->directories.csms_leaf_cert_directory; + root_type = CaCertificateType::CSMS; } else if (certificate_type == LeafCertificateType::V2G) { key_dir = this->directories.secc_leaf_key_directory; cert_dir = this->directories.secc_leaf_cert_directory; + root_type = CaCertificateType::V2G; } else { - EVLOG_warning << "Rejected attempt to retrieve MF key pair"; - result.status = GetKeyPairStatus::Rejected; + EVLOG_warning << "Rejected attempt to retrieve non CSMS/V2G key pair"; + result.status = GetCertificateInfoStatus::Rejected; return result; } + fs::path root_dir = ca_bundle_path_map[root_type]; + // choose appropriate cert (valid_from / valid_to) try { auto leaf_certificates = X509CertificateBundle(cert_dir, EncodingFormat::PEM); if (leaf_certificates.empty()) { EVLOG_warning << "Could not find any key pair"; - result.status = GetKeyPairStatus::NotFound; + result.status = GetCertificateInfoStatus::NotFound; return result; } @@ -978,13 +1101,13 @@ GetKeyPairResult EvseSecurity::get_key_pair_internal(LeafCertificateType certifi if (latest_valid.has_value() == false) { EVLOG_warning << "Could not find valid certificate"; - result.status = GetKeyPairStatus::NotFoundValid; + result.status = GetCertificateInfoStatus::NotFoundValid; return result; } if (found_private_key_path.has_value() == false) { EVLOG_warning << "Could not find private key for the valid certificate"; - result.status = GetKeyPairStatus::PrivateKeyNotFound; + result.status = GetCertificateInfoStatus::PrivateKeyNotFound; return result; } @@ -1000,6 +1123,7 @@ GetKeyPairResult EvseSecurity::get_key_pair_internal(LeafCertificateType certifi const std::vector* leaf_fullchain = nullptr; const std::vector* leaf_single = nullptr; + int chain_len = 1; // Defaults to 1, single certificate // We are searching for both the full leaf bundle, containing the leaf and the cso1/2 and the single leaf // without the cso1/2 @@ -1010,6 +1134,7 @@ GetKeyPairResult EvseSecurity::get_key_pair_internal(LeafCertificateType certifi if (bFound) { if (chain.size() > 1) { leaf_fullchain = &chain; + chain_len = chain.size(); } else if (chain.size() == 1) { leaf_single = &chain; } @@ -1022,6 +1147,8 @@ GetKeyPairResult EvseSecurity::get_key_pair_internal(LeafCertificateType certifi return true; }); + std::vector certificate_ocsp{}; + if (leaf_fullchain != nullptr) { chain_file = leaf_fullchain->at(0).get_file().value(); } else { @@ -1036,13 +1163,43 @@ GetKeyPairResult EvseSecurity::get_key_pair_internal(LeafCertificateType certifi << " single leaf not found at path: " << cert_dir; } - result.pair = {key_file, chain_file, certificate_file, this->private_key_password}; - result.status = GetKeyPairStatus::Accepted; + // Include OCSP data if possible + if (include_ocsp && (leaf_fullchain != nullptr || leaf_single != nullptr)) { + X509CertificateBundle root_bundle(root_dir, EncodingFormat::PEM); // Required for hierarchy + + auto hierarchy = + std::move(X509CertificateHierarchy::build_hierarchy(root_bundle.split(), leaf_directory.split())); + EVLOG_debug << "Hierarchy for OCSP data: \n" << hierarchy.to_debug_string(); + + // Search for OCSP data for each certificate + if (leaf_fullchain != nullptr) { + for (const auto& chain_certif : *leaf_fullchain) { + try { + CertificateHashData hash = hierarchy.get_certificate_hash(chain_certif); + std::optional data = retrieve_ocsp_cache_internal(hash); + + certificate_ocsp.push_back({hash, data}); + } catch (const NoCertificateFound& e) { + // Always add to preserve file order + certificate_ocsp.push_back({{}, std::nullopt}); + } + } + } else { + try { + CertificateHashData hash = hierarchy.get_certificate_hash(leaf_single->at(0)); + certificate_ocsp.push_back({hash, retrieve_ocsp_cache_internal(hash)}); + } catch (const NoCertificateFound& e) { + } + } + } + + result.info = {key_file, chain_file, certificate_file, chain_len, this->private_key_password, certificate_ocsp}; + result.status = GetCertificateInfoStatus::Accepted; return result; } catch (const CertificateLoadException& e) { EVLOG_warning << "Leaf certificate load exception"; - result.status = GetKeyPairStatus::NotFound; + result.status = GetCertificateInfoStatus::NotFound; return result; } } @@ -1061,28 +1218,31 @@ bool EvseSecurity::update_certificate_links(LeafCertificateType certificate_type fs::path chain_link_path = this->links.cpo_cert_chain_link; // Get the most recent valid certificate (internal since we already locked mutex) - const auto key_pair = this->get_key_pair_internal(certificate_type, EncodingFormat::PEM); - if ((key_pair.status == GetKeyPairStatus::Accepted) && key_pair.pair.has_value()) { + const auto key_pair = this->get_leaf_certificate_info_internal(certificate_type, EncodingFormat::PEM); + if ((key_pair.status == GetCertificateInfoStatus::Accepted) && key_pair.info.has_value()) { // Create or update symlinks to SECC leaf cert if (!cert_link_path.empty()) { - fs::path cert_path = key_pair.pair.value().certificate_single; - if (fs::is_symlink(cert_link_path)) { - if (fs::read_symlink(cert_link_path) != cert_path) { - fs::remove(cert_link_path); + std::optional cert_path = key_pair.info.value().certificate_single; + + if (cert_path.has_value()) { + if (fs::is_symlink(cert_link_path)) { + if (fs::read_symlink(cert_link_path) != cert_path.value()) { + fs::remove(cert_link_path); + changed = true; + } + } + if (!fs::exists(cert_link_path)) { + EVLOG_debug << "SECC cert link: " << cert_link_path << " -> " << cert_path.value(); + fs::create_symlink(cert_path.value(), cert_link_path); changed = true; } } - if (!fs::exists(cert_link_path)) { - EVLOG_debug << "SECC cert link: " << cert_link_path << " -> " << cert_path; - fs::create_symlink(cert_path, cert_link_path); - changed = true; - } } // Create or update symlinks to SECC leaf key if (!key_link_path.empty()) { - fs::path key_path = key_pair.pair.value().key; + fs::path key_path = key_pair.info.value().key; if (fs::is_symlink(key_link_path)) { if (fs::read_symlink(key_link_path) != key_path) { fs::remove(key_link_path); @@ -1097,19 +1257,21 @@ bool EvseSecurity::update_certificate_links(LeafCertificateType certificate_type } // Create or update symlinks to CPO chain - fs::path chain_path = key_pair.pair.value().certificate; - if (!chain_link_path.empty()) { - if (fs::is_symlink(chain_link_path)) { - if (fs::read_symlink(chain_link_path) != chain_path) { - fs::remove(chain_link_path); + if (key_pair.info.value().certificate.has_value()) { + fs::path chain_path = key_pair.info.value().certificate.value(); + if (!chain_link_path.empty()) { + if (fs::is_symlink(chain_link_path)) { + if (fs::read_symlink(chain_link_path) != chain_path) { + fs::remove(chain_link_path); + changed = true; + } + } + if (!fs::exists(chain_link_path)) { + EVLOG_debug << "CPO cert chain link: " << chain_link_path << " -> " << chain_path; + fs::create_symlink(chain_path, chain_link_path); changed = true; } } - if (!fs::exists(chain_link_path)) { - EVLOG_debug << "CPO cert chain link: " << chain_link_path << " -> " << chain_path; - fs::create_symlink(chain_path, chain_link_path); - changed = true; - } } } else { // Remove existing symlinks if no valid certificate is found @@ -1158,8 +1320,10 @@ std::string EvseSecurity::get_verify_file(CaCertificateType certificate_type) { << this->ca_bundle_path_map.at(certificate_type) << " with error: " << e.what(); } - throw NoCertificateFound("Could not find any CA certificate for: " + - conversions::ca_certificate_type_to_string(certificate_type)); + EVLOG_error << "Could not find any CA certificate for: " + << conversions::ca_certificate_type_to_string(certificate_type); + + return {}; } int EvseSecurity::get_leaf_expiry_days_count(LeafCertificateType certificate_type) { @@ -1168,21 +1332,25 @@ int EvseSecurity::get_leaf_expiry_days_count(LeafCertificateType certificate_typ EVLOG_info << "Requesting certificate expiry: " << conversions::leaf_certificate_type_to_string(certificate_type); // Internal since we already locked mutex - const auto key_pair = this->get_key_pair_internal(certificate_type, EncodingFormat::PEM); - if (key_pair.status == GetKeyPairStatus::Accepted) { + const auto key_pair = this->get_leaf_certificate_info_internal(certificate_type, EncodingFormat::PEM, false); + if (key_pair.status == GetCertificateInfoStatus::Accepted) { try { fs::path certificate_path; - if (key_pair.pair.value().certificate.empty() == false) - certificate_path = key_pair.pair.value().certificate; - else - certificate_path = key_pair.pair.value().certificate_single; + if (key_pair.info.has_value()) { + if (key_pair.info.value().certificate.has_value()) + certificate_path = key_pair.info.value().certificate.value(); + else + certificate_path = key_pair.info.value().certificate_single.value(); + } - // In case it is a bundle, we know the leaf is always the first - X509CertificateBundle cert(certificate_path, EncodingFormat::PEM); + if (certificate_path.empty() == false) { + // In case it is a bundle, we know the leaf is always the first + X509CertificateBundle cert(certificate_path, EncodingFormat::PEM); - int64_t seconds = cert.split().at(0).get_valid_to(); - return std::chrono::duration_cast(std::chrono::seconds(seconds)).count(); + int64_t seconds = cert.split().at(0).get_valid_to(); + return std::chrono::duration_cast(std::chrono::seconds(seconds)).count(); + } } catch (const CertificateLoadException& e) { EVLOG_error << "Could not obtain leaf expiry certificate: " << e.what(); } @@ -1197,16 +1365,16 @@ bool EvseSecurity::verify_file_signature(const fs::path& path, const std::string EVLOG_info << "Verifying file signature for " << path.string(); - std::vector sha256_digest; + std::vector sha256_digest; if (false == CryptoSupplier::digest_file_sha256(path, sha256_digest)) { EVLOG_error << "Error during digesting file: " << path; return false; } - std::vector signature_decoded; + std::vector signature_decoded; - if (false == CryptoSupplier::decode_base64_signature(signature, signature_decoded)) { + if (false == CryptoSupplier::base64_decode_to_bytes(signature, signature_decoded)) { EVLOG_error << "Error during decoding signature: " << signature; return false; } @@ -1229,6 +1397,46 @@ bool EvseSecurity::verify_file_signature(const fs::path& path, const std::string return false; } +std::vector EvseSecurity::base64_decode_to_bytes(const std::string& base64_string) { + std::vector decoded_bytes; + + if (false == CryptoSupplier::base64_decode_to_bytes(base64_string, decoded_bytes)) { + return {}; + } + + return decoded_bytes; +} + +std::string EvseSecurity::base64_decode_to_string(const std::string& base64_string) { + std::string decoded_string; + + if (false == CryptoSupplier::base64_decode_to_string(base64_string, decoded_string)) { + return {}; + } + + return decoded_string; +} + +std::string EvseSecurity::base64_encode_from_bytes(const std::vector& bytes) { + std::string encoded_string; + + if (false == CryptoSupplier::base64_encode_from_bytes(bytes, encoded_string)) { + return {}; + } + + return encoded_string; +} + +std::string EvseSecurity::base64_encode_from_string(const std::string& string) { + std::string encoded_string; + + if (false == CryptoSupplier::base64_encode_from_string(string, encoded_string)) { + return {}; + } + + return encoded_string; +} + CertificateValidationResult EvseSecurity::verify_certificate(const std::string& certificate_chain, LeafCertificateType certificate_type) { std::lock_guard guard(EvseSecurity::security_mutex); @@ -1326,12 +1534,12 @@ void EvseSecurity::garbage_collect() { EVLOG_info << "Starting garbage collect!"; - std::vector> leaf_paths; + 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)); + leaf_paths.push_back(std::make_tuple(this->directories.csms_leaf_cert_directory, + this->directories.csms_leaf_key_directory, CaCertificateType::CSMS)); + leaf_paths.push_back(std::make_tuple(this->directories.secc_leaf_cert_directory, + this->directories.secc_leaf_key_directory, CaCertificateType::V2G)); // Delete certificates first, give the option to cleanup the dangling keys afterwards std::set invalid_certificate_files; @@ -1340,26 +1548,32 @@ void EvseSecurity::garbage_collect() { std::set protected_private_keys; // Order by latest valid, and keep newest with a safety limit - for (auto const& [cert_dir, key_dir] : leaf_paths) { - X509CertificateBundle expired_certs(cert_dir, EncodingFormat::PEM); - - // Only handle if we have more than the minimum certificates entry - if (expired_certs.get_certificate_chains_count() > DEFAULT_MINIMUM_CERTIFICATE_ENTRIES) { - fs::path key_directory = key_dir; - int skipped = 0; - - // Order by expiry date, and keep even expired certificates with a minimum of 10 certificates - expired_certs.for_each_chain_ordered( - [this, &invalid_certificate_files, &skipped, &key_directory, - &protected_private_keys](const fs::path& file, const std::vector& chain) { - // By default delete all empty - if (chain.size() <= 0) { - invalid_certificate_files.emplace(file); - } + for (auto const& [cert_dir, key_dir, ca_type] : leaf_paths) { + // Root bundle required for hash of OCSP cache + try { + X509CertificateBundle root_bundle(ca_bundle_path_map[ca_type], EncodingFormat::PEM); + 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) { + fs::path key_directory = key_dir; + int skipped = 0; + + // Order by expiry date, and keep even expired certificates with a minimum of 10 certificates + expired_certs.for_each_chain_ordered( + [this, &invalid_certificate_files, &skipped, &key_directory, &protected_private_keys, + &root_bundle](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 (chain.empty()) { + return true; + } - if (++skipped > DEFAULT_MINIMUM_CERTIFICATE_ENTRIES) { - // If the chain contains the first expired (leafs are the first) - if (chain.size()) { + // If the chain contains the first expired (leafs are the first) if (chain[0].is_expired()) { invalid_certificate_files.emplace(file); @@ -1370,41 +1584,78 @@ void EvseSecurity::garbage_collect() { invalid_certificate_files.emplace(key_file); } catch (NoPrivateKeyException& e) { } + + auto leaf_chain = chain; + X509CertificateHierarchy hierarchy = std::move( + X509CertificateHierarchy::build_hierarchy(root_bundle.split(), leaf_chain)); + + try { + CertificateHashData ocsp_hash = hierarchy.get_certificate_hash(chain[0]); + + // Find OCSP cache with hash + if (chain[0].get_file().has_value()) { + const auto ocsp_path = chain[0].get_file().value().parent_path() / "ocsp"; + + if (fs::exists(ocsp_path)) { + for (const auto& hash_entry : fs::directory_iterator(ocsp_path)) { + if (hash_entry.is_regular_file() == false) { + continue; + } + // Attempt hash read + CertificateHashData read_hash; + + if (filesystem_utils::read_hash_from_file(hash_entry.path(), + read_hash) && + read_hash == ocsp_hash) { + + auto oscp_data_path = hash_entry.path(); + oscp_data_path.replace_extension(DER_EXTENSION); + + invalid_certificate_files.emplace(hash_entry.path()); + invalid_certificate_files.emplace(oscp_data_path); + } + } + } + } + } catch (const NoCertificateFound& e) { + } } - } - } else { - // Add to protected certificate list - try { - fs::path key_file = get_private_key_path_of_certificate(chain[0], key_directory, - this->private_key_password); - protected_private_keys.emplace(key_file); - - // Erase all protected keys from the managed CRSs - auto it = managed_csr.find(key_file); - if (it != managed_csr.end()) { - managed_csr.erase(it); + } else { + // Add to protected certificate list + try { + fs::path key_file = get_private_key_path_of_certificate(chain[0], key_directory, + this->private_key_password); + protected_private_keys.emplace(key_file); + + // Erase all protected keys from the managed CRSs + auto it = managed_csr.find(key_file); + if (it != managed_csr.end()) { + managed_csr.erase(it); + } + } catch (NoPrivateKeyException& e) { } - } catch (NoPrivateKeyException& e) { } - } - 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; - } - }); + 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; + } + }); + } + } catch (const CertificateLoadException& e) { + EVLOG_warning << "Could not load bundle from file: " << e.what(); } - } + } // End leaf for iteration for (const auto& expired_certificate_file : invalid_certificate_files) { if (filesystem_utils::delete_file(expired_certificate_file)) - EVLOG_debug << "Deleted expired certificate file: " << expired_certificate_file; + EVLOG_info << "Deleted expired certificate file: " << expired_certificate_file; else EVLOG_warning << "Error deleting expired certificate file: " << expired_certificate_file; } @@ -1414,7 +1665,7 @@ void EvseSecurity::garbage_collect() { // at a further invocation after the GC timer will elapse a few times. This behavior // was added so that if we have a reset and the CSMS sends us a CSR response while we were // down it should still be processed when we boot up and NOT delete the CSRs - for (auto const& [cert_dir, keys_dir] : leaf_paths) { + for (auto const& [cert_dir, keys_dir, ca_type] : leaf_paths) { fs::path cert_path = cert_dir; fs::path key_path = keys_dir; @@ -1460,6 +1711,81 @@ void EvseSecurity::garbage_collect() { ++it; } } + + std::set invalid_ocsp_files; + + // Delete all non-owned OCSP data + for (const auto& leaf_certificate_path : + {directories.secc_leaf_cert_directory, directories.csms_leaf_cert_directory}) { + try { + bool secc = (leaf_certificate_path == directories.secc_leaf_cert_directory); + bool csms = (leaf_certificate_path == directories.csms_leaf_cert_directory) || + (directories.csms_leaf_cert_directory == directories.secc_leaf_cert_directory); + + CaCertificateType load; + + if (secc) + load = CaCertificateType::V2G; + else if (csms) + load = CaCertificateType::CSMS; + + // Also load the roots since we need to build the hierarchy for correct certificate hashes + X509CertificateBundle root_bundle(ca_bundle_path_map[load], EncodingFormat::PEM); + X509CertificateBundle leaf_bundle(leaf_certificate_path, EncodingFormat::PEM); + + fs::path leaf_ocsp; + fs::path root_ocsp; + + if (root_bundle.is_using_bundle_file()) { + root_ocsp = root_bundle.get_path().parent_path() / "ocsp"; + } else { + root_ocsp = root_bundle.get_path() / "ocsp"; + } + + if (leaf_bundle.is_using_bundle_file()) { + leaf_ocsp = leaf_bundle.get_path().parent_path() / "ocsp"; + } else { + leaf_ocsp = leaf_bundle.get_path() / "ocsp"; + } + + X509CertificateHierarchy hierarchy = + std::move(X509CertificateHierarchy::build_hierarchy(root_bundle.split(), leaf_bundle.split())); + + // Iterate all hashes folders and see if any are missing + for (auto& ocsp_dir : {leaf_ocsp, root_ocsp}) { + if (fs::exists(ocsp_dir)) { + for (auto& ocsp_entry : fs::directory_iterator(ocsp_dir)) { + if (ocsp_entry.is_regular_file() == false) { + continue; + } + + // Attempt hash read + CertificateHashData read_hash; + + if (filesystem_utils::read_hash_from_file(ocsp_entry.path(), read_hash)) { + // If we can't find the has, it means it was deleted somehow, add to delete list + if (hierarchy.contains_certificate_hash(read_hash) == false) { + auto oscp_data_path = ocsp_entry.path(); + oscp_data_path.replace_extension(DER_EXTENSION); + + invalid_ocsp_files.emplace(ocsp_entry.path()); + invalid_ocsp_files.emplace(oscp_data_path); + } + } + } + } + } + } catch (const CertificateLoadException& e) { + EVLOG_warning << "Could not load ca bundle from file: " << leaf_certificate_path; + } + } + + for (const auto& invalid_ocsp : invalid_ocsp_files) { + if (filesystem_utils::delete_file(invalid_ocsp)) + EVLOG_info << "Deleted invalid ocsp file: " << invalid_ocsp; + else + EVLOG_warning << "Error deleting invalid ocsp file: " << invalid_ocsp; + } } bool EvseSecurity::is_filesystem_full() { diff --git a/lib/evse_security/evse_types.cpp b/lib/evse_security/evse_types.cpp index b599f10..c9d227e 100644 --- a/lib/evse_security/evse_types.cpp +++ b/lib/evse_security/evse_types.cpp @@ -93,6 +93,17 @@ std::string hash_algorithm_to_string(HashAlgorithm e) { } }; +HashAlgorithm string_to_hash_algorithm(const std::string& s) { + if (s == "SHA256") + return HashAlgorithm::SHA256; + else if (s == "SHA384") + return HashAlgorithm::SHA384; + else if (s == "SHA512") + return HashAlgorithm::SHA512; + + throw std::out_of_range("Could not convert string to HashAlgorithm"); +} + std::string install_certificate_result_to_string(InstallCertificateResult e) { switch (e) { case InstallCertificateResult::InvalidSignature: @@ -142,20 +153,20 @@ std::string get_installed_certificates_status_to_string(GetInstalledCertificates } }; -std::string get_key_pair_status_to_string(GetKeyPairStatus e) { +std::string get_key_pair_status_to_string(GetCertificateInfoStatus e) { switch (e) { - case GetKeyPairStatus::Accepted: + case GetCertificateInfoStatus::Accepted: return "Accepted"; - case GetKeyPairStatus::Rejected: + case GetCertificateInfoStatus::Rejected: return "Rejected"; - case GetKeyPairStatus::NotFound: + case GetCertificateInfoStatus::NotFound: return "NotFound"; - case GetKeyPairStatus::NotFoundValid: + case GetCertificateInfoStatus::NotFoundValid: return "NotFoundValid"; - case GetKeyPairStatus::PrivateKeyNotFound: + case GetCertificateInfoStatus::PrivateKeyNotFound: return "PrivateKeyNotFound"; default: - throw std::out_of_range("Could not convert GetKeyPairStatus to string"); + throw std::out_of_range("Could not convert GetCertificateInfoStatus to string"); } }; diff --git a/lib/evse_security/utils/evse_filesystem.cpp b/lib/evse_security/utils/evse_filesystem.cpp index 1144aac..65dc20c 100644 --- a/lib/evse_security/utils/evse_filesystem.cpp +++ b/lib/evse_security/utils/evse_filesystem.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2023 Pionix GmbH and Contributors to EVerest +#include #include #include @@ -73,7 +74,7 @@ bool write_to_file(const fs::path& file_path, const std::string& data, std::ios: } bool process_file(const fs::path& file_path, size_t buffer_size, - std::function&& func) { + std::function&& func) { std::ifstream file(file_path, std::ios::binary); if (!file) { @@ -81,7 +82,7 @@ bool process_file(const fs::path& file_path, size_t buffer_size, return false; } - std::vector buffer(buffer_size); + std::vector buffer(buffer_size); bool interupted = false; while (file.read(reinterpret_cast(buffer.data()), buffer_size)) { @@ -118,4 +119,54 @@ std::string get_random_file_name(const std::string& extension) { return buff.str(); } +bool read_hash_from_file(const fs::path& file_path, CertificateHashData& out_hash) { + if (file_path.extension() == CERT_HASH_EXTENSION) { + try { + std::ifstream hs(file_path); + std::string algo; + + hs >> algo; + hs >> out_hash.issuer_name_hash; + hs >> out_hash.issuer_key_hash; + hs >> out_hash.serial_number; + hs.close(); + + out_hash.hash_algorithm = conversions::string_to_hash_algorithm(algo); + + return true; + } catch (const std::exception& e) { + EVLOG_error << "Unknown error occurred while reading cert hash file: " << file_path << " err: " << e.what(); + return false; + } + } + + return false; +} + +bool write_hash_to_file(const fs::path& file_path, const CertificateHashData& hash) { + auto real_path = file_path; + + if (file_path.has_extension() == false || file_path.extension() != CERT_HASH_EXTENSION) { + real_path.replace_extension(CERT_HASH_EXTENSION); + } + + try { + // Write out the related hash + std::ofstream hs(real_path.c_str()); + + hs << conversions::hash_algorithm_to_string(hash.hash_algorithm) << "\n"; + hs << hash.issuer_name_hash << "\n"; + hs << hash.issuer_key_hash << "\n"; + hs << hash.serial_number << "\n"; + hs.close(); + + return true; + } catch (const std::exception& e) { + EVLOG_error << "Unknown error occurred writing cert hash file: " << file_path << " err: " << e.what(); + return false; + } + + return false; +} + } // namespace evse_security::filesystem_utils diff --git a/tests/tests.cpp b/tests/tests.cpp index 671ae29..264922c 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -21,7 +21,7 @@ #if USING_OPENSSL_3 // provider management has changed - ensure tests still work -#ifndef USING_TPM2 +#ifndef USING_TPM2contains_certificate_hash #include #else @@ -297,7 +297,7 @@ TEST_F(EvseSecurityTests, verify_basics) { } TEST_F(EvseSecurityTests, verify_directory_bundles) { - const auto child_cert_str = read_file_to_string(std::filesystem::path("certs/client/csms/CSMS_LEAF.pem")); + const auto child_cert_str = read_file_to_string(fs::path("certs/client/csms/CSMS_LEAF.pem")); ASSERT_EQ(this->evse_security->verify_certificate(child_cert_str, LeafCertificateType::CSMS), CertificateValidationResult::Valid); @@ -732,7 +732,8 @@ TEST_F(EvseSecurityTests, get_installed_certificates_and_delete_secc_leaf) { } TEST_F(EvseSecurityTests, leaf_cert_starts_in_future_accepted) { - const auto v2g_keypair_before = this->evse_security->get_key_pair(LeafCertificateType::V2G, EncodingFormat::PEM); + const auto v2g_keypair_before = + this->evse_security->get_leaf_certificate_info(LeafCertificateType::V2G, EncodingFormat::PEM); const auto new_root_ca = read_file_to_string(std::filesystem::path("future_leaf/V2G_ROOT_CA.pem")); const auto result_ca = this->evse_security->install_ca_certificate(new_root_ca, CaCertificateType::V2G); @@ -747,10 +748,11 @@ TEST_F(EvseSecurityTests, leaf_cert_starts_in_future_accepted) { ASSERT_TRUE(result_client == InstallCertificateResult::Accepted); // Check: The certificate is installed, but it isn't actually used - const auto v2g_keypair_after = this->evse_security->get_key_pair(LeafCertificateType::V2G, EncodingFormat::PEM); - ASSERT_EQ(v2g_keypair_after.pair.value().certificate, v2g_keypair_before.pair.value().certificate); - ASSERT_EQ(v2g_keypair_after.pair.value().key, v2g_keypair_before.pair.value().key); - ASSERT_EQ(v2g_keypair_after.pair.value().password, v2g_keypair_before.pair.value().password); + const auto v2g_keypair_after = + this->evse_security->get_leaf_certificate_info(LeafCertificateType::V2G, EncodingFormat::PEM); + ASSERT_EQ(v2g_keypair_after.info.value().certificate, v2g_keypair_before.info.value().certificate); + ASSERT_EQ(v2g_keypair_after.info.value().key, v2g_keypair_before.info.value().key); + ASSERT_EQ(v2g_keypair_after.info.value().password, v2g_keypair_before.info.value().password); } TEST_F(EvseSecurityTests, expired_leaf_cert_rejected) { @@ -785,6 +787,186 @@ TEST_F(EvseSecurityTests, verify_full_filesystem_install_reject) { ASSERT_TRUE(result == InstallCertificateResult::CertificateStoreMaxLengthExceeded); } +TEST_F(EvseSecurityTests, verify_oscp_cache) { + std::string ocsp_mock_response_data = "OCSP_MOCK_RESPONSE_DATA"; + std::string ocsp_mock_response_data_v2 = "OCSP_MOCK_RESPONSE_DATA_V2"; + + OCSPRequestDataList data = this->evse_security->get_v2g_ocsp_request_data(); + + ASSERT_EQ(data.ocsp_request_data_list.size(), 2); + + // Mock a response + for (auto& ocsp : data.ocsp_request_data_list) { + this->evse_security->update_ocsp_cache(ocsp.certificate_hash_data.value(), ocsp_mock_response_data); + } + + // Make sure all info was written and that it is correct + fs::path ocsp_path = "certs/ca/v2g/ocsp"; + + ASSERT_TRUE(fs::exists(ocsp_path)); + + for (auto& ocsp : data.ocsp_request_data_list) { + std::optional data = this->evse_security->retrieve_ocsp_cache(ocsp.certificate_hash_data.value()); + ASSERT_TRUE(data.has_value()); + ASSERT_EQ(read_file_to_string(data.value()), ocsp_mock_response_data); + } + + int entries = 0; + for (const auto& ocsp_entry : fs::directory_iterator(ocsp_path)) { + ASSERT_TRUE(ocsp_entry.is_regular_file()); + ASSERT_TRUE(ocsp_entry.path().has_extension()); + + auto ext = ocsp_entry.path().extension(); + + ASSERT_TRUE(ext == DER_EXTENSION || ext == CERT_HASH_EXTENSION); + + if (ext == DER_EXTENSION) { + ASSERT_EQ(read_file_to_string(ocsp_entry.path()), ocsp_mock_response_data); + } else if (ext == CERT_HASH_EXTENSION) { + CertificateHashData hash; + ASSERT_TRUE(filesystem_utils::read_hash_from_file(ocsp_entry.path(), hash)); + + // Check that is is contained + auto it = + std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), + [&hash](OCSPRequestData& req_data) { return (hash == req_data.certificate_hash_data); }); + + ASSERT_NE(it, data.ocsp_request_data_list.end()); + } + + entries++; + } + + ASSERT_EQ(entries, 4); // 2 for hash, 2 for data + + // Write data again to test over-writing + for (auto& ocsp : data.ocsp_request_data_list) { + this->evse_security->update_ocsp_cache(ocsp.certificate_hash_data.value(), ocsp_mock_response_data_v2); + } + + for (auto& ocsp : data.ocsp_request_data_list) { + std::optional data = this->evse_security->retrieve_ocsp_cache(ocsp.certificate_hash_data.value()); + ASSERT_TRUE(data.has_value()); + ASSERT_EQ(read_file_to_string(data.value()), ocsp_mock_response_data_v2); + } + + // Make sure the info was over-written + entries = 0; + for (const auto& ocsp_entry : fs::directory_iterator(ocsp_path)) { + ASSERT_TRUE(ocsp_entry.is_regular_file()); + ASSERT_TRUE(ocsp_entry.path().has_extension()); + + auto ext = ocsp_entry.path().extension(); + + ASSERT_TRUE(ext == DER_EXTENSION || ext == CERT_HASH_EXTENSION); + + if (ext == DER_EXTENSION) { + ASSERT_EQ(read_file_to_string(ocsp_entry.path()), ocsp_mock_response_data_v2); + } else if (ext == CERT_HASH_EXTENSION) { + CertificateHashData hash; + ASSERT_TRUE(filesystem_utils::read_hash_from_file(ocsp_entry.path(), hash)); + + // Check that is is contained + auto it = + std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), + [&hash](OCSPRequestData& req_data) { return (hash == req_data.certificate_hash_data); }); + + ASSERT_NE(it, data.ocsp_request_data_list.end()); + } + + entries++; + } + + ASSERT_EQ(entries, 4); // 4 still, since we have to over-write + + // Retrieve OCSP data along with certificates + GetCertificateInfoResult response = + this->evse_security->get_leaf_certificate_info(LeafCertificateType::V2G, EncodingFormat::PEM, true); + + ASSERT_EQ(response.status, GetCertificateInfoStatus::Accepted); + ASSERT_TRUE(response.info.has_value()); + + CertificateInfo info = response.info.value(); + + ASSERT_EQ(info.certificate_count, 3); + ASSERT_EQ(info.ocsp.size(), 3); + + // Skip first that does not have OCSP data + for (int i = 1; i < info.ocsp.size(); ++i) { + auto& ocsp = info.ocsp[i]; + + ASSERT_TRUE(ocsp.ocsp_path.has_value()); + ASSERT_EQ(read_file_to_string(ocsp.ocsp_path.value()), ocsp_mock_response_data_v2); + } +} + +TEST_F(EvseSecurityTests, verify_ocsp_garbage_collect) { + std::string ocsp_mock_response_data = "OCSP_MOCK_RESPONSE_DATA"; + + OCSPRequestDataList data = this->evse_security->get_v2g_ocsp_request_data(); + ASSERT_EQ(data.ocsp_request_data_list.size(), 2); + + // Mock a response + for (auto& ocsp : data.ocsp_request_data_list) { + this->evse_security->update_ocsp_cache(ocsp.certificate_hash_data.value(), ocsp_mock_response_data); + } + + // Make sure all info was written and that it is correct + fs::path ocsp_path = "certs/ca/v2g/ocsp"; + fs::path ocsp_path2 = "certs/client/cso/ocsp"; + + ASSERT_TRUE(fs::exists(ocsp_path)); + + for (auto& ocsp : data.ocsp_request_data_list) { + std::optional data = this->evse_security->retrieve_ocsp_cache(ocsp.certificate_hash_data.value()); + ASSERT_TRUE(data.has_value()); + ASSERT_EQ(read_file_to_string(data.value()), ocsp_mock_response_data); + } + + evse_security->max_fs_certificate_store_entries = 1; + ASSERT_TRUE(evse_security->is_filesystem_full()); + + // Garbage collect to see that we don't delete improper data + this->evse_security->garbage_collect(); + + ASSERT_TRUE(fs::exists(ocsp_path)); + ASSERT_TRUE(fs::exists(ocsp_path2)); + + // Check existence of OCSP data + int existing = 0; + for (auto& ocsp_path : {ocsp_path, ocsp_path2}) { + for (auto& ocsp_entry : fs::directory_iterator(ocsp_path)) { + auto ext = ocsp_entry.path().extension(); + if (ext == DER_EXTENSION || ext == CERT_HASH_EXTENSION) { + existing++; + } + } + } + + ASSERT_EQ(existing, 8); + + // Delete the certificates that had their OCSP data appended + fs::remove("certs/ca/v2g/V2G_CA_BUNDLE.pem"); + fs::remove("certs/ca/v2g/V2G_ROOT_CA.pem"); + fs::remove("certs/client/cso/CPO_CERT_CHAIN.pem"); + + // Garbage collect again + this->evse_security->garbage_collect(); + + // Check deletion + existing = 0; + for (auto& ocsp_path : {ocsp_path, ocsp_path2}) { + for (auto& ocsp_entry : fs::directory_iterator(ocsp_path)) { + auto ext = ocsp_entry.path().extension(); + if (ext == DER_EXTENSION || ext == CERT_HASH_EXTENSION) { + existing++; + } + } + } + + ASSERT_EQ(existing, 0); +} + TEST_F(EvseSecurityTestsExpired, verify_expired_leaf_deletion) { // Check that the FS is not full ASSERT_FALSE(evse_security->is_filesystem_full()); @@ -915,6 +1097,22 @@ TEST_F(EvseSecurityTests, verify_expired_csr_deletion) { ASSERT_FALSE(fs::exists(csr_key_path)); } +TEST_F(EvseSecurityTests, verify_base64) { + std::string test_string1 = "U29tZSBkYXRhIGZvciB0ZXN0IGNhc2VzLiBTb21lIGRhdGEgZm9yIHRlc3QgY2FzZXMuIFNvbWUgZGF0YSBmb3I" + "gdGVzdCBjYXNlcy4gU29tZSBkYXRhIGZvciB0ZXN0IGNhc2VzLg=="; + + std::string decoded = this->evse_security->base64_decode_to_string(test_string1); + ASSERT_EQ( + decoded, + std::string( + "Some data for test cases. Some data for test cases. Some data for test cases. Some data for test cases.")); + + std::string out_encoded = this->evse_security->base64_encode_from_string(decoded); + out_encoded.erase(std::remove(out_encoded.begin(), out_encoded.end(), '\n'), out_encoded.cend()); + + ASSERT_EQ(test_string1, out_encoded); +} + } // namespace evse_security // FIXME(piet): Add more tests for getRootCertificateHashData (incl. V2GCertificateChain etc.) \ No newline at end of file