Files
Eric F d398a6ced2 Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
2026-06-08 00:38:27 -04:00

1450 lines
66 KiB
C++

// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
#include <fstream>
#include <gtest/gtest.h>
#include <openssl/crypto.h>
#include <regex>
#include <sstream>
#include <string>
#include <thread>
#include <evse_security/certificate/x509_bundle.hpp>
#include <evse_security/certificate/x509_wrapper.hpp>
#include <evse_security/evse_security.hpp>
#include <evse_security/utils/evse_filesystem.hpp>
#include <evse_security/crypto/evse_crypto.hpp>
#include <openssl/opensslv.h>
#ifdef USING_TPM2
// updates so that existing tests run with the OpenSSLProvider
#include <evse_security/crypto/openssl/openssl_provider.hpp>
#include <openssl/provider.h>
namespace evse_security {
const char* PROVIDER_TPM = "tpm2";
const char* PROVIDER_DEFAULT = "default";
typedef OpenSSLProvider TPMScopedProvider;
} // namespace evse_security
#endif // USING_TPM2
std::string read_file_to_string(const fs::path filepath) {
fsstd::ifstream t(filepath.string());
std::stringstream buffer;
buffer << t.rdbuf();
return buffer.str();
}
bool equal_certificate_strings(const std::string& cert1, const std::string& cert2) {
for (int i = 0; i < cert1.length(); ++i) {
if (i < cert1.length() && i < cert2.length()) {
if (isalnum(cert1[i]) && isalnum(cert2[i]) && cert1[i] != cert2[i])
return false;
}
}
return true;
}
namespace evse_security {
class EvseSecurityTests : public ::testing::Test {
protected:
std::unique_ptr<EvseSecurity> evse_security;
void SetUp() override {
fs::remove_all("certs");
fs::remove_all("csr");
install_certs();
if (!fs::exists("key"))
fs::create_directory("key");
FilePaths file_paths;
file_paths.csms_ca_bundle = fs::path("certs/ca/v2g/V2G_CA_BUNDLE.pem");
file_paths.mf_ca_bundle = fs::path("certs/ca/v2g/V2G_CA_BUNDLE.pem");
file_paths.mo_ca_bundle = fs::path("certs/ca/mo/MO_CA_BUNDLE.pem");
file_paths.v2g_ca_bundle = fs::path("certs/ca/v2g/V2G_CA_BUNDLE.pem");
file_paths.directories.csms_leaf_cert_directory = fs::path("certs/client/csms/");
file_paths.directories.csms_leaf_key_directory = fs::path("certs/client/csms/");
file_paths.directories.secc_leaf_cert_directory = fs::path("certs/client/cso/");
file_paths.directories.secc_leaf_key_directory = fs::path("certs/client/cso/");
this->evse_security = std::make_unique<EvseSecurity>(file_paths, "123456");
}
void TearDown() override {
fs::remove_all("certs");
fs::remove_all("csr");
}
virtual void install_certs() {
std::system("./generate_test_certs.sh");
}
};
class EvseSecurityTestsMulti : public EvseSecurityTests {
protected:
void install_certs() override {
std::system("./generate_test_certs_root_multi.sh");
}
};
class EvseSecurityTestsMultiLeaf : public EvseSecurityTests {
protected:
void install_certs() override {
std::system("./generate_test_certs_leaf_multi.sh");
}
};
class EvseSecurityTestsExpired : public EvseSecurityTests {
protected:
static constexpr int GEN_CERTIFICATES = 30;
std::set<fs::path> generated_bulk_certificates;
void SetUp() override {
EvseSecurityTests::SetUp();
fs::remove_all("expired_bulk");
fs::create_directory("expired_bulk");
std::system("touch expired_bulk/index.txt");
std::system("echo \"1000\" > expired_bulk/serial");
// Generate many expired certificates
int serial = 4096; // Hex 1000
// Generate N certificates, N-5 expired, 5 non-expired
std::time_t t = std::time(nullptr);
std::tm* const time_info = std::localtime(&t);
int current_year = 1900 + time_info->tm_year;
for (int i = 0; i < GEN_CERTIFICATES; i++) {
std::string CN = "Pionix";
CN += std::to_string(i);
std::vector<char> buffer;
buffer.resize(2048);
bool expired = (i < (GEN_CERTIFICATES - 5));
int start_year;
int end_year;
if (expired) {
start_year = (current_year - 5 - i);
end_year = (current_year - 1 - i);
} else {
start_year = current_year;
end_year = (current_year + i);
}
std::sprintf(
buffer.data(),
"openssl req -newkey rsa:512 -keyout expired_bulk/cert.key -out expired_bulk/cert.csr -nodes -subj "
"\"/C=DE/L=Schonborn/CN=[%s]/emailAddress=email@pionix.com\"",
CN.c_str());
std::system(buffer.data());
std::sprintf(
buffer.data(),
"openssl ca -selfsign -config expired_runtime/conf.cnf -batch -keyfile expired_bulk/cert.key -in "
"expired_bulk/cert.csr -out expired_bulk/cert.pem -notext -startdate %d1213000000Z -enddate "
"%d1213000000Z",
start_year, end_year);
std::system(buffer.data());
// Copy certificates/keys over
std::string cert_filename = "expired_bulk/cert.pem";
std::string ckey_filename = "expired_bulk/cert.key";
std::string target_cert =
std::string(expired ? "certs/client/cso/SECC_LEAF_EXPIRED_" : "certs/client/cso/SECC_LEAF_VALID_") +
+"st_" + std::to_string(start_year) + "_en_" + std::to_string(end_year) + ".pem";
std::string target_ckey =
std::string(expired ? "certs/client/cso/SECC_LEAF_EXPIRED_" : "certs/client/cso/SECC_LEAF_VALID_") +
+"st_" + std::to_string(start_year) + "_en_" + std::to_string(end_year) + ".key";
fs::copy(cert_filename, target_cert);
fs::copy(ckey_filename, target_ckey);
generated_bulk_certificates.emplace(target_cert);
generated_bulk_certificates.emplace(target_ckey);
fs::remove(cert_filename);
fs::remove(ckey_filename);
}
}
void TearDown() override {
EvseSecurityTests::TearDown();
fs::remove_all("expired_bulk");
}
};
class EvseSecurityTestsCSMS : public ::testing::Test {
protected:
std::unique_ptr<EvseSecurity> evse_security;
void SetUp() override {
fs::remove_all("csms_certs_temp");
fs::create_directory("csms_certs_temp");
fs::copy("csms_certs", "csms_certs_temp", fs::copy_options::recursive);
FilePaths file_paths;
file_paths.v2g_ca_bundle = fs::path("csms_certs_temp/ca/V2G_ROOT_CA.pem");
file_paths.csms_ca_bundle = fs::path("certs/ca/v2g/V2G_CA_BUNDLE.pem");
file_paths.mf_ca_bundle = fs::path("certs/ca/v2g/V2G_CA_BUNDLE.pem");
file_paths.mo_ca_bundle = fs::path("certs/ca/mo/MO_CA_BUNDLE.pem");
file_paths.directories.csms_leaf_cert_directory = fs::path("certs/client/csms/");
file_paths.directories.csms_leaf_key_directory = fs::path("certs/client/csms/");
file_paths.directories.secc_leaf_cert_directory = fs::path("csms_certs_temp/client/");
file_paths.directories.secc_leaf_key_directory = fs::path("csms_certs_temp/client/");
this->evse_security = std::make_unique<EvseSecurity>(file_paths, "123456");
}
void TearDown() override {
fs::remove_all("csms_certs_temp");
}
};
TEST_F(EvseSecurityTests, verify_basics) {
const char* bundle_path = "certs/ca/v2g/V2G_CA_BUNDLE.pem";
fsstd::ifstream file(bundle_path, std::ios::binary);
std::string certificate_file((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
std::vector<std::string> certificate_strings;
static const std::regex cert_regex("-----BEGIN CERTIFICATE-----[\\s\\S]*?-----END CERTIFICATE-----");
std::string::const_iterator search_start(certificate_file.begin());
std::smatch match;
while (std::regex_search(search_start, certificate_file.cend(), match, cert_regex)) {
std::string cert_data = match.str();
try {
certificate_strings.emplace_back(cert_data);
} catch (const CertificateLoadException& e) {
std::cout << "Could not load single certificate while splitting CA bundle: " << e.what() << std::endl;
}
search_start = match.suffix().first;
}
ASSERT_TRUE(certificate_strings.size() == 3);
X509CertificateBundle bundle(fs::path(bundle_path), EncodingFormat::PEM);
ASSERT_TRUE(bundle.is_using_bundle_file());
std::cout << "Bundle hierarchy: " << std::endl << bundle.get_certificate_hierarchy().to_debug_string();
auto certificates = bundle.split();
ASSERT_TRUE(certificates.size() == 3);
for (int i = 0; i < certificate_strings.size() - 1; ++i) {
X509Wrapper cert(certificate_strings[i], EncodingFormat::PEM);
X509Wrapper parent(certificate_strings[i + 1], EncodingFormat::PEM);
ASSERT_TRUE(certificates[i].get_certificate_hash_data(parent) == cert.get_certificate_hash_data(parent));
ASSERT_TRUE(equal_certificate_strings(cert.get_export_string(), certificate_strings[i]));
}
auto root_cert_idx = certificate_strings.size() - 1;
X509Wrapper root_cert(certificate_strings[root_cert_idx], EncodingFormat::PEM);
ASSERT_TRUE(certificates[root_cert_idx].get_certificate_hash_data() == root_cert.get_certificate_hash_data());
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(fs::path("certs/client/csms/CSMS_LEAF.pem"));
ASSERT_EQ(this->evse_security->verify_certificate(child_cert_str, LeafCertificateType::CSMS),
CertificateValidationResult::Valid);
// 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),
CertificateValidationResult::Valid);
}
TEST_F(EvseSecurityTests, verify_bundle_management) {
const char* directory_path = "certs/ca/csms/";
X509CertificateBundle bundle(fs::path(directory_path), EncodingFormat::PEM);
ASSERT_TRUE(bundle.split().size() == 2);
std::cout << "Bundle hierarchy: " << std::endl << bundle.get_certificate_hierarchy().to_debug_string();
// Lowest in hierarchy
X509Wrapper intermediate_cert = bundle.get_certificate_hierarchy().get_hierarchy().at(0).children.at(0).certificate;
CertificateHashData hash;
ASSERT_TRUE(bundle.get_certificate_hierarchy().get_certificate_hash(intermediate_cert, hash));
bundle.delete_certificate(hash, true, false);
// Sync deleted
bundle.sync_to_certificate_store();
std::cout << "Deleted intermediate: " << std::endl << bundle.get_certificate_hierarchy().to_debug_string();
int items = 0;
for (const auto& entry : fs::recursive_directory_iterator(directory_path)) {
if (X509CertificateBundle::is_certificate_file(entry)) {
items++;
}
}
ASSERT_TRUE(items == 1);
}
TEST_F(EvseSecurityTests, verify_certificate_counts) {
// This contains the 'real' fs certifs, we have the leaf chain + the leaf in a seaparate folder
ASSERT_EQ(this->evse_security->get_count_of_installed_certificates({CertificateType::V2GCertificateChain}), 4);
// We have 3 certs in the root bundle
ASSERT_EQ(this->evse_security->get_count_of_installed_certificates({CertificateType::V2GRootCertificate}), 3);
// MF is using the same V2G bundle in our case
ASSERT_EQ(this->evse_security->get_count_of_installed_certificates({CertificateType::MFRootCertificate}), 3);
// None were defined
ASSERT_EQ(this->evse_security->get_count_of_installed_certificates({CertificateType::MORootCertificate}), 3);
}
TEST_F(EvseSecurityTestsMulti, verify_multi_root_leaf_retrieval) {
auto result =
this->evse_security->get_all_valid_certificates_info(LeafCertificateType::CSMS, EncodingFormat::PEM, false);
ASSERT_EQ(result.status, GetCertificateInfoStatus::Accepted);
// We have 2 leafs
ASSERT_EQ(result.info.size(), 2);
fs::path leaf_csms = fs::path("certs/client/csms/CSMS_LEAF.pem");
fs::path leaf_grid = fs::path("certs/client/csms/SECC_LEAF_GRIDSYNC.pem");
// File order is not guaranteed
ASSERT_TRUE(leaf_csms == result.info[0].certificate_single.value() ||
leaf_grid == result.info[0].certificate_single.value());
ASSERT_TRUE(leaf_csms == result.info[1].certificate_single.value() ||
leaf_grid == result.info[1].certificate_single.value());
ASSERT_TRUE(result.info[0].certificate_root.has_value());
ASSERT_TRUE(result.info[1].certificate_root.has_value());
std::string root_v2g = read_file_to_string("certs/ca/v2g/V2G_ROOT_CA.pem");
std::string root_grid = read_file_to_string("certs/ca/v2g/V2G_ROOT_GRIDSYNC_CA.pem");
ASSERT_TRUE(equal_certificate_strings(result.info[0].certificate_root.value(), root_v2g) ||
equal_certificate_strings(result.info[0].certificate_root.value(), root_grid));
ASSERT_TRUE(equal_certificate_strings(result.info[1].certificate_root.value(), root_v2g) ||
equal_certificate_strings(result.info[1].certificate_root.value(), root_grid));
}
TEST_F(EvseSecurityTestsMultiLeaf, verify_multi_leaf_retrieval) {
std::vector<CertificateType> certificate_types;
certificate_types.push_back(CertificateType::V2GCertificateChain);
const auto r = this->evse_security->get_installed_certificates(certificate_types);
ASSERT_EQ(r.status, GetInstalledCertificatesStatus::Accepted);
ASSERT_EQ(r.certificate_hash_data_chain.size(), 2);
// Order is not guaranteed — both chains have identical validity periods
auto& chain0 = r.certificate_hash_data_chain[0];
auto& chain1 = r.certificate_hash_data_chain[1];
std::string name0 = chain0.certificate_hash_data.debug_common_name;
std::string name1 = chain1.certificate_hash_data.debug_common_name;
ASSERT_TRUE((name0 == "SECCCert" && name1 == "SECCGridSyncCert") ||
(name0 == "SECCGridSyncCert" && name1 == "SECCCert"));
// Both chains should have 2 child certificates (SubCA2, SubCA1)
ASSERT_EQ(chain0.child_certificate_hash_data.size(), 2);
ASSERT_EQ(chain1.child_certificate_hash_data.size(), 2);
// Verify child ordering for both chains
ASSERT_EQ(chain0.child_certificate_hash_data[0].debug_common_name, std::string("CPOSubCA2"));
ASSERT_EQ(chain0.child_certificate_hash_data[1].debug_common_name, std::string("CPOSubCA1"));
ASSERT_EQ(chain1.child_certificate_hash_data[0].debug_common_name, std::string("CPOSubCA2"));
ASSERT_EQ(chain1.child_certificate_hash_data[1].debug_common_name, std::string("CPOSubCA1"));
}
TEST_F(EvseSecurityTests, verify_normal_keygen) {
KeyGenerationInfo info;
KeyHandle_ptr key;
info.key_type = CryptoKeyType::RSA_3072;
info.generate_on_custom = false;
info.public_key_file = fs::path("key/nrm_pubkey.key");
info.private_key_file = fs::path("key/nrm_privkey.key");
bool gen = CryptoSupplier::generate_key(info, key);
ASSERT_TRUE(gen);
}
TEST_F(EvseSecurityTests, verify_keygen_csr) {
KeyGenerationInfo info;
KeyHandle_ptr key;
info.key_type = CryptoKeyType::EC_prime256v1;
info.generate_on_custom = false;
info.public_key_file = fs::path("key/pubkey.key");
info.private_key_file = fs::path("key/privkey.key");
bool gen = CryptoSupplier::generate_key(info, key);
ASSERT_TRUE(gen);
CertificateSigningRequestInfo csr_info;
csr_info.n_version = 0;
csr_info.commonName = "pionix_01";
csr_info.organization = "PionixDE";
csr_info.country = "DE";
info.public_key_file = fs::path("key/csr_pubkey.tkey");
info.private_key_file = fs::path("key/csr_privkey.tkey");
info.key_type = CryptoKeyType::RSA_2048;
csr_info.key_info = info;
std::string csr;
auto csr_gen = CryptoSupplier::x509_generate_csr(csr_info, csr);
ASSERT_EQ(csr_gen, CertificateSignRequestResult::Valid);
std::cout << "Csr: " << std::endl << csr << std::endl;
}
/// \brief get_certificate_hash_data() throws exception if called with no issuer and a non-self-signed cert
TEST_F(EvseSecurityTests, get_certificate_hash_data_non_self_signed_requires_issuer) {
const auto non_self_signed_cert_str =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3_SUBCA2.pem"));
const X509Wrapper non_self_signed_cert(non_self_signed_cert_str, EncodingFormat::PEM);
ASSERT_THROW(non_self_signed_cert.get_certificate_hash_data(), std::logic_error);
}
/// \brief get_certificate_hash_data() throws exception if called with the wrong issuer
TEST_F(EvseSecurityTests, get_certificate_hash_data_wrong_issuer) {
const auto child_cert_str =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3_SUBCA2.pem"));
const X509Wrapper child_cert(child_cert_str, EncodingFormat::PEM);
const auto wrong_parent_cert_str =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3.pem"));
const X509Wrapper wrong_parent_cert(wrong_parent_cert_str, EncodingFormat::PEM);
ASSERT_THROW(child_cert.get_certificate_hash_data(wrong_parent_cert), std::logic_error);
}
/// \brief test verifyChargepointCertificate with valid cert
TEST_F(EvseSecurityTests, verify_chargepoint_cert_01) {
const auto client_certificate = read_file_to_string(fs::path("certs/client/csms/CSMS_LEAF.pem"));
std::cout << client_certificate << std::endl;
const auto result = this->evse_security->update_leaf_certificate(client_certificate, LeafCertificateType::CSMS);
ASSERT_TRUE(result == InstallCertificateResult::Accepted);
}
/// \brief test verifyChargepointCertificate with invalid cert
TEST_F(EvseSecurityTests, verify_chargepoint_cert_02) {
const auto result = this->evse_security->update_leaf_certificate("InvalidCertificate", LeafCertificateType::CSMS);
ASSERT_TRUE(result == InstallCertificateResult::InvalidFormat);
}
/// \brief test verifyV2GChargingStationCertificate with valid cert
TEST_F(EvseSecurityTests, verify_v2g_cert_01) {
const auto client_certificate = read_file_to_string(fs::path("certs/client/cso/SECC_LEAF.pem"));
const auto result = this->evse_security->update_leaf_certificate(client_certificate, LeafCertificateType::V2G);
ASSERT_TRUE(result == InstallCertificateResult::Accepted);
}
/// \brief test verifyV2GChargingStationCertificate with invalid cert
TEST_F(EvseSecurityTests, verify_v2g_cert_02) {
const auto invalid_certificate = read_file_to_string(fs::path("certs/client/invalid/INVALID_CSMS.pem"));
const auto result = this->evse_security->update_leaf_certificate(invalid_certificate, LeafCertificateType::V2G);
ASSERT_TRUE(result != InstallCertificateResult::Accepted);
}
TEST_F(EvseSecurityTests, retrieve_root_ca) {
std::string path = "certs/ca/v2g/V2G_CA_BUNDLE.pem";
std::string retrieved_path = this->evse_security->get_verify_file(CaCertificateType::V2G);
ASSERT_EQ(path, retrieved_path);
}
TEST_F(EvseSecurityTests, retrieve_root_location) {
std::string file_path = "certs/ca/v2g/V2G_CA_BUNDLE.pem";
std::string retrieved_file_location = this->evse_security->get_verify_location(CaCertificateType::V2G);
ASSERT_EQ(file_path, retrieved_file_location);
}
TEST_F(EvseSecurityTests, install_root_ca_01) {
const auto v2g_root_ca = read_file_to_string(fs::path("certs/ca/v2g/V2G_ROOT_CA_NEW.pem"));
const auto result = this->evse_security->install_ca_certificate(v2g_root_ca, CaCertificateType::V2G);
ASSERT_TRUE(result == InstallCertificateResult::Accepted);
std::string path = "certs/ca/v2g/V2G_CA_BUNDLE.pem";
ASSERT_EQ(this->evse_security->get_verify_file(CaCertificateType::V2G), path);
const auto read_v2g_root_ca = read_file_to_string(path);
X509CertificateBundle root_bundle(read_v2g_root_ca, EncodingFormat::PEM);
X509Wrapper new_root(v2g_root_ca, EncodingFormat::PEM);
// Assert it was really installed
ASSERT_TRUE(root_bundle.contains_certificate(new_root));
}
TEST_F(EvseSecurityTests, install_root_ca_02) {
const auto invalid_csms_ca = "-----BEGIN CERTIFICATE-----InvalidCertificate-----END CERTIFICATE-----";
const auto result = this->evse_security->install_ca_certificate(invalid_csms_ca, CaCertificateType::CSMS);
ASSERT_EQ(result, InstallCertificateResult::InvalidFormat);
}
/// \brief test install two new root certificates
TEST_F(EvseSecurityTests, install_root_ca_03) {
const auto pre_installed_certificates =
this->evse_security->get_installed_certificates({CertificateType::CSMSRootCertificate});
const auto new_root_ca_1 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA1.pem"));
const auto result = this->evse_security->install_ca_certificate(new_root_ca_1, CaCertificateType::CSMS);
ASSERT_TRUE(result == InstallCertificateResult::Accepted);
const auto new_root_ca_2 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA2.pem"));
const auto result2 = this->evse_security->install_ca_certificate(new_root_ca_2, CaCertificateType::CSMS);
ASSERT_TRUE(result2 == InstallCertificateResult::Accepted);
const auto post_installed_certificates =
this->evse_security->get_installed_certificates({CertificateType::CSMSRootCertificate});
ASSERT_EQ(post_installed_certificates.certificate_hash_data_chain.size(),
pre_installed_certificates.certificate_hash_data_chain.size() + 2);
for (auto& old_cert : pre_installed_certificates.certificate_hash_data_chain) {
ASSERT_NE(
std::find_if(post_installed_certificates.certificate_hash_data_chain.begin(),
post_installed_certificates.certificate_hash_data_chain.end(),
[&](auto value) { return value.certificate_hash_data == old_cert.certificate_hash_data; }),
post_installed_certificates.certificate_hash_data_chain.end());
}
ASSERT_NE(std::find_if(post_installed_certificates.certificate_hash_data_chain.begin(),
post_installed_certificates.certificate_hash_data_chain.end(),
[&](auto value) {
return X509Wrapper(new_root_ca_1, EncodingFormat::PEM).get_certificate_hash_data() ==
value.certificate_hash_data;
}),
post_installed_certificates.certificate_hash_data_chain.end());
ASSERT_NE(std::find_if(post_installed_certificates.certificate_hash_data_chain.begin(),
post_installed_certificates.certificate_hash_data_chain.end(),
[&](auto value) {
return X509Wrapper(new_root_ca_2, EncodingFormat::PEM).get_certificate_hash_data() ==
value.certificate_hash_data;
}),
post_installed_certificates.certificate_hash_data_chain.end());
}
/// \brief test install new root certificates + two child certificates
TEST_F(EvseSecurityTests, install_root_ca_04) {
const auto pre_installed_certificates =
this->evse_security->get_installed_certificates({CertificateType::CSMSRootCertificate});
const auto new_root_ca_1 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3.pem"));
const auto result = this->evse_security->install_ca_certificate(new_root_ca_1, CaCertificateType::CSMS);
ASSERT_TRUE(result == InstallCertificateResult::Accepted);
const auto new_root_sub_ca_1 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3_SUBCA1.pem"));
const auto result2 = this->evse_security->install_ca_certificate(new_root_sub_ca_1, CaCertificateType::CSMS);
ASSERT_TRUE(result2 == InstallCertificateResult::Accepted);
const auto new_root_sub_ca_2 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3_SUBCA2.pem"));
const auto result3 = this->evse_security->install_ca_certificate(new_root_sub_ca_2, CaCertificateType::CSMS);
ASSERT_TRUE(result3 == InstallCertificateResult::Accepted);
const auto post_installed_certificates =
this->evse_security->get_installed_certificates({CertificateType::CSMSRootCertificate});
ASSERT_EQ(post_installed_certificates.certificate_hash_data_chain.size(),
pre_installed_certificates.certificate_hash_data_chain.size() + 1);
const auto root_x509 = X509Wrapper(new_root_ca_1, EncodingFormat::PEM);
const auto subca1_x509 = X509Wrapper(new_root_sub_ca_1, EncodingFormat::PEM);
const auto subca2_x509 = X509Wrapper(new_root_sub_ca_2, EncodingFormat::PEM);
const auto root_hash_data = root_x509.get_certificate_hash_data();
const auto subca1_hash_data = subca1_x509.get_certificate_hash_data(root_x509);
const auto subca2_hash_data = subca2_x509.get_certificate_hash_data(subca1_x509);
auto result_hash_chain = std::find_if(post_installed_certificates.certificate_hash_data_chain.begin(),
post_installed_certificates.certificate_hash_data_chain.end(),
[&](auto chain) { return chain.certificate_hash_data == root_hash_data; });
ASSERT_NE(result_hash_chain, post_installed_certificates.certificate_hash_data_chain.end());
ASSERT_EQ(result_hash_chain->certificate_hash_data, root_hash_data);
ASSERT_EQ(result_hash_chain->child_certificate_hash_data.size(), 2);
ASSERT_EQ(result_hash_chain->child_certificate_hash_data[0], subca1_hash_data);
ASSERT_EQ(result_hash_chain->child_certificate_hash_data[1], subca2_hash_data);
}
/// \brief test install expired certificate must be rejected
TEST_F(EvseSecurityTests, install_root_ca_05) {
const auto expired_cert = std::string("-----BEGIN CERTIFICATE-----\n") +
"MIICsjCCAZqgAwIBAgICMDkwDQYJKoZIhvcNAQELBQAwHDEaMBgGA1UEAwwRT0NU\n" +
"VEV4cGlyZWRSb290Q0EwHhcNMjAwMTAxMDAwMDAwWhcNMjEwMTAxMDAwMDAwWjAc\n" +
"MRowGAYDVQQDDBFPQ1RURXhwaXJlZFJvb3RDQTCCASIwDQYJKoZIhvcNAQEBBQAD\n" +
"ggEPADCCAQoCggEBALA3xfKUgMaFfRHabFy27PhWvaeVDL6yd4qv4w4pe0NMJ0pE\n" +
"gr9ynzvXleVlOHF09rabgH99bW/ohLx3l7OliOjMk82e/77oGf0O8ZxViFrppA+z\n" +
"6WVhvRn7opso8KkrTCNUYyuzTH9u/n3EU9uFfueu+ifzD2qke7YJqTz7GY7aEqSb\n" +
"x7+3GDKhZV8lOw68T+WKkJxfuuafzczewHhu623ztc0bo5fTr3FSqWkuJXhB4Zg/\n" +
"GBMt1hS+O4IZeho8Ik9uu5zW39HQQNcJKN6dYDTIZdtQ8vNp6hYdOaRd05v77Ye0\n" +
"ywqqYVyUTgdfmqE5u7YeWUfO9vab3Qxq1IeHVd8CAwEAATANBgkqhkiG9w0BAQsF\n" +
"AAOCAQEAfDeemUzKXtqfCfuaGwTKTsj+Ld3A6VRiT/CSx1rh6BNAZZrve8OV2ckr\n" +
"2Ia+fol9mEkZPCBNLDzgxs5LLiJIOy4prjSTX4HJS5iqJBO8UJGakqXOAz0qBG1V\n" +
"8xWCJLeLGni9vi+dLVVFWpSfzTA/4iomtJPuvoXLdYzMvjLcGFT9RsE9q0oEbGHq\n" +
"ezKIzFaOdpCOtAt+FgW1lqqGHef2wNz15iWQLAU1juip+lgowI5YdhVJVPyqJTNz\n" +
"RUletvBeY2rFUKFWhj8QRPBwBlEDZqxRJSyIwQCe9t7Nhvbd9eyCFvRm9z3a8FDf\n" +
"FRmmZMWQkhBDQt15vxoDyyWn3hdwRA==\n" + "-----END CERTIFICATE-----";
const auto result = this->evse_security->install_ca_certificate(expired_cert, CaCertificateType::CSMS);
ASSERT_EQ(result, InstallCertificateResult::Expired);
}
TEST_F(EvseSecurityTestsCSMS, delete_csms_provided_certs) {
auto path = fs::path("csms_certs_temp/client/");
// Filesystem tests
ASSERT_TRUE(fs::exists("csms_certs_temp/client/SECC_LEAF_A.pem"));
ASSERT_TRUE(fs::exists("csms_certs_temp/client/SECC_LEAF_A.key"));
ASSERT_TRUE(fs::exists("csms_certs_temp/client/SECC_LEAF_B.pem"));
ASSERT_TRUE(fs::exists("csms_certs_temp/client/SECC_LEAF_B.key"));
ASSERT_TRUE(fs::exists("csms_certs_temp/client/CPO_CERT_SECC_LEAF_CHAIN_A.pem"));
ASSERT_TRUE(fs::exists("csms_certs_temp/client/CPO_CERT_SECC_LEAF_CHAIN_B.pem"));
ASSERT_TRUE(fs::exists("csms_certs_temp/client/ocsp/SECC_LEAF_ocsp.der"));
ASSERT_TRUE(fs::exists("csms_certs_temp/client/ocsp/SECC_LEAF_ocsp.hash"));
auto cached_secc_leaf1 = read_file_to_string("csms_certs_temp/client/SECC_LEAF_A.pem");
auto cached_secc_leaf2 = read_file_to_string("csms_certs_temp/client/SECC_LEAF_A.key");
auto cached_secc_chain = read_file_to_string("csms_certs_temp/client/CPO_CERT_SECC_LEAF_CHAIN_A.pem");
// SECC_LEAF_B
CertificateHashData certificate_hash_data;
certificate_hash_data.hash_algorithm = HashAlgorithm::SHA256;
certificate_hash_data.issuer_name_hash = "82addb4b47026c702b9ed9d482c6e3570bbae9c49b963ec18b0a3523dfb47fe3";
certificate_hash_data.issuer_key_hash = "e9d2a6d245233edbf5a8319b99087313e16307ca29b388373d951b50e93090aa";
certificate_hash_data.serial_number = "4ed698d63c724c6a61a0ccc4ff80b383192dfd7a";
// Code hash tests
try {
X509CertificateBundle leaf_bundle(path, EncodingFormat::PEM);
auto& hierarchy = leaf_bundle.get_certificate_hierarchy();
std::cout << hierarchy.to_debug_string();
ASSERT_TRUE(hierarchy.contains_certificate_hash(certificate_hash_data, true));
} catch (const CertificateLoadException& e) {
FAIL();
}
// Delete
DeleteResult result = this->evse_security->delete_certificate(certificate_hash_data);
ASSERT_EQ(result.result, DeleteCertificateResult::Accepted);
ASSERT_TRUE(result.leaf_certificate_type.has_value());
ASSERT_EQ(result.leaf_certificate_type.value(), LeafCertificateType::V2G);
try {
X509CertificateBundle leaf_bundle(path, EncodingFormat::PEM);
auto& hierarchy = leaf_bundle.get_certificate_hierarchy();
std::cout << hierarchy.to_debug_string();
ASSERT_FALSE(hierarchy.contains_certificate_hash(certificate_hash_data, true));
} catch (const CertificateLoadException& e) {
FAIL();
}
ASSERT_TRUE(fs::exists("csms_certs_temp/client/SECC_LEAF_A.pem"));
ASSERT_TRUE(fs::exists("csms_certs_temp/client/SECC_LEAF_A.key"));
ASSERT_TRUE(fs::exists("csms_certs_temp/client/CPO_CERT_SECC_LEAF_CHAIN_A.pem"));
ASSERT_FALSE(fs::exists("csms_certs_temp/client/SECC_LEAF_B.pem"));
ASSERT_FALSE(fs::exists("csms_certs_temp/client/SECC_LEAF_B.key"));
ASSERT_FALSE(fs::exists("csms_certs_temp/client/CPO_CERT_SECC_LEAF_CHAIN_B.pem"));
ASSERT_FALSE(fs::exists("csms_certs_temp/client/ocsp/SECC_LEAF_ocsp.der"));
ASSERT_FALSE(fs::exists("csms_certs_temp/client/ocsp/SECC_LEAF_ocsp.hash"));
ASSERT_EQ(cached_secc_leaf1, read_file_to_string("csms_certs_temp/client/SECC_LEAF_A.pem"));
ASSERT_EQ(cached_secc_leaf2, read_file_to_string("csms_certs_temp/client/SECC_LEAF_A.key"));
ASSERT_EQ(cached_secc_chain, read_file_to_string("csms_certs_temp/client/CPO_CERT_SECC_LEAF_CHAIN_A.pem"));
}
TEST_F(EvseSecurityTests, delete_root_ca_01) {
std::vector<CertificateType> certificate_types;
certificate_types.push_back(CertificateType::V2GRootCertificate);
certificate_types.push_back(CertificateType::MORootCertificate);
certificate_types.push_back(CertificateType::CSMSRootCertificate);
certificate_types.push_back(CertificateType::V2GCertificateChain);
certificate_types.push_back(CertificateType::MFRootCertificate);
const auto root_certs = this->evse_security->get_installed_certificates(certificate_types);
CaCertificateType root_type;
CertificateType deleted_type = root_certs.certificate_hash_data_chain.at(0).certificate_type;
switch (deleted_type) {
case CertificateType::V2GRootCertificate:
root_type = CaCertificateType::V2G;
break;
case CertificateType::MORootCertificate:
root_type = CaCertificateType::MO;
break;
case CertificateType::CSMSRootCertificate:
root_type = CaCertificateType::CSMS;
break;
case CertificateType::V2GCertificateChain:
root_type = CaCertificateType::V2G;
break;
case CertificateType::MFRootCertificate:
root_type = CaCertificateType::MF;
break;
}
ASSERT_TRUE(fs::exists("certs/ca/v2g/V2G_CA_BUNDLE.pem"));
auto cached_root_bundle = read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem");
ASSERT_NE(cached_root_bundle.find("BEGIN CERTIFICATE"), std::string::npos);
CertificateHashData certificate_hash_data;
certificate_hash_data.hash_algorithm = HashAlgorithm::SHA256;
certificate_hash_data.issuer_key_hash =
root_certs.certificate_hash_data_chain.at(0).certificate_hash_data.issuer_key_hash;
certificate_hash_data.issuer_name_hash =
root_certs.certificate_hash_data_chain.at(0).certificate_hash_data.issuer_name_hash;
certificate_hash_data.serial_number =
root_certs.certificate_hash_data_chain.at(0).certificate_hash_data.serial_number;
const auto result = this->evse_security->delete_certificate(certificate_hash_data);
ASSERT_EQ(result.result, DeleteCertificateResult::Accepted);
ASSERT_TRUE(result.ca_certificate_type.has_value());
ASSERT_EQ(result.ca_certificate_type.value(), root_type);
ASSERT_TRUE(fs::exists("certs/ca/v2g/V2G_CA_BUNDLE.pem"));
ASSERT_TRUE(read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem").empty());
}
TEST_F(EvseSecurityTests, delete_root_ca_02) {
CertificateHashData certificate_hash_data;
certificate_hash_data.hash_algorithm = HashAlgorithm::SHA256;
certificate_hash_data.issuer_key_hash = "UnknownKeyHash";
certificate_hash_data.issuer_name_hash = "7da88c3366c19488ee810c5408f612db98164a34e05a0b15c93914fbed228c0f";
certificate_hash_data.serial_number = "3046";
ASSERT_TRUE(fs::exists("certs/ca/v2g/V2G_CA_BUNDLE.pem"));
auto cached_root_bundle = read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem");
ASSERT_NE(cached_root_bundle.find("BEGIN CERTIFICATE"), std::string::npos);
const auto result = this->evse_security->delete_certificate(certificate_hash_data);
ASSERT_EQ(result.result, DeleteCertificateResult::NotFound);
ASSERT_FALSE(result.ca_certificate_type.has_value());
ASSERT_FALSE(result.leaf_certificate_type.has_value());
ASSERT_TRUE(fs::exists("certs/ca/v2g/V2G_CA_BUNDLE.pem"));
ASSERT_EQ(cached_root_bundle, read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem"));
}
TEST_F(EvseSecurityTests, delete_sub_ca_1) {
ASSERT_TRUE(fs::exists("certs/ca/v2g/V2G_CA_BUNDLE.pem"));
std::string root_bundle_content;
const auto new_root_ca_1 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3.pem"));
root_bundle_content = read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem");
const auto result = this->evse_security->install_ca_certificate(new_root_ca_1, CaCertificateType::CSMS);
ASSERT_TRUE(result == InstallCertificateResult::Accepted);
// Filesystem tests
ASSERT_NE(root_bundle_content, read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem"));
root_bundle_content = read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem");
ASSERT_NE(root_bundle_content.find(new_root_ca_1), std::string::npos);
const auto new_root_sub_ca_1 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3_SUBCA1.pem"));
root_bundle_content = read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem");
const auto result2 = this->evse_security->install_ca_certificate(new_root_sub_ca_1, CaCertificateType::CSMS);
ASSERT_TRUE(result2 == InstallCertificateResult::Accepted);
// Filesystem tests
ASSERT_NE(root_bundle_content, read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem"));
root_bundle_content = read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem");
ASSERT_NE(root_bundle_content.find(new_root_sub_ca_1), std::string::npos);
const auto new_root_sub_ca_2 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3_SUBCA2.pem"));
root_bundle_content = read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem");
const auto result3 = this->evse_security->install_ca_certificate(new_root_sub_ca_2, CaCertificateType::CSMS);
ASSERT_TRUE(result3 == InstallCertificateResult::Accepted);
// Filesystem tests
ASSERT_NE(root_bundle_content, read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem"));
root_bundle_content = read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem");
ASSERT_NE(root_bundle_content.find(new_root_sub_ca_2), std::string::npos);
const auto root_x509 = X509Wrapper(new_root_ca_1, EncodingFormat::PEM);
const auto subca1_x509 = X509Wrapper(new_root_sub_ca_1, EncodingFormat::PEM);
const auto subca1_hash_data = subca1_x509.get_certificate_hash_data(root_x509);
const auto delete_result = this->evse_security->delete_certificate(subca1_hash_data);
ASSERT_EQ(delete_result.result, DeleteCertificateResult::Accepted);
ASSERT_TRUE(delete_result.ca_certificate_type.has_value());
ASSERT_EQ(delete_result.ca_certificate_type.value(), CaCertificateType::V2G);
// Filesystem tests
root_bundle_content = read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem");
ASSERT_EQ(root_bundle_content.find(new_root_sub_ca_1), std::string::npos);
std::vector<CertificateType> certificate_types;
certificate_types.push_back(CertificateType::V2GRootCertificate);
certificate_types.push_back(CertificateType::MORootCertificate);
certificate_types.push_back(CertificateType::CSMSRootCertificate);
certificate_types.push_back(CertificateType::V2GCertificateChain);
certificate_types.push_back(CertificateType::MFRootCertificate);
const auto certs_after_delete =
this->evse_security->get_installed_certificates(certificate_types).certificate_hash_data_chain;
ASSERT_EQ(std::find_if(certs_after_delete.begin(), certs_after_delete.end(),
[&](auto value) {
return value.certificate_hash_data == subca1_hash_data ||
(std::find_if(value.child_certificate_hash_data.begin(),
value.child_certificate_hash_data.end(), [&](auto child_value) {
return child_value == subca1_hash_data;
}) != value.child_certificate_hash_data.end());
}),
certs_after_delete.end());
}
TEST_F(EvseSecurityTests, delete_sub_ca_2) {
const auto new_root_ca_1 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3.pem"));
const auto result = this->evse_security->install_ca_certificate(new_root_ca_1, CaCertificateType::CSMS);
ASSERT_TRUE(result == InstallCertificateResult::Accepted);
const auto new_root_sub_ca_1 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3_SUBCA1.pem"));
const auto result2 = this->evse_security->install_ca_certificate(new_root_sub_ca_1, CaCertificateType::CSMS);
ASSERT_TRUE(result2 == InstallCertificateResult::Accepted);
const auto new_root_sub_ca_2 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA3_SUBCA2.pem"));
const auto result3 = this->evse_security->install_ca_certificate(new_root_sub_ca_2, CaCertificateType::CSMS);
ASSERT_TRUE(result3 == InstallCertificateResult::Accepted);
const auto root_x509 = X509Wrapper(new_root_ca_1, EncodingFormat::PEM);
const auto subca1_x509 = X509Wrapper(new_root_sub_ca_1, EncodingFormat::PEM);
const auto subca2_x509 = X509Wrapper(new_root_sub_ca_2, EncodingFormat::PEM);
const auto subca2_hash_data = subca2_x509.get_certificate_hash_data(subca1_x509);
auto root_bundle_content = read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem");
const auto delete_result = this->evse_security->delete_certificate(subca2_hash_data);
ASSERT_EQ(delete_result.result, DeleteCertificateResult::Accepted);
ASSERT_TRUE(delete_result.ca_certificate_type.has_value());
ASSERT_EQ(delete_result.ca_certificate_type.value(), CaCertificateType::V2G);
// Filesystem tests
ASSERT_NE(root_bundle_content, read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem"));
root_bundle_content = read_file_to_string("certs/ca/v2g/V2G_CA_BUNDLE.pem");
ASSERT_EQ(root_bundle_content.find(new_root_sub_ca_2), std::string::npos);
std::vector<CertificateType> certificate_types;
certificate_types.push_back(CertificateType::V2GRootCertificate);
certificate_types.push_back(CertificateType::MORootCertificate);
certificate_types.push_back(CertificateType::CSMSRootCertificate);
certificate_types.push_back(CertificateType::V2GCertificateChain);
certificate_types.push_back(CertificateType::MFRootCertificate);
const auto certs_after_delete =
this->evse_security->get_installed_certificates(certificate_types).certificate_hash_data_chain;
ASSERT_EQ(std::find_if(certs_after_delete.begin(), certs_after_delete.end(),
[&](auto value) {
return value.certificate_hash_data == subca2_hash_data ||
(std::find_if(value.child_certificate_hash_data.begin(),
value.child_certificate_hash_data.end(), [&](auto child_value) {
return child_value == subca2_hash_data;
}) != value.child_certificate_hash_data.end());
}),
certs_after_delete.end());
}
TEST_F(EvseSecurityTests, get_installed_certificates_chain_order) {
std::vector<CertificateType> certificate_types;
certificate_types.push_back(CertificateType::V2GCertificateChain);
const auto r = this->evse_security->get_installed_certificates(certificate_types);
ASSERT_EQ(r.status, GetInstalledCertificatesStatus::Accepted);
ASSERT_EQ(r.certificate_hash_data_chain.size(), 1);
auto& v2g_chain = r.certificate_hash_data_chain.front();
// Assert the order with the SECCLeaf first
ASSERT_EQ(v2g_chain.certificate_hash_data.debug_common_name, std::string("SECCCert"));
ASSERT_EQ(v2g_chain.child_certificate_hash_data.size(), 2);
ASSERT_EQ(v2g_chain.child_certificate_hash_data[0].debug_common_name, std::string("CPOSubCA2"));
ASSERT_EQ(v2g_chain.child_certificate_hash_data[1].debug_common_name, std::string("CPOSubCA1"));
}
TEST_F(EvseSecurityTests, get_installed_certificates_and_delete_secc_leaf) {
std::vector<CertificateType> certificate_types;
certificate_types.push_back(CertificateType::V2GRootCertificate);
certificate_types.push_back(CertificateType::MORootCertificate);
certificate_types.push_back(CertificateType::CSMSRootCertificate);
certificate_types.push_back(CertificateType::V2GCertificateChain);
certificate_types.push_back(CertificateType::MFRootCertificate);
const auto r = this->evse_security->get_installed_certificates(certificate_types);
ASSERT_EQ(r.status, GetInstalledCertificatesStatus::Accepted);
ASSERT_EQ(r.certificate_hash_data_chain.size(), 5);
bool found_v2g_chain = false;
CertificateHashData secc_leaf_data;
for (const auto& certificate_hash_data_chain : r.certificate_hash_data_chain) {
if (certificate_hash_data_chain.certificate_type == CertificateType::V2GCertificateChain) {
found_v2g_chain = true;
secc_leaf_data = certificate_hash_data_chain.certificate_hash_data;
ASSERT_EQ(2, certificate_hash_data_chain.child_certificate_hash_data.size());
}
}
ASSERT_TRUE(found_v2g_chain);
// Do not allow the SECC delete since it's the ChargingStationCertificate
auto delete_response = this->evse_security->delete_certificate(secc_leaf_data);
ASSERT_EQ(delete_response.result, DeleteCertificateResult::Failed);
}
TEST_F(EvseSecurityTests, leaf_cert_starts_in_future_accepted) {
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);
ASSERT_TRUE(result_ca == InstallCertificateResult::Accepted);
std::filesystem::copy("future_leaf/SECC_LEAF_FUTURE.key", "certs/client/cso/SECC_LEAF_FUTURE.key");
const auto client_certificate = read_file_to_string(fs::path("future_leaf/SECC_LEAF_FUTURE.pem"));
std::cout << client_certificate << std::endl;
const auto result_client =
this->evse_security->update_leaf_certificate(client_certificate, LeafCertificateType::V2G);
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_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) {
const auto new_root_ca = read_file_to_string(std::filesystem::path("expired_leaf/V2G_ROOT_CA.pem"));
const auto result_ca = this->evse_security->install_ca_certificate(new_root_ca, CaCertificateType::V2G);
ASSERT_TRUE(result_ca == InstallCertificateResult::Accepted);
std::filesystem::copy("expired_leaf/SECC_LEAF_EXPIRED.key", "certs/client/cso/SECC_LEAF_EXPIRED.key");
const auto client_certificate = read_file_to_string(fs::path("expired_leaf/SECC_LEAF_EXPIRED.pem"));
std::cout << client_certificate << std::endl;
const auto result_client =
this->evse_security->update_leaf_certificate(client_certificate, LeafCertificateType::V2G);
ASSERT_TRUE(result_client == InstallCertificateResult::Expired);
}
TEST_F(EvseSecurityTests, verify_full_filesystem) {
ASSERT_EQ(evse_security->is_filesystem_full(), false);
evse_security->max_fs_usage_bytes = 1;
ASSERT_EQ(evse_security->is_filesystem_full(), true);
}
TEST_F(EvseSecurityTests, verify_full_filesystem_install_reject) {
evse_security->max_fs_usage_bytes = 1;
ASSERT_EQ(evse_security->is_filesystem_full(), true);
// Must have a rejection
const auto new_root_ca_1 =
read_file_to_string(std::filesystem::path("certs/to_be_installed/INSTALL_TEST_ROOT_CA1.pem"));
const auto result = this->evse_security->install_ca_certificate(new_root_ca_1, CaCertificateType::CSMS);
ASSERT_TRUE(result == InstallCertificateResult::CertificateStoreMaxLengthExceeded);
}
TEST_F(EvseSecurityTestsMultiLeaf, verify_ocsp_request_multi_valid) {
// Verify the OCSP request when we have multiple possible valid certificates
OCSPRequestDataList data = this->evse_security->get_v2g_ocsp_request_data();
ASSERT_EQ(data.ocsp_request_data_list.size(), 4);
ASSERT_TRUE(data.ocsp_request_data_list[0].certificate_hash_data.has_value());
ASSERT_TRUE(data.ocsp_request_data_list[1].certificate_hash_data.has_value());
ASSERT_TRUE(data.ocsp_request_data_list[2].certificate_hash_data.has_value());
ASSERT_TRUE(data.ocsp_request_data_list[3].certificate_hash_data.has_value());
ASSERT_TRUE(
std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), [](const auto& ocsp_data) {
return ocsp_data.certificate_hash_data.value().debug_common_name == std::string("CPOSubCA2");
}) != data.ocsp_request_data_list.end());
ASSERT_TRUE(
std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), [](const auto& ocsp_data) {
return ocsp_data.certificate_hash_data.value().debug_common_name == std::string("CPOSubCA1");
}) != data.ocsp_request_data_list.end());
ASSERT_TRUE(
std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), [](const auto& ocsp_data) {
return ocsp_data.certificate_hash_data.value().debug_common_name == std::string("SECCCert");
}) != data.ocsp_request_data_list.end());
ASSERT_TRUE(
std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), [](const auto& ocsp_data) {
return ocsp_data.certificate_hash_data.value().debug_common_name == std::string("SECCGridSyncCert");
}) != data.ocsp_request_data_list.end());
}
TEST_F(EvseSecurityTests, verify_ocsp_request_mo_generate) {
// Read a leaf, should work since this SECC will be tested against both MO and V2G
const auto secc_leaf = read_file_to_string("certs/client/cso/SECC_LEAF.pem");
OCSPRequestDataList data = this->evse_security->get_mo_ocsp_request_data(secc_leaf);
// Expect 3 chain certifs, since SECC_LEAF has an responder URL
ASSERT_EQ(data.ocsp_request_data_list.size(), 3);
// Assert a leaf->sub2->sub1 order
ASSERT_TRUE(data.ocsp_request_data_list[0].certificate_hash_data.has_value());
ASSERT_TRUE(data.ocsp_request_data_list[1].certificate_hash_data.has_value());
ASSERT_TRUE(data.ocsp_request_data_list[2].certificate_hash_data.has_value());
bool has_intermediate_1 =
std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), [](const auto& ocsp_data) {
return ocsp_data.certificate_hash_data.value().debug_common_name == std::string("CPOSubCA1");
}) != data.ocsp_request_data_list.end();
ASSERT_TRUE(has_intermediate_1);
bool has_intermediate_2 =
std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), [](const auto& ocsp_data) {
return ocsp_data.certificate_hash_data.value().debug_common_name == std::string("CPOSubCA2");
}) != data.ocsp_request_data_list.end();
ASSERT_TRUE(has_intermediate_2);
bool has_leaf =
std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), [](const auto& ocsp_data) {
return ocsp_data.certificate_hash_data.value().debug_common_name == std::string("SECCCert");
}) != data.ocsp_request_data_list.end();
ASSERT_TRUE(has_leaf);
// Read the MO leaf
const auto mo_leaf = read_file_to_string("certs/client/mo/MO_LEAF.pem");
data = this->evse_security->get_mo_ocsp_request_data(mo_leaf);
// Expect 2 chain certifs, since leaf does not have an responder URL
ASSERT_EQ(data.ocsp_request_data_list.size(), 2);
ASSERT_TRUE(data.ocsp_request_data_list[0].certificate_hash_data.has_value());
ASSERT_TRUE(data.ocsp_request_data_list[1].certificate_hash_data.has_value());
has_intermediate_1 =
std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), [](const auto& ocsp_data) {
return ocsp_data.certificate_hash_data.value().debug_common_name == std::string("MOSubCA2");
}) != data.ocsp_request_data_list.end();
ASSERT_TRUE(has_intermediate_1);
has_intermediate_2 =
std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), [](const auto& ocsp_data) {
return ocsp_data.certificate_hash_data.value().debug_common_name == std::string("MOSubCA1");
}) != data.ocsp_request_data_list.end();
ASSERT_TRUE(has_intermediate_2);
// Read the MO signed by V2G leaf
const auto mo_v2g_leaf = read_file_to_string("certs/client/mo/MO_LEAF_V2G.pem");
data = this->evse_security->get_mo_ocsp_request_data(mo_v2g_leaf);
// Again expect 2 since the mo leaf does not have a responder URL
ASSERT_EQ(data.ocsp_request_data_list.size(), 2);
ASSERT_TRUE(data.ocsp_request_data_list[0].certificate_hash_data.has_value());
ASSERT_TRUE(data.ocsp_request_data_list[1].certificate_hash_data.has_value());
has_intermediate_1 =
std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), [](const auto& ocsp_data) {
return ocsp_data.certificate_hash_data.value().debug_common_name == std::string("CPOSubCA1");
}) != data.ocsp_request_data_list.end();
ASSERT_TRUE(has_intermediate_1);
has_intermediate_2 =
std::find_if(data.ocsp_request_data_list.begin(), data.ocsp_request_data_list.end(), [](const auto& ocsp_data) {
return ocsp_data.certificate_hash_data.value().debug_common_name == std::string("CPOSubCA2");
}) != data.ocsp_request_data_list.end();
ASSERT_TRUE(has_intermediate_2);
}
TEST_F(EvseSecurityTests, verify_ocsp_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(), 3);
// 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/client/cso/ocsp";
ASSERT_TRUE(fs::exists(ocsp_path));
for (auto& ocsp : data.ocsp_request_data_list) {
std::optional<std::string> 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, 6); // 3 for hash, 3 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<std::string> 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, 6); // 6 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(), 3);
// 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<fs::path> 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, 10);
// 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());
// List of date sorted certificates
std::vector<X509Wrapper> sorted;
std::vector<fs::path> sorted_should_delete;
std::vector<fs::path> sorted_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) {
sorted_should_delete.push_back(cert.get_file().value());
} else {
sorted_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 : sorted_should_delete) {
ASSERT_FALSE(fs::exists(deleted));
}
for (const auto& not_deleted : sorted_should_keep) {
fs::path key_file = not_deleted;
key_file.replace_extension(".key");
ASSERT_TRUE(fs::exists(not_deleted));
// Ignore the CPO chain that does not have a properly
if (not_deleted.string().find("CPO_CERT_CHAIN") != std::string::npos) {
key_file = "certs/client/cso/SECC_LEAF.key";
}
// Check their respective keys exist
std::cout << key_file;
ASSERT_TRUE(fs::exists(key_file));
X509Wrapper cert = X509CertificateBundle(not_deleted, EncodingFormat::PEM).split().at(0);
fsstd::ifstream file(key_file, std::ios::binary);
std::string private_key((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
ASSERT_EQ(KeyValidationResult::Valid, CryptoSupplier::x509_check_private_key(
cert.get(), private_key, evse_security->private_key_password));
}
}
}
TEST_F(EvseSecurityTests, verify_expired_csr_deletion) {
// Generate a CSR
auto 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_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);
}
TEST_F(EvseSecurityTestsMulti, verify_with_multiple_leaf_types_accepts_valid_cert) {
const std::string certificate_chain = read_file_to_string("certs/client/csms/SECC_LEAF_GRIDSYNC.pem");
// Validate with both V2G and CSMS trust anchors
std::vector<LeafCertificateType> types = {LeafCertificateType::V2G, LeafCertificateType::CSMS};
auto result = this->evse_security->verify_certificate(certificate_chain, types);
ASSERT_EQ(result, CertificateValidationResult::Valid);
}
TEST_F(EvseSecurityTestsMulti, verify_with_missing_trust_anchor_fails) {
const std::string certificate_chain = read_file_to_string("certs/client/csms/SECC_LEAF_GRIDSYNC.pem");
// Intentionally omit the correct anchor (V2G or CSMS)
std::vector<LeafCertificateType> types = {LeafCertificateType::MO};
auto result = this->evse_security->verify_certificate(certificate_chain, types);
ASSERT_NE(result, CertificateValidationResult::Valid);
ASSERT_EQ(result, CertificateValidationResult::IssuerNotFound); // since MO is empty
}
TEST_F(EvseSecurityTestsMulti, verify_with_invalid_cert_fails) {
const std::string invalid_cert = "-----BEGIN CERTIFICATE-----\nINVALID\n-----END CERTIFICATE-----";
std::vector<LeafCertificateType> types = {LeafCertificateType::CSMS, LeafCertificateType::V2G};
auto result = this->evse_security->verify_certificate(invalid_cert, types);
ASSERT_EQ(result, CertificateValidationResult::Unknown);
}
} // namespace evse_security
// FIXME(piet): Add more tests for getRootCertificateHashData (incl. V2GCertificateChain etc.)