// SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef USING_TPM2 // updates so that existing tests run with the OpenSSLProvider #include #include 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 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(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 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 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 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(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(file)), std::istreambuf_iterator()); std::vector 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 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 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 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 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 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 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 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 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 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 sorted; std::vector sorted_should_delete; std::vector 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& 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(file)), std::istreambuf_iterator()); 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 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 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 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.)