Skip to content

Commit

Permalink
Added cleanup for OCSP
Browse files Browse the repository at this point in the history
Signed-off-by: AssemblyJohn <[email protected]>
  • Loading branch information
AssemblyJohn committed Apr 26, 2024
1 parent 30bbac7 commit ef65bfa
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 44 deletions.
14 changes: 14 additions & 0 deletions include/evse_security/certificate/x509_hierarchy.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once

#include <algorithm>
#include <queue>

#include <evse_security/certificate/x509_wrapper.hpp>
Expand Down Expand Up @@ -110,6 +111,19 @@ class X509CertificateHierarchy {
/// hierarchy can be incomplete, in case orphan certificates are present in the list
static X509CertificateHierarchy build_hierarchy(std::vector<X509Wrapper>& certificates);

template <typename... Args> static X509CertificateHierarchy build_hierarchy(Args... certificates) {
X509CertificateHierarchy ordered;

(std::for_each(certificates.begin(), certificates.end(),
[&ordered](X509Wrapper& cert) { ordered.insert(std::move(cert)); }),
...); // Fold expr

// Prune the tree
ordered.prune();

return ordered;
}

private:
/// @brief Inserts the certificate in the hierarchy. If it is not a root
/// and a parent is not found, it will be inserted as a temporary orphan
Expand Down
5 changes: 2 additions & 3 deletions include/evse_security/evse_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,9 @@ struct CertificateInfo {
fs::path key; ///< The path of the PEM or DER encoded private key
std::optional<fs::path> certificate; ///< The path of the PEM or DER encoded certificate chain if found
std::optional<fs::path> certificate_single; ///< The path of the PEM or DER encoded certificate if found
int certificate_count; ///< The count of certificates in the chain, if the chain is available, or if single 1
int certificate_count; ///< The count of certificates, if the chain is available, or 1 if single

Check notice on line 136 in include/evse_security/evse_types.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

include/evse_security/evse_types.hpp#L136

struct member 'CertificateInfo::certificate_count' is never used.
std::optional<std::string> password; ///< Specifies the password for the private key if encrypted
std::vector<CertificateOCSP>
oscsp; ///< Contains the ordered list of OCSP certificate data based on the chain file order
std::vector<CertificateOCSP> oscsp; ///< The ordered list of OCSP certificate data based on the chain file order

Check notice on line 138 in include/evse_security/evse_types.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

include/evse_security/evse_types.hpp#L138

struct member 'CertificateInfo::oscsp' is never used.
};

struct GetCertificateInfoResult {
Expand Down
4 changes: 4 additions & 0 deletions include/evse_security/utils/evse_filesystem.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ std::string get_random_file_name(const std::string& extension);
/// @return True if we could read, false otherwise
bool read_hash_from_file(const fs::path& file_path, CertificateHashData& out_hash);

/// @brief Attempts to write a certificate hash to a file
/// @return True if we could write, false otherwise
bool write_hash_to_file(const fs::path& file_path, const CertificateHashData& hash);

} // namespace evse_security::filesystem_utils
129 changes: 88 additions & 41 deletions lib/evse_security/evse_security.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,8 @@ DeleteCertificateResult EvseSecurity::delete_certificate(const CertificateHashDa
X509CertificateBundle root_bundle(ca_bundle_path_map[load], EncodingFormat::PEM);
X509CertificateBundle leaf_bundle(leaf_certificate_path, EncodingFormat::PEM);

auto full_list = root_bundle.split();
for (X509Wrapper& certif : leaf_bundle.split()) {
full_list.push_back(std::move(certif));
}

X509CertificateHierarchy hierarchy = X509CertificateHierarchy::build_hierarchy(full_list);
X509CertificateHierarchy hierarchy =
std::move(X509CertificateHierarchy::build_hierarchy(root_bundle.split(), leaf_bundle.split()));

EVLOG_debug << "Delete hierarchy:(" << leaf_certificate_path.string() << ")\n"
<< hierarchy.to_debug_string();
Expand Down Expand Up @@ -729,10 +725,9 @@ OCSPRequestDataList get_ocsp_request_data_internal(fs::path& root_path, std::vec

try {
std::vector<X509Wrapper> full_hierarchy = X509CertificateBundle(root_path, EncodingFormat::PEM).split();
std::move(std::begin(leaf_chain), std::end(leaf_chain), std::back_inserter(full_hierarchy));

// Build the full hierarchy
auto hierarchy = X509CertificateHierarchy::build_hierarchy(full_hierarchy);
auto hierarchy = std::move(X509CertificateHierarchy::build_hierarchy(full_hierarchy, leaf_chain));

// Search for the first valid root, and collect all the chain
for (auto& root : hierarchy.get_hierarchy()) {
Expand Down Expand Up @@ -780,10 +775,14 @@ void EvseSecurity::update_ocsp_cache(const CertificateHashData& certificate_hash

// TODO(ioan): shouldn't we also do this for the MO?
const auto ca_bundle_path = this->ca_bundle_path_map.at(CaCertificateType::V2G);
auto leaf_cert_dir = this->directories.secc_leaf_cert_directory; // V2G leafs

try {
X509CertificateBundle ca_bundle(ca_bundle_path, EncodingFormat::PEM);
auto& certificate_hierarchy = ca_bundle.get_certficate_hierarchy();
X509CertificateBundle leaf_bundle(leaf_cert_dir, EncodingFormat::PEM);

auto certificate_hierarchy =
std::move(X509CertificateHierarchy::build_hierarchy(ca_bundle.split(), leaf_bundle.split()));

// TODO: If we already have the hash, over-write, else create a new one
try {
Expand All @@ -798,7 +797,7 @@ void EvseSecurity::update_ocsp_cache(const CertificateHashData& certificate_hash
fs::create_directories(ocsp_path);
} else {
// Iterate existing hashes
for (const auto& hash_entry : fs::recursive_directory_iterator(ocsp_path)) {
for (const auto& hash_entry : fs::directory_iterator(ocsp_path)) {
if (hash_entry.is_regular_file()) {
CertificateHashData read_hash;

Expand All @@ -810,7 +809,8 @@ void EvseSecurity::update_ocsp_cache(const CertificateHashData& certificate_hash
fs::path ocsp_path = hash_entry.path();
ocsp_path.replace_extension(CERT_HASH_EXTENSION);

std::ofstream fs(ocsp_path.c_str());
// Discard previous content
std::ofstream fs(ocsp_path.c_str(), std::ios::trunc);
fs << ocsp_response;
fs.close();

Expand All @@ -827,16 +827,17 @@ void EvseSecurity::update_ocsp_cache(const CertificateHashData& certificate_hash
const auto hash_file_path = ocsp_path / name / CERT_HASH_EXTENSION;

// Write out OCSP data
std::ofstream fs(ocsp_file_path.c_str());
fs << ocsp_response;
fs.close();
try {
std::ofstream fs(ocsp_file_path.c_str());
fs << ocsp_response;
fs.close();
} catch (const std::exception& e) {
EVLOG_error << "Could not write OCSP certificate data!";
}

// Write out the related hash
std::ofstream hs(hash_file_path.c_str());
hs << certificate_hash_data.issuer_name_hash;
hs << certificate_hash_data.issuer_key_hash;
hs << certificate_hash_data.serial_number;
hs.close();
if (false == filesystem_utils::write_hash_to_file(hash_file_path, certificate_hash_data)) {
EVLOG_error << "Could not write OCSP certificate hash!";
}

EVLOG_debug << "OCSP certificate hash not found, written at path: " << ocsp_file_path;
} else {
Expand Down Expand Up @@ -882,7 +883,7 @@ EvseSecurity::retrieve_ocsp_cache_internal(const CertificateHashData& certificat
if (filesystem_utils::read_hash_from_file(ocsp_entry.path(), read_hash) &&
(read_hash == certificate_hash_data)) {
fs::path replaced_ext = ocsp_entry.path();
replaced_ext.replace_extension(".der");
replaced_ext.replace_extension(DER_EXTENSION);

std::ifstream in_fs(replaced_ext.c_str());
std::string ocsp_response;
Expand Down Expand Up @@ -1024,19 +1025,24 @@ GetCertificateInfoResult EvseSecurity::get_leaf_certificate_info_internal(LeafCe

fs::path key_dir;
fs::path cert_dir;
CaCertificateType root_type;

if (certificate_type == LeafCertificateType::CSMS) {
key_dir = this->directories.csms_leaf_key_directory;
cert_dir = this->directories.csms_leaf_cert_directory;
root_type = CaCertificateType::CSMS;
} else if (certificate_type == LeafCertificateType::V2G) {
key_dir = this->directories.secc_leaf_key_directory;
cert_dir = this->directories.secc_leaf_cert_directory;
root_type = CaCertificateType::V2G;
} else {
EVLOG_warning << "Rejected attempt to retrieve non CSMS/V2G key pair";
result.status = GetCertificateInfoStatus::Rejected;
return result;
}

fs::path root_dir = ca_bundle_path_map[root_type];

// choose appropriate cert (valid_from / valid_to)
try {
auto leaf_certificates = X509CertificateBundle(cert_dir, EncodingFormat::PEM);
Expand Down Expand Up @@ -1102,7 +1108,10 @@ GetCertificateInfoResult EvseSecurity::get_leaf_certificate_info_internal(LeafCe
fs::path chain_file;

X509CertificateBundle leaf_directory(cert_dir, EncodingFormat::PEM);
auto& hierarchy = leaf_directory.get_certficate_hierarchy();
X509CertificateBundle root_bundle(root_dir, EncodingFormat::PEM); // Required for hierarchy

auto hierarchy =
std::move(X509CertificateHierarchy::build_hierarchy(root_bundle.split(), leaf_directory.split()));

const std::vector<X509Wrapper>* leaf_fullchain = nullptr;
const std::vector<X509Wrapper>* leaf_single = nullptr;
Expand Down Expand Up @@ -1509,12 +1518,12 @@ void EvseSecurity::garbage_collect() {

EVLOG_info << "Starting garbage collect!";

std::vector<std::pair<fs::path, fs::path>> leaf_paths;
std::vector<std::tuple<fs::path, fs::path, CaCertificateType>> leaf_paths;

leaf_paths.push_back(
std::make_pair(this->directories.csms_leaf_cert_directory, this->directories.csms_leaf_key_directory));
leaf_paths.push_back(
std::make_pair(this->directories.secc_leaf_cert_directory, this->directories.secc_leaf_key_directory));
leaf_paths.push_back(std::make_tuple(this->directories.csms_leaf_cert_directory,
this->directories.csms_leaf_key_directory, CaCertificateType::CSMS));
leaf_paths.push_back(std::make_tuple(this->directories.secc_leaf_cert_directory,
this->directories.secc_leaf_key_directory, CaCertificateType::V2G));

// Delete certificates first, give the option to cleanup the dangling keys afterwards
std::set<fs::path> invalid_certificate_files;
Expand All @@ -1523,7 +1532,9 @@ void EvseSecurity::garbage_collect() {
std::set<fs::path> protected_private_keys;

// Order by latest valid, and keep newest with a safety limit
for (auto const& [cert_dir, key_dir] : leaf_paths) {
for (auto const& [cert_dir, key_dir, ca_type] : leaf_paths) {
// Root bundle required for hash of OCSP cache
X509CertificateBundle root_bundle(ca_bundle_path_map[ca_type], EncodingFormat::PEM);
X509CertificateBundle expired_certs(cert_dir, EncodingFormat::PEM);

// Only handle if we have more than the minimum certificates entry
Expand All @@ -1533,26 +1544,62 @@ void EvseSecurity::garbage_collect() {

// Order by expiry date, and keep even expired certificates with a minimum of 10 certificates
expired_certs.for_each_chain_ordered(
[this, &invalid_certificate_files, &skipped, &key_directory,
&protected_private_keys](const fs::path& file, const std::vector<X509Wrapper>& chain) {
[this, &invalid_certificate_files, &skipped, &key_directory, &protected_private_keys,
&root_bundle](const fs::path& file, const std::vector<X509Wrapper>& chain) {
// By default delete all empty
if (chain.size() <= 0) {
invalid_certificate_files.emplace(file);
}

if (++skipped > DEFAULT_MINIMUM_CERTIFICATE_ENTRIES) {
if (chain.empty()) {
return true;
}

// If the chain contains the first expired (leafs are the first)
if (chain.size()) {
if (chain[0].is_expired()) {
invalid_certificate_files.emplace(file);

// Also attempt to add the key for deletion
try {
fs::path key_file = get_private_key_path_of_certificate(chain[0], key_directory,
this->private_key_password);
invalid_certificate_files.emplace(key_file);
} catch (NoPrivateKeyException& e) {
if (chain[0].is_expired()) {
invalid_certificate_files.emplace(file);

// Also attempt to add the key for deletion
try {
fs::path key_file = get_private_key_path_of_certificate(chain[0], key_directory,
this->private_key_password);
invalid_certificate_files.emplace(key_file);
} catch (NoPrivateKeyException& e) {
}

auto leaf_chain = chain;
X509CertificateHierarchy hierarchy =
std::move(X509CertificateHierarchy::build_hierarchy(root_bundle.split(), leaf_chain));

try {
CertificateHashData ocsp_hash = hierarchy.get_certificate_hash(chain[0]);

// Find OCSP cache with hash
if (chain[0].get_file().has_value()) {
const auto ocsp_path = chain[0].get_file().value().parent_path() / "ocsp";

if (fs::exists(ocsp_path)) {
for (const auto& hash_entry : fs::directory_iterator(ocsp_path)) {
if (hash_entry.is_regular_file() == false) {
continue;
}
// Attempt hash read
CertificateHashData read_hash;

if (filesystem_utils::read_hash_from_file(hash_entry.path(), read_hash) &&
read_hash == ocsp_hash) {

auto oscp_data_path = hash_entry.path();
oscp_data_path.replace_extension(CERT_HASH_EXTENSION);

invalid_certificate_files.emplace(hash_entry.path());
invalid_certificate_files.emplace(oscp_data_path);
}
}
}
}
} catch (const NoCertificateFound& e) {
}
}
} else {
Expand Down Expand Up @@ -1597,7 +1644,7 @@ void EvseSecurity::garbage_collect() {
// at a further invocation after the GC timer will elapse a few times. This behavior
// was added so that if we have a reset and the CSMS sends us a CSR response while we were
// down it should still be processed when we boot up and NOT delete the CSRs
for (auto const& [cert_dir, keys_dir] : leaf_paths) {
for (auto const& [cert_dir, keys_dir, ca_type] : leaf_paths) {
fs::path cert_path = cert_dir;
fs::path key_path = keys_dir;

Expand Down
24 changes: 24 additions & 0 deletions lib/evse_security/utils/evse_filesystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,28 @@ bool read_hash_from_file(const fs::path& file_path, CertificateHashData& out_has
return false;
}

bool write_hash_to_file(const fs::path& file_path, const CertificateHashData& hash) {
auto real_path = file_path;

if (file_path.has_extension() == false || file_path.extension() != CERT_HASH_EXTENSION) {
real_path.replace_extension(CERT_HASH_EXTENSION);
}

try {
// Write out the related hash
std::ofstream hs(real_path.c_str());
hs << hash.issuer_name_hash;
hs << hash.issuer_key_hash;
hs << hash.serial_number;
hs.close();

return true;
} catch (const std::exception& e) {
EVLOG_error << "Unknown error occurred writing cert hash file: " << file_path;
return false;
}

return false;
}

} // namespace evse_security::filesystem_utils
2 changes: 2 additions & 0 deletions tests/tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,8 @@ TEST_F(EvseSecurityTestsExpired, verify_expired_leaf_deletion) {
// Garbage collect
evse_security->garbage_collect();

// TODO: (ioan) test OCSP cache deletion

// Ensure that we have 10 certificates, since we only keep 10, the newest
{
X509CertificateBundle full_certs(fs::path("certs/client/cso"), EncodingFormat::PEM);
Expand Down

0 comments on commit ef65bfa

Please sign in to comment.