diff --git a/include/evse_security/evse_security.hpp b/include/evse_security/evse_security.hpp index 18366f8..fb6e833 100644 --- a/include/evse_security/evse_security.hpp +++ b/include/evse_security/evse_security.hpp @@ -226,6 +226,7 @@ class EvseSecurity { private: // Define here all tests that require internal function usage #ifdef BUILD_TESTING_EVSE_SECURITY + FRIEND_TEST(EvseSecurityTests, verify_directory_bundles); FRIEND_TEST(EvseSecurityTests, verify_full_filesystem_install_reject); FRIEND_TEST(EvseSecurityTests, verify_full_filesystem); FRIEND_TEST(EvseSecurityTests, verify_expired_csr_deletion); diff --git a/lib/evse_security/crypto/openssl/openssl_supplier.cpp b/lib/evse_security/crypto/openssl/openssl_supplier.cpp index 4c6f489..af07320 100644 --- a/lib/evse_security/crypto/openssl/openssl_supplier.cpp +++ b/lib/evse_security/crypto/openssl/openssl_supplier.cpp @@ -574,9 +574,10 @@ CertificateValidationError OpenSSLSupplier::x509_verify_certificate_chain(X509Ha const char* c_dir_path = dir_path.has_value() ? dir_path.value().c_str() : nullptr; const char* c_file_path = file_path.has_value() ? file_path.value().c_str() : nullptr; - if (X509_STORE_load_locations(store_ptr.get(), c_file_path, c_dir_path) != 1) { + if (1 != X509_STORE_load_locations(store_ptr.get(), c_file_path, c_dir_path)) { return CertificateValidationError::Unknown; } + if (dir_path.has_value()) { if (X509_STORE_add_lookup(store_ptr.get(), X509_LOOKUP_file()) == nullptr) { return CertificateValidationError::Unknown; diff --git a/lib/evse_security/evse_security.cpp b/lib/evse_security/evse_security.cpp index f9aef33..e8d046e 100644 --- a/lib/evse_security/evse_security.cpp +++ b/lib/evse_security/evse_security.cpp @@ -260,7 +260,7 @@ InstallCertificateResult EvseSecurity::install_ca_certificate(const std::string& X509CertificateBundle existing_certs(ca_bundle_path, EncodingFormat::PEM); if (existing_certs.is_using_directory()) { - std::string filename = conversions::ca_certificate_type_to_string(certificate_type) + "_" + + std::string filename = conversions::ca_certificate_type_to_string(certificate_type) + "_ROOT_" + filesystem_utils::get_random_file_name(PEM_EXTENSION.string()); fs::path new_path = ca_bundle_path / filename; @@ -1077,14 +1077,26 @@ InstallCertificateResult EvseSecurity::verify_certificate_internal(const std::st store = this->ca_bundle_path_map.at(CaCertificateType::MF); } + // We use a root chain instead of relying on OpenSSL since that requires to have + // the name of the certificates in the format "hash.0", hash being the subject hash + // or to have symlinks in the mentioned format to the certificates in the directory + std::vector root_chain; + if (fs::is_directory(store)) { - store_dir = store; + // In case of a directory load the certificates manually and add them + // to the parent certificates + X509CertificateBundle roots(store, EncodingFormat::PEM); + root_chain = roots.split(); + + for (size_t i = 0; i < root_chain.size(); i++) { + parent_certificates.emplace_back(root_chain[i].get()); + } } else { store_file = store; } CertificateValidationError validated = CryptoSupplier::x509_verify_certificate_chain( - leaf_certificate.get(), parent_certificates, true, store_dir, store_file); + leaf_certificate.get(), parent_certificates, true, std::nullopt, store_file); if (validated != CertificateValidationError::NoError) { return to_install_certificate_result(validated); diff --git a/tests/tests.cpp b/tests/tests.cpp index bb3d6fe..30f4d8f 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -246,116 +246,6 @@ class EvseSecurityTestsExpired : public EvseSecurityTests { } }; -TEST_F(EvseSecurityTestsExpired, verify_expired_leaf_deletion) { - // Check that the FS is not full - ASSERT_FALSE(evse_security->is_filesystem_full()); - - // List of date sorted certificates - std::vector sorted; - std::vector sorded_should_delete; - std::vector sorded_should_keep; - - // Ensure that we have GEN_CERTIFICATES + 2 (CPO_CERT_CHAIN.pem + SECC_LEAF.pem) - { - X509CertificateBundle full_certs(fs::path("certs/client/cso"), EncodingFormat::PEM); - ASSERT_EQ(full_certs.get_certificate_chains_count(), GEN_CERTIFICATES + 2); - - full_certs.for_each_chain([&sorted](const fs::path& path, const std::vector& certifs) { - sorted.push_back(certifs.at(0)); - - return true; - }); - - ASSERT_EQ(sorted.size(), GEN_CERTIFICATES + 2); - } - - // Sort by end expiry date - std::sort(std::begin(sorted), std::end(sorted), - [](X509Wrapper& a, X509Wrapper& b) { return (a.get_valid_to() > b.get_valid_to()); }); - - // Collect all should-delete and kept certificates - int skipped = 0; - - for (const auto& cert : sorted) { - if (++skipped > DEFAULT_MINIMUM_CERTIFICATE_ENTRIES) { - sorded_should_delete.push_back(cert.get_file().value()); - } else { - sorded_should_keep.push_back(cert.get_file().value()); - } - } - - // Fill the disk - evse_security->max_fs_certificate_store_entries = 20; - - ASSERT_TRUE(evse_security->is_filesystem_full()); - - // Garbage collect - evse_security->garbage_collect(); - - // Ensure that we have 10 certificates, since we only keep 10, the newest - { - X509CertificateBundle full_certs(fs::path("certs/client/cso"), EncodingFormat::PEM); - ASSERT_EQ(full_certs.get_certificate_chains_count(), DEFAULT_MINIMUM_CERTIFICATE_ENTRIES); - - // Ensure that we only have the newest ones - for (const auto& deleted : sorded_should_delete) { - ASSERT_FALSE(fs::exists(deleted)); - } - - for (const auto& deleted : sorded_should_keep) { - ASSERT_TRUE(fs::exists(deleted)); - } - } -} - -TEST_F(EvseSecurityTests, verify_expired_csr_deletion) { - // Generate a CSR - std::string csr = - evse_security->generate_certificate_signing_request(LeafCertificateType::CSMS, "DE", "Pionix", "NA"); - fs::path csr_key_path = evse_security->managed_csr.begin()->first; - - // Simulate a full fs else no deletion will take place - evse_security->max_fs_usage_bytes = 1; - - ASSERT_EQ(evse_security->managed_csr.size(), 1); - ASSERT_TRUE(fs::exists(csr_key_path)); - - // Check that is is NOT deleted - evse_security->garbage_collect(); - ASSERT_TRUE(fs::exists(csr_key_path)); - - // Sleep 1 second AND it must be deleted - evse_security->csr_expiry = std::chrono::seconds(0); - std::this_thread::sleep_for(std::chrono::seconds(1)); - evse_security->garbage_collect(); - - ASSERT_FALSE(fs::exists(csr_key_path)); - ASSERT_EQ(evse_security->managed_csr.size(), 0); - - // Delete unmanaged, future expired CSRs - csr = evse_security->generate_certificate_signing_request(LeafCertificateType::CSMS, "DE", "Pionix", "NA"); - csr_key_path = evse_security->managed_csr.begin()->first; - - ASSERT_EQ(evse_security->managed_csr.size(), 1); - - // Remove from managed (simulate a reboot/reinit) - evse_security->managed_csr.clear(); - - // at this GC the should re-add the key to our managed list - evse_security->csr_expiry = std::chrono::seconds(10); - evse_security->garbage_collect(); - ASSERT_EQ(evse_security->managed_csr.size(), 1); - ASSERT_TRUE(fs::exists(csr_key_path)); - - // Now it is technically expired again - evse_security->csr_expiry = std::chrono::seconds(0); - std::this_thread::sleep_for(std::chrono::seconds(1)); - - // Garbage collect should delete the expired managed key - evse_security->garbage_collect(); - ASSERT_FALSE(fs::exists(csr_key_path)); -} - TEST_F(EvseSecurityTests, verify_basics) { // Check that we have the default provider ASSERT_TRUE(check_openssl_providers({PROVIDER_DEFAULT})); @@ -405,6 +295,21 @@ TEST_F(EvseSecurityTests, verify_basics) { ASSERT_TRUE(equal_certificate_strings(root_cert.get_export_string(), certificate_strings[root_cert_idx])); } +TEST_F(EvseSecurityTests, verify_directory_bundles) { + const auto child_cert_str = read_file_to_string(std::filesystem::path("certs/client/csms/CSMS_LEAF.pem")); + + ASSERT_EQ(this->evse_security->verify_certificate(child_cert_str, LeafCertificateType::CSMS), + InstallCertificateResult::Accepted); + + // Verifies that directory bundles properly function when verifying a certificate + this->evse_security->ca_bundle_path_map[CaCertificateType::CSMS] = fs::path("certs/ca/v2g/"); + this->evse_security->ca_bundle_path_map[CaCertificateType::V2G] = fs::path("certs/ca/v2g/"); + + // Verify a leaf + ASSERT_EQ(this->evse_security->verify_certificate(child_cert_str, LeafCertificateType::CSMS), + InstallCertificateResult::Accepted); +} + TEST_F(EvseSecurityTests, verify_bundle_management) { // Check that we have the default provider ASSERT_TRUE(check_openssl_providers({PROVIDER_DEFAULT})); @@ -971,6 +876,116 @@ TEST_F(EvseSecurityTests, verify_full_filesystem_install_reject) { ASSERT_TRUE(result == InstallCertificateResult::CertificateStoreMaxLengthExceeded); } +TEST_F(EvseSecurityTestsExpired, verify_expired_leaf_deletion) { + // Check that the FS is not full + ASSERT_FALSE(evse_security->is_filesystem_full()); + + // List of date sorted certificates + std::vector sorted; + std::vector sorded_should_delete; + std::vector sorded_should_keep; + + // Ensure that we have GEN_CERTIFICATES + 2 (CPO_CERT_CHAIN.pem + SECC_LEAF.pem) + { + X509CertificateBundle full_certs(fs::path("certs/client/cso"), EncodingFormat::PEM); + ASSERT_EQ(full_certs.get_certificate_chains_count(), GEN_CERTIFICATES + 2); + + full_certs.for_each_chain([&sorted](const fs::path& path, const std::vector& certifs) { + sorted.push_back(certifs.at(0)); + + return true; + }); + + ASSERT_EQ(sorted.size(), GEN_CERTIFICATES + 2); + } + + // Sort by end expiry date + std::sort(std::begin(sorted), std::end(sorted), + [](X509Wrapper& a, X509Wrapper& b) { return (a.get_valid_to() > b.get_valid_to()); }); + + // Collect all should-delete and kept certificates + int skipped = 0; + + for (const auto& cert : sorted) { + if (++skipped > DEFAULT_MINIMUM_CERTIFICATE_ENTRIES) { + sorded_should_delete.push_back(cert.get_file().value()); + } else { + sorded_should_keep.push_back(cert.get_file().value()); + } + } + + // Fill the disk + evse_security->max_fs_certificate_store_entries = 20; + + ASSERT_TRUE(evse_security->is_filesystem_full()); + + // Garbage collect + evse_security->garbage_collect(); + + // Ensure that we have 10 certificates, since we only keep 10, the newest + { + X509CertificateBundle full_certs(fs::path("certs/client/cso"), EncodingFormat::PEM); + ASSERT_EQ(full_certs.get_certificate_chains_count(), DEFAULT_MINIMUM_CERTIFICATE_ENTRIES); + + // Ensure that we only have the newest ones + for (const auto& deleted : sorded_should_delete) { + ASSERT_FALSE(fs::exists(deleted)); + } + + for (const auto& deleted : sorded_should_keep) { + ASSERT_TRUE(fs::exists(deleted)); + } + } +} + +TEST_F(EvseSecurityTests, verify_expired_csr_deletion) { + // Generate a CSR + std::string csr = + evse_security->generate_certificate_signing_request(LeafCertificateType::CSMS, "DE", "Pionix", "NA"); + fs::path csr_key_path = evse_security->managed_csr.begin()->first; + + // Simulate a full fs else no deletion will take place + evse_security->max_fs_usage_bytes = 1; + + ASSERT_EQ(evse_security->managed_csr.size(), 1); + ASSERT_TRUE(fs::exists(csr_key_path)); + + // Check that is is NOT deleted + evse_security->garbage_collect(); + ASSERT_TRUE(fs::exists(csr_key_path)); + + // Sleep 1 second AND it must be deleted + evse_security->csr_expiry = std::chrono::seconds(0); + std::this_thread::sleep_for(std::chrono::seconds(1)); + evse_security->garbage_collect(); + + ASSERT_FALSE(fs::exists(csr_key_path)); + ASSERT_EQ(evse_security->managed_csr.size(), 0); + + // Delete unmanaged, future expired CSRs + csr = evse_security->generate_certificate_signing_request(LeafCertificateType::CSMS, "DE", "Pionix", "NA"); + csr_key_path = evse_security->managed_csr.begin()->first; + + ASSERT_EQ(evse_security->managed_csr.size(), 1); + + // Remove from managed (simulate a reboot/reinit) + evse_security->managed_csr.clear(); + + // at this GC the should re-add the key to our managed list + evse_security->csr_expiry = std::chrono::seconds(10); + evse_security->garbage_collect(); + ASSERT_EQ(evse_security->managed_csr.size(), 1); + ASSERT_TRUE(fs::exists(csr_key_path)); + + // Now it is technically expired again + evse_security->csr_expiry = std::chrono::seconds(0); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + // Garbage collect should delete the expired managed key + evse_security->garbage_collect(); + ASSERT_FALSE(fs::exists(csr_key_path)); +} + } // namespace evse_security // FIXME(piet): Add more tests for getRootCertificateHashData (incl. V2GCertificateChain etc.)