Skip to content

Commit

Permalink
Support hashing a certificate directory (#96)
Browse files Browse the repository at this point in the history
* Add the method to return either a certification file or a directory of certficates
* Support hashing a certificate directory 

---------

Signed-off-by: Ivan Rogach <[email protected]>
  • Loading branch information
jannejy authored Nov 26, 2024
1 parent ebef3e1 commit 9ba21d4
Show file tree
Hide file tree
Showing 6 changed files with 362 additions and 0 deletions.
310 changes: 310 additions & 0 deletions 3rd_party/cert_rehash/c_rehash.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
/* c_rehash.c - Create hash symlinks for certificates
* C implementation based on the original Perl and shell versions
*
* Copyright (c) 2013-2014 Timo Teräs <[email protected]>
* All rights reserved.
*
* This software is licensed under the MIT License.
* Full license available at: http://opensource.org/licenses/MIT
*/

#include <dirent.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/x509.h>

#include <everest/logging.hpp>

#define MAX_COLLISIONS 256
#define countof(x) (sizeof(x) / sizeof(x[0]))

namespace evse_security {

struct entry_info {
struct entry_info* next;
char* filename;
unsigned short old_id;
unsigned char need_symlink;
unsigned char digest[EVP_MAX_MD_SIZE];
};

struct bucket_info {
struct bucket_info* next;
struct entry_info *first_entry, *last_entry;
unsigned int hash;
unsigned short type;
unsigned short num_needed;
};

enum Type {
TYPE_CERT = 0,
TYPE_CRL
};

static const char* symlink_extensions[] = {"", "r"};
static const char* file_extensions[] = {"pem", "crt", "cer", "crl"};

static int evpmdsize;
static const EVP_MD* evpmd;

static struct bucket_info* hash_table[257];

static void bit_set(unsigned char* set, unsigned bit) {
set[bit / 8] |= 1 << (bit % 8);
}

static int bit_isset(unsigned char* set, unsigned bit) {
return set[bit / 8] & (1 << (bit % 8));
}

static void add_entry(int type, unsigned int hash, const char* filename, const unsigned char* digest, int need_symlink,
unsigned short old_id) {
struct bucket_info* bi;
struct entry_info *ei, *found = NULL;
unsigned int ndx = (type + hash) % countof(hash_table);

for (bi = hash_table[ndx]; bi; bi = bi->next)
if (bi->type == type && bi->hash == hash)
break;
if (!bi) {
bi = (bucket_info*)(calloc(1, sizeof(*bi)));
if (!bi)
return;
bi->next = hash_table[ndx];
bi->type = type;
bi->hash = hash;
hash_table[ndx] = bi;
}

for (ei = bi->first_entry; ei; ei = ei->next) {
if (digest && memcmp(digest, ei->digest, evpmdsize) == 0) {
EVLOG_warning << "Skipping duplicate certificate in file " << std::string(filename);
return;
}
if (!strcmp(filename, ei->filename)) {
found = ei;
if (!digest)
break;
}
}
ei = found;
if (!ei) {
if (bi->num_needed >= MAX_COLLISIONS)
return;
ei = (entry_info*)(calloc(1, sizeof(*ei)));
if (!ei)
return;

ei->old_id = ~0;
ei->filename = strdup(filename);
if (bi->last_entry)
bi->last_entry->next = ei;
if (!bi->first_entry)
bi->first_entry = ei;
bi->last_entry = ei;
}

if (old_id < ei->old_id)
ei->old_id = old_id;
if (need_symlink && !ei->need_symlink) {
ei->need_symlink = 1;
bi->num_needed++;
memcpy(ei->digest, digest, evpmdsize);
}
}

static int handle_symlink(const char* filename, const char* fullpath) {
static char xdigit[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11,
12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15};
char linktarget[NAME_MAX], *endptr;
unsigned int hash = 0;
unsigned char ch;
int i, type, id;
ssize_t n;

for (i = 0; i < 8; i++) {
ch = filename[i] - '0';
if (ch >= countof(xdigit) || xdigit[ch] < 0)
return -1;
hash <<= 4;
hash += xdigit[ch];
}
if (filename[i++] != '.')
return -1;
for (type = countof(symlink_extensions) - 1; type > 0; type--)
if (strcasecmp(symlink_extensions[type], &filename[i]) == 0)
break;
i += strlen(symlink_extensions[type]);

id = strtoul(&filename[i], &endptr, 10);
if (*endptr != 0)
return -1;

n = readlink(fullpath, linktarget, sizeof(linktarget));
if (n >= sizeof(linktarget) || n < 0)
return -1;
linktarget[n] = 0;

EVLOG_debug << "Found existing symlink " << std::string(filename) << " for " << hash << " (" << type
<< "), certname " << std::string(linktarget, strlen(linktarget));
add_entry(type, hash, linktarget, NULL, 0, id);
return 0;
}

static int handle_certificate(const char* filename, const char* fullpath) {
STACK_OF(X509_INFO) * inf;
X509_INFO* x;
BIO* b;
const char* ext;
unsigned char digest[EVP_MAX_MD_SIZE];
X509_NAME* name = NULL;
int i, type, ret = -1;

ext = strrchr(filename, '.');
if (ext == NULL)
return 0;
for (i = 0; i < countof(file_extensions); i++) {
if (strcasecmp(file_extensions[i], ext + 1) == 0)
break;
}
if (i >= countof(file_extensions))
return -1;

b = BIO_new_file(fullpath, "r");
if (!b)
return -1;
inf = PEM_X509_INFO_read_bio(b, NULL, NULL, NULL);
BIO_free(b);
if (!inf)
return -1;

if (sk_X509_INFO_num(inf) == 1) {
x = sk_X509_INFO_value(inf, 0);
if (x->x509) {
type = TYPE_CERT;
name = X509_get_subject_name(x->x509);
X509_digest(x->x509, evpmd, digest, NULL);
} else if (x->crl) {
type = TYPE_CRL;
name = X509_CRL_get_issuer(x->crl);
X509_CRL_digest(x->crl, evpmd, digest, NULL);
}
if (name)
add_entry(type, X509_NAME_hash(name), filename, digest, 1, ~0);
} else {
EVLOG_warning << std::string(filename) << " does not contain exactly one certificate or CRL: skipping";
}

sk_X509_INFO_pop_free(inf, X509_INFO_free);

return ret;
}

static int hash_dir(const char* dirname) {
struct bucket_info *bi, *nextbi;
struct entry_info *ei, *nextei;
struct dirent* de;
struct stat st;
unsigned char idmask[MAX_COLLISIONS / 8];
int i, n, nextid, buflen, ret = -1;
const char* pathsep;
char* buf;
DIR* d;

evpmd = EVP_sha1();
evpmdsize = EVP_MD_size(evpmd);

if (access(dirname, R_OK | W_OK | X_OK) != 0) {
EVLOG_error << "Access denied '" << std::string(dirname) << "'";
return -1;
}

buflen = strlen(dirname);
pathsep = (buflen && dirname[buflen - 1] == '/') ? "" : "/";
buflen += NAME_MAX + 2;
buf = (char*)(malloc(buflen));
if (buf == NULL)
goto err;

EVLOG_debug << "Doing " << std::string(dirname);
d = opendir(dirname);
if (!d)
goto err;

while ((de = readdir(d)) != NULL) {
if (snprintf(buf, buflen, "%s%s%s", dirname, pathsep, de->d_name) >= buflen)
continue;
if (lstat(buf, &st) < 0)
continue;
if (S_ISLNK(st.st_mode) && handle_symlink(de->d_name, buf) == 0)
continue;
if (strcmp(buf, "/etc/ssl/certs/ca-certificates.crt") == 0) {
/* Ignore the /etc/ssl/certs/ca-certificates.crt file */
EVLOG_debug << "Skipping /etc/ssl/certs/ca-certificates.crt file";
continue;
}
handle_certificate(de->d_name, buf);
}
closedir(d);

for (i = 0; i < countof(hash_table); i++) {
for (bi = hash_table[i]; bi; bi = nextbi) {
nextbi = bi->next;
EVLOG_debug << "Type " << bi->type << " hash " << bi->hash << " num entries " << bi->num_needed << ":";

nextid = 0;
memset(idmask, 0, (bi->num_needed + 7) / 8);
for (ei = bi->first_entry; ei; ei = ei->next)
if (ei->old_id < bi->num_needed)
bit_set(idmask, ei->old_id);

for (ei = bi->first_entry; ei; ei = nextei) {
nextei = ei->next;
EVLOG_debug << "\t(old_id " << ei->old_id << ", need_symlink " << static_cast<int>(ei->need_symlink)
<< ") Cert " << std::string(ei->filename, strlen(ei->filename)) << ":";

if (ei->old_id < bi->num_needed) {
/* Link exists, and is used as-is */
snprintf(buf, buflen, "%08x.%s%d", bi->hash, symlink_extensions[bi->type], ei->old_id);
EVLOG_debug << "link " << std::string(ei->filename, strlen(ei->filename)) << " -> "
<< std::string(buf, strlen(buf));
} else if (ei->need_symlink) {
/* New link needed (it may replace something) */
while (bit_isset(idmask, nextid))
nextid++;

snprintf(buf, buflen, "%s%s%n%08x.%s%d", dirname, pathsep, &n, bi->hash,
symlink_extensions[bi->type], nextid);
EVLOG_debug << "link " << std::string(ei->filename, strlen(ei->filename)) << " -> "
<< std::string(buf + n, strlen(buf + n));
unlink(buf);
symlink(ei->filename, buf);
} else {
/* Link to be deleted */
snprintf(buf, buflen, "%s%s%n%08x.%s%d", dirname, pathsep, &n, bi->hash,
symlink_extensions[bi->type], ei->old_id);
EVLOG_debug << "unlink " << std::string(buf + n, strlen(buf + n));
unlink(buf);
}
free(ei->filename);
free(ei);
}
free(bi);
}
hash_table[i] = NULL;
}

ret = 0;
err:
free(buf);
return ret;
}

} // namespace evse_security
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ if (EVSE_SECURITY_INSTALL)
PATTERN "detail" EXCLUDE
)

install(
DIRECTORY 3rd_party/
TYPE INCLUDE
)

evc_setup_package(
NAME everest-evse_security
NAMESPACE everest
Expand Down
7 changes: 7 additions & 0 deletions include/evse_security/evse_security.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,13 @@ class EvseSecurity {
/// @return CA certificate file
std::string get_verify_file(CaCertificateType certificate_type);

/// @brief Retrieves the PEM formatted CA bundle location for the given \p certificate_type It is not recommended to
/// add the SUBCAs to any root certificate bundle, but to leave them in the leaf file. Returns either file
/// or directory where the certificates are located. In the case of directory, does also rehashing the directory.
/// @param certificate_type
/// @return CA certificate location
std::string get_verify_location(CaCertificateType certificate_type);

/// @brief An extension of 'get_verify_file' with error handling included
GetCertificateInfoResult get_ca_certificate_info(CaCertificateType certificate_type);

Expand Down
1 change: 1 addition & 0 deletions lib/evse_security/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ endif()
target_include_directories(evse_security
PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/3rd_party>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

Expand Down
32 changes: 32 additions & 0 deletions lib/evse_security/evse_security.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include <set>
#include <stdio.h>

#include <cert_rehash/c_rehash.hpp>

#include <evse_security/certificate/x509_bundle.hpp>
#include <evse_security/certificate/x509_hierarchy.hpp>
#include <evse_security/certificate/x509_wrapper.hpp>
Expand Down Expand Up @@ -1555,6 +1557,36 @@ std::string EvseSecurity::get_verify_file(CaCertificateType certificate_type) {
return {};
}

std::string EvseSecurity::get_verify_location(CaCertificateType certificate_type) {

std::lock_guard<std::mutex> guard(EvseSecurity::security_mutex);

try {
// Support bundle files, in case the certificates contain
// multiple entries (should be 3) as per the specification
X509CertificateBundle verify_location(this->ca_bundle_path_map.at(certificate_type), EncodingFormat::PEM);

const auto location_path = verify_location.get_path();

EVLOG_info << "Requesting certificate location: ["
<< conversions::ca_certificate_type_to_string(certificate_type) << "] location:" << location_path;

if (!verify_location.empty() &&
(!verify_location.is_using_directory() || hash_dir(location_path.c_str()) == 0)) {
return location_path;
}

} catch (const CertificateLoadException& e) {
EVLOG_error << "Could not obtain verify location, wrong format for certificate: "
<< this->ca_bundle_path_map.at(certificate_type) << " with error: " << e.what();
}

EVLOG_error << "Could not find any CA certificate for: "
<< conversions::ca_certificate_type_to_string(certificate_type);

return {};
}

int EvseSecurity::get_leaf_expiry_days_count(LeafCertificateType certificate_type) {
std::lock_guard<std::mutex> guard(EvseSecurity::security_mutex);

Expand Down
Loading

0 comments on commit 9ba21d4

Please sign in to comment.