Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support hashing a certificate directory #96

Merged
merged 7 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)));

Check notice on line 76 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L76

C-style pointer casting
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)));

Check notice on line 100 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L100

C-style pointer casting
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);

Check failure on line 118 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L118

The `memcpy` family of functions require the developer to validate that the destination buffer is the same size or larger than the source buffer.
}
}

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;

Check notice on line 128 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L128

The scope of the variable 'ch' can be reduced.
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]);

Check notice on line 144 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L144

Does not handle strings that are not \0-terminated; if given one it may perform an over-read (it could cause a crash if unprotected) (CWE-126).

Check failure on line 144 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L144

The `strlen` family of functions does not handle strings that are not null terminated.

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

n = readlink(fullpath, linktarget, sizeof(linktarget));

Check warning on line 150 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L150

Usage of the `readlink` function call hints at a potential Time Of Check Time Of Use (TOCTOU) vulnerability.
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));

Check notice on line 156 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L156

Does not handle strings that are not \0-terminated; if given one it may perform an over-read (it could cause a crash if unprotected) (CWE-126).
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;

Check notice on line 163 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L163

The scope of the variable 'x' can be reduced.
BIO* b;
const char* ext;
unsigned char digest[EVP_MAX_MD_SIZE];

Check notice on line 166 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L166

The scope of the variable 'digest' can be reduced.
X509_NAME* name = NULL;
int i, type, ret = -1;

Check notice on line 168 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L168

The scope of the variable 'type' can be reduced.

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) {

Check warning on line 224 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L224

Usage of the `access` function call hints at a potential Time Of Check Time Of Use (TOCTOU) vulnerability.
EVLOG_error << "Access denied '" << std::string(dirname) << "'";
return -1;
}

buflen = strlen(dirname);

Check notice on line 229 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L229

Does not handle strings that are not \0-terminated; if given one it may perform an over-read (it could cause a crash if unprotected) (CWE-126).

Check failure on line 229 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L229

The `strlen` family of functions does not handle strings that are not null terminated.
pathsep = (buflen && dirname[buflen - 1] == '/') ? "" : "/";
buflen += NAME_MAX + 2;
buf = (char*)(malloc(buflen));

Check notice on line 232 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L232

C-style pointer casting
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);

Check warning on line 263 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L263

When handling sensitive information in a buffer, it's important to ensure that the data is securely erased before the buffer is deleted or reused.
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)) << ":";

Check notice on line 271 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L271

Does not handle strings that are not \0-terminated; if given one it may perform an over-read (it could cause a crash if unprotected) (CWE-126).

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)) << " -> "

Check notice on line 276 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L276

Does not handle strings that are not \0-terminated; if given one it may perform an over-read (it could cause a crash if unprotected) (CWE-126).

Check failure on line 276 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L276

The `strlen` family of functions does not handle strings that are not null terminated.
<< std::string(buf, strlen(buf));

Check notice on line 277 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L277

Does not handle strings that are not \0-terminated; if given one it may perform an over-read (it could cause a crash if unprotected) (CWE-126).

Check failure on line 277 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L277

The `strlen` family of functions does not handle strings that are not null terminated.
} 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));

Check notice on line 286 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L286

Does not handle strings that are not \0-terminated; if given one it may perform an over-read (it could cause a crash if unprotected) (CWE-126).

Check failure on line 286 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L286

The `strlen` family of functions does not handle strings that are not null terminated.
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));

Check notice on line 293 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L293

Does not handle strings that are not \0-terminated; if given one it may perform an over-read (it could cause a crash if unprotected) (CWE-126).

Check failure on line 293 in 3rd_party/cert_rehash/c_rehash.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

3rd_party/cert_rehash/c_rehash.hpp#L293

The `strlen` family of functions does not handle strings that are not null terminated.
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
/// @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