Skip to content

Commit

Permalink
Fixed directory certificate verification
Browse files Browse the repository at this point in the history
Signed-off-by: AssemblyJohn <[email protected]>
  • Loading branch information
AssemblyJohn committed Feb 29, 2024
1 parent 605deb6 commit 8d43713
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 114 deletions.
1 change: 1 addition & 0 deletions include/evse_security/evse_security.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion lib/evse_security/crypto/openssl/openssl_supplier.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 15 additions & 3 deletions lib/evse_security/evse_security.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<X509Wrapper> root_chain;

Check notice on line 1083 in lib/evse_security/evse_security.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/evse_security/evse_security.cpp#L1083

The scope of the variable 'root_chain' can be reduced.

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);
Expand Down
235 changes: 125 additions & 110 deletions tests/tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<X509Wrapper> sorted;
std::vector<fs::path> sorded_should_delete;
std::vector<fs::path> 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<X509Wrapper>& 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}));
Expand Down Expand Up @@ -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}));
Expand Down Expand Up @@ -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<X509Wrapper> sorted;
std::vector<fs::path> sorded_should_delete;
std::vector<fs::path> 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<X509Wrapper>& 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.)

0 comments on commit 8d43713

Please sign in to comment.