- 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
1450 lines
66 KiB
C++
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.)
|