Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
60
tools/EVerest-main/lib/everest/evse_security/.clang-tidy
Normal file
60
tools/EVerest-main/lib/everest/evse_security/.clang-tidy
Normal file
@@ -0,0 +1,60 @@
|
||||
Checks: >
|
||||
*,
|
||||
bugprone-*,
|
||||
cert-*,
|
||||
concurrency-*,
|
||||
cppcoreguidelines-*,
|
||||
misc-*,
|
||||
performance-*,
|
||||
-abseil-*,
|
||||
-altera-*,
|
||||
-android-*,
|
||||
-boost-*,
|
||||
-fuchsia-*,
|
||||
-google-*,
|
||||
-hicpp-*,
|
||||
-llvm-*,
|
||||
-llvmlibc-*,
|
||||
-zircon-*,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-cert-err60-cpp,
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||
-cppcoreguidelines-use-default-member-init,
|
||||
-misc-include-cleaner,
|
||||
-misc-non-private-member-variables-in-classes,
|
||||
-modernize-pass-by-value,
|
||||
-modernize-use-trailing-return-type,
|
||||
-modernize-use-default-member-init,
|
||||
-modernize-return-braced-init-list,
|
||||
-modernize-use-scoped-lock,
|
||||
-modernize-concat-nested-namespaces,
|
||||
-modernize-use-nodiscard,
|
||||
-modernize-raw-string-literal,
|
||||
-performance-avoid-endl,
|
||||
-performance-enum-size,
|
||||
-performance-unnecessary-value-param,
|
||||
-readability-avoid-const-params-in-decls,
|
||||
-readability-function-cognitive-complexity,
|
||||
-readability-identifier-length,
|
||||
-readability-magic-numbers,
|
||||
-readability-qualified-auto,
|
||||
-readability-simplify-boolean-expr,
|
||||
-readability-use-anyofallof,
|
||||
-readability-use-std-min-max,
|
||||
-readability-redundant-inline-specifier,
|
||||
-concurrency-mt-unsafe,
|
||||
-portability-avoid-pragma-once,
|
||||
-portability-template-virtual-member-function,
|
||||
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
||||
-cppcoreguidelines-avoid-do-while,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
-cppcoreguidelines-special-member-functions,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
-cppcoreguidelines-pro-type-member-init,
|
||||
-cert-err58-cpp,
|
||||
# (the last -cppcoreguidelines and -cert-err58-cpp lines are temporary)
|
||||
HeaderFilterRegex: ".*"
|
||||
CheckOptions:
|
||||
- { key: performance-unnecessary-value-param.AllowedTypes, value: ((std::shared_ptr)) }
|
||||
- { key: bugprone-implicit-widening-of-multiplication-result.IgnoreConstantIntExpr, value: true }
|
||||
3
tools/EVerest-main/lib/everest/evse_security/.github/CODEOWNERS
vendored
Normal file
3
tools/EVerest-main/lib/everest/evse_security/.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
* @pietfried @hikinggrass @james-ctc
|
||||
|
||||
/.github/ @pietfried @hikinggrass
|
||||
8
tools/EVerest-main/lib/everest/evse_security/.gitignore
vendored
Normal file
8
tools/EVerest-main/lib/everest/evse_security/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.*
|
||||
!.clang-format
|
||||
!.clang-tidy
|
||||
!.ci
|
||||
!.github
|
||||
!.gitignore
|
||||
build*
|
||||
clang-tidy-*
|
||||
358
tools/EVerest-main/lib/everest/evse_security/3rd_party/cert_rehash/c_rehash.hpp
vendored
Normal file
358
tools/EVerest-main/lib/everest/evse_security/3rd_party/cert_rehash/c_rehash.hpp
vendored
Normal file
@@ -0,0 +1,358 @@
|
||||
/* 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 <timo.teras@iki.fi>
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is licensed under the MIT License.
|
||||
* Full license available at: http://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include <array>
|
||||
#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>
|
||||
|
||||
constexpr auto MAX_COLLISIONS = 256;
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
struct entry_info {
|
||||
struct entry_info* next;
|
||||
char* filename;
|
||||
unsigned short old_id;
|
||||
unsigned char need_symlink;
|
||||
std::array<unsigned char, EVP_MAX_MD_SIZE> digest;
|
||||
};
|
||||
|
||||
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 std::array<const char*, 2> symlink_extensions = {"", "r"};
|
||||
static const std::array<const char*, 4> file_extensions = {"pem", "crt", "cer", "crl"};
|
||||
|
||||
static int evpmdsize;
|
||||
static const EVP_MD* evpmd;
|
||||
|
||||
static std::array<struct bucket_info*, 257> hash_table;
|
||||
|
||||
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 = nullptr;
|
||||
struct entry_info* ei = nullptr;
|
||||
struct entry_info* found = nullptr;
|
||||
const unsigned int ndx = (type + hash) % hash_table.size();
|
||||
|
||||
for (bi = hash_table.at(ndx); bi != nullptr; bi = bi->next) {
|
||||
if (bi->type == type && bi->hash == hash) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bi == nullptr) {
|
||||
bi = static_cast<bucket_info*>((calloc(1, sizeof(*bi))));
|
||||
if (bi == nullptr) {
|
||||
return;
|
||||
}
|
||||
bi->next = hash_table.at(ndx);
|
||||
bi->type = type;
|
||||
bi->hash = hash;
|
||||
hash_table.at(ndx) = bi;
|
||||
}
|
||||
|
||||
for (ei = bi->first_entry; ei != nullptr; ei = ei->next) {
|
||||
if ((digest != nullptr) && memcmp(digest, ei->digest.data(), evpmdsize) == 0) {
|
||||
EVLOG_warning << "Skipping duplicate certificate in file " << std::string(filename);
|
||||
return;
|
||||
}
|
||||
if (strcmp(filename, ei->filename) == 0) {
|
||||
found = ei;
|
||||
if (digest == nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ei = found;
|
||||
if (ei == nullptr) {
|
||||
if (bi->num_needed >= MAX_COLLISIONS) {
|
||||
return;
|
||||
}
|
||||
ei = static_cast<entry_info*>(calloc(1, sizeof(*ei)));
|
||||
if (ei == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
ei->old_id = ~0;
|
||||
ei->filename = strdup(filename);
|
||||
if (bi->last_entry != nullptr) {
|
||||
bi->last_entry->next = ei;
|
||||
}
|
||||
if (bi->first_entry == nullptr) {
|
||||
bi->first_entry = ei;
|
||||
}
|
||||
bi->last_entry = ei;
|
||||
}
|
||||
|
||||
if (old_id < ei->old_id) {
|
||||
ei->old_id = old_id;
|
||||
}
|
||||
if ((need_symlink != 0) && (ei->need_symlink == 0U)) {
|
||||
ei->need_symlink = 1;
|
||||
bi->num_needed++;
|
||||
memcpy(ei->digest.data(), digest, evpmdsize);
|
||||
}
|
||||
}
|
||||
|
||||
static int handle_symlink(const char* filename, const char* fullpath) {
|
||||
static std::array<const signed char, 55> 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};
|
||||
std::array<char, NAME_MAX> linktarget;
|
||||
char* endptr = nullptr; // NOLINT(misc-const-correctness): would not work with call to strtoul
|
||||
unsigned int hash = 0;
|
||||
unsigned char ch = 0;
|
||||
int i = 0;
|
||||
int type = 0;
|
||||
int id = 0;
|
||||
ssize_t n = 0;
|
||||
|
||||
for (i = 0; i < 8; i++) {
|
||||
ch = filename[i] - '0';
|
||||
if (ch >= xdigit.size() || xdigit.at(ch) < 0) {
|
||||
return -1;
|
||||
}
|
||||
hash <<= 4;
|
||||
hash += xdigit.at(ch);
|
||||
}
|
||||
if (filename[i++] != '.') {
|
||||
return -1;
|
||||
}
|
||||
for (type = symlink_extensions.size() - 1; type > 0; type--) {
|
||||
if (strcasecmp(symlink_extensions.at(type), &filename[i]) == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
i += strlen(symlink_extensions.at(type));
|
||||
|
||||
id = strtoul(&filename[i], &endptr, 10);
|
||||
if (*endptr != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto linktarget_size = linktarget.size() * sizeof(decltype(linktarget)::value_type);
|
||||
n = readlink(fullpath, linktarget.data(), linktarget_size);
|
||||
if (n >= linktarget_size || n < 0) {
|
||||
return -1;
|
||||
}
|
||||
linktarget.at(n) = 0;
|
||||
|
||||
EVLOG_debug << "Found existing symlink " << std::string(filename) << " for " << hash << " (" << type
|
||||
<< "), certname " << std::string(linktarget.data(), strlen(linktarget.data()));
|
||||
add_entry(type, hash, linktarget.data(), nullptr, 0, id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int handle_certificate(const char* filename, const char* fullpath) {
|
||||
STACK_OF(X509_INFO)* inf = nullptr;
|
||||
const X509_INFO* x = nullptr;
|
||||
BIO* b = nullptr;
|
||||
const char* ext = nullptr;
|
||||
std::array<unsigned char, EVP_MAX_MD_SIZE> digest;
|
||||
const X509_NAME* name = nullptr;
|
||||
int i = 0;
|
||||
int type = 0;
|
||||
const int ret = -1;
|
||||
|
||||
ext = strrchr(filename, '.');
|
||||
if (ext == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
for (i = 0; i < file_extensions.size(); i++) {
|
||||
if (strcasecmp(file_extensions.at(i), ext + 1) == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i >= file_extensions.size()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
b = BIO_new_file(fullpath, "r");
|
||||
if (b == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
inf = PEM_X509_INFO_read_bio(b, nullptr, nullptr, nullptr);
|
||||
BIO_free(b);
|
||||
if (inf == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (sk_X509_INFO_num(inf) == 1) {
|
||||
x = sk_X509_INFO_value(inf, 0);
|
||||
if (x->x509 != nullptr) {
|
||||
type = TYPE_CERT;
|
||||
name = X509_get_subject_name(x->x509);
|
||||
X509_digest(x->x509, evpmd, digest.data(), nullptr);
|
||||
} else if (x->crl != nullptr) {
|
||||
type = TYPE_CRL;
|
||||
name = X509_CRL_get_issuer(x->crl);
|
||||
X509_CRL_digest(x->crl, evpmd, digest.data(), nullptr);
|
||||
}
|
||||
if (name != nullptr) {
|
||||
add_entry(type, X509_NAME_hash(name), filename, digest.data(), 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 = nullptr;
|
||||
struct bucket_info* nextbi = nullptr;
|
||||
struct entry_info* ei = nullptr;
|
||||
struct entry_info* nextei = nullptr;
|
||||
struct dirent* de = nullptr;
|
||||
struct stat st;
|
||||
std::array<unsigned char, MAX_COLLISIONS / 8> idmask;
|
||||
int i = 0;
|
||||
int n = 0;
|
||||
int nextid = 0;
|
||||
int buflen = 0;
|
||||
int ret = -1;
|
||||
const char* pathsep = nullptr;
|
||||
char* buf = nullptr;
|
||||
DIR* d = nullptr;
|
||||
|
||||
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 != 0) && dirname[buflen - 1] == '/') ? "" : "/";
|
||||
buflen += NAME_MAX + 2;
|
||||
buf = static_cast<char*>(malloc(buflen));
|
||||
if (buf == nullptr) {
|
||||
goto err;
|
||||
}
|
||||
|
||||
EVLOG_debug << "Doing " << std::string(dirname);
|
||||
d = opendir(dirname);
|
||||
if (d == nullptr) {
|
||||
goto err;
|
||||
}
|
||||
|
||||
while ((de = readdir(d)) != nullptr) {
|
||||
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 < hash_table.size(); i++) {
|
||||
for (bi = hash_table.at(i); bi != nullptr; bi = nextbi) {
|
||||
nextbi = bi->next;
|
||||
EVLOG_debug << "Type " << bi->type << " hash " << bi->hash << " num entries " << bi->num_needed << ":";
|
||||
|
||||
nextid = 0;
|
||||
memset(idmask.data(), 0, (bi->num_needed + 7) / 8);
|
||||
for (ei = bi->first_entry; ei != nullptr; ei = ei->next) {
|
||||
if (ei->old_id < bi->num_needed) {
|
||||
bit_set(idmask.data(), ei->old_id);
|
||||
}
|
||||
}
|
||||
|
||||
for (ei = bi->first_entry; ei != nullptr; 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 */
|
||||
if (snprintf(buf, buflen, "%08x.%s%d", bi->hash, symlink_extensions.at(bi->type), ei->old_id) < 0) {
|
||||
return -1;
|
||||
}
|
||||
EVLOG_debug << "link " << std::string(ei->filename, strlen(ei->filename)) << " -> "
|
||||
<< std::string(buf, strlen(buf));
|
||||
} else if (ei->need_symlink != 0U) {
|
||||
/* New link needed (it may replace something) */
|
||||
while (bit_isset(idmask.data(), nextid) != 0) {
|
||||
nextid++;
|
||||
}
|
||||
|
||||
if (snprintf(buf, buflen, "%s%s%n%08x.%s%d", dirname, pathsep, &n, bi->hash,
|
||||
symlink_extensions.at(bi->type), nextid) < 0) {
|
||||
return -1;
|
||||
};
|
||||
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 */
|
||||
if (snprintf(buf, buflen, "%s%s%n%08x.%s%d", dirname, pathsep, &n, bi->hash,
|
||||
symlink_extensions.at(bi->type), ei->old_id) < 0) {
|
||||
return -1;
|
||||
};
|
||||
EVLOG_debug << "unlink " << std::string(buf + n, strlen(buf + n));
|
||||
unlink(buf);
|
||||
}
|
||||
free(ei->filename);
|
||||
free(ei);
|
||||
}
|
||||
free(bi);
|
||||
}
|
||||
hash_table.at(i) = nullptr;
|
||||
}
|
||||
|
||||
ret = 0;
|
||||
err:
|
||||
free(buf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace evse_security
|
||||
38
tools/EVerest-main/lib/everest/evse_security/BUILD.bazel
Normal file
38
tools/EVerest-main/lib/everest/evse_security/BUILD.bazel
Normal file
@@ -0,0 +1,38 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_library")
|
||||
|
||||
cc_library(
|
||||
name = "cert_rehash",
|
||||
srcs = [],
|
||||
hdrs = [
|
||||
"3rd_party/cert_rehash/c_rehash.hpp",
|
||||
],
|
||||
includes = ["3rd_party"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "libevse-security",
|
||||
srcs = glob(["lib/**/*.cpp"]),
|
||||
hdrs = glob([
|
||||
"include/**/*.hpp",
|
||||
]),
|
||||
defines = ["LIBEVSE_CRYPTO_SUPPLIER_OPENSSL"],
|
||||
local_defines = [
|
||||
"BUILD_TZ_LIB=ON",
|
||||
"USE_SYSTEM_TZ_DB=ON",
|
||||
"USE_OS_TZDB=1",
|
||||
"USE_AUTOLOAD=0",
|
||||
"HAS_REMOTE_API=0",
|
||||
],
|
||||
strip_include_prefix = "include",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":cert_rehash",
|
||||
"//lib/everest/log:liblog",
|
||||
"//lib/everest/timer:libtimer",
|
||||
"//third-party/bazel/openssl:crypto",
|
||||
"//third-party/bazel/openssl:ssl",
|
||||
"@com_github_HowardHinnant_date//:date",
|
||||
],
|
||||
)
|
||||
94
tools/EVerest-main/lib/everest/evse_security/CMakeLists.txt
Normal file
94
tools/EVerest-main/lib/everest/evse_security/CMakeLists.txt
Normal file
@@ -0,0 +1,94 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
project(everest-evse_security VERSION 0.10.1
|
||||
DESCRIPTION "Implementation of EVSE related security operations"
|
||||
LANGUAGES CXX C
|
||||
)
|
||||
|
||||
find_package(everest-cmake 0.1 REQUIRED
|
||||
PATHS ../everest-cmake
|
||||
)
|
||||
|
||||
# options
|
||||
option(${PROJECT_NAME}_BUILD_TESTING "Build unit tests, used if included as dependency" OFF)
|
||||
option(BUILD_TESTING "Build unit tests, used if standalone project" OFF)
|
||||
option(EVSE_SECURITY_INSTALL "Install the library (shared data might be installed anyway)" ${EVC_MAIN_PROJECT})
|
||||
option(USING_TPM2 "Include code for using OpenSSL 3 and the tpm2 provider" OFF)
|
||||
option(USING_CUSTOM_PROVIDER "Include code for using OpenSSL 3 and the custom provider" OFF)
|
||||
|
||||
if((${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME} OR ${PROJECT_NAME}_BUILD_TESTING) AND BUILD_TESTING)
|
||||
set(LIBEVSE_SECURITY_BUILD_TESTING ON)
|
||||
evc_include(CodeCoverage)
|
||||
append_coverage_compiler_flags()
|
||||
endif()
|
||||
|
||||
if(USING_TPM2 AND USING_CUSTOM_PROVIDER)
|
||||
message(FATAL_ERROR, "TPM2 provider and custom provider are incompatible")
|
||||
endif()
|
||||
|
||||
if(USING_TPM2)
|
||||
set(CUSTOM_PROVIDER_NAME "tpm2")
|
||||
endif()
|
||||
|
||||
if(USING_CUSTOM_PROVIDER)
|
||||
set(CUSTOM_PROVIDER_NAME "custom_provider")
|
||||
endif()
|
||||
|
||||
if(USING_TPM2 OR USING_CUSTOM_PROVIDER)
|
||||
# OpenSSL property string when using the default provider
|
||||
set(PROPQUERY_PROVIDER_DEFAULT "?provider=default")
|
||||
# OpenSSL property string when using the tpm2/custom provider
|
||||
set(PROPQUERY_PROVIDER_CUSTOM "?provider=${CUSTOM_PROVIDER_NAME}")
|
||||
endif()
|
||||
|
||||
# dependencies
|
||||
if (NOT DISABLE_EDM)
|
||||
evc_setup_edm()
|
||||
# In EDM mode, we can't install exports (because the dependencies usually do not install their exports)
|
||||
set(EVSE_SECURITY_INSTALL OFF)
|
||||
else()
|
||||
find_package(date REQUIRED)
|
||||
endif()
|
||||
|
||||
option(LIBEVSE_SECURITY_USE_BOOST_FILESYSTEM "Usage of boost/filesystem.hpp instead of std::filesystem" OFF)
|
||||
|
||||
option(LIBEVSE_CRYPTO_SUPPLIER_OPENSSL "Default OpenSSL cryptography supplier" ON)
|
||||
|
||||
# dependencies
|
||||
if (LIBEVSE_CRYPTO_SUPPLIER_OPENSSL)
|
||||
find_package(OpenSSL 3 REQUIRED)
|
||||
endif()
|
||||
|
||||
add_subdirectory(lib)
|
||||
|
||||
# packaging
|
||||
if (EVSE_SECURITY_INSTALL)
|
||||
install(
|
||||
TARGETS evse_security
|
||||
EXPORT evse_security-targets
|
||||
LIBRARY
|
||||
)
|
||||
|
||||
install(
|
||||
DIRECTORY include/
|
||||
TYPE INCLUDE
|
||||
PATTERN "detail" EXCLUDE
|
||||
)
|
||||
|
||||
install(
|
||||
DIRECTORY 3rd_party/
|
||||
TYPE INCLUDE
|
||||
)
|
||||
|
||||
evc_setup_package(
|
||||
NAME everest-evse_security
|
||||
NAMESPACE everest
|
||||
EXPORT evse_security-targets
|
||||
)
|
||||
endif()
|
||||
|
||||
if(LIBEVSE_SECURITY_BUILD_TESTING)
|
||||
include(CTest)
|
||||
add_subdirectory(tests)
|
||||
set(CMAKE_BUILD_TYPE Debug)
|
||||
endif()
|
||||
201
tools/EVerest-main/lib/everest/evse_security/LICENSE
Normal file
201
tools/EVerest-main/lib/everest/evse_security/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
117
tools/EVerest-main/lib/everest/evse_security/README.md
Normal file
117
tools/EVerest-main/lib/everest/evse_security/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# libevse-security
|
||||
|
||||

|
||||
|
||||
This is a C++ library for security related operations for charging stations. It respects the requirements specified in OCPP and ISO15118 and can be used in combination with OCPP and ISO15118 implementations.
|
||||
|
||||
All documentation and the issue tracking can be found in our main repository here: https://github.com/EVerest/everest
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The library requires OpenSSL 3.
|
||||
|
||||
## Build Instructions
|
||||
|
||||
Clone this repository and build with CMake.
|
||||
|
||||
```bash
|
||||
git clone git@github.com:EVerest/libevsesecurity.git
|
||||
cd libevsesecurity
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make -j$(nproc) install
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
GTest is required for building the test cases target.
|
||||
To build the target and run the tests use:
|
||||
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
cmake -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=./dist ..
|
||||
make -j$(nproc) install
|
||||
make test
|
||||
```
|
||||
|
||||
In order to run a single test use:
|
||||
```bash
|
||||
./everest-evse_security_tests --gtest_filter=EvseSecurityTests.test_name
|
||||
```
|
||||
|
||||
## Certificate Structure
|
||||
|
||||
We allow any certificate structure with the following recommendations:
|
||||
|
||||
- Root CA certificate directories/bundles should not overlap leaf certificates
|
||||
- It is not recommended to store any SUBCAs in the root certificate bundle (if using files)
|
||||
|
||||
**Important:** when requesting leaf certificates with [get_leaf_certificate_info](https://github.com/EVerest/libevse-security/blob/b140c17b0a5eaf09b60035605ed8aeb84627eb78/include/evse_security/evse_security.hpp#L195) care should be taken if you require the full certificate chain.
|
||||
|
||||
If a full chain is **Leaf->SubCA2->SubCA1->Root**, it is recommended to have the root certificate in a single file, **V2G_ROOT_CA.pem** for example. The **Leaf->SubCA2->SubCA1** should be placed in a file e.g. **SECC_CERT_CHAIN.pem**.
|
||||
|
||||
## Certificate Signing Request
|
||||
|
||||
There are two configuration options that will add a DNS name and IP address to the
|
||||
subject alternative name in the certificate signing request.
|
||||
By default they are not added.
|
||||
|
||||
- `cmake -DCSR_DNS_NAME=charger.pionix.de ...` to include a DNS name
|
||||
- `cmake -DCSR_IP_ADDRESS=192.168.2.1 ...` to include an IPv4 address
|
||||
|
||||
When receiving back a signed CSR, the library will take care to create two files, one containing the **Leaf->SubCA2->SubCA1** chain and another containing the single **Leaf**. When they both exist, the return of [get_leaf_certificate_info](https://github.com/EVerest/libevse-security/blob/b140c17b0a5eaf09b60035605ed8aeb84627eb78/include/evse_security/evse_security.hpp#L195) will contain a path to both the single file and the chain file.
|
||||
|
||||
## TPM Provider
|
||||
There is a configuration option to configure OpenSSL for use with a TPM.<br>
|
||||
`cmake` ... `-DUSING_TPM2=ON`<br>
|
||||
|
||||
The library will use the `UseTPM` flag and the PEM private key file to
|
||||
configure whether to use the `default` provider or the `tpm2` provider.
|
||||
|
||||
Configuration is managed via propquery strings (see CMakeLists.txt)
|
||||
|
||||
- `PROPQUERY_PROVIDER_DEFAULT` is the string to use when selecting the default provider
|
||||
- `PROPQUERY_PROVIDER_CUSTOM` is the string to use when selecting the tpm2 provider
|
||||
|
||||
propquery|action
|
||||
---------|------
|
||||
"provider=default"|use the default provider
|
||||
"provider=tpm2"|use the tpm2 provider
|
||||
"provider!=tpm2"|don't use the tpm provider
|
||||
"?provider=tpm2,tpm2.digest!=yes"|prefer the tpm2 provider but not for message digests
|
||||
|
||||
For more information see:
|
||||
|
||||
- [Provider for integration of TPM 2.0 to OpenSSL 3.x](https://github.com/tpm2-software/tpm2-openssl)
|
||||
- [OpenSSL property](https://www.openssl.org/docs/man3.0/man7/property.html)
|
||||
- [OpenSSL provider](https://www.openssl.org/docs/man3.0/man7/provider.html)
|
||||
|
||||
<b>Note:</b> In case of errors related to CSR signing, update tpm2-openssl to v 1.2.0.
|
||||
|
||||
## Custom Provider
|
||||
There is a configuration option to configure OpenSSL for use with a custom provider.<br>
|
||||
`cmake` ... `-DUSING_CUSTOM_PROVIDER=ON`<br>
|
||||
|
||||
The workflow follows the same steps as using the TPM provider. The library will
|
||||
have a flag to configure whether it uses the `default` provider or the `custom` one.
|
||||
|
||||
<b>Note:</b> The custom provider name has to be defined [here](https://github.com/EVerest/libevse-security/blob/4afe644cb62d0bf06fff1e2ca5d2dbc489342e0c/CMakeLists.txt#L32). Change the name from "custom_provider" to the required provider.
|
||||
|
||||
## Garbage Collect
|
||||
|
||||
By default a garbage collect function will run and delete all expired leaf certificates and their respective keys, only if the certificate storage is full. A minimum count of leaf certificates will be kept even if they are expired.
|
||||
|
||||
Certificate signing requests have an expiry time. If the CSMS does not respond to them within that timeframe, CSRs will be deleted.
|
||||
|
||||
Defaults:
|
||||
- Garbage collect time: 20 minutes
|
||||
- CSR expiry: 60 minutes
|
||||
- Minimum certificates kept: 10
|
||||
- Maximum storage space: 50 MB
|
||||
- Maximum certificate entries: 2000
|
||||
|
||||
## Limitations
|
||||
|
||||
Based on information from [ssl](https://www.ssl.com/article/what-are-root-certificates-and-why-do-they-matter/), self-signed roots are possible, but not supported in our library at the moment.
|
||||
|
||||
Cross-signed certificate chains (see [ssl](https://www.ssl.com/blogs/ssl-com-legacy-cross-signed-root-certificate-expiring-on-september-11-2023/)), required for seamless root transitions are not supported at the moment.
|
||||
@@ -0,0 +1 @@
|
||||
_Use this file to list out any third-party dependencies used by this project. You may choose to point to a Gemfile or other language specific packaging file for details._
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
gtest:
|
||||
# GoogleTest now follows the Abseil Live at Head philosophy. We recommend updating to the latest commit in the main branch as often as possible.
|
||||
git: https://github.com/google/googletest.git
|
||||
git_tag: release-1.12.1
|
||||
cmake_condition: "LIBEVSE_SECURITY_BUILD_TESTING"
|
||||
@@ -0,0 +1,211 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
|
||||
#include <evse_security/certificate/x509_hierarchy.hpp>
|
||||
#include <evse_security/certificate/x509_wrapper.hpp>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
/// @brief Custom exception that is thrown when no private key could be found for a selected certificate
|
||||
class NoPrivateKeyException : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
/// @brief Custom exception that is thrown when no valid certificate could be found for the specified filesystem
|
||||
/// locations
|
||||
class NoCertificateValidException : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
/// @brief Custom exception that is thrown when a invalid operation is with the current state
|
||||
/// on the certificate bundle
|
||||
class InvalidOperationException : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
/// @brief X509 certificate bundle, used for holding multiple X509Wrappers. Supports
|
||||
/// operations like add/delete importing/exporting certificates. Can use either a
|
||||
/// directory with multiple certificates (or bundles of certificates) or a single
|
||||
/// file with one or more certificates in it.
|
||||
class X509CertificateBundle {
|
||||
public:
|
||||
X509CertificateBundle(const fs::path& path, const EncodingFormat encoding);
|
||||
X509CertificateBundle(const std::string& certificate, const EncodingFormat encoding);
|
||||
|
||||
X509CertificateBundle(X509CertificateBundle&& other) = default;
|
||||
X509CertificateBundle(const X509CertificateBundle& other) = delete;
|
||||
|
||||
/// @brief Gets if this certificate bundle comes from a single certificate bundle file
|
||||
/// @return
|
||||
bool is_using_bundle_file() const {
|
||||
return (source == X509CertificateSource::FILE);
|
||||
}
|
||||
|
||||
/// @brief Gets if this certificate bundle comes from an entire directory
|
||||
/// @return
|
||||
bool is_using_directory() const {
|
||||
return (source == X509CertificateSource::DIRECTORY);
|
||||
}
|
||||
|
||||
/// @return True if multiple certificates are contained within
|
||||
bool is_bundle() const {
|
||||
return (get_certificate_count() > 1);
|
||||
}
|
||||
|
||||
bool empty() const {
|
||||
return certificates.empty();
|
||||
}
|
||||
|
||||
/// @return Contained certificate count
|
||||
int get_certificate_count() const;
|
||||
|
||||
/// @return Contained certificate chains count
|
||||
int get_certificate_chains_count() const;
|
||||
|
||||
fs::path get_path() const {
|
||||
return path;
|
||||
}
|
||||
|
||||
X509CertificateSource get_source() const {
|
||||
return source;
|
||||
}
|
||||
|
||||
/// @brief Iterates through all the contained certificate chains (file, certificates)
|
||||
/// while the provided function returns true
|
||||
template <typename function> void for_each_chain(function func) {
|
||||
for (const auto& chain : certificates) {
|
||||
if (!func(chain.first, chain.second)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Same as 'for_each_chain' but it also uses a predicate for ordering
|
||||
template <typename function, typename ordering> void for_each_chain_ordered(function func, ordering order) {
|
||||
struct Chain {
|
||||
const fs::path* path;
|
||||
const std::vector<X509Wrapper>* certificates;
|
||||
};
|
||||
|
||||
std::vector<Chain> ordered;
|
||||
ordered.reserve(certificates.size());
|
||||
for (auto& [path, certs] : certificates) {
|
||||
ordered.push_back(Chain{&path, &certs});
|
||||
}
|
||||
|
||||
std::sort(std::begin(ordered), std::end(ordered),
|
||||
[&order](Chain& a, Chain& b) { return order(*a.certificates, *b.certificates); });
|
||||
|
||||
for (const auto& chain : ordered) {
|
||||
if (!func(*chain.path, *chain.certificates)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Splits the certificate (chain) into single certificates
|
||||
/// @return vector containing single certificates
|
||||
std::vector<X509Wrapper> split();
|
||||
|
||||
/// @brief If we already have the certificate
|
||||
bool contains_certificate(const X509Wrapper& certificate);
|
||||
/// @brief If we already have the certificate hash
|
||||
bool contains_certificate(const CertificateHashData& certificate_hash);
|
||||
|
||||
/// @brief Finds a certificate based on its hash, returning an empty optional if none is found
|
||||
std::optional<X509Wrapper> find_certificate(const CertificateHashData& certificate_hash,
|
||||
bool case_insensitive_comparison = false);
|
||||
|
||||
/// @brief Adds a single certificate in the bundle. Only in memory, use @ref export_certificates to filesystem
|
||||
void add_certificate(X509Wrapper&& certificate);
|
||||
|
||||
/// @brief Adds a single certificate in the bundle, only if it is not contained
|
||||
/// already. Only in memory, use @ref export_certificates to filesystem
|
||||
void add_certificate_unique(X509Wrapper&& certificate);
|
||||
|
||||
/// @brief Updates an already existing certificate if it is found
|
||||
bool update_certificate(X509Wrapper&& certificate);
|
||||
|
||||
/// @brief Deletes all instances of the provided certificate. Only in memory, use @ref export_certificates
|
||||
/// to filesystem export
|
||||
/// @param include_issued If true the child certificates will also be deleted, if any are found, for
|
||||
/// example if we delete CA1 from CA1->CA2->Leaf, all the chain will be deleted
|
||||
/// @param include_top If true the certificates that issues this will also be deleted if any are found, for
|
||||
/// example if we have a chain CA1->CA2->Leaf and we delete the Leaf, CA1 and CA2 will also be deleted
|
||||
/// @return the certificates that have been removed from memory
|
||||
std::vector<X509Wrapper> delete_certificate(const X509Wrapper& certificate, bool include_issued, bool include_top);
|
||||
|
||||
/// @brief Deletes all certificates with the provided certificate hash. Only in memory,
|
||||
/// use @ref export_certificates to filesystem export
|
||||
/// @param include_issued If true the child certificates will also be deleted, if any are found, for
|
||||
/// example if we delete CA1 from CA1->CA2->Leaf, all the chain will be deleted
|
||||
/// @param include_top If true the certificates that issues this will also be deleted if any are found, for
|
||||
/// example if we have a chain CA1->CA2->Leaf and we delete the Leaf, CA1 and CA2 will also be deleted
|
||||
/// @return the certificates that have been removed from memory
|
||||
std::vector<X509Wrapper> delete_certificate(const CertificateHashData& data, bool include_issued, bool include_top);
|
||||
|
||||
/// @brief Deletes all certificates. Only in memory, use @ref export_certificates to filesystem export
|
||||
void delete_all_certificates();
|
||||
|
||||
/// @brief Returns a full exportable representation of a certificate bundle file in PEM format
|
||||
std::string to_export_string() const;
|
||||
|
||||
/// @brief Returns a full exportable representation of a certificate sub-chain, if found
|
||||
std::string to_export_string(const fs::path& chain) const;
|
||||
|
||||
/// @brief Exports the full certificate chain either as individual files if it is using a directory
|
||||
/// or as a bundle if it uses a bundle file, at the initially provided path. Also deletes/adds the updated
|
||||
/// certificates
|
||||
/// @return true on success, false otherwise
|
||||
bool export_certificates();
|
||||
|
||||
/// @brief Syncs the file structure with the certificate store adding certificates that are not found on the
|
||||
/// storage and deleting the certificates that are not contained in this bundle
|
||||
bool sync_to_certificate_store();
|
||||
|
||||
/// @brief returns the latest valid certificate within this bundle
|
||||
X509Wrapper get_latest_valid_certificate();
|
||||
|
||||
/// @brief Utility that returns current the certificate hierarchy of this bundle
|
||||
/// Invalidated on any add/delete operation
|
||||
X509CertificateHierarchy& get_certificate_hierarchy();
|
||||
|
||||
X509CertificateBundle& operator=(X509CertificateBundle&& other) = default;
|
||||
|
||||
/// @brief Returns the latest valid certif that we might contain
|
||||
static X509Wrapper get_latest_valid_certificate(const std::vector<X509Wrapper>& certificates);
|
||||
|
||||
static bool is_certificate_file(const fs::path& file) {
|
||||
return fs::is_regular_file(file) &&
|
||||
((file.extension() == PEM_EXTENSION) || (file.extension() == DER_EXTENSION));
|
||||
}
|
||||
|
||||
private:
|
||||
/// @brief Adds to our certificate list the certificates found in the file
|
||||
/// @return number of added certificates
|
||||
void add_certificates(const std::string& data, const EncodingFormat encoding, const std::optional<fs::path>& path);
|
||||
|
||||
/// @brief operation to be executed after each add/delete to this bundle
|
||||
void invalidate_hierarchy();
|
||||
|
||||
// Structure of the bundle - maps files to the certificates stored in them
|
||||
// For certificates coming from a string, uses a default empty path
|
||||
std::map<fs::path, std::vector<X509Wrapper>> certificates;
|
||||
// Relevant bundle file or directory for the certificates
|
||||
fs::path path;
|
||||
// Source from where we created the certificates. If 'string' the 'export' functions will not work
|
||||
X509CertificateSource source;
|
||||
|
||||
// Cached certificate hierarchy, invalidated on any operation
|
||||
X509CertificateHierarchy hierarchy;
|
||||
bool hierarchy_invalidated;
|
||||
};
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,166 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <queue>
|
||||
|
||||
#include <evse_security/certificate/x509_wrapper.hpp>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
class NoCertificateFound : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
class InvalidStateException : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
struct NodeState {
|
||||
std::uint32_t is_selfsigned : 1;
|
||||
std::uint32_t is_orphan : 1; // 0 means temporary orphan, 1 is permanent orphan, no relevance if it is self-signed
|
||||
};
|
||||
|
||||
struct X509Node {
|
||||
NodeState state;
|
||||
|
||||
X509Wrapper certificate; ///< Certificate that we hold
|
||||
std::optional<CertificateHashData>
|
||||
hash; ///< Precomputed certificate hash, in case of an orphan certificate it might not be set
|
||||
|
||||
X509Wrapper issuer; ///< Issuer of this certificate, can be == with certificate if we are a root
|
||||
std::vector<X509Node> children;
|
||||
};
|
||||
|
||||
/// @brief Utility class that is able to build a immutable certificate hierarchy
|
||||
/// with a list of self-signed root certificates and their respective sub-certificates
|
||||
/// Note: non self-signed roots and cross-signed certificates are not supported now
|
||||
class X509CertificateHierarchy {
|
||||
public:
|
||||
const std::vector<X509Node>& get_hierarchy() const {
|
||||
return hierarchy;
|
||||
}
|
||||
|
||||
/// @brief Checks if the provided certificate is a self-signed root CA certificate
|
||||
/// contained in our hierarchy
|
||||
bool is_internal_root(const X509Wrapper& certificate) const;
|
||||
|
||||
/// @brief Collects all the descendants of the provided certificate, in the order
|
||||
/// from the root towards the leaf (ROOT->SUBCA1->SUBCA2->LEAF)
|
||||
/// NOTE: this can be a problem when returning certificates to the CSMS that can
|
||||
/// require to have an order from the leaf to the root, case in which the descendants
|
||||
/// have to be reverse iterated
|
||||
///
|
||||
/// @param top Certificate that issued the descendants
|
||||
std::vector<X509Wrapper> collect_descendants(const X509Wrapper& top);
|
||||
|
||||
/// @brief Collects all the top certificates of the provided leaf, in the
|
||||
/// order from the leaf towards the top (LEAF->SUBCA2->SUBCA1)
|
||||
/// @param leaf Leaf certificate for which we collect the top certificates
|
||||
std::vector<X509Wrapper> collect_top(const X509Wrapper& leaf);
|
||||
|
||||
/// @brief Obtains the hash data of the certificate, finding its issuer if needed
|
||||
/// @return True if a hash could be found, false otherwise
|
||||
bool get_certificate_hash(const X509Wrapper& certificate, CertificateHashData& out_hash);
|
||||
|
||||
/// @brief returns true if we contain a certificate with the following hash
|
||||
bool contains_certificate_hash(const CertificateHashData& hash, bool case_insensitive_comparison);
|
||||
|
||||
/// @brief Searches for the root of the provided leaf, returning an empty optional if none was found
|
||||
std::optional<X509Wrapper> find_certificate_root(const X509Wrapper& leaf);
|
||||
|
||||
/// @brief Searches for the provided hash, returning an empty optional if none was found
|
||||
std::optional<X509Wrapper> find_certificate(const CertificateHashData& hash,
|
||||
bool case_insensitive_comparison = false);
|
||||
|
||||
/// @brief Searches for all the certificates with the provided hash, throwing a NoCertificateFound
|
||||
// if none were found. Can be useful when we have SUB-CAs in multiple bundles
|
||||
std::vector<X509Wrapper> find_certificates_multi(const CertificateHashData& hash);
|
||||
|
||||
std::string to_debug_string();
|
||||
|
||||
/// @brief Breadth-first iteration through all the hierarchy of
|
||||
/// certificates. Will break when the function returns false
|
||||
template <typename function> void for_each(function func) {
|
||||
std::queue<std::reference_wrapper<X509Node>> queue;
|
||||
for (auto& root : hierarchy) {
|
||||
// Process roots
|
||||
if (!func(root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& child : root.children) {
|
||||
queue.push(child); // NOLINT(modernize-use-emplace)
|
||||
}
|
||||
}
|
||||
|
||||
while (!queue.empty()) {
|
||||
X509Node& top = queue.front();
|
||||
queue.pop();
|
||||
|
||||
// Process node
|
||||
if (!func(top)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& child : top.children) {
|
||||
queue.push(child); // NOLINT(modernize-use-emplace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Depth-first descendant iteration
|
||||
template <typename function> static void for_each_descendant(function func, const X509Node& node, int depth = 0) {
|
||||
if (node.children.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& child : node.children) {
|
||||
func(child, depth);
|
||||
|
||||
if (!child.children.empty()) {
|
||||
for_each_descendant(func, child, (depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Builds a proper certificate hierarchy from the provided certificates. The
|
||||
/// 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(),
|
||||
#if defined(__GNUC__) && !defined(__clang__) && (__GNUC__ < 9)
|
||||
[ordered_ptr = &ordered](X509Wrapper& cert) { ordered_ptr->insert(std::move(cert)); }),
|
||||
#else
|
||||
[&ordered](X509Wrapper& cert) { ordered.insert(std::move(cert)); }),
|
||||
#endif
|
||||
...); // Fold expr
|
||||
|
||||
// Prune the tree
|
||||
ordered.prune();
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<std::pair<const X509Node*, int>> find_certificate_root_node(const X509Wrapper& leaf);
|
||||
|
||||
/// @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
|
||||
void insert(X509Wrapper&& certificate);
|
||||
|
||||
/// @brief After inserting all certificates in the hierarchy, attempts
|
||||
/// to parent all temporarly orphan certificates, marking the ones that
|
||||
/// were not successfully parented as permanently orphan
|
||||
void prune();
|
||||
|
||||
std::vector<X509Node> hierarchy;
|
||||
};
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,141 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include <evse_security/crypto/interface/crypto_types.hpp>
|
||||
#include <evse_security/evse_types.hpp>
|
||||
#include <evse_security/utils/evse_filesystem_types.hpp>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
enum class X509CertificateSource {
|
||||
// Built from a certificate file
|
||||
FILE,
|
||||
// Built from a directory of certificates
|
||||
DIRECTORY,
|
||||
// Build from a raw string
|
||||
STRING
|
||||
};
|
||||
|
||||
/// @brief Convenience wrapper around openssl X509 certificate
|
||||
class X509Wrapper {
|
||||
public:
|
||||
X509Wrapper(const fs::path& file, const EncodingFormat encoding);
|
||||
X509Wrapper(const std::string& data, const EncodingFormat encoding);
|
||||
|
||||
explicit X509Wrapper(X509Handle_ptr&& x509);
|
||||
X509Wrapper(X509Handle_ptr&& x509, const fs::path& file);
|
||||
|
||||
X509Wrapper(const X509Wrapper& other);
|
||||
X509Wrapper(X509Wrapper&& other) = default;
|
||||
|
||||
~X509Wrapper() = default;
|
||||
|
||||
/// @brief Returns true if this certificate is the child of the provided parent
|
||||
bool is_child(const X509Wrapper& parent) const;
|
||||
|
||||
/// @brief Returns true if this certificate is self-signed
|
||||
bool is_selfsigned() const;
|
||||
|
||||
/// @brief Gets x509 raw handle
|
||||
inline X509Handle* get() const {
|
||||
return x509.get();
|
||||
}
|
||||
|
||||
/// @brief Gets valid_in
|
||||
/// @return seconds until certificate is valid; if > 0 cert is not yet valid
|
||||
int64_t get_valid_in() const;
|
||||
|
||||
/// @brief Gets valid_in
|
||||
/// @return seconds until certificate is expired; if < 0 cert has expired
|
||||
int64_t get_valid_to() const;
|
||||
|
||||
/// @brief Gets optional file of certificate
|
||||
/// @result
|
||||
std::optional<fs::path> get_file() const;
|
||||
|
||||
void set_file(fs::path& path);
|
||||
|
||||
/// @brief Gets the source of this certificate, if it is from a file it's 'FILE'
|
||||
/// but it can also be constructed from a string, or another certificate
|
||||
X509CertificateSource get_source() const;
|
||||
|
||||
/// @brief Gets CN of certificate
|
||||
/// @result
|
||||
std::string get_common_name() const;
|
||||
|
||||
/// @brief Gets issuer name hash of certificate
|
||||
/// @result
|
||||
std::string get_issuer_name_hash() const;
|
||||
|
||||
/// @brief Gets issuer key hash of certificate. As per the specification
|
||||
/// this is not the hash of our public key, but the hash of the pubkey of
|
||||
/// the parent certificate. If it is a self-signed root, then the key hash
|
||||
/// and the issuer key hash are the same
|
||||
/// @result
|
||||
std::string get_issuer_key_hash() const;
|
||||
|
||||
/// @brief Gets key hash of this certificate
|
||||
/// @result
|
||||
std::string get_key_hash() const;
|
||||
|
||||
/// @brief Gets serial number of certificate
|
||||
/// @result
|
||||
std::string get_serial_number() const;
|
||||
|
||||
/// @brief Gets certificate hash data of a self-signed certificate
|
||||
/// @return
|
||||
CertificateHashData get_certificate_hash_data() const;
|
||||
|
||||
/// @brief Gets certificate hash data of certificate with an issuer
|
||||
/// @return
|
||||
CertificateHashData get_certificate_hash_data(const X509Wrapper& issuer) const;
|
||||
|
||||
/// @brief Gets OCSP responder URL of certificate if present, else returns an empty string
|
||||
/// @return
|
||||
std::string get_responder_url() const;
|
||||
|
||||
/// @brief Gets the export string representation for this certificate
|
||||
/// @return
|
||||
std::string get_export_string() const;
|
||||
|
||||
/// @brief If the certificate is within the validity date. Can return false in 2 cases,
|
||||
/// if it is expired (current date > valid_to) or if (current data < valid_in), that is
|
||||
/// we are not in force yet
|
||||
bool is_valid() const;
|
||||
|
||||
/// @brief If the certificate will be valid in the future, that is (current date > valid_in)
|
||||
/// and (current data > valid_to)
|
||||
bool is_valid_in_future() const;
|
||||
|
||||
/// @brief If the certificate has expired
|
||||
bool is_expired() const;
|
||||
|
||||
X509Wrapper& operator=(X509Wrapper&& other) = default;
|
||||
|
||||
/// @return true if the two certificates are the same
|
||||
bool operator==(const X509Wrapper& other) const;
|
||||
|
||||
bool operator==(const CertificateHashData& other) const {
|
||||
return get_issuer_name_hash() == other.issuer_name_hash && get_issuer_key_hash() == other.issuer_key_hash &&
|
||||
get_serial_number() == other.serial_number;
|
||||
}
|
||||
|
||||
private:
|
||||
void update_validity();
|
||||
|
||||
X509Handle_ptr x509; // X509 wrapper object
|
||||
std::int64_t valid_in; // seconds; if > 0 cert is not yet valid, negative value means past, positive is in future
|
||||
std::int64_t valid_to; // seconds; if < 0 cert has expired, negative value means past, positive is in future
|
||||
|
||||
// Relevant file in which this certificate resides
|
||||
std::optional<fs::path> file;
|
||||
|
||||
std::string debug_common_name;
|
||||
};
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <evse_security/crypto/interface/crypto_supplier.hpp>
|
||||
|
||||
// Include other required suppliers here
|
||||
#ifdef LIBEVSE_CRYPTO_SUPPLIER_OPENSSL
|
||||
#include <evse_security/crypto/openssl/openssl_crypto_supplier.hpp>
|
||||
namespace evse_security {
|
||||
using CryptoSupplier = OpenSSLSupplier; // Define others with the same 'CryptoSupplier' name
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,88 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#include <evse_security/crypto/interface/crypto_types.hpp>
|
||||
#include <evse_security/evse_types.hpp>
|
||||
#include <evse_security/utils/evse_filesystem_types.hpp>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
/// @brief All cryptography suppliers must conform to this class. Do not
|
||||
/// use this class directly, just include the evse_crypto.hpp
|
||||
class AbstractCryptoSupplier {
|
||||
public:
|
||||
/// @brief Name of supplier, char is not required to be released
|
||||
static const char* get_supplier_name();
|
||||
|
||||
/// @brief If any TPM operations are supported
|
||||
static bool supports_tpm();
|
||||
static bool supports_tpm_key_creation();
|
||||
/// @brief If creation from a custom provider is supported
|
||||
static bool supports_custom_key_creation();
|
||||
|
||||
// Key utilities
|
||||
static bool generate_key(const KeyGenerationInfo& generation_info, KeyHandle_ptr& out_key);
|
||||
|
||||
/// @brief Loads all certificates from the string data that can contain multiple cetifs
|
||||
static std::vector<X509Handle_ptr> load_certificates(const std::string& data, const EncodingFormat encoding);
|
||||
|
||||
// X509 certificate utilities
|
||||
static std::string x509_to_string(X509Handle* handle);
|
||||
static std::string x509_get_responder_url(X509Handle* handle);
|
||||
static std::string x509_get_key_hash(X509Handle* handle);
|
||||
static std::string x509_get_serial_number(X509Handle* handle);
|
||||
static std::string x509_get_issuer_name_hash(X509Handle* handle);
|
||||
static std::string x509_get_common_name(X509Handle* handle);
|
||||
|
||||
/// @brief Returns the time validity for a certificate
|
||||
/// @param out_valid_in Valid in amount of seconds. A negative value is in the past, a positive one is in the future
|
||||
/// (not yet valid)
|
||||
/// @param out_valid_to Valid amount of seconds. A negative value is in the past (expired), a positive one is in the
|
||||
/// future
|
||||
static bool x509_get_validity(X509Handle* handle, std::int64_t& out_valid_in, std::int64_t& out_valid_to);
|
||||
|
||||
static bool x509_is_selfsigned(X509Handle* handle);
|
||||
static bool x509_is_child(X509Handle* child, X509Handle* parent);
|
||||
static bool x509_is_equal(X509Handle* a, X509Handle* b);
|
||||
|
||||
static X509Handle_ptr x509_duplicate_unique(X509Handle* handle);
|
||||
|
||||
/// @brief Verifies the provided target against the certificate chain
|
||||
/// @param target Target to verify
|
||||
/// @param parents Parents chain, until the root
|
||||
/// @param dir_path Optional directory path that can be used for certificate store lookup
|
||||
/// @param file_path Optional certificate file path that can be used for certificate store lookup
|
||||
/// @return
|
||||
static CertificateValidationResult
|
||||
x509_verify_certificate_chain(X509Handle* target, const std::vector<X509Handle*>& parents,
|
||||
const std::vector<X509Handle*>& untrusted_subcas, bool allow_future_certificates,
|
||||
const std::optional<fs::path> dir_path, const std::optional<fs::path> file_path);
|
||||
|
||||
/// @brief Checks if the private key is consistent with the provided handle
|
||||
static KeyValidationResult x509_check_private_key(X509Handle* handle, std::string private_key,
|
||||
std::optional<std::string> password);
|
||||
|
||||
/// @brief Verifies the signature with the certificate handle public key against the data
|
||||
static bool x509_verify_signature(X509Handle* handle, const std::vector<std::uint8_t>& signature,
|
||||
const std::vector<std::uint8_t>& data);
|
||||
|
||||
/// @brief Generates a certificate signing request with the provided parameters
|
||||
static CertificateSignRequestResult x509_generate_csr(const CertificateSigningRequestInfo& generation_info,
|
||||
std::string& out_csr);
|
||||
|
||||
// Digesting/decoding utils
|
||||
static bool digest_file_sha256(const fs::path& path, std::vector<std::uint8_t>& out_digest);
|
||||
|
||||
static bool base64_decode_to_bytes(const std::string& base64_string, std::vector<std::uint8_t>& out_decoded);
|
||||
static bool base64_decode_to_string(const std::string& base64_string, std::string& out_decoded);
|
||||
|
||||
static bool base64_encode_from_bytes(const std::vector<std::uint8_t>& bytes, std::string& out_encoded);
|
||||
static bool base64_encode_from_string(const std::string& string, std::string& out_encoded);
|
||||
};
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,98 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <evse_security/utils/evse_filesystem_types.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
// NOLINTNEXTLINE(cert-int09-c, readability-enum-initial-value): prefer implicit initialization for readability
|
||||
enum class CryptoKeyType {
|
||||
EC_prime256v1, // Default EC. P-256, ~equiv to rsa 3072
|
||||
EC_secp384r1, // P-384, ~equiv to rsa 7680
|
||||
RSA_2048,
|
||||
RSA_TPM20 = RSA_2048, // Default TPM RSA, only option allowed for TPM (universal support), 2048 bits
|
||||
RSA_3072, // Default RSA. Protection lifetime: ~2030
|
||||
RSA_7680, // Protection lifetime: >2031. Very long generation time 8-40s on 16 core PC
|
||||
};
|
||||
|
||||
enum class KeyValidationResult {
|
||||
Valid,
|
||||
KeyLoadFailure, // The key could not be loaded, might be an password or invalid string
|
||||
Invalid, // The key is not linked to the specified certificate
|
||||
Unknown, // Unknown error, not related to provider validation
|
||||
};
|
||||
|
||||
enum class CertificateSignRequestResult {
|
||||
Valid,
|
||||
KeyGenerationError, // Error when generating the key, maybe invalid key type
|
||||
VersioningError, // The version could not be set
|
||||
PubkeyError, // The public key could not be attached
|
||||
ExtensionsError, // The extensions could not be appended
|
||||
SigningError, // The CSR could not be signed, maybe key or signing algo invalid
|
||||
Unknown, // Any other error
|
||||
};
|
||||
|
||||
struct KeyGenerationInfo {
|
||||
CryptoKeyType key_type;
|
||||
|
||||
/// @brief If the key should be generated using the custom provider. The custom
|
||||
/// provider can be the TPM if it was so configured. Should check before if
|
||||
/// the provider supports the operation, or the operation will fail by default
|
||||
bool generate_on_custom;
|
||||
|
||||
/// @brief If we should export the public key to a file
|
||||
std::optional<fs::path> public_key_file;
|
||||
|
||||
/// @brief If we should export the private key to a file
|
||||
std::optional<fs::path> private_key_file;
|
||||
/// @brief If we should have a pass for the private key file
|
||||
std::optional<std::string> private_key_pass;
|
||||
};
|
||||
|
||||
struct CertificateSigningRequestInfo {
|
||||
// Minimal mandatory
|
||||
int n_version;
|
||||
std::string country;
|
||||
std::string organization;
|
||||
std::string commonName;
|
||||
|
||||
/// @brief incude a subjectAlternativeName DNSName
|
||||
std::optional<std::string> dns_name;
|
||||
/// @brief incude a subjectAlternativeName IPAddress
|
||||
std::optional<std::string> ip_address;
|
||||
|
||||
KeyGenerationInfo key_info;
|
||||
};
|
||||
class CertificateLoadException : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
struct CryptoHandle {
|
||||
virtual ~CryptoHandle() = default;
|
||||
};
|
||||
|
||||
/// @brief Handle abstraction to crypto lib X509 certificate
|
||||
struct X509Handle : public CryptoHandle {};
|
||||
|
||||
/// @brief Handle abstraction to crypto lib key
|
||||
struct KeyHandle : public CryptoHandle {};
|
||||
|
||||
using X509Handle_ptr = std::unique_ptr<X509Handle>;
|
||||
using KeyHandle_ptr = std::unique_ptr<KeyHandle>;
|
||||
|
||||
// Transforms a duration of days into seconds
|
||||
using days_to_seconds = std::chrono::duration<std::int64_t, std::ratio<86400>>;
|
||||
|
||||
namespace conversions {
|
||||
std::string get_certificate_sign_request_result_to_string(CertificateSignRequestResult e);
|
||||
}
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#ifdef LIBEVSE_CRYPTO_SUPPLIER_OPENSSL
|
||||
|
||||
#include <evse_security/crypto/interface/crypto_supplier.hpp>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
class OpenSSLSupplier : public AbstractCryptoSupplier {
|
||||
public:
|
||||
static const char* get_supplier_name();
|
||||
|
||||
static bool supports_tpm();
|
||||
static bool supports_tpm_key_creation();
|
||||
static bool supports_custom_key_creation();
|
||||
|
||||
static bool generate_key(const KeyGenerationInfo& key_info, KeyHandle_ptr& out_key);
|
||||
|
||||
static std::vector<X509Handle_ptr> load_certificates(const std::string& data, const EncodingFormat encoding);
|
||||
|
||||
static std::string x509_to_string(X509Handle* handle);
|
||||
static std::string x509_get_responder_url(X509Handle* handle);
|
||||
static std::string x509_get_key_hash(X509Handle* handle);
|
||||
static std::string x509_get_serial_number(X509Handle* handle);
|
||||
static std::string x509_get_issuer_name_hash(X509Handle* handle);
|
||||
static std::string x509_get_common_name(X509Handle* handle);
|
||||
static bool x509_get_validity(X509Handle* handle, std::int64_t& out_valid_in, std::int64_t& out_valid_to);
|
||||
static bool x509_is_selfsigned(X509Handle* handle);
|
||||
static bool x509_is_child(X509Handle* child, X509Handle* parent);
|
||||
static bool x509_is_equal(X509Handle* a, X509Handle* b);
|
||||
static X509Handle_ptr x509_duplicate_unique(X509Handle* handle);
|
||||
static CertificateValidationResult
|
||||
x509_verify_certificate_chain(X509Handle* target, const std::vector<X509Handle*>& parents,
|
||||
const std::vector<X509Handle*>& untrusted_subcas, bool allow_future_certificates,
|
||||
const std::optional<fs::path> dir_path, const std::optional<fs::path> file_path);
|
||||
static KeyValidationResult x509_check_private_key(X509Handle* handle, std::string private_key,
|
||||
std::optional<std::string> password);
|
||||
static bool x509_verify_signature(X509Handle* handle, const std::vector<std::uint8_t>& signature,
|
||||
const std::vector<std::uint8_t>& data);
|
||||
|
||||
static CertificateSignRequestResult x509_generate_csr(const CertificateSigningRequestInfo& csr_info,
|
||||
std::string& out_csr);
|
||||
|
||||
static bool digest_file_sha256(const fs::path& path, std::vector<std::uint8_t>& out_digest);
|
||||
|
||||
static bool base64_decode_to_bytes(const std::string& base64_string, std::vector<std::uint8_t>& out_decoded);
|
||||
static bool base64_decode_to_string(const std::string& base64_string, std::string& out_decoded);
|
||||
|
||||
static bool base64_encode_from_bytes(const std::vector<std::uint8_t>& bytes, std::string& out_encoded);
|
||||
static bool base64_encode_from_string(const std::string& string, std::string& out_encoded);
|
||||
};
|
||||
|
||||
} // namespace evse_security
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,171 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef OPENSSL_TPM_HPP
|
||||
#define OPENSSL_TPM_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include <evse_security/utils/evse_filesystem_types.hpp>
|
||||
|
||||
// opaque types (from OpenSSL)
|
||||
struct ossl_lib_ctx_st; // OpenSSL OSSL_LIB_CTX;
|
||||
struct ossl_provider_st; // OpenSSL OSSL_PROVIDER
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
/// @brief determine if the PEM string is a custom private key. Will
|
||||
/// only work for private keys, public keys will always return true
|
||||
/// @param private_key_pem string containing the PEM encoded key
|
||||
/// @return true when file does not start "-----BEGIN PRIVATE KEY-----"
|
||||
/// @note works irrespective of OpenSSL version
|
||||
bool is_custom_private_key_string(const std::string& private_key_pem);
|
||||
|
||||
/// @brief determine if the PEM file contains a custom private key. Will
|
||||
/// only work for private keys, public keys will always return true
|
||||
/// @param private_key_file_pem filename of the PEM file
|
||||
/// @return true when file does not start "-----BEGIN PRIVATE KEY-----"
|
||||
/// @note works irrespective of OpenSSL version
|
||||
bool is_custom_private_key_file(const fs::path& private_key_file_pem);
|
||||
|
||||
/// @brief Manage the loading and configuring of OpenSSL providers
|
||||
///
|
||||
/// There are two providers considered:
|
||||
/// - 'default'
|
||||
/// - 'tpm2' for working with TSS2 keys (protected by a TPM)
|
||||
/// The 'tpm2' can be replaced with a custom provider (see CMakeLists.txt)
|
||||
///
|
||||
/// There are two contexts:
|
||||
/// - 'global' for general use
|
||||
/// - 'tls' for TLS connections
|
||||
///
|
||||
/// The class also acts as a scoped mutex to prevent changes in the
|
||||
/// provider configuration during crypto operations
|
||||
///
|
||||
/// @note OpenSSL SSL_CTX caches the propquery so updates via
|
||||
/// this class may not be effective. See SSL_CTX_new_ex()
|
||||
///
|
||||
/// This code provides a null implementation when OpenSSL 3 or later isn't
|
||||
/// used. The null implementation is also used when -DUSING_TPM2=OFF is
|
||||
/// set with cmake.
|
||||
///
|
||||
/// @note the tpm2-abrmd daemon is needed to support openssl-tpm2 for TLS
|
||||
class OpenSSLProvider {
|
||||
public:
|
||||
/// @brief supported propquery strings
|
||||
enum class mode_t {
|
||||
default_provider,
|
||||
custom_provider,
|
||||
};
|
||||
|
||||
private:
|
||||
using flags_underlying_t = std::uint8_t;
|
||||
enum class flags_t : flags_underlying_t {
|
||||
initialised,
|
||||
custom_provider_available,
|
||||
global_custom_provider,
|
||||
tls_custom_provider,
|
||||
};
|
||||
|
||||
static std::mutex s_mux;
|
||||
static flags_underlying_t s_flags;
|
||||
|
||||
static struct ossl_provider_st* s_global_prov_default_p;
|
||||
static struct ossl_provider_st* s_global_prov_custom_p;
|
||||
static struct ossl_provider_st* s_tls_prov_default_p;
|
||||
static struct ossl_provider_st* s_tls_prov_custom_p;
|
||||
static struct ossl_lib_ctx_st* s_tls_libctx_p;
|
||||
|
||||
static inline void reset(flags_t f) {
|
||||
s_flags &= ~(1 << static_cast<flags_underlying_t>(f));
|
||||
}
|
||||
|
||||
static inline void set(flags_t f) {
|
||||
s_flags |= 1 << static_cast<flags_underlying_t>(f);
|
||||
}
|
||||
|
||||
static inline bool is_set(flags_t f) {
|
||||
return (s_flags & (1 << static_cast<flags_underlying_t>(f))) != 0;
|
||||
}
|
||||
|
||||
static inline bool is_reset(flags_t f) {
|
||||
return !is_set(f);
|
||||
}
|
||||
|
||||
/// @brief uodate the flag
|
||||
/// @param f - flag to update
|
||||
/// @param val - whether to set or reset the flag
|
||||
/// @return true when the flag was changed
|
||||
static inline bool update(flags_t f, bool val) {
|
||||
const bool result = (val != is_set(f));
|
||||
if (val) {
|
||||
set(f);
|
||||
} else {
|
||||
reset(f);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool load(struct ossl_provider_st*& default_p, struct ossl_provider_st*& custom_p, struct ossl_lib_ctx_st* libctx_p,
|
||||
mode_t mode);
|
||||
inline bool load_global(mode_t mode) {
|
||||
return load(s_global_prov_default_p, s_global_prov_custom_p, nullptr, mode);
|
||||
}
|
||||
inline bool load_tls(mode_t mode) {
|
||||
return load(s_tls_prov_default_p, s_tls_prov_custom_p, s_tls_libctx_p, mode);
|
||||
}
|
||||
|
||||
bool set_propstr(struct ossl_lib_ctx_st* libctx, mode_t mode);
|
||||
bool set_mode(struct ossl_lib_ctx_st* libctx, mode_t mode);
|
||||
|
||||
public:
|
||||
OpenSSLProvider();
|
||||
~OpenSSLProvider(); // NOLINT(performance-trivially-destructible): dtor impl dependent on ifdef
|
||||
|
||||
inline void set_global_mode(mode_t mode) {
|
||||
set_mode(nullptr, mode);
|
||||
}
|
||||
|
||||
inline void set_tls_mode(mode_t mode) {
|
||||
set_mode(s_tls_libctx_p, mode);
|
||||
}
|
||||
|
||||
const char* propquery(mode_t mode) const;
|
||||
|
||||
inline mode_t propquery_global() const {
|
||||
return (is_set(flags_t::global_custom_provider)) ? mode_t::custom_provider : mode_t::default_provider;
|
||||
}
|
||||
inline mode_t propquery_tls() const {
|
||||
return (is_set(flags_t::tls_custom_provider)) ? mode_t::custom_provider : mode_t::default_provider;
|
||||
}
|
||||
|
||||
inline const char* propquery_global_str() const {
|
||||
return propquery(propquery_global());
|
||||
}
|
||||
inline const char* propquery_tls_str() const {
|
||||
return propquery(propquery_tls());
|
||||
}
|
||||
inline const char* propquery_default() const {
|
||||
return propquery(mode_t::default_provider);
|
||||
}
|
||||
inline const char* propquery_custom() const {
|
||||
return propquery(mode_t::custom_provider);
|
||||
}
|
||||
|
||||
/// @brief return the TLS OSSL library context
|
||||
inline operator struct ossl_lib_ctx_st *() {
|
||||
return s_tls_libctx_p;
|
||||
}
|
||||
|
||||
static bool supports_provider_tpm();
|
||||
static bool supports_provider_custom();
|
||||
|
||||
static void cleanup();
|
||||
};
|
||||
|
||||
} // namespace evse_security
|
||||
|
||||
#endif // OPENSSL_TPM_HPP
|
||||
@@ -0,0 +1,124 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#ifdef LIBEVSE_CRYPTO_SUPPLIER_OPENSSL
|
||||
|
||||
#include <memory>
|
||||
#include <openssl/x509v3.h>
|
||||
|
||||
template <> class std::default_delete<X509> {
|
||||
public:
|
||||
void operator()(X509* ptr) const {
|
||||
::X509_free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
template <> class std::default_delete<X509_STORE> {
|
||||
public:
|
||||
void operator()(X509_STORE* ptr) const {
|
||||
::X509_STORE_free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
template <> class std::default_delete<X509_STORE_CTX> {
|
||||
public:
|
||||
void operator()(X509_STORE_CTX* ptr) const {
|
||||
::X509_STORE_CTX_free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
template <> class std::default_delete<X509_REQ> {
|
||||
public:
|
||||
void operator()(X509_REQ* ptr) const {
|
||||
::X509_REQ_free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
template <> class std::default_delete<STACK_OF(X509)> {
|
||||
public:
|
||||
void operator()(STACK_OF(X509) * ptr) const {
|
||||
sk_X509_free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
template <> class std::default_delete<EVP_PKEY> {
|
||||
public:
|
||||
void operator()(EVP_PKEY* ptr) const {
|
||||
::EVP_PKEY_free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
template <> class std::default_delete<EVP_PKEY_CTX> {
|
||||
public:
|
||||
void operator()(EVP_PKEY_CTX* ptr) const {
|
||||
::EVP_PKEY_CTX_free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
template <> class std::default_delete<BIO> {
|
||||
public:
|
||||
void operator()(BIO* ptr) const {
|
||||
::BIO_free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
template <> class std::default_delete<EVP_MD_CTX> {
|
||||
public:
|
||||
void operator()(EVP_MD_CTX* ptr) const {
|
||||
::EVP_MD_CTX_destroy(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
template <> class std::default_delete<EVP_ENCODE_CTX> {
|
||||
public:
|
||||
void operator()(EVP_ENCODE_CTX* ptr) const {
|
||||
::EVP_ENCODE_CTX_free(ptr);
|
||||
}
|
||||
};
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
using X509_ptr = std::unique_ptr<X509>;
|
||||
using X509_STORE_ptr = std::unique_ptr<X509_STORE>;
|
||||
using X509_STORE_CTX_ptr = std::unique_ptr<X509_STORE_CTX>;
|
||||
// Unsafe since it does not free contained elements, only the stack, the element
|
||||
// cleanup has to be done manually
|
||||
using X509_STACK_UNSAFE_ptr = std::unique_ptr<STACK_OF(X509)>;
|
||||
using X509_REQ_ptr = std::unique_ptr<X509_REQ>;
|
||||
using EVP_PKEY_ptr = std::unique_ptr<EVP_PKEY>;
|
||||
using EVP_PKEY_CTX_ptr = std::unique_ptr<EVP_PKEY_CTX>;
|
||||
using BIO_ptr = std::unique_ptr<BIO>;
|
||||
using EVP_MD_CTX_ptr = std::unique_ptr<EVP_MD_CTX>;
|
||||
using EVP_ENCODE_CTX_ptr = std::unique_ptr<EVP_ENCODE_CTX>;
|
||||
|
||||
struct X509Handle;
|
||||
struct KeyHandle;
|
||||
|
||||
struct X509HandleOpenSSL : public X509Handle {
|
||||
X509HandleOpenSSL(X509* certificate) : x509(certificate) {
|
||||
}
|
||||
|
||||
X509* get() {
|
||||
return x509.get();
|
||||
}
|
||||
|
||||
private:
|
||||
X509_ptr x509;
|
||||
};
|
||||
|
||||
struct KeyHandleOpenSSL : public KeyHandle {
|
||||
KeyHandleOpenSSL(EVP_PKEY* key) : key(key) {
|
||||
}
|
||||
|
||||
EVP_PKEY* get() {
|
||||
return key.get();
|
||||
}
|
||||
|
||||
private:
|
||||
EVP_PKEY_ptr key;
|
||||
};
|
||||
|
||||
} // namespace evse_security
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,370 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <everest/timer.hpp>
|
||||
|
||||
#include <evse_security/crypto/evse_crypto.hpp>
|
||||
#include <evse_security/evse_types.hpp>
|
||||
#include <evse_security/utils/evse_filesystem_types.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
|
||||
#ifdef BUILD_TESTING_EVSE_SECURITY
|
||||
#include <gtest/gtest_prod.h>
|
||||
#endif
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
struct LinkPaths {
|
||||
fs::path secc_leaf_cert_link;
|
||||
fs::path secc_leaf_key_link;
|
||||
fs::path cpo_cert_chain_link;
|
||||
};
|
||||
|
||||
struct DirectoryPaths {
|
||||
fs::path csms_leaf_cert_directory; /**< csms leaf certificate for OCPP shall be located in this directory */
|
||||
fs::path csms_leaf_key_directory; /**< csms leaf key shall be located in this directory */
|
||||
fs::path secc_leaf_cert_directory; /**< secc leaf certificate for ISO15118 shall be located in this directory */
|
||||
fs::path secc_leaf_key_directory; /**< secc leaf key shall be located in this directory */
|
||||
};
|
||||
struct FilePaths {
|
||||
// bundle paths
|
||||
fs::path csms_ca_bundle;
|
||||
fs::path mf_ca_bundle;
|
||||
fs::path mo_ca_bundle;
|
||||
fs::path v2g_ca_bundle;
|
||||
|
||||
DirectoryPaths directories;
|
||||
LinkPaths links;
|
||||
};
|
||||
|
||||
struct CertificateQueryParams {
|
||||
LeafCertificateType certificate_type;
|
||||
EncodingFormat encoding{EncodingFormat::PEM};
|
||||
/// if OCSP information should be included
|
||||
bool include_ocsp{false};
|
||||
/// if the root certificate of the leaf should be included in the returned list
|
||||
bool include_root{false};
|
||||
/// if true, all valid leafs will be included, sorted in order, with the newest being
|
||||
/// first. If false, only the newest one will be returned
|
||||
bool include_all_valid{false};
|
||||
/// if true the leafs that will be valid in the future will be included, with the newest
|
||||
/// being first
|
||||
bool include_future_valid{false};
|
||||
/// if true, will remove all duplicates found, since we can find a leaf for example
|
||||
/// in 2 files, one in 'leaf_single' and one in 'leaf_chain'. For delete routines
|
||||
/// we need both files returned, while for queries (v2g_chain) we don't need duplicates
|
||||
bool remove_duplicates{false};
|
||||
};
|
||||
|
||||
// Unchangeable security limit for certificate deletion, a min entry count will be always kept (newest)
|
||||
static constexpr std::size_t DEFAULT_MINIMUM_CERTIFICATE_ENTRIES = 10;
|
||||
// 50 MB default limit for filesystem usage
|
||||
static constexpr std::uintmax_t DEFAULT_MAX_FILESYSTEM_SIZE = 1024 * 1024 * 50;
|
||||
// Default maximum 2000 certificate entries
|
||||
static constexpr std::uintmax_t DEFAULT_MAX_CERTIFICATE_ENTRIES = 2000;
|
||||
|
||||
// Expiry for CSRs that did not receive a response CSR, 60 minutes
|
||||
static constexpr std::chrono::seconds DEFAULT_CSR_EXPIRY(3600);
|
||||
|
||||
// Garbage collect default time, 20 minutes
|
||||
static constexpr std::chrono::seconds DEFAULT_GARBAGE_COLLECT_TIME(20 * 60);
|
||||
|
||||
/// @brief This class holds filesystem paths to CA bundle file locations and directories for leaf certificates
|
||||
class EvseSecurity {
|
||||
|
||||
public:
|
||||
/// @brief Constructor initializes the certificate and key storage using the given \p file_paths for the different
|
||||
/// PKIs. For CA certificates either CA bundle files or directories containing the certificates can be specified.
|
||||
/// For the SECC and CSMS leaf certificates, directories must be specified.
|
||||
/// @param file_paths specifies the certificate and key storage locations on the filesystem
|
||||
/// @param private_key_password optional password for encrypted private keys
|
||||
/// @param max_fs_usage_bytes optional maximum filesystem usage for certificates. Defaults to
|
||||
/// 'DEFAULT_MAX_FILESYSTEM_SIZE'
|
||||
/// @param max_fs_certificate_store_entries optional maximum certificate entries. Defaults to
|
||||
/// 'DEFAULT_MAX_CERTIFICATE_ENTRIES'
|
||||
/// @param csr_expiry optional expiry time for a CSR entry. If the time is exceeded it will be deleted. Defaults to
|
||||
/// 'DEFAULT_CSR_EXPIRY'
|
||||
/// @param garbage_collect_time optional garbage collect time. How often we will delete expired CSRs and
|
||||
/// certificates. Defaults to 'DEFAULT_GARBAGE_COLLECT_TIME'
|
||||
EvseSecurity(const FilePaths& file_paths, const std::optional<std::string>& private_key_password = std::nullopt,
|
||||
const std::optional<std::uintmax_t>& max_fs_usage_bytes = std::nullopt,
|
||||
const std::optional<std::uintmax_t>& max_fs_certificate_store_entries = std::nullopt,
|
||||
const std::optional<std::chrono::seconds>& csr_expiry = std::nullopt,
|
||||
const std::optional<std::chrono::seconds>& garbage_collect_time = std::nullopt);
|
||||
|
||||
/// @brief Destructor
|
||||
~EvseSecurity();
|
||||
|
||||
/// @brief Installs the given \p certificate within the specified CA bundle file or directory if directories are
|
||||
/// used. If the certificate already exists it will only be updated
|
||||
/// @param certificate PEM formatted CA certificate
|
||||
/// @param certificate_type specifies the CA certificate type
|
||||
/// @return result of the operation
|
||||
InstallCertificateResult install_ca_certificate(const std::string& certificate, CaCertificateType certificate_type);
|
||||
|
||||
/// @brief Deletes the certificate specified by \p certificate_hash_data . If a CA certificate is specified, the
|
||||
/// certificate is removed from the bundle or directory. If a leaf certificate is specified, the file will be
|
||||
/// removed from the filesystem. It will also delete all certificates issued by this certificate, so that no invalid
|
||||
/// hierarchies persisted on the filesystem
|
||||
/// @param certificate_hash_data specifies the certificate to be deleted
|
||||
/// @return result of the operation
|
||||
DeleteResult delete_certificate(const CertificateHashData& certificate_hash_data);
|
||||
|
||||
/// @brief Verifies the given \p certificate_chain against the respective CA
|
||||
/// trust anchors (indicated by the given \p certificate_type) for the leaf.
|
||||
/// @param certificate_chain PEM formatted certificate or certificate chain
|
||||
/// @param certificate_type type of the root certificate for which the chain is verified
|
||||
/// @return result of the operation
|
||||
CertificateValidationResult verify_certificate(const std::string& certificate_chain,
|
||||
const LeafCertificateType certificate_type);
|
||||
|
||||
/// @brief Verifies the given \p certificate_chain against the respective CA
|
||||
/// trust anchors (indicated by the given \p certificate_types) for the leaf.
|
||||
/// @param certificate_chain PEM formatted certificate or certificate chain
|
||||
/// @param certificate_types types of the root certificates for which the chain is verified
|
||||
/// @return result of the operation
|
||||
CertificateValidationResult verify_certificate(const std::string& certificate_chain,
|
||||
const std::vector<LeafCertificateType>& certificate_types);
|
||||
|
||||
/// @brief Verifies the given \p certificate_chain for the given \p certificate_type against the respective CA
|
||||
/// certificates for the leaf and if valid installs the certificate on the filesystem. Before installing on the
|
||||
/// filesystem, this function checks if a private key is present for the given certificate on the filesystem. Two
|
||||
/// files are installed, one containing the single leaf (presuming it is the first in the chain) and also the full
|
||||
/// certificate chain. The \ref get_key_pair function will return a path to both files if they exist, the one
|
||||
/// containing the single leaf, and the file containing the leaf including the SUBCAs if present
|
||||
/// @param certificate_chain PEM formatted certificate or certificate chain
|
||||
/// @param certificate_type type of the leaf certificate
|
||||
/// @return result of the operation
|
||||
InstallCertificateResult update_leaf_certificate(const std::string& certificate_chain,
|
||||
LeafCertificateType certificate_type);
|
||||
|
||||
/// @brief Retrieves all certificates installed on the filesystem applying the \p certificate_type filter.
|
||||
/// In the case of the 'V2GCertificateChain', it will return all valid leafs chains, with the newest being
|
||||
/// the first in the list
|
||||
/// @param certificate_types
|
||||
/// @return contains the certificate hash data chains of the requested \p certificate_type
|
||||
GetInstalledCertificatesResult get_installed_certificate(CertificateType certificate_type);
|
||||
|
||||
/// @brief Retrieves all certificates installed on the filesystem applying the \p certificate_types filter.
|
||||
/// @param certificate_types
|
||||
/// @return contains the certificate hash data chains of the requested \p certificate_types
|
||||
GetInstalledCertificatesResult get_installed_certificates(const std::vector<CertificateType>& certificate_types);
|
||||
|
||||
/// @brief Retrieves the certificate count applying the \p certificate_types filter.
|
||||
int get_count_of_installed_certificates(const std::vector<CertificateType>& certificate_types);
|
||||
|
||||
/// @brief Command to retrieve the OCSP request data of the V2G certificates (SubCAs and possibly V2G leaf)
|
||||
/// @return contains OCSP request data
|
||||
OCSPRequestDataList get_v2g_ocsp_request_data();
|
||||
|
||||
/// @brief Retrieves the OCSP request data of the given MO contract \p certificate_chain. Since
|
||||
/// the provided MO chain can be signed by both the MO_ROOT and the V2G_ROOT, the received chain
|
||||
/// will be tested against both
|
||||
/// @param certificate_chain PEM formatted MO certificate or MO certificate chain
|
||||
/// @return contains OCSP request data
|
||||
OCSPRequestDataList get_mo_ocsp_request_data(const std::string& certificate_chain);
|
||||
|
||||
/// @brief Updates the OCSP cache for the given \p certificate_hash_data with the given \p ocsp_response
|
||||
/// @param certificate_hash_data identifies the certificate for which the \p ocsp_response is specified
|
||||
/// @param ocsp_response the actual OCSP data
|
||||
void update_ocsp_cache(const CertificateHashData& certificate_hash_data, const std::string& ocsp_response);
|
||||
|
||||
// TODO: Switch to path
|
||||
/// @brief Retrieves from the OCSP cache for the given \p certificate_hash_data
|
||||
/// @param certificate_hash_data identifies the certificate for which the \p ocsp_response is specified
|
||||
/// @return the actual OCSP data or an empty value
|
||||
std::optional<fs::path> retrieve_ocsp_cache(const CertificateHashData& certificate_hash_data);
|
||||
|
||||
/// @brief Indicates if a CA certificate for the given \p certificate_type is installed on the filesystem
|
||||
/// Supports both CA certificate bundles and directories
|
||||
/// @param certificate_type
|
||||
/// @return true if CA certificate is present, else false
|
||||
bool is_ca_certificate_installed(CaCertificateType certificate_type);
|
||||
|
||||
/// @brief Should be invoked when a certificate CSR was not properly generated by the CSMS
|
||||
/// and that the pairing key that was generated should be deleted
|
||||
void certificate_signing_request_failed(const std::string& csr, LeafCertificateType certificate_type);
|
||||
|
||||
/// @brief Generates a certificate signing request for the given \p certificate_type , \p country , \p organization
|
||||
/// and \p common , uses the TPM if \p use_tpm is true
|
||||
/// @param certificate_type
|
||||
/// @param country
|
||||
/// @param organization
|
||||
/// @param common
|
||||
/// @param use_custom_provider If the custom provider (which can be the TPM if using -DUSING_TPM2=ON) should be
|
||||
/// used for the CSR request
|
||||
/// @return the status and an optional PEM formatted certificate signing request string
|
||||
GetCertificateSignRequestResult generate_certificate_signing_request(LeafCertificateType certificate_type,
|
||||
const std::string& country,
|
||||
const std::string& organization,
|
||||
const std::string& common,
|
||||
bool use_custom_provider);
|
||||
|
||||
/// @brief Generates a certificate signing request for the given \p certificate_type , \p country , \p organization
|
||||
/// and \p common without using the TPM
|
||||
/// @param certificate_type
|
||||
/// @param country
|
||||
/// @param organization
|
||||
/// @param common
|
||||
/// @return the status and an optional PEM formatted certificate signing request string
|
||||
GetCertificateSignRequestResult generate_certificate_signing_request(LeafCertificateType certificate_type,
|
||||
const std::string& country,
|
||||
const std::string& organization,
|
||||
const std::string& common);
|
||||
|
||||
/// @brief Searches the filesystem on the specified directories for the given \p certificate_type and retrieves the
|
||||
/// most recent certificate that is already valid and the respective key. If no certificate is present or no key is
|
||||
/// matching the certificate, this function returns a GetKeyPairStatus other than "Accepted". The function \ref
|
||||
/// update_leaf_certificate will install two files for each leaf, one containing the single leaf and one containing
|
||||
/// the leaf including any possible SUBCAs
|
||||
/// @param certificate_type type of the leaf certificate
|
||||
/// @param encoding specifies PEM or DER format
|
||||
/// @param include_ocsp if OCSP data should be included
|
||||
/// @return contains response result, with info related to the certificate chain and response status
|
||||
GetCertificateInfoResult get_leaf_certificate_info(LeafCertificateType certificate_type, EncodingFormat encoding,
|
||||
bool include_ocsp = false);
|
||||
|
||||
/// @brief Finds the latest valid leafs, for each root certificate that is present on the filesystem, and
|
||||
/// returns all the newest valid leafs that are present for different roots. This is required, because
|
||||
/// a query parameter when requesting the leaf is not advisable during the TLS handshake
|
||||
/// Existing filesystem:
|
||||
/// ROOT_V2G_Hubject->SUB_CA1->SUB_CA2->Leaf_Invalid_A
|
||||
/// ROOT_V2G_Hubject->SUB_CA1->SUB_CA2->Leaf_Valid_A
|
||||
/// ROOT_V2G_Hubject->SUB_CA1->SUB_CA2->Leaf_Valid_B
|
||||
/// ROOT_V2G_OtherProvider->SUB_CA_O1->SUB_CA_O2->Leav_Valid_A
|
||||
/// will return:
|
||||
/// ROOT_V2G_Hubject->SUB_CA1->SUB_CA2->Leaf_Valid_B +
|
||||
/// ROOT_V2G_OtherProvider->SUB_CA_O1->SUB_CA_O2->Leav_Valid_A
|
||||
/// Note: non self-signed roots and cross-signed certificates are not supported
|
||||
/// @param certificate_type type of leaf certificate that we start the search from
|
||||
/// @param encoding specifies PEM or DER format
|
||||
/// @param include_ocsp if OCSP data should be included
|
||||
/// @return contains response result, with info related to the full certificate chains info and response status
|
||||
GetCertificateFullInfoResult get_all_valid_certificates_info(LeafCertificateType certificate_type,
|
||||
EncodingFormat encoding, bool include_ocsp = false);
|
||||
|
||||
/// @brief Checks and updates the symlinks for the V2G leaf certificates and keys to the most recent valid one
|
||||
/// @return true if one of the links was updated
|
||||
bool update_certificate_links(LeafCertificateType certificate_type);
|
||||
|
||||
/// @brief Retrieves the PEM formatted CA bundle file 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. See \ref
|
||||
/// update_leaf_certificate
|
||||
/// @param certificate_type
|
||||
/// @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);
|
||||
|
||||
/// @brief Gets the expiry day count for the leaf certificate of the given \p certificate_type
|
||||
/// @param certificate_type
|
||||
/// @return day count until the leaf certificate expires
|
||||
int get_leaf_expiry_days_count(LeafCertificateType certificate_type);
|
||||
|
||||
/// @brief Collects and deletes unfulfilled CSR private keys. It also deletes the expired
|
||||
/// certificates. The caller must be sure the system clock is properly set for detecting expired
|
||||
/// certificates. A minimum of 'DEFAULT_MINIMUM_CERTIFICATE_ENTRIES' certificates to
|
||||
/// have a safeguard against a poorly set system clock
|
||||
void garbage_collect();
|
||||
|
||||
/// @brief Verifies the file at the given \p path using the provided \p signing_certificate and \p signature
|
||||
/// @param path
|
||||
/// @param signing_certificate
|
||||
/// @param signature
|
||||
/// @return true if the verification was successful, false if not
|
||||
static bool verify_file_signature(const fs::path& path, const std::string& signing_certificate,
|
||||
const std::string signature);
|
||||
|
||||
/// @brief Decodes the base64 encoded string to the raw byte representation
|
||||
/// @param base64_string base64 encoded string
|
||||
/// @return decoded byte vector
|
||||
static std::vector<std::uint8_t> base64_decode_to_bytes(const std::string& base64_string);
|
||||
|
||||
/// @brief Decodes the base64 encoded string to string representation
|
||||
/// @param base64_string base64 encoded string
|
||||
/// @return decoded string array
|
||||
static std::string base64_decode_to_string(const std::string& base64_string);
|
||||
|
||||
/// @brief Encodes the raw bytes to a base64 string
|
||||
/// @param decoded_bytes raw byte array
|
||||
/// @return encoded base64 string
|
||||
static std::string base64_encode_from_bytes(const std::vector<std::uint8_t>& bytes);
|
||||
|
||||
/// @brief Encodes the string containing raw bytes to a base64 string
|
||||
/// @param decoded_bytes string containing raw bytes
|
||||
/// @return encoded base64 string
|
||||
static std::string base64_encode_from_string(const std::string& string);
|
||||
|
||||
private:
|
||||
// Internal versions of the functions do not lock the mutex
|
||||
CertificateValidationResult verify_certificate_internal(const std::string& certificate_chain,
|
||||
const std::vector<LeafCertificateType>& certificate_types);
|
||||
|
||||
GetCertificateInfoResult get_leaf_certificate_info_internal(LeafCertificateType certificate_type,
|
||||
EncodingFormat encoding, bool include_ocsp = false);
|
||||
|
||||
/// @brief Retrieves information related to leaf certificates
|
||||
GetCertificateFullInfoResult get_full_leaf_certificate_info_internal(const CertificateQueryParams& params);
|
||||
|
||||
GetCertificateInfoResult get_ca_certificate_info_internal(CaCertificateType certificate_type);
|
||||
std::optional<fs::path> retrieve_ocsp_cache_internal(const CertificateHashData& certificate_hash_data);
|
||||
|
||||
bool is_ca_certificate_installed_internal(CaCertificateType certificate_type);
|
||||
|
||||
GetCertificateSignRequestResult
|
||||
generate_certificate_signing_request_internal(LeafCertificateType certificate_type,
|
||||
const CertificateSigningRequestInfo& info);
|
||||
|
||||
/// @brief Determines if the total filesize of certificates is > than the max_filesystem_usage bytes
|
||||
bool is_filesystem_full();
|
||||
|
||||
static std::mutex security_mutex;
|
||||
|
||||
// why not reusing the FilePaths here directly (storage duplication)
|
||||
std::map<CaCertificateType, fs::path> ca_bundle_path_map;
|
||||
DirectoryPaths directories;
|
||||
LinkPaths links;
|
||||
|
||||
// CSRs that were generated and require an expiry time
|
||||
std::map<fs::path, std::chrono::time_point<std::chrono::steady_clock>> managed_csr;
|
||||
|
||||
// Maximum filesystem usage
|
||||
std::uintmax_t max_fs_usage_bytes;
|
||||
// Maximum filesystem certificate entries
|
||||
std::uintmax_t max_fs_certificate_store_entries;
|
||||
// Default csr expiry in seconds
|
||||
std::chrono::seconds csr_expiry;
|
||||
// Default time to garbage collect
|
||||
std::chrono::seconds garbage_collect_time;
|
||||
|
||||
// GC timer
|
||||
Everest::SteadyTimer garbage_collect_timer;
|
||||
|
||||
// FIXME(piet): map passwords to encrypted private key files
|
||||
// is there only one password for all private keys?
|
||||
std::optional<std::string> private_key_password; // used to decrypt encrypted private keys
|
||||
|
||||
// Define here all tests that require internal function usage
|
||||
#ifdef BUILD_TESTING_EVSE_SECURITY
|
||||
FRIEND_TEST(EvseSecurityTests, verify_directory_bundles);
|
||||
FRIEND_TEST(EvseSecurityTests, verify_full_filesystem_install_reject);
|
||||
FRIEND_TEST(EvseSecurityTests, verify_full_filesystem);
|
||||
FRIEND_TEST(EvseSecurityTests, verify_expired_csr_deletion);
|
||||
FRIEND_TEST(EvseSecurityTests, verify_ocsp_garbage_collect);
|
||||
FRIEND_TEST(EvseSecurityTestsExpired, verify_expired_leaf_deletion);
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,225 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <evse_security/utils/evse_filesystem_types.hpp>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
const fs::path PEM_EXTENSION = ".pem";
|
||||
const fs::path DER_EXTENSION = ".der";
|
||||
const fs::path KEY_EXTENSION = ".key";
|
||||
const fs::path CUSTOM_KEY_EXTENSION = ".tkey";
|
||||
const fs::path CERT_HASH_EXTENSION = ".hash";
|
||||
|
||||
enum class EncodingFormat {
|
||||
DER,
|
||||
PEM,
|
||||
};
|
||||
|
||||
enum class CaCertificateType {
|
||||
V2G,
|
||||
MO,
|
||||
CSMS,
|
||||
MF,
|
||||
};
|
||||
|
||||
enum class LeafCertificateType {
|
||||
CSMS,
|
||||
V2G,
|
||||
MF,
|
||||
MO,
|
||||
};
|
||||
|
||||
enum class CertificateType {
|
||||
V2GRootCertificate,
|
||||
MORootCertificate,
|
||||
CSMSRootCertificate,
|
||||
V2GCertificateChain,
|
||||
MFRootCertificate,
|
||||
};
|
||||
|
||||
enum class HashAlgorithm {
|
||||
SHA256,
|
||||
SHA384,
|
||||
SHA512,
|
||||
};
|
||||
|
||||
enum class CertificateValidationResult {
|
||||
Valid,
|
||||
Expired,
|
||||
InvalidSignature,
|
||||
IssuerNotFound,
|
||||
InvalidLeafSignature,
|
||||
InvalidChain,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
enum class InstallCertificateResult {
|
||||
InvalidSignature,
|
||||
InvalidCertificateChain,
|
||||
InvalidFormat,
|
||||
InvalidCommonName,
|
||||
NoRootCertificateInstalled,
|
||||
Expired,
|
||||
CertificateStoreMaxLengthExceeded,
|
||||
WriteError,
|
||||
Accepted,
|
||||
};
|
||||
|
||||
enum class DeleteCertificateResult {
|
||||
Accepted,
|
||||
Failed,
|
||||
NotFound,
|
||||
};
|
||||
|
||||
enum class GetInstalledCertificatesStatus {
|
||||
Accepted,
|
||||
NotFound,
|
||||
};
|
||||
|
||||
enum class GetCertificateInfoStatus {
|
||||
Accepted,
|
||||
Rejected,
|
||||
NotFound,
|
||||
NotFoundValid,
|
||||
PrivateKeyNotFound,
|
||||
};
|
||||
|
||||
enum class GetCertificateSignRequestStatus {
|
||||
Accepted,
|
||||
InvalidRequestedType, ///< Requested a CSR for non CSMS/V2G leafs
|
||||
KeyGenError, ///< The key could not be generated with the requested/default parameters
|
||||
GenerationError, ///< Any other error when creating the CSR
|
||||
};
|
||||
|
||||
inline bool str_cast_insensitive_cmp(const std::string& a, const std::string& b) {
|
||||
if (a.size() != b.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](std::string::value_type a, std::string::value_type b) {
|
||||
return (std::tolower(a) == std::tolower(b));
|
||||
});
|
||||
}
|
||||
|
||||
// types of evse_security
|
||||
|
||||
struct CertificateHashData {
|
||||
HashAlgorithm hash_algorithm; ///< Algorithm used for the hashes provided
|
||||
std::string issuer_name_hash; ///< The hash of the issuer's distinguished name (DN), calculated over the DER
|
||||
///< encoding of the issuer's name field.
|
||||
std::string issuer_key_hash; ///< The hash of the DER encoded public key: the value (excluding tag and length) of
|
||||
///< the subject public key field
|
||||
std::string serial_number; ///< The string representation of the hexadecimal value of the serial number without the
|
||||
///< prefix "0x" and without leading zeroes.
|
||||
|
||||
std::string debug_common_name; ///< Common name for easy debug
|
||||
|
||||
bool operator==(const CertificateHashData& Other) const {
|
||||
return hash_algorithm == Other.hash_algorithm && issuer_name_hash == Other.issuer_name_hash &&
|
||||
issuer_key_hash == Other.issuer_key_hash && serial_number == Other.serial_number;
|
||||
}
|
||||
|
||||
bool case_insensitive_comparison(const CertificateHashData& Other) const {
|
||||
if (hash_algorithm != Other.hash_algorithm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false == str_cast_insensitive_cmp(issuer_name_hash, Other.issuer_name_hash)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false == str_cast_insensitive_cmp(issuer_key_hash, Other.issuer_key_hash)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false == str_cast_insensitive_cmp(serial_number, Other.serial_number)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_valid() const {
|
||||
return (false == issuer_name_hash.empty()) && (false == issuer_key_hash.empty()) &&
|
||||
(false == serial_number.empty());
|
||||
}
|
||||
};
|
||||
struct CertificateHashDataChain {
|
||||
CertificateType certificate_type; ///< Indicates the type of the certificate for which the hash data is provided
|
||||
CertificateHashData certificate_hash_data; ///< Contains the hash data of the certificate
|
||||
std::vector<CertificateHashData>
|
||||
child_certificate_hash_data; ///< Contains the hash data of the child's certificates
|
||||
};
|
||||
struct GetInstalledCertificatesResult {
|
||||
GetInstalledCertificatesStatus status; ///< Indicates the status of the request
|
||||
std::vector<CertificateHashDataChain>
|
||||
certificate_hash_data_chain; ///< the hashed certificate data for each requested certificates
|
||||
};
|
||||
struct DeleteResult {
|
||||
DeleteCertificateResult result; ///< Indicates the status of the request
|
||||
std::optional<CaCertificateType> ca_certificate_type; ///< Valid if we deleted a root
|
||||
std::optional<LeafCertificateType> leaf_certificate_type; ///< Valid if we deleted a leaf
|
||||
};
|
||||
struct OCSPRequestData {
|
||||
std::optional<CertificateHashData> certificate_hash_data; ///< Contains the hash data of the certificate
|
||||
std::optional<std::string> responder_url; ///< Contains the responder URL
|
||||
};
|
||||
struct OCSPRequestDataList {
|
||||
std::vector<OCSPRequestData> ocsp_request_data_list; ///< A list of OCSP request data
|
||||
};
|
||||
|
||||
struct CertificateOCSP {
|
||||
CertificateHashData hash; ///< Hash of the certificate for which the OCSP data is held
|
||||
std::optional<fs::path> ocsp_path; ///< Path to the file in which the certificate OCSP data is held
|
||||
};
|
||||
|
||||
struct CertificateInfo {
|
||||
fs::path key; ///< The path of the PEM or DER encoded private key
|
||||
std::optional<std::string> certificate_root; ///< The PEM of root certificate used by the leaf, has a value only
|
||||
/// when using 'get_all_valid_certificates_info'
|
||||
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 single certificate if found
|
||||
int certificate_count; ///< The count of certificates, if the chain is available, or 1 if single
|
||||
/// (the root is not taken into account because of the OCSP cache)
|
||||
std::optional<std::string> password; ///< Specifies the password for the private key if encrypted
|
||||
std::vector<CertificateOCSP> ocsp; ///< The ordered list of OCSP certificate data based on the chain file order
|
||||
};
|
||||
|
||||
struct GetCertificateInfoResult {
|
||||
GetCertificateInfoStatus status;
|
||||
std::optional<CertificateInfo> info;
|
||||
};
|
||||
|
||||
struct GetCertificateFullInfoResult {
|
||||
GetCertificateInfoStatus status;
|
||||
std::vector<CertificateInfo> info;
|
||||
};
|
||||
|
||||
struct GetCertificateSignRequestResult {
|
||||
GetCertificateSignRequestStatus status;
|
||||
std::optional<std::string> csr;
|
||||
};
|
||||
|
||||
namespace conversions {
|
||||
std::string encoding_format_to_string(EncodingFormat e);
|
||||
std::string ca_certificate_type_to_string(CaCertificateType e);
|
||||
std::string leaf_certificate_type_to_string(LeafCertificateType e);
|
||||
std::string leaf_certificate_type_to_filename(LeafCertificateType e);
|
||||
std::string certificate_type_to_string(CertificateType e);
|
||||
std::string hash_algorithm_to_string(HashAlgorithm e);
|
||||
HashAlgorithm string_to_hash_algorithm(const std::string& s);
|
||||
std::string install_certificate_result_to_string(InstallCertificateResult e);
|
||||
std::string delete_certificate_result_to_string(DeleteCertificateResult e);
|
||||
std::string get_installed_certificates_status_to_string(GetInstalledCertificatesStatus e);
|
||||
std::string get_certificate_info_status_to_string(GetCertificateInfoStatus e);
|
||||
} // namespace conversions
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <evse_security/utils/evse_filesystem_types.hpp>
|
||||
|
||||
namespace evse_security {
|
||||
struct CertificateHashData;
|
||||
}
|
||||
|
||||
namespace evse_security::filesystem_utils {
|
||||
|
||||
bool is_subdirectory(const fs::path& base, const fs::path& subdir);
|
||||
|
||||
/// @brief Should be used to ensure file exists, not for directories
|
||||
bool create_file_if_nonexistent(const fs::path& file_path);
|
||||
/// @brief Ensure a file exists (if there's an extension), or a directory if no extension is found
|
||||
bool create_file_or_dir_if_nonexistent(const fs::path& file_path);
|
||||
bool delete_file(const fs::path& file_path);
|
||||
|
||||
bool read_from_file(const fs::path& file_path, std::string& out_data);
|
||||
bool write_to_file(const fs::path& file_path, const std::string& data, std::ios::openmode mode);
|
||||
|
||||
/// @brief Process the file in chunks with the provided function. If the process function
|
||||
/// returns false, this function will also immediately return
|
||||
/// @return True if the file was properly opened false otherwise
|
||||
bool process_file(const fs::path& file_path, size_t buffer_size,
|
||||
std::function<bool(const std::uint8_t*, std::size_t, bool last_chunk)>&& func);
|
||||
|
||||
std::string get_random_file_name(const std::string& extension);
|
||||
|
||||
/// @brief Attempts to read a certificate hash from a file. The extension is taken into account
|
||||
/// @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
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#ifndef LIBEVSE_SECURITY_USE_BOOST_FILESYSTEM
|
||||
#include <filesystem>
|
||||
#else
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/fstream.hpp>
|
||||
#endif
|
||||
|
||||
#ifndef LIBEVSE_SECURITY_USE_BOOST_FILESYSTEM
|
||||
namespace fs = std::filesystem;
|
||||
namespace fsstd = std;
|
||||
#else
|
||||
namespace fs = boost::filesystem;
|
||||
namespace fsstd = boost::filesystem;
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
add_subdirectory(evse_security)
|
||||
@@ -0,0 +1,129 @@
|
||||
|
||||
add_library(evse_security)
|
||||
add_library(everest::evse_security ALIAS evse_security)
|
||||
|
||||
target_sources(evse_security
|
||||
PRIVATE
|
||||
evse_security.cpp
|
||||
evse_types.cpp
|
||||
|
||||
certificate/x509_bundle.cpp
|
||||
certificate/x509_hierarchy.cpp
|
||||
certificate/x509_wrapper.cpp
|
||||
|
||||
utils/evse_filesystem.cpp
|
||||
|
||||
crypto/interface/crypto_supplier.cpp
|
||||
crypto/interface/crypto_types.cpp
|
||||
)
|
||||
|
||||
if (LIBEVSE_CRYPTO_SUPPLIER_OPENSSL)
|
||||
target_sources(evse_security
|
||||
PRIVATE
|
||||
crypto/openssl/openssl_crypto_supplier.cpp
|
||||
crypto/openssl/openssl_provider.cpp
|
||||
)
|
||||
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}>
|
||||
)
|
||||
|
||||
set_target_properties(evse_security
|
||||
PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
|
||||
#############
|
||||
# Logging configuration
|
||||
#############
|
||||
if (EVEREST_CUSTOM_LOGGING_LIBRARY)
|
||||
if(NOT TARGET ${EVEREST_CUSTOM_LOGGING_LIBRARY})
|
||||
message(FATAL_ERROR "${EVEREST_CUSTOM_LOGGING_LIBRARY} is not a valid library")
|
||||
else()
|
||||
target_link_libraries(evse_security
|
||||
PUBLIC
|
||||
${EVEREST_CUSTOM_LOGGING_LIBRARY}
|
||||
)
|
||||
message(STATUS "Using custom logging library: ${EVEREST_CUSTOM_LOGGING_LIBRARY}")
|
||||
endif()
|
||||
elseif (EVEREST_CUSTOM_LOGGING_INCLUDE_PATH)
|
||||
if (NOT EXISTS "${EVEREST_CUSTOM_LOGGING_INCLUDE_PATH}/everest/logging.hpp")
|
||||
message(FATAL_ERROR "everest/logging.hpp not found in directory ${EVEREST_CUSTOM_LOGGING_INCLUDE_PATH}")
|
||||
else()
|
||||
target_include_directories(evse_security
|
||||
PUBLIC
|
||||
include
|
||||
${EVEREST_CUSTOM_LOGGING_INCLUDE_PATH}
|
||||
)
|
||||
endif()
|
||||
message(STATUS "Using the following logging header: ${EVEREST_CUSTOM_LOGGING_INCLUDE_PATH}/everest/logging.hpp")
|
||||
endif()
|
||||
|
||||
if (NOT EVEREST_CUSTOM_LOGGING_INCLUDE_PATH)
|
||||
target_link_libraries(evse_security
|
||||
PUBLIC
|
||||
everest::log
|
||||
)
|
||||
message(STATUS "Using the default logging header")
|
||||
endif()
|
||||
|
||||
#############
|
||||
# End logging configuration
|
||||
#############
|
||||
|
||||
target_link_libraries(evse_security
|
||||
PUBLIC
|
||||
everest::timer
|
||||
PRIVATE
|
||||
OpenSSL::SSL
|
||||
OpenSSL::Crypto
|
||||
)
|
||||
|
||||
if(LIBEVSE_SECURITY_BUILD_TESTING)
|
||||
target_compile_definitions(evse_security PRIVATE
|
||||
DEBUG_MODE_EVSE_SECURITY
|
||||
)
|
||||
endif()
|
||||
|
||||
if(LIBEVSE_SECURITY_USE_BOOST_FILESYSTEM)
|
||||
find_package(Boost REQUIRED COMPONENTS filesystem)
|
||||
target_link_libraries(evse_security
|
||||
PRIVATE
|
||||
Boost::filesystem
|
||||
)
|
||||
target_compile_definitions(evse_security
|
||||
PRIVATE
|
||||
LIBEVSE_SECURITY_USE_BOOST_FILESYSTEM
|
||||
)
|
||||
endif()
|
||||
|
||||
if(LIBEVSE_CRYPTO_SUPPLIER_OPENSSL)
|
||||
add_compile_definitions(LIBEVSE_CRYPTO_SUPPLIER_OPENSSL)
|
||||
endif()
|
||||
|
||||
if(USING_TPM2 OR USING_CUSTOM_PROVIDER)
|
||||
target_compile_definitions(evse_security PRIVATE
|
||||
USING_CUSTOM_PROVIDER
|
||||
CUSTOM_PROVIDER_NAME="${CUSTOM_PROVIDER_NAME}"
|
||||
PROPQUERY_PROVIDER_DEFAULT="${PROPQUERY_PROVIDER_DEFAULT}"
|
||||
PROPQUERY_PROVIDER_CUSTOM="${PROPQUERY_PROVIDER_CUSTOM}"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(CSR_DNS_NAME)
|
||||
target_compile_definitions(evse_security PRIVATE
|
||||
CSR_DNS_NAME="${CSR_DNS_NAME}"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(CSR_IP_ADDRESS)
|
||||
target_compile_definitions(evse_security PRIVATE
|
||||
CSR_IP_ADDRESS="${CSR_IP_ADDRESS}"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_compile_features(evse_security PUBLIC cxx_std_17)
|
||||
@@ -0,0 +1,435 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <evse_security/certificate/x509_bundle.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
#include <evse_security/crypto/evse_crypto.hpp>
|
||||
#include <evse_security/utils/evse_filesystem.hpp>
|
||||
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/x509v3.h>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
X509Wrapper X509CertificateBundle::get_latest_valid_certificate(const std::vector<X509Wrapper>& certificates) {
|
||||
// Filter certificates with valid_in > 0
|
||||
std::vector<X509Wrapper> valid_certificates;
|
||||
for (const auto& cert : certificates) {
|
||||
if (cert.is_valid()) {
|
||||
valid_certificates.push_back(cert);
|
||||
}
|
||||
}
|
||||
|
||||
if (valid_certificates.empty()) {
|
||||
// No valid certificates found
|
||||
throw NoCertificateValidException("No valid certificates available.");
|
||||
}
|
||||
|
||||
// Find the certificate with the latest valid_in
|
||||
auto latest_certificate = std::max_element(
|
||||
valid_certificates.begin(), valid_certificates.end(),
|
||||
[](const X509Wrapper& cert1, const X509Wrapper& cert2) { return cert1.get_valid_in() < cert2.get_valid_in(); });
|
||||
|
||||
return *latest_certificate;
|
||||
}
|
||||
|
||||
X509CertificateBundle::X509CertificateBundle(const std::string& certificate, const EncodingFormat encoding) :
|
||||
hierarchy_invalidated(true), source(X509CertificateSource::STRING) {
|
||||
add_certificates(certificate, encoding, std::nullopt);
|
||||
}
|
||||
|
||||
X509CertificateBundle::X509CertificateBundle(const fs::path& path, const EncodingFormat encoding) :
|
||||
hierarchy_invalidated(true), path(path) {
|
||||
|
||||
// Attempt creation
|
||||
filesystem_utils::create_file_or_dir_if_nonexistent(path);
|
||||
|
||||
if (fs::is_directory(path)) {
|
||||
source = X509CertificateSource::DIRECTORY;
|
||||
|
||||
// Iterate directory, not recursively since we might have the ocsp sub-directory
|
||||
// that contains the .der OCSP data that we don't have to use
|
||||
for (const auto& entry : fs::directory_iterator(path)) {
|
||||
if (is_certificate_file(entry)) {
|
||||
std::string certificate{};
|
||||
if (filesystem_utils::read_from_file(entry.path(), certificate)) {
|
||||
add_certificates(certificate, encoding, entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (is_certificate_file(path)) {
|
||||
source = X509CertificateSource::FILE;
|
||||
|
||||
std::string certificate{};
|
||||
if (filesystem_utils::read_from_file(path, certificate)) {
|
||||
add_certificates(certificate, encoding, path);
|
||||
}
|
||||
} else {
|
||||
throw CertificateLoadException("Failed to create certificate info from path: " + path.string());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<X509Wrapper> X509CertificateBundle::split() {
|
||||
std::vector<X509Wrapper> full_certificates;
|
||||
|
||||
// Append all chains
|
||||
for (const auto& chains : certificates) {
|
||||
for (const auto& cert : chains.second) {
|
||||
full_certificates.push_back(cert);
|
||||
}
|
||||
}
|
||||
|
||||
return full_certificates;
|
||||
}
|
||||
|
||||
int X509CertificateBundle::get_certificate_count() const {
|
||||
int count = 0;
|
||||
for (const auto& chain : certificates) {
|
||||
count += chain.second.size();
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
int X509CertificateBundle::get_certificate_chains_count() const {
|
||||
return certificates.size();
|
||||
}
|
||||
|
||||
void X509CertificateBundle::add_certificates(const std::string& data, const EncodingFormat encoding,
|
||||
const std::optional<fs::path>& path) {
|
||||
auto loaded = CryptoSupplier::load_certificates(data, encoding);
|
||||
auto& list = certificates[path.value_or(fs::path())];
|
||||
|
||||
for (auto& x509 : loaded) {
|
||||
if (path.has_value()) {
|
||||
list.emplace_back(std::move(x509), path.value());
|
||||
} else {
|
||||
list.emplace_back(std::move(x509));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool X509CertificateBundle::contains_certificate(const X509Wrapper& certificate) {
|
||||
// Search through all the chains
|
||||
for (const auto& chain : certificates) {
|
||||
for (const auto& certif : chain.second) {
|
||||
if (certif == certificate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool X509CertificateBundle::contains_certificate(const CertificateHashData& certificate_hash) {
|
||||
// Try an initial search for root certificates, else a hierarchy build will be required
|
||||
for (const auto& chain : certificates) {
|
||||
const bool found = std::find_if(std::begin(chain.second), std::end(chain.second), [&](const X509Wrapper& cert) {
|
||||
return cert.is_selfsigned() && cert == certificate_hash;
|
||||
}) != std::end(chain.second);
|
||||
|
||||
if (found) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found, build the hierarchy and search by the issued hash
|
||||
X509CertificateHierarchy& hierarchy = get_certificate_hierarchy();
|
||||
return hierarchy.contains_certificate_hash(certificate_hash, true);
|
||||
}
|
||||
|
||||
std::optional<X509Wrapper> X509CertificateBundle::find_certificate(const CertificateHashData& certificate_hash,
|
||||
bool case_insensitive_comparison) {
|
||||
// Try an initial search for root certificates, else a hierarchy build will be required
|
||||
for (const auto& chain : certificates) {
|
||||
for (const auto& certif : chain.second) {
|
||||
if (certif.is_selfsigned()) {
|
||||
bool matches = false;
|
||||
|
||||
if (case_insensitive_comparison) {
|
||||
const CertificateHashData certif_hash = certif.get_certificate_hash_data();
|
||||
matches = certif_hash.case_insensitive_comparison(certificate_hash);
|
||||
} else {
|
||||
matches = (certif == certificate_hash);
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
return certif;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found, build the hierarchy and search by the issued hash
|
||||
X509CertificateHierarchy& hierarchy = get_certificate_hierarchy();
|
||||
return hierarchy.find_certificate(certificate_hash, case_insensitive_comparison);
|
||||
}
|
||||
|
||||
std::vector<X509Wrapper> X509CertificateBundle::delete_certificate(const X509Wrapper& certificate, bool include_issued,
|
||||
bool include_top) {
|
||||
std::vector<X509Wrapper> to_delete;
|
||||
std::vector<X509Wrapper> deleted;
|
||||
|
||||
if (include_issued || include_top) {
|
||||
// Include all descendants in the delete list
|
||||
auto& hierarchy = get_certificate_hierarchy();
|
||||
|
||||
if (include_issued) {
|
||||
auto issued = hierarchy.collect_descendants(certificate);
|
||||
|
||||
to_delete.insert(to_delete.end(), std::make_move_iterator(issued.begin()),
|
||||
std::make_move_iterator(issued.end()));
|
||||
}
|
||||
|
||||
if (include_top) {
|
||||
auto top = hierarchy.collect_top(certificate);
|
||||
|
||||
to_delete.insert(to_delete.end(), std::make_move_iterator(top.begin()), std::make_move_iterator(top.end()));
|
||||
}
|
||||
}
|
||||
|
||||
// Include default delete
|
||||
to_delete.push_back(certificate);
|
||||
|
||||
for (auto& chains : certificates) {
|
||||
auto& certifs = chains.second;
|
||||
|
||||
certifs.erase(std::remove_if(certifs.begin(), certifs.end(),
|
||||
[&](const auto& certif) {
|
||||
const bool found =
|
||||
std::find(to_delete.begin(), to_delete.end(), certif) != to_delete.end();
|
||||
|
||||
if (found) {
|
||||
deleted.push_back(certif);
|
||||
}
|
||||
|
||||
return found;
|
||||
}),
|
||||
certifs.end());
|
||||
}
|
||||
|
||||
// If we deleted any, invalidate the built hierarchy
|
||||
if (false == deleted.empty()) {
|
||||
invalidate_hierarchy();
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
std::vector<X509Wrapper> X509CertificateBundle::delete_certificate(const CertificateHashData& data, bool include_issued,
|
||||
bool include_top) {
|
||||
auto& hierarchy = get_certificate_hierarchy();
|
||||
|
||||
std::optional<X509Wrapper> to_delete = hierarchy.find_certificate(data, true /* = Case insensitive search */);
|
||||
if (to_delete.has_value()) {
|
||||
return delete_certificate(to_delete.value(), include_issued, include_top);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void X509CertificateBundle::delete_all_certificates() {
|
||||
certificates.clear();
|
||||
}
|
||||
|
||||
void X509CertificateBundle::add_certificate(X509Wrapper&& certificate) {
|
||||
if (source == X509CertificateSource::DIRECTORY) {
|
||||
// If it is in directory mode only allow sub-directories of that directory
|
||||
const fs::path certif_path = certificate.get_file().value_or(fs::path());
|
||||
|
||||
if (filesystem_utils::is_subdirectory(path, certif_path)) {
|
||||
certificates[certif_path].push_back(std::move(certificate));
|
||||
invalidate_hierarchy();
|
||||
} else {
|
||||
throw InvalidOperationException(
|
||||
"Added certificate with directory bundle, must be subdir of the main directory: " + path.string());
|
||||
}
|
||||
} else {
|
||||
// The bundle came from a file, so there is only one file we could add the certificate to
|
||||
certificates.begin()->second.push_back(certificate);
|
||||
invalidate_hierarchy();
|
||||
}
|
||||
}
|
||||
|
||||
void X509CertificateBundle::add_certificate_unique(X509Wrapper&& certificate) {
|
||||
if (!contains_certificate(certificate)) {
|
||||
add_certificate(std::move(certificate));
|
||||
invalidate_hierarchy();
|
||||
}
|
||||
}
|
||||
|
||||
bool X509CertificateBundle::update_certificate(X509Wrapper&& certificate) {
|
||||
for (auto& chain : certificates) {
|
||||
for (auto& certif : chain.second) {
|
||||
if (certif == certificate) {
|
||||
certif = std::move(certificate);
|
||||
invalidate_hierarchy();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool X509CertificateBundle::export_certificates() {
|
||||
if (source == X509CertificateSource::STRING) {
|
||||
EVLOG_error << "Export for string is invalid!";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add/delete certifs
|
||||
if (!sync_to_certificate_store()) {
|
||||
EVLOG_error << "Sync to certificate store failed!";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (source == X509CertificateSource::DIRECTORY) {
|
||||
bool exported_all = true;
|
||||
|
||||
// Write updated certificates
|
||||
for (auto& chains : certificates) {
|
||||
// Ignore empty chains (the file was deleted)
|
||||
if (chains.second.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Each chain is a single file
|
||||
if (!filesystem_utils::write_to_file(chains.first, to_export_string(chains.first), std::ios::trunc)) {
|
||||
exported_all = false;
|
||||
}
|
||||
}
|
||||
|
||||
return exported_all;
|
||||
}
|
||||
if (source == X509CertificateSource::FILE) {
|
||||
// write to a separate file to minimise corruption and data loss; then rename
|
||||
bool result{false};
|
||||
|
||||
try {
|
||||
const auto tmp_file = path.string() + '$';
|
||||
fs::remove(tmp_file);
|
||||
|
||||
// We're using a single file, no need to check for deleted certificates
|
||||
result = filesystem_utils::write_to_file(tmp_file, to_export_string(), std::ios::trunc);
|
||||
|
||||
fs::rename(tmp_file, path);
|
||||
} catch (const fs::filesystem_error& ex) {
|
||||
EVLOG_error << "Error export_certificates: " << ex.path1() << ' ' << ex.path2() << ": " << ex.what();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool X509CertificateBundle::sync_to_certificate_store() {
|
||||
if (source == X509CertificateSource::STRING) {
|
||||
EVLOG_error << "Sync for string is invalid!";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (source == X509CertificateSource::DIRECTORY) {
|
||||
// Get existing certificates from filesystem
|
||||
X509CertificateBundle fs_certificates(path, EncodingFormat::PEM);
|
||||
bool success = true;
|
||||
|
||||
// Delete filesystem certificate chains missing from our map
|
||||
for (const auto& fs_chain : fs_certificates.certificates) {
|
||||
if (certificates.find(fs_chain.first) == certificates.end()) {
|
||||
// fs certif chain not existing in our certificate list, delete
|
||||
if (!filesystem_utils::delete_file(fs_chain.first)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the certificates that are not existing in the filesystem. Each chain represents a single file
|
||||
for (const auto& chain : certificates) {
|
||||
if (chain.second.empty()) {
|
||||
// If it's an empty chain, delete
|
||||
if (!filesystem_utils::delete_file(chain.first)) {
|
||||
success = false;
|
||||
}
|
||||
} else if (fs_certificates.certificates.find(chain.first) == fs_certificates.certificates.end()) {
|
||||
// Certif not existing in fs certificates write it out
|
||||
if (!filesystem_utils::write_to_file(chain.first, to_export_string(chain.first), std::ios::trunc)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After fs deletion erase all empty files from our certificate list, so that we don't write them out
|
||||
for (auto first = certificates.begin(); first != certificates.end();) {
|
||||
if (first->second.empty()) {
|
||||
first = certificates.erase(first);
|
||||
} else {
|
||||
++first;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
if (source == X509CertificateSource::FILE) {
|
||||
// Delete source file if we're empty
|
||||
if (certificates.empty()) {
|
||||
return filesystem_utils::delete_file(path);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
X509Wrapper X509CertificateBundle::get_latest_valid_certificate() {
|
||||
return get_latest_valid_certificate(split());
|
||||
}
|
||||
|
||||
void X509CertificateBundle::invalidate_hierarchy() {
|
||||
hierarchy_invalidated = true;
|
||||
}
|
||||
|
||||
X509CertificateHierarchy& X509CertificateBundle::get_certificate_hierarchy() {
|
||||
if (hierarchy_invalidated) {
|
||||
EVLOG_info << "Building new certificate hierarchy!";
|
||||
hierarchy_invalidated = false;
|
||||
|
||||
auto certificates = split();
|
||||
hierarchy = X509CertificateHierarchy::build_hierarchy(certificates);
|
||||
}
|
||||
|
||||
return hierarchy;
|
||||
}
|
||||
|
||||
std::string X509CertificateBundle::to_export_string() const {
|
||||
std::string export_string;
|
||||
|
||||
for (auto& chain : certificates) {
|
||||
for (auto& certificate : chain.second) {
|
||||
export_string += certificate.get_export_string();
|
||||
}
|
||||
}
|
||||
|
||||
return export_string;
|
||||
}
|
||||
|
||||
std::string X509CertificateBundle::to_export_string(const fs::path& chain) const {
|
||||
std::string export_string;
|
||||
|
||||
auto found = certificates.find(chain);
|
||||
if (found != certificates.end()) {
|
||||
for (auto& certificate : found->second) {
|
||||
export_string += certificate.get_export_string();
|
||||
}
|
||||
}
|
||||
|
||||
return export_string;
|
||||
}
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,383 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <evse_security/certificate/x509_hierarchy.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
namespace evse_security {
|
||||
|
||||
bool X509CertificateHierarchy::is_internal_root(const X509Wrapper& certificate) const {
|
||||
if (certificate.is_selfsigned()) {
|
||||
return (std::find_if(hierarchy.begin(), hierarchy.end(), [&certificate](const X509Node& node) {
|
||||
return node.certificate == certificate;
|
||||
}) != hierarchy.end());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<X509Wrapper> X509CertificateHierarchy::collect_descendants(const X509Wrapper& top) {
|
||||
std::vector<X509Wrapper> descendants;
|
||||
|
||||
for_each([&](const X509Node& node) {
|
||||
// If we found the certificate
|
||||
if (node.certificate == top) {
|
||||
// Collect all descendants
|
||||
if (!node.children.empty()) {
|
||||
for_each_descendant(
|
||||
[&](const X509Node& descendant, int /*depth*/) { descendants.push_back(descendant.certificate); },
|
||||
node);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return descendants;
|
||||
}
|
||||
|
||||
std::vector<X509Wrapper> X509CertificateHierarchy::collect_top(const X509Wrapper& leaf) {
|
||||
auto root_node = find_certificate_root_node(leaf);
|
||||
|
||||
if (root_node.has_value()) {
|
||||
std::vector<X509Wrapper> top_nodes;
|
||||
auto tuple = root_node.value();
|
||||
|
||||
const X509Node* root = std::get<0>(tuple);
|
||||
int found_depth = std::get<1>(tuple);
|
||||
|
||||
// Iterate all the descendants of the root until we find the leaf level
|
||||
for_each_descendant(
|
||||
[&](const X509Node& node, int depth) {
|
||||
if (depth < found_depth) {
|
||||
// Collect all owned
|
||||
top_nodes.push_back(node.certificate);
|
||||
}
|
||||
},
|
||||
*root, 1);
|
||||
|
||||
return top_nodes;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool X509CertificateHierarchy::get_certificate_hash(const X509Wrapper& certificate, CertificateHashData& out_hash) {
|
||||
if (certificate.is_selfsigned()) {
|
||||
out_hash = certificate.get_certificate_hash_data();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Search for certificate in the hierarchy and return the hash
|
||||
CertificateHashData hash;
|
||||
bool found = false;
|
||||
|
||||
for_each([&](const X509Node& node) {
|
||||
if (node.certificate == certificate && node.hash.has_value()) {
|
||||
hash = node.hash.value();
|
||||
found = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (found) {
|
||||
out_hash = std::move(hash);
|
||||
return true;
|
||||
}
|
||||
|
||||
EVLOG_warning << "Could not find owner for certificate: " << certificate.get_common_name();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool X509CertificateHierarchy::contains_certificate_hash(const CertificateHashData& hash,
|
||||
bool case_insensitive_comparison) {
|
||||
bool contains = false;
|
||||
|
||||
for_each([&](const X509Node& node) {
|
||||
if (node.hash.has_value()) {
|
||||
bool matches = false;
|
||||
|
||||
if (case_insensitive_comparison) {
|
||||
matches = (node.hash.value().case_insensitive_comparison(hash));
|
||||
} else {
|
||||
matches = (node.hash.value() == hash);
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
contains = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return contains;
|
||||
}
|
||||
|
||||
std::optional<X509Wrapper> X509CertificateHierarchy::find_certificate_root(const X509Wrapper& leaf) {
|
||||
auto root = find_certificate_root_node(leaf);
|
||||
|
||||
if (root.has_value()) {
|
||||
auto root_ptr = std::get<0>(root.value());
|
||||
return root_ptr->certificate;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::pair<const X509Node*, int>>
|
||||
X509CertificateHierarchy::find_certificate_root_node(const X509Wrapper& leaf) {
|
||||
const X509Node* root_ptr = nullptr;
|
||||
int found_depth = 0;
|
||||
|
||||
for (const auto& root : hierarchy) {
|
||||
if (root.state.is_selfsigned) {
|
||||
for_each_descendant(
|
||||
[&](const X509Node& node, int depth) {
|
||||
// If we found our matching certificate, we also found the root
|
||||
if (node.certificate == leaf) {
|
||||
root_ptr = &root;
|
||||
found_depth = depth;
|
||||
}
|
||||
},
|
||||
root, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (root_ptr != nullptr) {
|
||||
return std::make_pair(root_ptr, found_depth);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<X509Wrapper> X509CertificateHierarchy::find_certificate(const CertificateHashData& hash,
|
||||
bool case_insensitive_comparison) {
|
||||
X509Wrapper* certificate = nullptr;
|
||||
|
||||
for_each([&](X509Node& node) {
|
||||
if (node.hash.has_value()) {
|
||||
bool matches = false;
|
||||
|
||||
if (case_insensitive_comparison) {
|
||||
matches = (node.hash.value().case_insensitive_comparison(hash));
|
||||
} else {
|
||||
matches = (node.hash.value() == hash);
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
certificate = &node.certificate;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (certificate != nullptr) {
|
||||
return *certificate;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<X509Wrapper> X509CertificateHierarchy::find_certificates_multi(const CertificateHashData& hash) {
|
||||
std::vector<X509Wrapper> certificates;
|
||||
|
||||
for_each([&](X509Node& node) {
|
||||
if (node.hash == hash) {
|
||||
certificates.push_back(node.certificate);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return certificates;
|
||||
}
|
||||
|
||||
std::string X509CertificateHierarchy::to_debug_string() {
|
||||
std::stringstream str;
|
||||
|
||||
for (const auto& root : hierarchy) {
|
||||
if (root.state.is_selfsigned) {
|
||||
str << "* [ROOT]";
|
||||
} else {
|
||||
str << "+ [ORPH]";
|
||||
}
|
||||
|
||||
str << ' ' << root.certificate.get_common_name() << std::endl;
|
||||
|
||||
for_each_descendant(
|
||||
[&](const X509Node& node, int depth) {
|
||||
while (depth-- > 0) {
|
||||
str << "---";
|
||||
}
|
||||
|
||||
str << ' ' << node.certificate.get_common_name() << std::endl;
|
||||
},
|
||||
root, 1);
|
||||
}
|
||||
|
||||
return str.str();
|
||||
}
|
||||
|
||||
void X509CertificateHierarchy::insert(X509Wrapper&& inserted_certificate) {
|
||||
if (false == inserted_certificate.is_selfsigned()) {
|
||||
// If this certif has any link to any of the existing certificates
|
||||
bool hierarchy_found = false;
|
||||
|
||||
// Create a new node, is not self-signed and is not a permanent orphan
|
||||
X509Node new_node = {{0, 0}, inserted_certificate, std::nullopt, inserted_certificate, {}};
|
||||
|
||||
// Search through all the list for a link
|
||||
for_each([&](X509Node& top) {
|
||||
if (top.certificate.is_child(inserted_certificate)) {
|
||||
// Some sanity checks
|
||||
if (top.state.is_selfsigned) {
|
||||
throw InvalidStateException(
|
||||
"Newly added certificate can't be parent of a self-signed certificate!");
|
||||
}
|
||||
|
||||
// If the top certificate is a descendant of the certificate we're adding
|
||||
|
||||
// Cache top node
|
||||
auto temp_top = std::move(top);
|
||||
|
||||
// Set the new state of the top node
|
||||
temp_top.state = {0, 0};
|
||||
temp_top.hash = temp_top.certificate.get_certificate_hash_data(new_node.certificate);
|
||||
temp_top.issuer = X509Wrapper(new_node.certificate);
|
||||
|
||||
// Set the top as a child of the new_node
|
||||
new_node.children.push_back(std::move(temp_top));
|
||||
|
||||
// Set the new top
|
||||
top = std::move(new_node);
|
||||
hierarchy_found = true; // Found a link
|
||||
} else if (inserted_certificate.is_child(top.certificate)) {
|
||||
// If the certificate is the descendant of top certificate
|
||||
|
||||
// Calculate hash and set issuer
|
||||
new_node.state = {0, 0};
|
||||
new_node.hash = inserted_certificate.get_certificate_hash_data(top.certificate);
|
||||
new_node.issuer = X509Wrapper(top.certificate); // Set the new issuer
|
||||
|
||||
// Add it to the top's descendant list
|
||||
top.children.push_back(new_node);
|
||||
hierarchy_found = true; // Found a link
|
||||
}
|
||||
|
||||
// Keep iterating while we did not find a link
|
||||
return (false == hierarchy_found);
|
||||
});
|
||||
|
||||
// Else insert it in the roots as a potentially orphan certificate
|
||||
if (hierarchy_found == false) {
|
||||
hierarchy.push_back(new_node);
|
||||
}
|
||||
} else {
|
||||
// If it is self-signed insert it in the roots, with the state set as a self-signed and a properly computed hash
|
||||
hierarchy.push_back(
|
||||
{{1, 0}, inserted_certificate, inserted_certificate.get_certificate_hash_data(), inserted_certificate, {}});
|
||||
|
||||
// Attempt a partial prune, by searching through all the contained temporary orphan certificates
|
||||
// and trying to add them to the newly inserted root certificate, if that is possible
|
||||
|
||||
// Only iterate until last (not including) since the last is the new node
|
||||
for (int i = 0; i < (hierarchy.size() - 1); ++i) {
|
||||
auto& node = hierarchy[i];
|
||||
auto& state = node.state;
|
||||
|
||||
// If we have a temporary orphan, that is if we are in the roots
|
||||
// and we are not self-signed then it means that we are an orphan
|
||||
if (state.is_selfsigned == 0) {
|
||||
// Some sanity checks
|
||||
if (node.hash.has_value()) {
|
||||
throw InvalidStateException("Orphan certificate can't have a proper hash!");
|
||||
}
|
||||
|
||||
// If it is a child of the new root certificate insert it to it's list and break
|
||||
if (node.certificate.is_child(inserted_certificate)) {
|
||||
auto& new_root = hierarchy.back();
|
||||
|
||||
// Hash is properly computed now
|
||||
node.hash = node.certificate.get_certificate_hash_data(new_root.certificate);
|
||||
node.state.is_orphan = 0; // Not an orphan any more
|
||||
node.issuer = X509Wrapper(inserted_certificate); // Set the new valid issuer
|
||||
|
||||
// Add to the newly inserted root child list
|
||||
new_root.children.push_back(std::move(node));
|
||||
|
||||
// Erase element since it was added to the root's descendants
|
||||
hierarchy.erase(hierarchy.begin() + i);
|
||||
// Decrement i again since it was erased
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // End insert
|
||||
|
||||
void X509CertificateHierarchy::prune() {
|
||||
if (hierarchy.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < hierarchy.size(); ++i) {
|
||||
// Possible orphan
|
||||
auto& orphan = hierarchy[i];
|
||||
|
||||
const bool is_orphan = (orphan.state.is_selfsigned) == 0 && (orphan.state.is_orphan == 0);
|
||||
if (is_orphan == false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found a non-permanent orphan, search for a issuer
|
||||
bool found_issuer = false;
|
||||
|
||||
for_each([&](X509Node& top) {
|
||||
if (orphan.certificate.is_child(top.certificate)) {
|
||||
orphan.hash = orphan.certificate.get_certificate_hash_data(top.certificate);
|
||||
orphan.state.is_orphan = 0; // Not an orphan any more
|
||||
orphan.issuer = X509Wrapper(top.certificate); // Set the new valid issuer
|
||||
|
||||
top.children.push_back(std::move(orphan));
|
||||
found_issuer = true;
|
||||
}
|
||||
|
||||
return (false == found_issuer);
|
||||
});
|
||||
|
||||
if (false == found_issuer) {
|
||||
// Mark as permanent orphan
|
||||
orphan.state.is_orphan = 1; // Permanent orphan
|
||||
} else {
|
||||
// Erase from hierarchy list and decrement iterator
|
||||
hierarchy.erase(std::begin(hierarchy) + i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
X509CertificateHierarchy X509CertificateHierarchy::build_hierarchy(std::vector<X509Wrapper>& certificates) {
|
||||
X509CertificateHierarchy ordered;
|
||||
|
||||
while (!certificates.empty()) {
|
||||
ordered.insert(std::move(certificates.back()));
|
||||
certificates.pop_back();
|
||||
}
|
||||
|
||||
// Prune the tree
|
||||
ordered.prune();
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,215 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <evse_security/certificate/x509_wrapper.hpp>
|
||||
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <regex>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
#include <evse_security/crypto/evse_crypto.hpp>
|
||||
#include <evse_security/utils/evse_filesystem.hpp>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
X509Wrapper::X509Wrapper(const fs::path& file, const EncodingFormat encoding) {
|
||||
if (fs::is_regular_file(file) == false) {
|
||||
throw CertificateLoadException("X509Wrapper can only load from files!");
|
||||
}
|
||||
|
||||
fsstd::ifstream read(file, std::ios::binary);
|
||||
const std::string certificate((std::istreambuf_iterator<char>(read)), std::istreambuf_iterator<char>());
|
||||
|
||||
auto loaded = CryptoSupplier::load_certificates(certificate, encoding);
|
||||
if (loaded.size() != 1) {
|
||||
std::string error = "X509Wrapper can only load a single certificate! Loaded: ";
|
||||
error += std::to_string(loaded.size());
|
||||
|
||||
throw CertificateLoadException(error);
|
||||
}
|
||||
|
||||
this->file = file;
|
||||
x509 = std::move(loaded[0]);
|
||||
update_validity();
|
||||
}
|
||||
|
||||
X509Wrapper::X509Wrapper(const std::string& data, const EncodingFormat encoding) {
|
||||
auto loaded = CryptoSupplier::load_certificates(data, encoding);
|
||||
if (loaded.size() != 1) {
|
||||
std::string error = "X509Wrapper can only load a single certificate! Loaded: ";
|
||||
error += std::to_string(loaded.size());
|
||||
|
||||
throw CertificateLoadException(error);
|
||||
}
|
||||
|
||||
x509 = std::move(loaded[0]);
|
||||
update_validity();
|
||||
}
|
||||
|
||||
X509Wrapper::X509Wrapper(X509Handle_ptr&& x509) : x509(std::move(x509)) {
|
||||
update_validity();
|
||||
}
|
||||
|
||||
X509Wrapper::X509Wrapper(X509Handle_ptr&& x509, const fs::path& file) : x509(std::move(x509)), file(file) {
|
||||
if (fs::is_regular_file(file) == false) {
|
||||
throw CertificateLoadException("X509Wrapper can only load from files!");
|
||||
}
|
||||
|
||||
update_validity();
|
||||
}
|
||||
|
||||
X509Wrapper::X509Wrapper(const X509Wrapper& other) :
|
||||
x509(std::move(CryptoSupplier::x509_duplicate_unique(other.get()))),
|
||||
file(other.file),
|
||||
valid_in(other.valid_in),
|
||||
valid_to(other.valid_to) {
|
||||
#ifdef DEBUG_MODE_EVSE_SECURITY
|
||||
debug_common_name = other.debug_common_name;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool X509Wrapper::operator==(const X509Wrapper& other) const {
|
||||
if (this == &other) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return CryptoSupplier::x509_is_equal(get(), other.get());
|
||||
}
|
||||
|
||||
void X509Wrapper::update_validity() {
|
||||
if (false == CryptoSupplier::x509_get_validity(get(), valid_in, valid_to)) {
|
||||
EVLOG_error << "Could not update validity for certificate: " << get_common_name();
|
||||
}
|
||||
|
||||
#ifdef DEBUG_MODE_EVSE_SECURITY
|
||||
debug_common_name = get_common_name();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool X509Wrapper::is_child(const X509Wrapper& parent) const {
|
||||
// A certif can't be it's own parent, use is_selfsigned if that is intended (operator ==)
|
||||
if (*this == parent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return CryptoSupplier::x509_is_child(get(), parent.get());
|
||||
}
|
||||
|
||||
bool X509Wrapper::is_selfsigned() const {
|
||||
return CryptoSupplier::x509_is_selfsigned(get());
|
||||
}
|
||||
|
||||
int64_t X509Wrapper::get_valid_in() const {
|
||||
return valid_in;
|
||||
}
|
||||
|
||||
/// \brief Gets valid_in
|
||||
int64_t X509Wrapper::get_valid_to() const {
|
||||
return valid_to;
|
||||
}
|
||||
|
||||
bool X509Wrapper::is_valid() const {
|
||||
// The valid_in must be in the past and the valid_to must be in the future
|
||||
return (get_valid_in() <= 0) && (get_valid_to() >= 0);
|
||||
}
|
||||
|
||||
bool X509Wrapper::is_valid_in_future() const {
|
||||
// valid_in must be in the future, and valid_to must also be in the future
|
||||
return (get_valid_in() > 0) && (get_valid_to() > 0);
|
||||
}
|
||||
|
||||
bool X509Wrapper::is_expired() const {
|
||||
return (get_valid_to() < 0);
|
||||
}
|
||||
|
||||
std::optional<fs::path> X509Wrapper::get_file() const {
|
||||
return this->file;
|
||||
}
|
||||
|
||||
void X509Wrapper::set_file(fs::path& path) {
|
||||
if (fs::is_directory(path)) {
|
||||
throw std::logic_error("X509 wrapper set_file must only be used for files, not directories!");
|
||||
}
|
||||
|
||||
file = path;
|
||||
}
|
||||
|
||||
X509CertificateSource X509Wrapper::get_source() const {
|
||||
if (file.has_value()) {
|
||||
return X509CertificateSource::FILE;
|
||||
}
|
||||
return X509CertificateSource::STRING;
|
||||
}
|
||||
|
||||
std::string X509Wrapper::get_common_name() const {
|
||||
return CryptoSupplier::x509_get_common_name(get());
|
||||
}
|
||||
|
||||
std::string X509Wrapper::get_issuer_name_hash() const {
|
||||
return CryptoSupplier::x509_get_issuer_name_hash(get());
|
||||
}
|
||||
|
||||
std::string X509Wrapper::get_serial_number() const {
|
||||
return CryptoSupplier::x509_get_serial_number(get());
|
||||
}
|
||||
|
||||
std::string X509Wrapper::get_issuer_key_hash() const {
|
||||
if (is_selfsigned()) {
|
||||
return get_key_hash();
|
||||
} // See 'OCPP 2.0.1 Spec: 2.6. CertificateHashDataType'
|
||||
throw std::logic_error("get_issuer_key_hash must only be used on self-signed certs");
|
||||
}
|
||||
|
||||
std::string X509Wrapper::get_key_hash() const {
|
||||
return CryptoSupplier::x509_get_key_hash(get());
|
||||
}
|
||||
|
||||
CertificateHashData X509Wrapper::get_certificate_hash_data() const {
|
||||
CertificateHashData certificate_hash_data;
|
||||
certificate_hash_data.hash_algorithm = HashAlgorithm::SHA256;
|
||||
certificate_hash_data.issuer_name_hash = this->get_issuer_name_hash();
|
||||
certificate_hash_data.issuer_key_hash = this->get_issuer_key_hash();
|
||||
certificate_hash_data.serial_number = this->get_serial_number();
|
||||
|
||||
#ifdef DEBUG_MODE_EVSE_SECURITY
|
||||
certificate_hash_data.debug_common_name = this->get_common_name();
|
||||
#endif
|
||||
|
||||
return certificate_hash_data;
|
||||
}
|
||||
|
||||
CertificateHashData X509Wrapper::get_certificate_hash_data(const X509Wrapper& issuer) const {
|
||||
if (CryptoSupplier::x509_is_child(get(), issuer.get()) == false) {
|
||||
throw std::logic_error("The specified issuer is not the correct issuer for this certificate.");
|
||||
}
|
||||
|
||||
CertificateHashData certificate_hash_data;
|
||||
certificate_hash_data.hash_algorithm = HashAlgorithm::SHA256;
|
||||
certificate_hash_data.issuer_name_hash = this->get_issuer_name_hash();
|
||||
|
||||
// OCPP 2.0.1 Spec: 2.6. CertificateHashDataType
|
||||
// issuerKeyHash: The hash of the DER encoded public key: the
|
||||
// value (excluding tag and length) of the subject public key
|
||||
// field in the issuer’s certificate.
|
||||
|
||||
// Issuer key hash
|
||||
certificate_hash_data.issuer_key_hash = issuer.get_key_hash();
|
||||
certificate_hash_data.serial_number = this->get_serial_number();
|
||||
|
||||
#ifdef DEBUG_MODE_EVSE_SECURITY
|
||||
certificate_hash_data.debug_common_name = this->get_common_name();
|
||||
#endif
|
||||
|
||||
return certificate_hash_data;
|
||||
}
|
||||
|
||||
std::string X509Wrapper::get_responder_url() const {
|
||||
return CryptoSupplier::x509_get_responder_url(get());
|
||||
}
|
||||
|
||||
std::string X509Wrapper::get_export_string() const {
|
||||
return CryptoSupplier::x509_to_string(get());
|
||||
}
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,126 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <everest/logging.hpp>
|
||||
#include <evse_security/crypto/interface/crypto_supplier.hpp>
|
||||
|
||||
#define default_crypto_supplier_usage_error() \
|
||||
EVLOG_error << "Invoked default unimplemented crypto function: [" << __func__ << "]";
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
const char* AbstractCryptoSupplier::get_supplier_name() {
|
||||
return "AbstractCryptoSupplier";
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::supports_tpm() {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::supports_tpm_key_creation() {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::generate_key(const KeyGenerationInfo& /*key_info*/, KeyHandle_ptr& /*out_key*/) {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
/// @brief Loads all certificates from the string data that can contain multiple cetifs
|
||||
std::vector<X509Handle_ptr> AbstractCryptoSupplier::load_certificates(const std::string& /*data*/,
|
||||
const EncodingFormat /*encoding*/) {
|
||||
default_crypto_supplier_usage_error() return {};
|
||||
}
|
||||
|
||||
std::string AbstractCryptoSupplier::x509_to_string(X509Handle* /*handle*/) {
|
||||
default_crypto_supplier_usage_error() return {};
|
||||
}
|
||||
|
||||
std::string AbstractCryptoSupplier::x509_get_responder_url(X509Handle* /*handle*/) {
|
||||
default_crypto_supplier_usage_error() return {};
|
||||
}
|
||||
|
||||
std::string AbstractCryptoSupplier::x509_get_key_hash(X509Handle* /*handle*/) {
|
||||
default_crypto_supplier_usage_error() return {};
|
||||
}
|
||||
|
||||
std::string AbstractCryptoSupplier::x509_get_serial_number(X509Handle* /*handle*/) {
|
||||
default_crypto_supplier_usage_error() return {};
|
||||
}
|
||||
|
||||
std::string AbstractCryptoSupplier::x509_get_issuer_name_hash(X509Handle* /*handle*/) {
|
||||
default_crypto_supplier_usage_error() return {};
|
||||
}
|
||||
|
||||
std::string AbstractCryptoSupplier::x509_get_common_name(X509Handle* /*handle*/) {
|
||||
default_crypto_supplier_usage_error() return {};
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::x509_get_validity(X509Handle* /*handle*/, std::int64_t& /*out_valid_in*/,
|
||||
std::int64_t& /*out_valid_to*/) {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::x509_is_selfsigned(X509Handle* /*handle*/) {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::x509_is_child(X509Handle* /*child*/, X509Handle* /*parent*/) {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::x509_is_equal(X509Handle* a, X509Handle* b) {
|
||||
default_crypto_supplier_usage_error() return (a == b);
|
||||
}
|
||||
|
||||
namespace {
|
||||
X509Handle_ptr x509_duplicate_unique() {
|
||||
default_crypto_supplier_usage_error() return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CertificateValidationResult AbstractCryptoSupplier::x509_verify_certificate_chain(
|
||||
X509Handle* /*target*/, const std::vector<X509Handle*>& /*parents*/,
|
||||
const std::vector<X509Handle*>& /*untrusted_subcas*/, bool /*allow_future_certificates*/,
|
||||
const std::optional<fs::path> /*dir_path*/, const std::optional<fs::path> /*file_path*/) {
|
||||
default_crypto_supplier_usage_error() return CertificateValidationResult::Unknown;
|
||||
}
|
||||
|
||||
KeyValidationResult AbstractCryptoSupplier::x509_check_private_key(X509Handle* /*handle*/, std::string /*private_key*/,
|
||||
std::optional<std::string> /*password*/) {
|
||||
default_crypto_supplier_usage_error() return KeyValidationResult::Unknown;
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::x509_verify_signature(X509Handle* /*handle*/,
|
||||
const std::vector<std::uint8_t>& /*signature*/,
|
||||
const std::vector<std::uint8_t>& /*data*/) {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
CertificateSignRequestResult
|
||||
AbstractCryptoSupplier::x509_generate_csr(const CertificateSigningRequestInfo& /*csr_info*/, std::string& /*out_csr*/) {
|
||||
default_crypto_supplier_usage_error() return CertificateSignRequestResult::Unknown;
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::digest_file_sha256(const fs::path& /*path*/, std::vector<std::uint8_t>& /*out_digest*/) {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::base64_decode_to_bytes(const std::string& /*base64_string*/,
|
||||
std::vector<std::uint8_t>& /*out_decoded*/) {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::base64_decode_to_string(const std::string& /*base64_string*/,
|
||||
std::string& /*out_decoded*/) {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::base64_encode_from_bytes(const std::vector<std::uint8_t>& /*bytes*/,
|
||||
std::string& /*out_encoded*/) {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
bool AbstractCryptoSupplier::base64_encode_from_string(const std::string& /*string*/, std::string& /*out_encoded*/) {
|
||||
default_crypto_supplier_usage_error() return false;
|
||||
}
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <evse_security/crypto/interface/crypto_types.hpp>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
namespace conversions {
|
||||
|
||||
std::string get_certificate_sign_request_result_to_string(CertificateSignRequestResult e) {
|
||||
|
||||
switch (e) {
|
||||
case CertificateSignRequestResult::Valid:
|
||||
return "Valid";
|
||||
case CertificateSignRequestResult::KeyGenerationError:
|
||||
return "KeyGenerationError";
|
||||
case CertificateSignRequestResult::VersioningError:
|
||||
return "VersioningError";
|
||||
case CertificateSignRequestResult::PubkeyError:
|
||||
return "PubkeyError";
|
||||
case CertificateSignRequestResult::ExtensionsError:
|
||||
return "ExtensionsError";
|
||||
case CertificateSignRequestResult::SigningError:
|
||||
return "SigningError";
|
||||
case CertificateSignRequestResult::Unknown:
|
||||
break;
|
||||
}
|
||||
|
||||
throw std::out_of_range("No known string conversion for provided enum of type CertificateSignRequestResult");
|
||||
}
|
||||
|
||||
} // namespace conversions
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,943 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <evse_security/crypto/openssl/openssl_crypto_supplier.hpp>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/opensslv.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/x509v3.h>
|
||||
|
||||
#include <evse_security/crypto/openssl/openssl_provider.hpp>
|
||||
#include <evse_security/crypto/openssl/openssl_types.hpp>
|
||||
#include <evse_security/utils/evse_filesystem.hpp>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
namespace {
|
||||
X509* get(X509Handle* handle) {
|
||||
if (auto* ssl_handle = dynamic_cast<X509HandleOpenSSL*>(handle)) {
|
||||
return ssl_handle->get();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EVP_PKEY* get(KeyHandle* handle) {
|
||||
if (auto* ssl_handle = dynamic_cast<KeyHandleOpenSSL*>(handle)) {
|
||||
return ssl_handle->get();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CertificateValidationResult to_certificate_error(const int ec) {
|
||||
switch (ec) {
|
||||
case X509_V_ERR_CERT_HAS_EXPIRED:
|
||||
return CertificateValidationResult::Expired;
|
||||
case X509_V_ERR_CERT_SIGNATURE_FAILURE:
|
||||
return CertificateValidationResult::InvalidSignature;
|
||||
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
|
||||
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
|
||||
return CertificateValidationResult::IssuerNotFound;
|
||||
case X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE:
|
||||
return CertificateValidationResult::InvalidLeafSignature;
|
||||
case X509_V_ERR_CERT_CHAIN_TOO_LONG:
|
||||
case X509_V_ERR_CERT_UNTRUSTED:
|
||||
return CertificateValidationResult::InvalidChain;
|
||||
default:
|
||||
EVLOG_warning << X509_verify_cert_error_string(ec);
|
||||
return CertificateValidationResult::Unknown;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const char* OpenSSLSupplier::get_supplier_name() {
|
||||
return OPENSSL_VERSION_TEXT;
|
||||
}
|
||||
|
||||
bool OpenSSLSupplier::supports_tpm_key_creation() {
|
||||
const OpenSSLProvider provider;
|
||||
return provider.supports_provider_tpm();
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool export_key_internal(const KeyGenerationInfo& key_info, const EVP_PKEY_ptr& evp_key) {
|
||||
// write private key to file
|
||||
if (key_info.private_key_file.has_value()) {
|
||||
const BIO_ptr key_bio(BIO_new_file(key_info.private_key_file.value().c_str(), "w"));
|
||||
|
||||
if (!key_bio) {
|
||||
EVLOG_error << "Failed to create private key file!";
|
||||
return false;
|
||||
}
|
||||
|
||||
int success = 0;
|
||||
if (key_info.private_key_pass.has_value()) {
|
||||
success = PEM_write_bio_PrivateKey(key_bio.get(), evp_key.get(), EVP_aes_128_cbc(), nullptr, 0, nullptr,
|
||||
(void*)key_info.private_key_pass.value().c_str());
|
||||
} else {
|
||||
success = PEM_write_bio_PrivateKey(key_bio.get(), evp_key.get(), nullptr, nullptr, 0, nullptr, nullptr);
|
||||
}
|
||||
|
||||
if (0 == success) {
|
||||
EVLOG_error << "Failed to write private key!";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (key_info.public_key_file.has_value()) {
|
||||
const BIO_ptr key_bio(BIO_new_file(key_info.public_key_file.value().c_str(), "w"));
|
||||
|
||||
if (!key_bio) {
|
||||
EVLOG_error << "Failed to create private key file!";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (0 == PEM_write_bio_PUBKEY(key_bio.get(), evp_key.get())) {
|
||||
EVLOG_error << "Failed to write pubkey!";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
constexpr const char* kt_rsa = "RSA";
|
||||
constexpr const char* kt_ec = "EC";
|
||||
|
||||
namespace {
|
||||
bool s_generate_key(const KeyGenerationInfo& key_info, KeyHandle_ptr& out_key, EVP_PKEY_CTX_ptr& ctx) {
|
||||
unsigned int bits = 0;
|
||||
std::string group_256 = "P-256";
|
||||
std::string group_384 = "P-384";
|
||||
char* group = nullptr;
|
||||
std::size_t group_sz = 0;
|
||||
int nid = NID_undef;
|
||||
|
||||
bool bResult = true;
|
||||
bool bEC = true;
|
||||
|
||||
OpenSSLProvider provider;
|
||||
if (key_info.generate_on_custom) {
|
||||
provider.set_global_mode(OpenSSLProvider::mode_t::custom_provider);
|
||||
} else {
|
||||
provider.set_global_mode(OpenSSLProvider::mode_t::default_provider);
|
||||
}
|
||||
|
||||
// note when using tpm2 some key_types may not be supported.
|
||||
|
||||
EVLOG_info << "Key parameters";
|
||||
switch (key_info.key_type) {
|
||||
case CryptoKeyType::RSA_TPM20:
|
||||
bits = 2048;
|
||||
bEC = false;
|
||||
break;
|
||||
case CryptoKeyType::RSA_3072:
|
||||
bits = 3072;
|
||||
bEC = false;
|
||||
break;
|
||||
case CryptoKeyType::RSA_7680:
|
||||
bits = 7680;
|
||||
bEC = false;
|
||||
break;
|
||||
case CryptoKeyType::EC_prime256v1:
|
||||
group = group_256.data();
|
||||
group_sz = group_256.length();
|
||||
nid = NID_X9_62_prime256v1;
|
||||
break;
|
||||
case CryptoKeyType::EC_secp384r1:
|
||||
default:
|
||||
group = group_384.data();
|
||||
group_sz = group_384.length();
|
||||
nid = NID_secp384r1;
|
||||
break;
|
||||
}
|
||||
|
||||
std::array<OSSL_PARAM, 2> params = {};
|
||||
|
||||
if (bEC) {
|
||||
params[0] = OSSL_PARAM_construct_utf8_string("group", group, group_sz);
|
||||
EVLOG_info << "Key parameters: EC";
|
||||
ctx = EVP_PKEY_CTX_ptr(EVP_PKEY_CTX_new_from_name(nullptr, kt_ec, nullptr));
|
||||
} else {
|
||||
params[0] = OSSL_PARAM_construct_uint("bits", &bits);
|
||||
EVLOG_info << "Key parameters: RSA";
|
||||
ctx = EVP_PKEY_CTX_ptr(EVP_PKEY_CTX_new_from_name(nullptr, kt_rsa, nullptr));
|
||||
}
|
||||
|
||||
params[1] = OSSL_PARAM_construct_end();
|
||||
|
||||
if (bResult) {
|
||||
EVLOG_info << "Key parameters done";
|
||||
if (nullptr == ctx.get()) {
|
||||
EVLOG_error << "create key context failed!";
|
||||
ERR_print_errors_fp(stderr);
|
||||
bResult = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (bResult) {
|
||||
EVLOG_info << "Keygen init";
|
||||
if (EVP_PKEY_keygen_init(ctx.get()) <= 0 || EVP_PKEY_CTX_set_params(ctx.get(), params.data()) <= 0) {
|
||||
EVLOG_error << "Keygen init failed";
|
||||
ERR_print_errors_fp(stderr);
|
||||
bResult = false;
|
||||
}
|
||||
}
|
||||
|
||||
EVP_PKEY* pkey = nullptr;
|
||||
|
||||
if (bResult) {
|
||||
EVLOG_info << "Key generate";
|
||||
if (EVP_PKEY_generate(ctx.get(), &pkey) <= 0) {
|
||||
EVLOG_error << "Failed to generate tpm2 key!";
|
||||
ERR_print_errors_fp(stderr);
|
||||
bResult = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto evp_key = EVP_PKEY_ptr(pkey);
|
||||
|
||||
if (bResult) {
|
||||
EVLOG_info << "Key export";
|
||||
// Export keys too
|
||||
bResult = export_key_internal(key_info, evp_key);
|
||||
// NOLINTNEXTLINE(misc-const-correctness): would be problematic in the following make_unique statement
|
||||
EVP_PKEY* raw_key_handle = evp_key.release();
|
||||
out_key = std::make_unique<KeyHandleOpenSSL>(raw_key_handle);
|
||||
}
|
||||
|
||||
return bResult;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool OpenSSLSupplier::generate_key(const KeyGenerationInfo& key_info, KeyHandle_ptr& /*out_key*/) {
|
||||
KeyHandle_ptr gen_key;
|
||||
EVP_PKEY_CTX_ptr ctx;
|
||||
bool bResult = true;
|
||||
|
||||
bResult = s_generate_key(key_info, gen_key, ctx);
|
||||
if (!bResult) {
|
||||
EVLOG_error << "Failed to generate csr pub/priv key!";
|
||||
}
|
||||
|
||||
return bResult;
|
||||
}
|
||||
|
||||
std::vector<X509Handle_ptr> OpenSSLSupplier::load_certificates(const std::string& data, const EncodingFormat encoding) {
|
||||
std::vector<X509Handle_ptr> certificates;
|
||||
|
||||
const BIO_ptr bio(BIO_new_mem_buf(data.data(), static_cast<int>(data.size())));
|
||||
|
||||
if (!bio) {
|
||||
throw CertificateLoadException("Failed to create BIO from data");
|
||||
}
|
||||
|
||||
if (encoding == EncodingFormat::PEM) {
|
||||
STACK_OF(X509_INFO)* allcerts = PEM_X509_INFO_read_bio(bio.get(), nullptr, nullptr, nullptr);
|
||||
|
||||
if (allcerts != nullptr) {
|
||||
for (int i = 0; i < sk_X509_INFO_num(allcerts); i++) {
|
||||
X509_INFO* xi = sk_X509_INFO_value(allcerts, i);
|
||||
|
||||
if ((xi != nullptr) && (xi->x509 != nullptr)) {
|
||||
// Transfer ownership, safely, push_back since emplace_back can cause a memory leak
|
||||
certificates.push_back(std::make_unique<X509HandleOpenSSL>(xi->x509));
|
||||
xi->x509 = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
sk_X509_INFO_pop_free(allcerts, X509_INFO_free);
|
||||
} else {
|
||||
throw CertificateLoadException("Certificate (PEM) parsing error");
|
||||
}
|
||||
} else if (encoding == EncodingFormat::DER) {
|
||||
// NOLINTNEXTLINE(misc-const-correctness): would be problematic in the following make_unique statement
|
||||
X509* x509 = d2i_X509_bio(bio.get(), nullptr);
|
||||
|
||||
if (x509 != nullptr) {
|
||||
certificates.push_back(std::make_unique<X509HandleOpenSSL>(x509));
|
||||
} else {
|
||||
throw CertificateLoadException("Certificate (DER) parsing error");
|
||||
}
|
||||
} else {
|
||||
throw CertificateLoadException("Unsupported encoding format");
|
||||
}
|
||||
|
||||
return certificates;
|
||||
}
|
||||
|
||||
std::string OpenSSLSupplier::x509_to_string(X509Handle* handle) {
|
||||
// NOLINTNEXTLINE(misc-const-correctness): would be problematic in the following PEM_write_bio_X509 statement
|
||||
if (X509* x509 = get(handle)) {
|
||||
const BIO_ptr bio_write(BIO_new(BIO_s_mem()));
|
||||
|
||||
const int rc = PEM_write_bio_X509(bio_write.get(), x509);
|
||||
|
||||
if (rc == 1) {
|
||||
const BUF_MEM* mem = nullptr;
|
||||
BIO_get_mem_ptr(bio_write.get(), &mem);
|
||||
|
||||
return std::string(mem->data, mem->length);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string OpenSSLSupplier::x509_get_common_name(X509Handle* handle) {
|
||||
const X509* x509 = get(handle);
|
||||
|
||||
if (x509 == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const X509_NAME* subject = X509_get_subject_name(x509);
|
||||
const int nid = OBJ_txt2nid("CN");
|
||||
const int index = X509_NAME_get_index_by_NID(subject, nid, -1);
|
||||
|
||||
if (index == -1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const X509_NAME_ENTRY* entry = X509_NAME_get_entry(subject, index);
|
||||
const ASN1_STRING* ca_asn1 = X509_NAME_ENTRY_get_data(entry);
|
||||
|
||||
if (ca_asn1 == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const unsigned char* cn_str = ASN1_STRING_get0_data(ca_asn1);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
std::string common_name(reinterpret_cast<const char*>(cn_str), ASN1_STRING_length(ca_asn1));
|
||||
return common_name;
|
||||
}
|
||||
|
||||
std::string OpenSSLSupplier::x509_get_issuer_name_hash(X509Handle* handle) {
|
||||
const X509* x509 = get(handle);
|
||||
|
||||
if (x509 == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::array<unsigned char, SHA256_DIGEST_LENGTH> md;
|
||||
const X509_NAME* name = X509_get_issuer_name(x509);
|
||||
X509_NAME_digest(name, EVP_sha256(), md.data(), nullptr);
|
||||
|
||||
std::stringstream ss;
|
||||
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
|
||||
ss << std::setw(2) << std::setfill('0') << std::hex << (int)md.at(i);
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string OpenSSLSupplier::x509_get_serial_number(X509Handle* handle) {
|
||||
X509* x509 = get(handle);
|
||||
|
||||
if (x509 == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ASN1_INTEGER* serial_asn1 = X509_get_serialNumber(x509);
|
||||
if (serial_asn1 == nullptr) {
|
||||
ERR_print_errors_fp(stderr);
|
||||
return {};
|
||||
}
|
||||
|
||||
BIGNUM* bn_serial = ASN1_INTEGER_to_BN(serial_asn1, nullptr);
|
||||
|
||||
if (bn_serial == nullptr) {
|
||||
ERR_print_errors_fp(stderr);
|
||||
return {};
|
||||
}
|
||||
|
||||
char* hex_serial = BN_bn2hex(bn_serial);
|
||||
|
||||
if (hex_serial == nullptr) {
|
||||
ERR_print_errors_fp(stderr);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string serial(hex_serial);
|
||||
for (char& i : serial) {
|
||||
i = static_cast<char>(std::tolower(static_cast<unsigned char>(i)));
|
||||
}
|
||||
|
||||
BN_free(bn_serial);
|
||||
OPENSSL_free(hex_serial);
|
||||
|
||||
serial.erase(0, std::min(serial.find_first_not_of('0'), serial.size() - 1));
|
||||
return serial;
|
||||
}
|
||||
|
||||
std::string OpenSSLSupplier::x509_get_key_hash(X509Handle* handle) {
|
||||
const X509* x509 = get(handle);
|
||||
|
||||
if (x509 == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::array<unsigned char, SHA256_DIGEST_LENGTH> tmphash;
|
||||
X509_pubkey_digest(x509, EVP_sha256(), tmphash.data(), nullptr);
|
||||
std::stringstream ss;
|
||||
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
|
||||
ss << std::setw(2) << std::setfill('0') << std::hex << (int)tmphash.at(i);
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string OpenSSLSupplier::x509_get_responder_url(X509Handle* handle) {
|
||||
X509* x509 = get(handle);
|
||||
|
||||
if (x509 == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto ocsp = X509_get1_ocsp(x509);
|
||||
std::string responder_url;
|
||||
for (int i = 0; i < sk_OPENSSL_STRING_num(ocsp); i++) {
|
||||
responder_url.append(sk_OPENSSL_STRING_value(ocsp, i));
|
||||
}
|
||||
|
||||
if (responder_url.empty()) {
|
||||
EVLOG_warning << "Could not retrieve OCSP Responder URL from certificate";
|
||||
}
|
||||
|
||||
return responder_url;
|
||||
}
|
||||
|
||||
bool OpenSSLSupplier::x509_get_validity(X509Handle* handle, std::int64_t& out_valid_in, std::int64_t& out_valid_to) {
|
||||
const X509* x509 = get(handle);
|
||||
|
||||
if (x509 == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For valid_in and valid_to
|
||||
const ASN1_TIME* notBefore = X509_get_notBefore(x509);
|
||||
const ASN1_TIME* notAfter = X509_get_notAfter(x509);
|
||||
|
||||
int day = 0;
|
||||
int sec = 0;
|
||||
ASN1_TIME_diff(&day, &sec, nullptr, notBefore);
|
||||
out_valid_in =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(days_to_seconds(day)).count() + sec; // Convert days to seconds
|
||||
ASN1_TIME_diff(&day, &sec, nullptr, notAfter);
|
||||
out_valid_to =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(days_to_seconds(day)).count() + sec; // Convert days to seconds
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenSSLSupplier::x509_is_child(X509Handle* child, X509Handle* parent) {
|
||||
// A certif can't be it's own parent, use is_selfsigned if that is intended
|
||||
if (child == parent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
X509* x509_parent = get(parent);
|
||||
X509* x509_child = get(child);
|
||||
|
||||
if (x509_parent == nullptr || x509_child == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const X509_STORE_ptr store(X509_STORE_new());
|
||||
X509_STORE_add_cert(store.get(), x509_parent);
|
||||
|
||||
const X509_STORE_CTX_ptr ctx(X509_STORE_CTX_new());
|
||||
X509_STORE_CTX_init(ctx.get(), store.get(), x509_child, nullptr);
|
||||
|
||||
// If the parent is not a self-signed certificate, assume we have a partial chain
|
||||
if (x509_is_selfsigned(parent) == false) {
|
||||
// TODO(ioan): see if this strict flag is required, caused many problems
|
||||
// X509_STORE_CTX_set_flags(ctx.get(), X509_V_FLAG_X509_STRICT);
|
||||
|
||||
X509_STORE_CTX_set_flags(ctx.get(), X509_V_FLAG_PARTIAL_CHAIN);
|
||||
}
|
||||
|
||||
if (X509_verify_cert(ctx.get()) != 1) {
|
||||
const int ec = X509_STORE_CTX_get_error(ctx.get());
|
||||
const char* error = X509_verify_cert_error_string(ec);
|
||||
|
||||
EVLOG_debug << "Certificate issued by error: " << ((error != nullptr) ? error : "UNKNOWN");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenSSLSupplier::x509_is_selfsigned(X509Handle* handle) {
|
||||
X509* x509 = get(handle);
|
||||
|
||||
if (x509 == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (X509_self_signed(x509, 0) == 1);
|
||||
}
|
||||
|
||||
bool OpenSSLSupplier::x509_is_equal(X509Handle* a, X509Handle* b) {
|
||||
return (X509_cmp(get(a), get(b)) == 0);
|
||||
}
|
||||
|
||||
X509Handle_ptr OpenSSLSupplier::x509_duplicate_unique(X509Handle* handle) {
|
||||
return std::make_unique<X509HandleOpenSSL>(X509_dup(get(handle)));
|
||||
}
|
||||
|
||||
CertificateValidationResult OpenSSLSupplier::x509_verify_certificate_chain(
|
||||
X509Handle* target, const std::vector<X509Handle*>& parents, const std::vector<X509Handle*>& untrusted_subcas,
|
||||
bool allow_future_certificates, const std::optional<fs::path> dir_path, const std::optional<fs::path> file_path) {
|
||||
|
||||
const X509_STORE_ptr store_ptr(X509_STORE_new());
|
||||
const X509_STORE_CTX_ptr store_ctx_ptr(X509_STORE_CTX_new());
|
||||
|
||||
for (auto parent : parents) {
|
||||
X509_STORE_add_cert(store_ptr.get(), get(parent));
|
||||
}
|
||||
|
||||
if (dir_path.has_value() || file_path.has_value()) {
|
||||
const char* c_dir_path = dir_path.has_value() ? dir_path.value().c_str() : nullptr;
|
||||
const char* c_file_path = file_path.has_value() ? file_path.value().c_str() : nullptr;
|
||||
|
||||
if (1 != X509_STORE_load_locations(store_ptr.get(), c_file_path, c_dir_path)) {
|
||||
EVLOG_warning << "X509 could not load store locations!";
|
||||
return CertificateValidationResult::Unknown;
|
||||
}
|
||||
|
||||
if (dir_path.has_value()) {
|
||||
if (X509_STORE_add_lookup(store_ptr.get(), X509_LOOKUP_file()) == nullptr) {
|
||||
EVLOG_warning << "X509 could not add store lookup!";
|
||||
return CertificateValidationResult::Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
X509_STACK_UNSAFE_ptr untrusted = nullptr;
|
||||
|
||||
// Build potentially untrusted intermediary (subca) certificates
|
||||
if (false == untrusted_subcas.empty()) {
|
||||
untrusted = X509_STACK_UNSAFE_ptr(sk_X509_new_null());
|
||||
const int flags = X509_ADD_FLAG_NO_DUP | X509_ADD_FLAG_NO_SS;
|
||||
|
||||
for (auto& untrusted_cert : untrusted_subcas) {
|
||||
if (1 != X509_add_cert(untrusted.get(), get(untrusted_cert), flags)) {
|
||||
EVLOG_error << "X509 could not create untrusted store stack!";
|
||||
return CertificateValidationResult::Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (1 != X509_STORE_CTX_init(store_ctx_ptr.get(), store_ptr.get(), get(target), untrusted.get())) {
|
||||
EVLOG_error << "X509 could not init x509 store ctx!";
|
||||
return CertificateValidationResult::Unknown;
|
||||
}
|
||||
|
||||
if (allow_future_certificates) {
|
||||
// Manually check if cert is expired
|
||||
int day = 0;
|
||||
int sec = 0;
|
||||
ASN1_TIME_diff(&day, &sec, nullptr, X509_get_notAfter(get(target)));
|
||||
if (day < 0 || sec < 0) {
|
||||
// certificate is expired
|
||||
return CertificateValidationResult::Expired;
|
||||
}
|
||||
// certificate is not expired, but may not be valid yet. Since we allow future certs, disable time checks.
|
||||
X509_STORE_CTX_set_flags(store_ctx_ptr.get(), X509_V_FLAG_NO_CHECK_TIME);
|
||||
}
|
||||
|
||||
// verifies the certificate chain based on ctx
|
||||
// verifies the certificate has not expired and is already valid
|
||||
if (X509_verify_cert(store_ctx_ptr.get()) != 1) {
|
||||
const int ec = X509_STORE_CTX_get_error(store_ctx_ptr.get());
|
||||
return to_certificate_error(ec);
|
||||
}
|
||||
|
||||
return CertificateValidationResult::Valid;
|
||||
}
|
||||
|
||||
KeyValidationResult OpenSSLSupplier::x509_check_private_key(X509Handle* handle, std::string private_key,
|
||||
std::optional<std::string> password) {
|
||||
const X509* x509 = get(handle);
|
||||
|
||||
if (x509 == nullptr) {
|
||||
return KeyValidationResult::Unknown;
|
||||
}
|
||||
|
||||
{
|
||||
const OpenSSLProvider provider; // ensure providers are loaded
|
||||
// minimise holding the mutex
|
||||
}
|
||||
|
||||
const BIO_ptr bio(BIO_new_mem_buf(private_key.c_str(), -1));
|
||||
// Passing password string since if NULL is provided, the password CB will be called
|
||||
const EVP_PKEY_ptr evp_pkey(
|
||||
PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, (void*)password.value_or("").c_str()));
|
||||
|
||||
const bool bResult = true;
|
||||
if (!evp_pkey) {
|
||||
EVLOG_warning << "Invalid evp_pkey: " << private_key << " error: " << ERR_error_string(ERR_get_error(), nullptr)
|
||||
<< " Password configured correctly?";
|
||||
ERR_print_errors_fp(stderr);
|
||||
|
||||
return KeyValidationResult::KeyLoadFailure;
|
||||
}
|
||||
|
||||
KeyValidationResult result = KeyValidationResult::Unknown;
|
||||
|
||||
if (X509_check_private_key(x509, evp_pkey.get()) == 1) {
|
||||
result = KeyValidationResult::Valid;
|
||||
} else {
|
||||
result = KeyValidationResult::Invalid;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool OpenSSLSupplier::x509_verify_signature(X509Handle* handle, const std::vector<std::uint8_t>& signature,
|
||||
const std::vector<std::uint8_t>& data) {
|
||||
{
|
||||
const OpenSSLProvider provider; // ensure providers are loaded
|
||||
// minimise holding the mutex
|
||||
}
|
||||
|
||||
// extract public key
|
||||
X509* x509 = get(handle);
|
||||
|
||||
if (x509 == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const EVP_PKEY_ptr public_key_ptr(X509_get_pubkey(x509));
|
||||
|
||||
if (public_key_ptr.get() == nullptr) {
|
||||
EVLOG_error << "Error during X509_get_pubkey";
|
||||
return false;
|
||||
}
|
||||
|
||||
// verify file signature
|
||||
const EVP_PKEY_CTX_ptr public_key_context_ptr(EVP_PKEY_CTX_new(public_key_ptr.get(), nullptr));
|
||||
|
||||
if (public_key_context_ptr.get() == nullptr) {
|
||||
EVLOG_error << "Error setting up public key context";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (EVP_PKEY_verify_init(public_key_context_ptr.get()) <= 0) {
|
||||
EVLOG_error << "Error during EVP_PKEY_verify_init";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (EVP_PKEY_CTX_set_signature_md(public_key_context_ptr.get(), EVP_sha256()) <= 0) {
|
||||
EVLOG_error << "Error during EVP_PKEY_CTX_set_signature_md";
|
||||
return false;
|
||||
};
|
||||
|
||||
const int result =
|
||||
EVP_PKEY_verify(public_key_context_ptr.get(),
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
reinterpret_cast<const unsigned char*>(signature.data()), signature.size(),
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
reinterpret_cast<const unsigned char*>(data.data()), data.size());
|
||||
|
||||
EVP_cleanup();
|
||||
|
||||
if (result != 1) {
|
||||
EVLOG_error << "Failure to verify: " << result;
|
||||
return false;
|
||||
}
|
||||
EVLOG_debug << "Successful verification";
|
||||
return true;
|
||||
}
|
||||
|
||||
CertificateSignRequestResult OpenSSLSupplier::x509_generate_csr(const CertificateSigningRequestInfo& csr_info,
|
||||
std::string& out_csr) {
|
||||
|
||||
KeyHandle_ptr gen_key;
|
||||
EVP_PKEY_CTX_ptr ctx;
|
||||
|
||||
if (false == s_generate_key(csr_info.key_info, gen_key, ctx)) {
|
||||
return CertificateSignRequestResult::KeyGenerationError;
|
||||
}
|
||||
|
||||
EVP_PKEY* key = get(gen_key.get());
|
||||
|
||||
// X509 CSR request
|
||||
const X509_REQ_ptr x509_req_ptr(X509_REQ_new());
|
||||
|
||||
if (nullptr == x509_req_ptr.get()) {
|
||||
EVLOG_error << "Failed to create CSR request!";
|
||||
ERR_print_errors_fp(stderr);
|
||||
|
||||
return CertificateSignRequestResult::Unknown;
|
||||
}
|
||||
|
||||
// set version of x509 req
|
||||
const int n_version = csr_info.n_version;
|
||||
|
||||
if (0 == X509_REQ_set_version(x509_req_ptr.get(), n_version)) {
|
||||
EVLOG_error << "Failed to set csr version!";
|
||||
ERR_print_errors_fp(stderr);
|
||||
|
||||
return CertificateSignRequestResult::VersioningError;
|
||||
}
|
||||
|
||||
// set public key of x509 req
|
||||
if (0 == X509_REQ_set_pubkey(x509_req_ptr.get(), key)) {
|
||||
EVLOG_error << "Failed to set csr pubkey!";
|
||||
ERR_print_errors_fp(stderr);
|
||||
|
||||
return CertificateSignRequestResult::PubkeyError;
|
||||
}
|
||||
|
||||
X509_NAME* x509Name = X509_REQ_get_subject_name(x509_req_ptr.get());
|
||||
|
||||
// set subject of x509 req
|
||||
X509_NAME_add_entry_by_txt(
|
||||
x509Name, "C", MBSTRING_ASC,
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
reinterpret_cast<const unsigned char*>(csr_info.country.c_str()), -1, -1, 0);
|
||||
X509_NAME_add_entry_by_txt(
|
||||
x509Name, "O", MBSTRING_ASC,
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
reinterpret_cast<const unsigned char*>(csr_info.organization.c_str()), -1, -1, 0);
|
||||
X509_NAME_add_entry_by_txt(
|
||||
x509Name, "CN", MBSTRING_ASC,
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
reinterpret_cast<const unsigned char*>(csr_info.commonName.c_str()), -1, -1, 0);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
X509_NAME_add_entry_by_txt(x509Name, "DC", MBSTRING_ASC, reinterpret_cast<const unsigned char*>("CPO"), -1, -1, 0);
|
||||
|
||||
STACK_OF(X509_EXTENSION)* extensions = sk_X509_EXTENSION_new_null();
|
||||
X509_EXTENSION* ext_key_usage =
|
||||
X509V3_EXT_conf_nid(nullptr, nullptr, NID_key_usage, "digitalSignature, keyAgreement");
|
||||
X509_EXTENSION* ext_basic_constraints =
|
||||
X509V3_EXT_conf_nid(nullptr, nullptr, NID_basic_constraints, "critical,CA:false");
|
||||
sk_X509_EXTENSION_push(extensions, ext_key_usage);
|
||||
sk_X509_EXTENSION_push(extensions, ext_basic_constraints);
|
||||
|
||||
std::vector<std::string> names;
|
||||
if (csr_info.dns_name.has_value()) {
|
||||
names.push_back({std::string("DNS:") + csr_info.dns_name.value()});
|
||||
}
|
||||
if (csr_info.ip_address.has_value()) {
|
||||
names.push_back({std::string("IP:") + csr_info.ip_address.value()});
|
||||
}
|
||||
|
||||
X509_EXTENSION* ext_san = nullptr;
|
||||
if (!names.empty()) {
|
||||
auto comma_fold = [](std::string a, const std::string& b) { return std::move(a) + ',' + b; };
|
||||
const std::string value =
|
||||
std::accumulate(std::next(names.begin()), names.end(), std::string(names[0]), comma_fold);
|
||||
ext_san = X509V3_EXT_conf_nid(nullptr, nullptr, NID_subject_alt_name, value.c_str());
|
||||
sk_X509_EXTENSION_push(extensions, ext_san);
|
||||
}
|
||||
|
||||
const bool result = X509_REQ_add_extensions(x509_req_ptr.get(), extensions) != 0;
|
||||
X509_EXTENSION_free(ext_key_usage);
|
||||
X509_EXTENSION_free(ext_basic_constraints);
|
||||
X509_EXTENSION_free(ext_san);
|
||||
sk_X509_EXTENSION_free(extensions);
|
||||
|
||||
if (!result) {
|
||||
EVLOG_error << "Failed to add csr extensions!";
|
||||
ERR_print_errors_fp(stderr);
|
||||
|
||||
return CertificateSignRequestResult::ExtensionsError;
|
||||
}
|
||||
|
||||
// sign the certificate with the private key
|
||||
const bool x509_signed = X509_REQ_sign(x509_req_ptr.get(), key, EVP_sha256()) != 0;
|
||||
|
||||
if (x509_signed == false) {
|
||||
EVLOG_error << "Failed to sign csr with error!";
|
||||
ERR_print_errors_fp(stderr);
|
||||
|
||||
return CertificateSignRequestResult::SigningError;
|
||||
}
|
||||
|
||||
// write csr
|
||||
const BIO_ptr bio(BIO_new(BIO_s_mem()));
|
||||
PEM_write_bio_X509_REQ(bio.get(), x509_req_ptr.get());
|
||||
|
||||
const BUF_MEM* mem_csr = nullptr;
|
||||
BIO_get_mem_ptr(bio.get(), &mem_csr);
|
||||
|
||||
out_csr = std::string(mem_csr->data, mem_csr->length);
|
||||
|
||||
return CertificateSignRequestResult::Valid;
|
||||
}
|
||||
|
||||
bool OpenSSLSupplier::digest_file_sha256(const fs::path& path, std::vector<std::uint8_t>& out_digest) {
|
||||
EVP_MD_CTX_ptr md_context_ptr(EVP_MD_CTX_create());
|
||||
if (md_context_ptr.get() == nullptr) {
|
||||
EVLOG_error << "Could not create EVP_MD_CTX";
|
||||
return false;
|
||||
}
|
||||
|
||||
const EVP_MD* md = EVP_get_digestbyname("SHA256");
|
||||
if (EVP_DigestInit_ex(md_context_ptr.get(), md, nullptr) == 0) {
|
||||
EVLOG_error << "Error during EVP_DigestInit_ex";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool digest_error = false;
|
||||
|
||||
unsigned int sha256_out_length = 0;
|
||||
std::array<std::uint8_t, EVP_MAX_MD_SIZE> sha256_out;
|
||||
|
||||
// calculate sha256 of file
|
||||
const bool processed_file = filesystem_utils::process_file(
|
||||
path, BUFSIZ, [&](const std::uint8_t* bytes, std::size_t read, bool last_chunk) -> bool {
|
||||
if (read > 0) {
|
||||
if (EVP_DigestUpdate(md_context_ptr.get(), bytes, read) == 0) {
|
||||
EVLOG_error << "Error during EVP_DigestUpdate";
|
||||
digest_error = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (last_chunk) {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
if (EVP_DigestFinal_ex(md_context_ptr.get(), reinterpret_cast<unsigned char*>(sha256_out.data()),
|
||||
&sha256_out_length) == 0) {
|
||||
EVLOG_error << "Error during EVP_DigestFinal_ex";
|
||||
digest_error = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if ((processed_file == false) || (digest_error == true)) {
|
||||
EVLOG_error << "Could not digest file at: " << path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
out_digest.clear();
|
||||
std::copy_n(sha256_out.begin(), sha256_out_length, std::back_inserter((out_digest)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
template <typename T> bool base64_decode(const std::string& base64_string, T& out_decoded) {
|
||||
const EVP_ENCODE_CTX_ptr base64_decode_context_ptr(EVP_ENCODE_CTX_new());
|
||||
if (!base64_decode_context_ptr.get()) {
|
||||
EVLOG_error << "Error during EVP_ENCODE_CTX_new";
|
||||
return false;
|
||||
}
|
||||
|
||||
EVP_DecodeInit(base64_decode_context_ptr.get());
|
||||
if (!base64_decode_context_ptr.get()) {
|
||||
EVLOG_error << "Error during EVP_DecodeInit";
|
||||
return false;
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
const auto* encoded_str = reinterpret_cast<const unsigned char*>(base64_string.data());
|
||||
const int base64_length = base64_string.size();
|
||||
|
||||
std::vector<std::uint8_t> decoded_out;
|
||||
decoded_out.reserve(base64_length);
|
||||
|
||||
int decoded_out_length = 0;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
if (EVP_DecodeUpdate(base64_decode_context_ptr.get(), reinterpret_cast<unsigned char*>(decoded_out.data()),
|
||||
&decoded_out_length, encoded_str, base64_length) < 0) {
|
||||
EVLOG_error << "Error during DecodeUpdate";
|
||||
return false;
|
||||
}
|
||||
|
||||
int decode_final_out = 0;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
if (EVP_DecodeFinal(base64_decode_context_ptr.get(), reinterpret_cast<unsigned char*>(decoded_out.data()),
|
||||
&decode_final_out) < 0) {
|
||||
EVLOG_error << "Error during EVP_DecodeFinal";
|
||||
return false;
|
||||
}
|
||||
|
||||
out_decoded.clear();
|
||||
std::copy_n(decoded_out.begin(), decoded_out_length, std::back_inserter((out_decoded)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool base64_encode(const unsigned char* bytes_str, int bytes_size, std::string& out_encoded) {
|
||||
const EVP_ENCODE_CTX_ptr base64_encode_context_ptr(EVP_ENCODE_CTX_new());
|
||||
if (base64_encode_context_ptr.get() == nullptr) {
|
||||
EVLOG_error << "Error during EVP_ENCODE_CTX_new";
|
||||
return false;
|
||||
}
|
||||
|
||||
EVP_EncodeInit(base64_encode_context_ptr.get());
|
||||
// evp_encode_ctx_set_flags(base64_encode_context_ptr.get(), EVP_ENCODE_CTX_NO_NEWLINES); // Of course it's not
|
||||
// public
|
||||
|
||||
if (base64_encode_context_ptr.get() == nullptr) {
|
||||
EVLOG_error << "Error during EVP_EncodeInit";
|
||||
return false;
|
||||
}
|
||||
|
||||
const int base64_length = ((bytes_size / 3) * 4) + 2;
|
||||
// If it causes issues, replace with 'alloca' on different platform
|
||||
std::vector<char> base64_out;
|
||||
base64_out.reserve(base64_length + 66); // + 66 bytes for final block
|
||||
int full_len = 0;
|
||||
|
||||
int base64_out_length = 0;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
if (EVP_EncodeUpdate(base64_encode_context_ptr.get(), reinterpret_cast<unsigned char*>(base64_out.data()),
|
||||
&base64_out_length, bytes_str, bytes_size) < 0) {
|
||||
EVLOG_error << "Error during EVP_EncodeUpdate";
|
||||
return false;
|
||||
}
|
||||
full_len += base64_out_length;
|
||||
|
||||
EVP_EncodeFinal(base64_encode_context_ptr.get(),
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed because of OpenSSL API
|
||||
std::next(reinterpret_cast<unsigned char*>(base64_out.data()), base64_out_length),
|
||||
&base64_out_length);
|
||||
full_len += base64_out_length;
|
||||
|
||||
out_encoded.assign(base64_out.data(), full_len);
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool OpenSSLSupplier::base64_decode_to_bytes(const std::string& base64_string, std::vector<std::uint8_t>& out_decoded) {
|
||||
return base64_decode<std::vector<std::uint8_t>>(base64_string, out_decoded);
|
||||
}
|
||||
|
||||
bool OpenSSLSupplier::base64_decode_to_string(const std::string& base64_string, std::string& out_decoded) {
|
||||
return base64_decode<std::string>(base64_string, out_decoded);
|
||||
}
|
||||
|
||||
bool OpenSSLSupplier::base64_encode_from_bytes(const std::vector<std::uint8_t>& bytes, std::string& out_encoded) {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed for API usage
|
||||
return base64_encode(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size(), out_encoded);
|
||||
}
|
||||
|
||||
bool OpenSSLSupplier::base64_encode_from_string(const std::string& string, std::string& out_encoded) {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed for API usage
|
||||
return base64_encode(reinterpret_cast<const unsigned char*>(string.data()), string.size(), out_encoded);
|
||||
}
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,294 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <evse_security/crypto/openssl/openssl_provider.hpp>
|
||||
#include <evse_security/evse_types.hpp>
|
||||
|
||||
#include <openssl/opensslv.h>
|
||||
|
||||
#if USING_CUSTOM_PROVIDER
|
||||
// OpenSSL3 without TPM will use the default provider anyway
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/provider.h>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
#else
|
||||
// dummy structures for non-OpenSSL 3
|
||||
struct ossl_provider_st {};
|
||||
using OSSL_PROVIDER = struct ossl_provider_st;
|
||||
struct ossl_lib_ctx_st;
|
||||
using OSSL_LIB_CTX = struct ossl_lib_ctx_st;
|
||||
#endif
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
namespace {
|
||||
const auto KEY_HEADER_DEFAULT = "-----BEGIN PRIVATE KEY-----";
|
||||
const auto KEY_HEADER_DEFAULT_ENCRYPTED = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
|
||||
|
||||
const auto KEY_HEADER_TPM2 = "-----BEGIN TSS2 PRIVATE KEY-----";
|
||||
} // namespace
|
||||
|
||||
bool is_custom_private_key_string(const std::string& private_key_pem) {
|
||||
// If we can't find the standard header it means it's a custom key
|
||||
return private_key_pem.find(KEY_HEADER_DEFAULT) == std::string::npos &&
|
||||
private_key_pem.find(KEY_HEADER_DEFAULT_ENCRYPTED) == std::string::npos;
|
||||
}
|
||||
|
||||
bool is_custom_private_key_file(const fs::path& private_key_file_pem) {
|
||||
if (fs::is_regular_file(private_key_file_pem)) {
|
||||
fsstd::ifstream key_file(private_key_file_pem);
|
||||
std::string line;
|
||||
std::getline(key_file, line);
|
||||
key_file.close();
|
||||
|
||||
// Search for the standard header
|
||||
return is_custom_private_key_string(line);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef USING_CUSTOM_PROVIDER
|
||||
|
||||
constexpr bool is_custom_provider_tpm() {
|
||||
// custom provider string (see CMakeLists.txt)
|
||||
constexpr const std::string_view custom_provider(CUSTOM_PROVIDER_NAME);
|
||||
return (custom_provider == "tpm2");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// class OpenSSLProvider OpenSSL 3
|
||||
|
||||
static const char* mode_t_str[2] = {
|
||||
"default provider", // mode_t::default_provider
|
||||
"custom provider" // mode_t::custom_provider
|
||||
};
|
||||
|
||||
static_assert(static_cast<int>(OpenSSLProvider::mode_t::default_provider) == 0);
|
||||
static_assert(static_cast<int>(OpenSSLProvider::mode_t::custom_provider) == 1);
|
||||
|
||||
std::ostream& operator<<(std::ostream& out, OpenSSLProvider::mode_t mode) {
|
||||
const unsigned int idx = static_cast<unsigned int>(mode);
|
||||
if (idx <= static_cast<unsigned int>(OpenSSLProvider::mode_t::custom_provider)) {
|
||||
out << mode_t_str[idx];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool s_load_and_test_provider(OSSL_PROVIDER*& provider, OSSL_LIB_CTX* libctx, const char* provider_name) {
|
||||
bool result = true;
|
||||
#ifdef DEBUG
|
||||
const char* modestr = (libctx == nullptr) ? "global" : "TLS";
|
||||
EVLOG_info << "Loading " << modestr << " provider: " << provider_name;
|
||||
#endif
|
||||
if ((provider = OSSL_PROVIDER_load(libctx, provider_name)) == nullptr) {
|
||||
EVLOG_error << "Unable to load OSSL_PROVIDER: " << provider_name;
|
||||
ERR_print_errors_fp(stderr);
|
||||
result = false;
|
||||
} else {
|
||||
#ifdef DEBUG
|
||||
EVLOG_info << "Testing " << modestr << " provider: " << provider_name;
|
||||
#endif
|
||||
if (OSSL_PROVIDER_self_test(provider) == 0) {
|
||||
EVLOG_error << "Self-test failed: OSSL_PROVIDER: " << provider_name;
|
||||
ERR_print_errors_fp(stderr);
|
||||
OSSL_PROVIDER_unload(provider);
|
||||
provider = nullptr;
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::mutex OpenSSLProvider::s_mux;
|
||||
OpenSSLProvider::flags_underlying_t OpenSSLProvider::s_flags = 0;
|
||||
|
||||
OSSL_PROVIDER* OpenSSLProvider::s_global_prov_default_p = nullptr;
|
||||
OSSL_PROVIDER* OpenSSLProvider::s_global_prov_custom_p = nullptr;
|
||||
OSSL_PROVIDER* OpenSSLProvider::s_tls_prov_default_p = nullptr;
|
||||
OSSL_PROVIDER* OpenSSLProvider::s_tls_prov_custom_p = nullptr;
|
||||
OSSL_LIB_CTX* OpenSSLProvider::s_tls_libctx_p = nullptr;
|
||||
|
||||
// propquery strings (see CMakeLists.txt)
|
||||
static const char* s_default_provider = PROPQUERY_PROVIDER_DEFAULT;
|
||||
static const char* s_custom_provider = PROPQUERY_PROVIDER_CUSTOM;
|
||||
|
||||
OpenSSLProvider::OpenSSLProvider() {
|
||||
s_mux.lock();
|
||||
|
||||
if (is_reset(flags_t::initialised)) {
|
||||
set(flags_t::initialised);
|
||||
OPENSSL_atexit(&OpenSSLProvider::cleanup);
|
||||
|
||||
if (s_tls_libctx_p == nullptr) {
|
||||
s_tls_libctx_p = OSSL_LIB_CTX_new();
|
||||
if (s_tls_libctx_p == nullptr) {
|
||||
EVLOG_error << "Unable to create OpenSSL library context";
|
||||
ERR_print_errors_fp(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
// load providers for global context
|
||||
(void)load_global(mode_t::default_provider);
|
||||
(void)load_global(mode_t::custom_provider);
|
||||
(void)set_propstr(nullptr, mode_t::default_provider);
|
||||
|
||||
// load providers for tls context
|
||||
(void)load_tls(mode_t::default_provider);
|
||||
(void)load_tls(mode_t::custom_provider);
|
||||
(void)set_propstr(s_tls_libctx_p, mode_t::default_provider);
|
||||
}
|
||||
}
|
||||
|
||||
OpenSSLProvider::~OpenSSLProvider() {
|
||||
set_global_mode(OpenSSLProvider::mode_t::default_provider);
|
||||
s_mux.unlock();
|
||||
}
|
||||
|
||||
bool OpenSSLProvider::load(OSSL_PROVIDER*& default_p, OSSL_PROVIDER*& custom_p, OSSL_LIB_CTX* libctx_p, mode_t mode) {
|
||||
bool result = true;
|
||||
switch (mode) {
|
||||
case mode_t::custom_provider:
|
||||
if (custom_p == nullptr) {
|
||||
// custom provider string (see CMakeLists.txt)
|
||||
result = s_load_and_test_provider(custom_p, libctx_p, CUSTOM_PROVIDER_NAME);
|
||||
update(flags_t::custom_provider_available, result);
|
||||
}
|
||||
break;
|
||||
case mode_t::default_provider:
|
||||
default:
|
||||
if (default_p == nullptr) {
|
||||
result = s_load_and_test_provider(default_p, libctx_p, "default");
|
||||
}
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool OpenSSLProvider::set_propstr(OSSL_LIB_CTX* libctx, mode_t mode) {
|
||||
const char* propstr = propquery(mode);
|
||||
#ifdef DEBUG
|
||||
EVLOG_info << "Setting " << ((libctx == nullptr) ? "global" : "tls") << " propquery: " << propstr;
|
||||
#endif
|
||||
const bool result = EVP_set_default_properties(libctx, propstr) == 1;
|
||||
if (!result) {
|
||||
EVLOG_error << "Unable to set OpenSSL provider: " << mode;
|
||||
ERR_print_errors_fp(stderr);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool OpenSSLProvider::set_mode(OSSL_LIB_CTX* libctx, mode_t mode) {
|
||||
bool result;
|
||||
const flags_t f = (libctx == nullptr) ? flags_t::global_custom_provider : flags_t::tls_custom_provider;
|
||||
|
||||
const bool apply = update(f, mode == mode_t::custom_provider);
|
||||
if (apply) {
|
||||
result = set_propstr(libctx, mode);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* OpenSSLProvider::propquery(mode_t mode) const {
|
||||
const char* propquery_str = nullptr;
|
||||
|
||||
switch (mode) {
|
||||
case mode_t::default_provider:
|
||||
propquery_str = s_default_provider;
|
||||
break;
|
||||
case mode_t::custom_provider:
|
||||
propquery_str = s_custom_provider;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return propquery_str;
|
||||
}
|
||||
|
||||
void OpenSSLProvider::cleanup() {
|
||||
// at the point this is called logging may not be available
|
||||
// relying on OpenSSL errors
|
||||
std::lock_guard guard(s_mux);
|
||||
if (OSSL_PROVIDER_unload(s_tls_prov_custom_p) == 0) {
|
||||
ERR_print_errors_fp(stderr);
|
||||
}
|
||||
if (OSSL_PROVIDER_unload(s_tls_prov_default_p) == 0) {
|
||||
ERR_print_errors_fp(stderr);
|
||||
}
|
||||
if (OSSL_PROVIDER_unload(s_global_prov_custom_p) == 0) {
|
||||
ERR_print_errors_fp(stderr);
|
||||
}
|
||||
if (OSSL_PROVIDER_unload(s_global_prov_default_p) == 0) {
|
||||
ERR_print_errors_fp(stderr);
|
||||
}
|
||||
|
||||
s_tls_prov_custom_p = nullptr;
|
||||
s_tls_prov_default_p = nullptr;
|
||||
s_global_prov_custom_p = nullptr;
|
||||
s_global_prov_default_p = nullptr;
|
||||
|
||||
OSSL_LIB_CTX_free(s_tls_libctx_p);
|
||||
|
||||
s_tls_libctx_p = nullptr;
|
||||
s_flags = 0;
|
||||
}
|
||||
|
||||
bool OpenSSLProvider::supports_provider_tpm() {
|
||||
return is_set(flags_t::custom_provider_available) && is_custom_provider_tpm();
|
||||
}
|
||||
|
||||
bool OpenSSLProvider::supports_provider_custom() {
|
||||
return is_set(flags_t::custom_provider_available);
|
||||
}
|
||||
|
||||
#else // USING_OPENSSL_3_TPM
|
||||
// ----------------------------------------------------------------------------
|
||||
// class OpenSSLProvider dummy where OpenSSL 3 is not available
|
||||
|
||||
OpenSSLProvider::flags_underlying_t OpenSSLProvider::s_flags = 0;
|
||||
|
||||
OSSL_PROVIDER* OpenSSLProvider::s_global_prov_default_p = nullptr;
|
||||
OSSL_PROVIDER* OpenSSLProvider::s_global_prov_custom_p = nullptr;
|
||||
OSSL_PROVIDER* OpenSSLProvider::s_tls_prov_default_p = nullptr;
|
||||
OSSL_PROVIDER* OpenSSLProvider::s_tls_prov_custom_p = nullptr;
|
||||
OSSL_LIB_CTX* OpenSSLProvider::s_tls_libctx_p = nullptr;
|
||||
|
||||
OpenSSLProvider::OpenSSLProvider() = default;
|
||||
|
||||
OpenSSLProvider::~OpenSSLProvider() = default;
|
||||
|
||||
bool OpenSSLProvider::load(OSSL_PROVIDER*& /*unused*/, OSSL_PROVIDER*& /*unused*/, OSSL_LIB_CTX* /*unused*/,
|
||||
mode_t /*unused*/) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OpenSSLProvider::set_propstr(OSSL_LIB_CTX* /*unused*/, mode_t /*unused*/) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OpenSSLProvider::set_mode(OSSL_LIB_CTX* /*unused*/, mode_t /*unused*/) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* OpenSSLProvider::propquery(mode_t /*mode*/) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void OpenSSLProvider::cleanup() {
|
||||
}
|
||||
|
||||
bool OpenSSLProvider::supports_provider_tpm() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OpenSSLProvider::supports_provider_custom() {
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif // USING_OPENSSL_3_TPM
|
||||
|
||||
} // namespace evse_security
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <evse_security/evse_types.hpp>
|
||||
|
||||
namespace evse_security {
|
||||
|
||||
namespace conversions {
|
||||
|
||||
std::string encoding_format_to_string(EncodingFormat e) {
|
||||
switch (e) {
|
||||
case EncodingFormat::DER:
|
||||
return "DER";
|
||||
case EncodingFormat::PEM:
|
||||
return "PEM";
|
||||
default:
|
||||
throw std::out_of_range("Could not convert EncodingFormat to string");
|
||||
}
|
||||
};
|
||||
|
||||
std::string ca_certificate_type_to_string(CaCertificateType e) {
|
||||
switch (e) {
|
||||
case CaCertificateType::V2G:
|
||||
return "V2G";
|
||||
case CaCertificateType::MO:
|
||||
return "MO";
|
||||
case CaCertificateType::CSMS:
|
||||
return "CSMS";
|
||||
case CaCertificateType::MF:
|
||||
return "MF";
|
||||
default:
|
||||
throw std::out_of_range("Could not convert CaCertificateType to string");
|
||||
}
|
||||
};
|
||||
|
||||
std::string leaf_certificate_type_to_string(LeafCertificateType e) {
|
||||
switch (e) {
|
||||
case LeafCertificateType::CSMS:
|
||||
return "CSMS";
|
||||
case LeafCertificateType::V2G:
|
||||
return "V2G";
|
||||
case LeafCertificateType::MF:
|
||||
return "MF";
|
||||
case LeafCertificateType::MO:
|
||||
return "MO";
|
||||
default:
|
||||
throw std::out_of_range("Could not convert LeafCertificateType to string");
|
||||
}
|
||||
};
|
||||
|
||||
std::string leaf_certificate_type_to_filename(LeafCertificateType e) {
|
||||
switch (e) {
|
||||
case LeafCertificateType::CSMS:
|
||||
return "CSMS_LEAF_";
|
||||
case LeafCertificateType::V2G:
|
||||
return "SECC_LEAF_";
|
||||
case LeafCertificateType::MF:
|
||||
return "MF_LEAF_";
|
||||
case LeafCertificateType::MO:
|
||||
return "MO_LEAF_";
|
||||
default:
|
||||
throw std::out_of_range("Could not convert LeafCertificateType to string");
|
||||
}
|
||||
}
|
||||
|
||||
std::string certificate_type_to_string(CertificateType e) {
|
||||
switch (e) {
|
||||
case CertificateType::V2GRootCertificate:
|
||||
return "V2GRootCertificate";
|
||||
case CertificateType::MORootCertificate:
|
||||
return "MORootCertificate";
|
||||
case CertificateType::CSMSRootCertificate:
|
||||
return "CSMSRootCertificate";
|
||||
case CertificateType::V2GCertificateChain:
|
||||
return "V2GCertificateChain";
|
||||
case CertificateType::MFRootCertificate:
|
||||
return "MFRootCertificate";
|
||||
default:
|
||||
throw std::out_of_range("Could not convert CertificateType to string");
|
||||
}
|
||||
};
|
||||
|
||||
std::string hash_algorithm_to_string(HashAlgorithm e) {
|
||||
switch (e) {
|
||||
case HashAlgorithm::SHA256:
|
||||
return "SHA256";
|
||||
case HashAlgorithm::SHA384:
|
||||
return "SHA384";
|
||||
case HashAlgorithm::SHA512:
|
||||
return "SHA512";
|
||||
default:
|
||||
throw std::out_of_range("Could not convert HashAlgorithm to string");
|
||||
}
|
||||
};
|
||||
|
||||
HashAlgorithm string_to_hash_algorithm(const std::string& s) {
|
||||
if (s == "SHA256") {
|
||||
return HashAlgorithm::SHA256;
|
||||
}
|
||||
if (s == "SHA384") {
|
||||
return HashAlgorithm::SHA384;
|
||||
}
|
||||
if (s == "SHA512") {
|
||||
return HashAlgorithm::SHA512;
|
||||
}
|
||||
|
||||
throw std::out_of_range("Could not convert string to HashAlgorithm");
|
||||
}
|
||||
|
||||
std::string install_certificate_result_to_string(InstallCertificateResult e) {
|
||||
switch (e) {
|
||||
case InstallCertificateResult::InvalidSignature:
|
||||
return "InvalidSignature";
|
||||
case InstallCertificateResult::InvalidCertificateChain:
|
||||
return "InvalidCertificateChain";
|
||||
case InstallCertificateResult::InvalidFormat:
|
||||
return "InvalidFormat";
|
||||
case InstallCertificateResult::InvalidCommonName:
|
||||
return "InvalidCommonName";
|
||||
case InstallCertificateResult::NoRootCertificateInstalled:
|
||||
return "NoRootCertificateInstalled";
|
||||
case InstallCertificateResult::Expired:
|
||||
return "Expired";
|
||||
case InstallCertificateResult::CertificateStoreMaxLengthExceeded:
|
||||
return "CertificateStoreMaxLengthExceeded";
|
||||
case InstallCertificateResult::WriteError:
|
||||
return "WriteError";
|
||||
case InstallCertificateResult::Accepted:
|
||||
return "Accepted";
|
||||
default:
|
||||
throw std::out_of_range("Could not convert InstallCertificateResult to string");
|
||||
}
|
||||
};
|
||||
|
||||
std::string delete_certificate_result_to_string(DeleteCertificateResult e) {
|
||||
switch (e) {
|
||||
case DeleteCertificateResult::Accepted:
|
||||
return "Accepted";
|
||||
case DeleteCertificateResult::Failed:
|
||||
return "Failed";
|
||||
case DeleteCertificateResult::NotFound:
|
||||
return "NotFound";
|
||||
default:
|
||||
throw std::out_of_range("Could not convert DeleteCertificateResult to string");
|
||||
}
|
||||
};
|
||||
|
||||
std::string get_installed_certificates_status_to_string(GetInstalledCertificatesStatus e) {
|
||||
switch (e) {
|
||||
case GetInstalledCertificatesStatus::Accepted:
|
||||
return "Accepted";
|
||||
case GetInstalledCertificatesStatus::NotFound:
|
||||
return "NotFound";
|
||||
default:
|
||||
throw std::out_of_range("Could not convert GetInstalledCertificatesStatus to string");
|
||||
}
|
||||
};
|
||||
|
||||
namespace {
|
||||
std::string get_key_pair_status_to_string(GetCertificateInfoStatus e) {
|
||||
switch (e) {
|
||||
case GetCertificateInfoStatus::Accepted:
|
||||
return "Accepted";
|
||||
case GetCertificateInfoStatus::Rejected:
|
||||
return "Rejected";
|
||||
case GetCertificateInfoStatus::NotFound:
|
||||
return "NotFound";
|
||||
case GetCertificateInfoStatus::NotFoundValid:
|
||||
return "NotFoundValid";
|
||||
case GetCertificateInfoStatus::PrivateKeyNotFound:
|
||||
return "PrivateKeyNotFound";
|
||||
default:
|
||||
throw std::out_of_range("Could not convert GetCertificateInfoStatus to string");
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
} // namespace conversions
|
||||
|
||||
} // namespace evse_security
|
||||
@@ -0,0 +1,216 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2023 Pionix GmbH and Contributors to EVerest
|
||||
#include <evse_security/evse_types.hpp>
|
||||
#include <evse_security/utils/evse_filesystem.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
namespace evse_security::filesystem_utils {
|
||||
|
||||
bool is_subdirectory(const fs::path& base, const fs::path& subdir) {
|
||||
const fs::path relativePath = fs::relative(subdir, base);
|
||||
return !relativePath.empty();
|
||||
}
|
||||
|
||||
bool delete_file(const fs::path& file_path) {
|
||||
try {
|
||||
if (fs::is_regular_file(file_path)) {
|
||||
return fs::remove(file_path);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "Filesystem error: " << e.what();
|
||||
}
|
||||
|
||||
EVLOG_error << "Error deleting file: " << file_path;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool read_from_file(const fs::path& file_path, std::string& out_data) {
|
||||
try {
|
||||
if (fs::is_regular_file(file_path)) {
|
||||
fsstd::ifstream file(file_path, std::ios::binary);
|
||||
|
||||
if (file.is_open()) {
|
||||
out_data = std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "Error while reading from file: " << e.what();
|
||||
}
|
||||
|
||||
EVLOG_error << "Error reading file: " << file_path;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool create_file_if_nonexistent(const fs::path& file_path) {
|
||||
if (file_path.empty()) {
|
||||
EVLOG_warning << "Provided empty path!";
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fs::exists(file_path)) {
|
||||
const fsstd::ofstream file(file_path);
|
||||
return true;
|
||||
}
|
||||
if (fs::is_directory(file_path)) {
|
||||
EVLOG_error << "Attempting to create file over existing directory: " << file_path;
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "Error while creating file: " << e.what();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool create_file_or_dir_if_nonexistent(const fs::path& path) {
|
||||
if (path.empty()) {
|
||||
EVLOG_warning << "Provided empty path!";
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// In case the path is missing, create it
|
||||
if (fs::exists(path) == false) {
|
||||
if (path.has_extension()) {
|
||||
std::ofstream new_file(path.c_str());
|
||||
new_file.close();
|
||||
return true;
|
||||
} // Else create a directory
|
||||
fs::create_directories(path);
|
||||
return true;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "Error while creating dir/file: " << e.what();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool write_to_file(const fs::path& file_path, const std::string& data, std::ios::openmode mode) {
|
||||
try {
|
||||
fsstd::ofstream fs(file_path, mode | std::ios::binary);
|
||||
if (!fs.is_open()) {
|
||||
EVLOG_error << "Error opening file: " << file_path;
|
||||
return false;
|
||||
}
|
||||
fs.write(data.c_str(), static_cast<std::ptrdiff_t>(data.size()));
|
||||
|
||||
if (!fs) {
|
||||
EVLOG_error << "Error writing to file: " << file_path;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "Unknown error occurred while writing to file: " << file_path;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool process_file(const fs::path& file_path, size_t buffer_size,
|
||||
std::function<bool(const std::uint8_t*, std::size_t, bool last_chunk)>&& func) {
|
||||
fsstd::ifstream file(file_path, std::ios::binary);
|
||||
|
||||
if (!file) {
|
||||
EVLOG_error << "Error opening file: " << file_path;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> buffer(buffer_size);
|
||||
bool interupted = false;
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): needed for API usage
|
||||
while (file.read(reinterpret_cast<char*>(buffer.data()), static_cast<std::ptrdiff_t>(buffer_size))) {
|
||||
interupted = func(buffer.data(), buffer_size, false);
|
||||
|
||||
if (interupted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the remaining bytes
|
||||
if (interupted == false) {
|
||||
const size_t remaining = file.gcount();
|
||||
func(buffer.data(), remaining, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string get_random_file_name(const std::string& extension) {
|
||||
static std::random_device rd;
|
||||
static std::mt19937 generator(rd());
|
||||
static std::uniform_int_distribution<int> distribution(1, std::numeric_limits<int>::max());
|
||||
|
||||
static int increment = 0;
|
||||
|
||||
std::ostringstream buff;
|
||||
|
||||
auto now = std::chrono::system_clock::now();
|
||||
const std::time_t time = std::chrono::system_clock::to_time_t(now);
|
||||
buff << std::put_time(std::gmtime(&time), "M%m_D%d_Y%Y_H%H_M%M_S%S_") << "i" << std::to_string(++increment) << "_r"
|
||||
<< distribution(generator) << extension;
|
||||
|
||||
return buff.str();
|
||||
}
|
||||
|
||||
bool read_hash_from_file(const fs::path& file_path, CertificateHashData& out_hash) {
|
||||
if (file_path.extension() == CERT_HASH_EXTENSION) {
|
||||
try {
|
||||
fsstd::ifstream hs(file_path);
|
||||
std::string algo;
|
||||
|
||||
hs >> algo;
|
||||
hs >> out_hash.issuer_name_hash;
|
||||
hs >> out_hash.issuer_key_hash;
|
||||
hs >> out_hash.serial_number;
|
||||
hs.close();
|
||||
|
||||
out_hash.hash_algorithm = conversions::string_to_hash_algorithm(algo);
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "Unknown error occurred while reading cert hash file: " << file_path << " err: " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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 << conversions::hash_algorithm_to_string(hash.hash_algorithm) << "\n";
|
||||
hs << hash.issuer_name_hash << "\n";
|
||||
hs << hash.issuer_key_hash << "\n";
|
||||
hs << hash.serial_number << "\n";
|
||||
hs.close();
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "Unknown error occurred writing cert hash file: " << file_path << " err: " << e.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace evse_security::filesystem_utils
|
||||
@@ -0,0 +1,90 @@
|
||||
set(TEST_TARGET_NAME ${PROJECT_NAME}_evse_security_tests)
|
||||
add_executable(${TEST_TARGET_NAME})
|
||||
|
||||
target_sources(${TEST_TARGET_NAME} PRIVATE
|
||||
tests.cpp
|
||||
openssl_supplier_test.cpp
|
||||
)
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
|
||||
evse_security
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
if(USING_TPM2)
|
||||
target_sources(${TEST_TARGET_NAME} PRIVATE
|
||||
openssl_supplier_test_tpm.cpp
|
||||
)
|
||||
target_compile_definitions(${TEST_TARGET_NAME} PRIVATE
|
||||
USING_TPM2
|
||||
PROPQUERY_DEFAULT="${PROPQUERY_DEFAULT}"
|
||||
PROPQUERY_TPM2="${PROPQUERY_TPM2}"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(LIBEVSE_CRYPTO_SUPPLIER_OPENSSL)
|
||||
add_compile_definitions(LIBEVSE_CRYPTO_SUPPLIER_OPENSSL)
|
||||
endif()
|
||||
|
||||
add_compile_definitions(BUILD_TESTING_EVSE_SECURITY)
|
||||
add_compile_definitions(DEBUG_MODE_EVSE_SECURITY)
|
||||
|
||||
set(LIBEVSE_SECURITY_TEST_DIR "${CMAKE_BINARY_DIR}")
|
||||
if (EVEREST_CORE_BUILD_TESTING)
|
||||
set(LIBEVSE_SECURITY_TEST_DIR "${CMAKE_BINARY_DIR}/lib/everest/evse_security")
|
||||
endif()
|
||||
|
||||
add_test(
|
||||
NAME ${TEST_TARGET_NAME}
|
||||
COMMAND ${TEST_TARGET_NAME}
|
||||
WORKING_DIRECTORY "${LIBEVSE_SECURITY_TEST_DIR}/tests"
|
||||
)
|
||||
|
||||
file(COPY
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/generate_test_certs.sh"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/generate_test_certs_root_multi.sh"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/generate_test_certs_leaf_multi.sh"
|
||||
DESTINATION "${LIBEVSE_SECURITY_TEST_DIR}/tests"
|
||||
)
|
||||
|
||||
file(COPY
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/configs"
|
||||
DESTINATION "${LIBEVSE_SECURITY_TEST_DIR}/tests"
|
||||
FILES_MATCHING PATTERN "*.cnf"
|
||||
)
|
||||
|
||||
file(COPY
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/future_leaf"
|
||||
DESTINATION "${LIBEVSE_SECURITY_TEST_DIR}/tests"
|
||||
FILES_MATCHING PATTERN "*"
|
||||
)
|
||||
|
||||
file(COPY
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/csms_certs"
|
||||
DESTINATION "${LIBEVSE_SECURITY_TEST_DIR}/tests"
|
||||
FILES_MATCHING PATTERN "*"
|
||||
)
|
||||
|
||||
file(COPY
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/expired_leaf"
|
||||
DESTINATION "${LIBEVSE_SECURITY_TEST_DIR}/tests"
|
||||
FILES_MATCHING PATTERN "*"
|
||||
)
|
||||
|
||||
file(COPY
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/expired_runtime"
|
||||
DESTINATION "${LIBEVSE_SECURITY_TEST_DIR}/tests"
|
||||
FILES_MATCHING PATTERN "*"
|
||||
)
|
||||
|
||||
file(COPY
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/create-pki.sh"
|
||||
DESTINATION "${LIBEVSE_SECURITY_TEST_DIR}/tests"
|
||||
)
|
||||
|
||||
file(COPY
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/openssl-pki.conf"
|
||||
DESTINATION "${LIBEVSE_SECURITY_TEST_DIR}/tests"
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = MOCertLeaf
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = MO
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:false
|
||||
keyUsage = critical,digitalSignature,keyAgreement
|
||||
subjectKeyIdentifier = hash
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = MOCertLeaf_V2G
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = MO
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:false
|
||||
keyUsage = critical,digitalSignature,keyAgreement
|
||||
subjectKeyIdentifier = hash
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = MORootCA
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = MO
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:true
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = MOSubCA1
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = MO
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:true,pathlen:1
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Intermediate-CA.cer
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = MOSubCA2
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = MO
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:true,pathlen:0
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Intermediate-CA.cer
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = CPOSubCA1
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = V2G
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:true,pathlen:1
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Intermediate-CA.cer
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = CPOSubCA2
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = V2G
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:true,pathlen:0
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Intermediate-CA.cer
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = InstallTestCA
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = V2G
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:true
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = InstallTestSubCA1
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = V2G
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:true
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = InstallTestSubCA2
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = V2G
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:true
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = SECCCert
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = CPO
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:false
|
||||
keyUsage = critical,digitalSignature,keyAgreement
|
||||
subjectKeyIdentifier = hash
|
||||
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Leaf-CA.cer
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = SECCGridSyncCert
|
||||
organizationName = GridSync
|
||||
countryName = DE
|
||||
domainComponent = CPO
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:false
|
||||
keyUsage = critical,digitalSignature,keyAgreement
|
||||
subjectKeyIdentifier = hash
|
||||
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Leaf-CA.cer
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = V2GRootCA
|
||||
organizationName = EVerest
|
||||
countryName = DE
|
||||
domainComponent = V2G
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:true
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[req]
|
||||
prompt = no
|
||||
distinguished_name = ca_dn
|
||||
|
||||
[ca_dn]
|
||||
commonName = V2GRootGridSyncCA
|
||||
organizationName = GridSync
|
||||
countryName = DE
|
||||
domainComponent = V2G
|
||||
|
||||
[ext]
|
||||
basicConstraints = critical,CA:true
|
||||
keyUsage = critical,keyCertSign,cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
|
||||
67
tools/EVerest-main/lib/everest/evse_security/tests/create-pki.sh
Executable file
67
tools/EVerest-main/lib/everest/evse_security/tests/create-pki.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/bin/sh
|
||||
|
||||
base=.
|
||||
cfg=./openssl-pki.conf
|
||||
tpm=$1
|
||||
|
||||
if [ -z "$tpm" ]; then
|
||||
dir=pki
|
||||
else
|
||||
dir=tpm_pki
|
||||
fi
|
||||
|
||||
[ ! -f "$cfg" ] && echo "missing openssl-pki.conf" && exit 1
|
||||
|
||||
generate() {
|
||||
local base=$1
|
||||
local dir=$2
|
||||
mkdir -p ${base}/${dir}
|
||||
|
||||
local root_priv=${base}/${dir}/root_priv.pem
|
||||
local ca_priv=${base}/${dir}/ca_priv.pem
|
||||
local server_priv=${base}/${dir}/server_priv.pem
|
||||
|
||||
local root_cert=${base}/${dir}/root_cert.pem
|
||||
local ca_cert=${base}/${dir}/ca_cert.pem
|
||||
local server_cert=${base}/${dir}/server_cert.pem
|
||||
local cert_path=${base}/${dir}/cert_path.pem
|
||||
|
||||
local tpmA tpmB
|
||||
local propA propB
|
||||
if [ -n "$3" ]; then
|
||||
tpmA="-provider"
|
||||
tpmB="tpm2"
|
||||
propA="-propquery"
|
||||
propB="?provider=tpm2"
|
||||
fi
|
||||
|
||||
# generate keys
|
||||
for i in ${root_priv} ${ca_priv} ${server_priv}
|
||||
do
|
||||
openssl genpkey -config ${cfg} ${tpmA} ${tpmB} ${propA} ${propB} -algorithm RSA -pkeyopt bits:2048 -out $i
|
||||
done
|
||||
|
||||
export OPENSSL_CONF=${cfg}
|
||||
# generate root cert
|
||||
echo "Generate root"
|
||||
openssl req ${tpmA} ${tpmB} -provider default ${propA} ${propB} \
|
||||
-config ${cfg} -x509 -section req_root -extensions v3_root \
|
||||
-key ${root_priv} -out ${root_cert}
|
||||
# generate ca cert
|
||||
echo "Generate ca"
|
||||
openssl req ${tpmA} ${tpmB} -provider default ${propA} ${propB} \
|
||||
-config ${cfg} -x509 -section req_ca -extensions v3_ca \
|
||||
-key ${ca_priv} -CA ${root_cert} \
|
||||
-CAkey ${root_priv} -out ${ca_cert}
|
||||
# generate server cert
|
||||
echo "Generate server"
|
||||
openssl req ${tpmA} ${tpmB} -provider default ${propA} ${propB} \
|
||||
-config ${cfg} -x509 -section req_server -extensions v3_server \
|
||||
-key ${server_priv} -CA ${ca_cert} \
|
||||
-CAkey ${ca_priv} -out ${server_cert}
|
||||
|
||||
# create bundle
|
||||
cat ${server_cert} ${ca_cert} > ${cert_path}
|
||||
}
|
||||
|
||||
generate $base $dir $tpm
|
||||
@@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIJ9RbIOPOVCNRhrcq6Fw/3qWw6J00lF/yT7FdrSXCuhzoAoGCCqGSM49
|
||||
AwEHoUQDQgAEQplOIWUtl6KOnRhM9OQRu7TawKd0SAExZwztsJChemlIXEJ9D5dc
|
||||
K0/+rKjpTgHoDg9LdluA+tv9nmeeyiX8pQ==
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBkDCCATWgAwIBAgIUUhx2j1hK10LEIz7YWfqxrImxoRkwCgYIKoZIzj0EAwIw
|
||||
HDEaMBgGA1UEAwwRVjJHUm9vdENBX1BYX0NTTVMwIBcNMjUwNjEyMTI1OTIzWhgP
|
||||
MjA1MjEwMjgxMjU5MjNaMBwxGjAYBgNVBAMMEVYyR1Jvb3RDQV9QWF9DU01TMFkw
|
||||
EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQplOIWUtl6KOnRhM9OQRu7TawKd0SAEx
|
||||
ZwztsJChemlIXEJ9D5dcK0/+rKjpTgHoDg9LdluA+tv9nmeeyiX8paNTMFEwHQYD
|
||||
VR0OBBYEFNuKFuy+RkEgJd1HDGiEHLMb4AkkMB8GA1UdIwQYMBaAFNuKFuy+RkEg
|
||||
Jd1HDGiEHLMb4AkkMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIh
|
||||
AKAp7QkWQAGVld3ZNN6g9uJrk0w0QweSMNQrr7T4+qarAiEAt33b6cX+o8JrVkOu
|
||||
uglyjLACI4LdKyETkSotKAF/Pqw=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,42 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICJTCCAcqgAwIBAgIUKg1NxeDzrOBL1u7mC0CNQ9+qDNgwCgYIKoZIzj0EAwIw
|
||||
HzEdMBsGA1UEAwwUQ1BPIFN1YiBOZXh0IENBIDIgVjEwHhcNMjUwNjEyMTMzNDAx
|
||||
WhcNNDQwODExMTMzNDAxWjAYMRYwFAYDVQQDDA1ERVBpb25peExlYWZBMFkwEwYH
|
||||
KoZIzj0CAQYIKoZIzj0DAQcDQgAEEvKLd+Kd3aPOhE7LFHRQYTYQdR63u5UdtUcm
|
||||
E443vsTPPIRpF+8664YxEBtcVyPjLDtcX8JfpzTySJpxudvmjKOB6jCB5zAfBgNV
|
||||
HSMEGDAWgBR2E1GkzArGFjN/mYT0p+hlnPNY9zAPBgNVHRMECDAGAQH/AgEBMA4G
|
||||
A1UdDwEB/wQEAwIBBjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwZQYI
|
||||
KwYBBQUHAQEEWTBXMCQGCCsGAQUFBzABhhhodHRwczovL3d3dy5leGFtcGxlLmNv
|
||||
bS8wLwYIKwYBBQUHMAKGI2h0dHBzOi8vd3d3LmV4YW1wbGUuY29tL0xlYWYtQ0Eu
|
||||
Y2VyMB0GA1UdDgQWBBS69kEneMseD8T1X3U5sBt2wx/eIjAKBggqhkjOPQQDAgNJ
|
||||
ADBGAiEAqQD6cffwNrvS0RvY/LkL5aTzPp6iJGDQ3by3qqzd/m8CIQCvKsGoyB8U
|
||||
MpVqnqoOu+kCNhf07XcEYSawtL5VYCkwRA==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICJTCCAcugAwIBAgIUJWEONn4Mv4A9h06SwukrdjXTFL0wCgYIKoZIzj0EAwIw
|
||||
GTEXMBUGA1UEAwwOQ1BPIFN1YiAyIENBIDEwHhcNMjUwNjEyMTMzMTA4WhcNNDQw
|
||||
ODExMTMzMTA4WjAfMR0wGwYDVQQDDBRDUE8gU3ViIE5leHQgQ0EgMiBWMTBZMBMG
|
||||
ByqGSM49AgEGCCqGSM49AwEHA0IABLbrtvRnzyKourVxRIuVXj8NbTf3AWZuK7Q4
|
||||
qhWb1M0cTSnW8FM2SdExffFdoiEHH1J5LvJ+VCesoq1b9FjEBcejgeowgecwHwYD
|
||||
VR0jBBgwFoAUixCrftMh4UK67fiP0R8DnAb1020wDwYDVR0TBAgwBgEB/wIBATAO
|
||||
BgNVHQ8BAf8EBAMCAQYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGUG
|
||||
CCsGAQUFBwEBBFkwVzAkBggrBgEFBQcwAYYYaHR0cHM6Ly93d3cuZXhhbXBsZS5j
|
||||
b20vMC8GCCsGAQUFBzAChiNodHRwczovL3d3dy5leGFtcGxlLmNvbS9MZWFmLUNB
|
||||
LmNlcjAdBgNVHQ4EFgQUdhNRpMwKxhYzf5mE9KfoZZzzWPcwCgYIKoZIzj0EAwID
|
||||
SAAwRQIgKJ3mzMjDScrH8V6lWA00gLXl2QTtIO4ahksUBCQp0TICIQCDWIN16mdI
|
||||
qs6JAH1uuXay9UVWZovEmzzR1wdeMtu/7A==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICITCCAcigAwIBAgIUJlspPfMG7ECCjCO0ma/8vbVKuL8wCgYIKoZIzj0EAwIw
|
||||
HDEaMBgGA1UEAwwRVjJHUm9vdENBX1BYX0NTTVMwHhcNMjUwNjEyMTMyMDMyWhcN
|
||||
NDQwODExMTMyMDMyWjAZMRcwFQYDVQQDDA5DUE8gU3ViIDIgQ0EgMTBZMBMGByqG
|
||||
SM49AgEGCCqGSM49AwEHA0IABFDiKpE0Z1AUKYewLLtFHwtEwy7xDsVptoSkSMzi
|
||||
xYTngcu8DHjOt0rQIvw951SOM7vdrOnrkzICXs60LMTn6L6jgeowgecwHwYDVR0j
|
||||
BBgwFoAU24oW7L5GQSAl3UcMaIQcsxvgCSQwDwYDVR0TBAgwBgEB/wIBATAOBgNV
|
||||
HQ8BAf8EBAMCAQYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGUGCCsG
|
||||
AQUFBwEBBFkwVzAkBggrBgEFBQcwAYYYaHR0cHM6Ly93d3cuZXhhbXBsZS5jb20v
|
||||
MC8GCCsGAQUFBzAChiNodHRwczovL3d3dy5leGFtcGxlLmNvbS9MZWFmLUNBLmNl
|
||||
cjAdBgNVHQ4EFgQUixCrftMh4UK67fiP0R8DnAb1020wCgYIKoZIzj0EAwIDRwAw
|
||||
RAIgS2BRfHq2gTeqZWgnp1Onc65klD3SbOoOk+cb1hI6mYICIFGNkSkGN3D/gH95
|
||||
p0vw0eFHBFE+mHP1ubcMvVYDu8AR
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,42 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICJDCCAcqgAwIBAgIUTtaY1jxyTGphoMzE/4Czgxkt/XowCgYIKoZIzj0EAwIw
|
||||
HzEdMBsGA1UEAwwUQ1BPIFN1YiBOZXh0IENBIDIgVjIwHhcNMjUwNjEyMTMzNDEz
|
||||
WhcNNDQwODExMTMzNDEzWjAYMRYwFAYDVQQDDA1ERVBpb25peExlYWZCMFkwEwYH
|
||||
KoZIzj0CAQYIKoZIzj0DAQcDQgAEieno+61njtJx4QY7j6M8eAelAR5AwFLrnP2h
|
||||
G5dGX7EYWsouYp7R6SKuuGxtTIR7w5VU+mnHiSd+wItjJA6sXaOB6jCB5zAfBgNV
|
||||
HSMEGDAWgBS3pelS2jYhYHpxd4AS3pXF049VYzAPBgNVHRMECDAGAQH/AgEBMA4G
|
||||
A1UdDwEB/wQEAwIBBjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwZQYI
|
||||
KwYBBQUHAQEEWTBXMCQGCCsGAQUFBzABhhhodHRwczovL3d3dy5leGFtcGxlLmNv
|
||||
bS8wLwYIKwYBBQUHMAKGI2h0dHBzOi8vd3d3LmV4YW1wbGUuY29tL0xlYWYtQ0Eu
|
||||
Y2VyMB0GA1UdDgQWBBSUSqYHsin+1h5Acqw18D7rrhQkITAKBggqhkjOPQQDAgNI
|
||||
ADBFAiAGRhek1Z2JUU/vuQa9VHFeJ9leP3DVDfIjQkYibIyP/wIhAO/BUNBWwIY0
|
||||
wj1mIDddUxh4MlEMIjT3vsRRk5OrqBKz
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICJTCCAcugAwIBAgIUKQ+ojAkrsU7LBTYLb7uZ5zjHT5cwCgYIKoZIzj0EAwIw
|
||||
GTEXMBUGA1UEAwwOQ1BPIFN1YiAyIENBIDEwHhcNMjUwNjEyMTMzMTI2WhcNNDQw
|
||||
ODExMTMzMTI2WjAfMR0wGwYDVQQDDBRDUE8gU3ViIE5leHQgQ0EgMiBWMjBZMBMG
|
||||
ByqGSM49AgEGCCqGSM49AwEHA0IABKu/v1xCrE6QZf+VGTjfi7aH8gsMs0T3RDVk
|
||||
ZEIQk/cL6EsTE8irqg8Jw2wzPPMBSi7gRJPqbnbFtaT1Lx571NejgeowgecwHwYD
|
||||
VR0jBBgwFoAUFqJIf6+UCGHOPxco7gu0ABOZmiQwDwYDVR0TBAgwBgEB/wIBATAO
|
||||
BgNVHQ8BAf8EBAMCAQYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGUG
|
||||
CCsGAQUFBwEBBFkwVzAkBggrBgEFBQcwAYYYaHR0cHM6Ly93d3cuZXhhbXBsZS5j
|
||||
b20vMC8GCCsGAQUFBzAChiNodHRwczovL3d3dy5leGFtcGxlLmNvbS9MZWFmLUNB
|
||||
LmNlcjAdBgNVHQ4EFgQUt6XpUto2IWB6cXeAEt6VxdOPVWMwCgYIKoZIzj0EAwID
|
||||
SAAwRQIhAMlqhQthKokgDSHaFw+bG1JTcxggSl8/pMoxVmMlmQZbAiBWMe+14M9L
|
||||
OIvJlR8ru9umEcCihOY5ijueKT47vpIQfw==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICIjCCAcigAwIBAgIUJlspPfMG7ECCjCO0ma/8vbVKuMAwCgYIKoZIzj0EAwIw
|
||||
HDEaMBgGA1UEAwwRVjJHUm9vdENBX1BYX0NTTVMwHhcNMjUwNjEyMTMyMDUyWhcN
|
||||
NDQwODExMTMyMDUyWjAZMRcwFQYDVQQDDA5DUE8gU3ViIDIgQ0EgMTBZMBMGByqG
|
||||
SM49AgEGCCqGSM49AwEHA0IABPOaA6ir8KL2FHGhjMVzNnc6RzkefDX2a59GqDM4
|
||||
HSJmWTbFhNdaNUgLeszINiFo4JdDiHX4Yi84sDfUci0O9p+jgeowgecwHwYDVR0j
|
||||
BBgwFoAU24oW7L5GQSAl3UcMaIQcsxvgCSQwDwYDVR0TBAgwBgEB/wIBATAOBgNV
|
||||
HQ8BAf8EBAMCAQYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGUGCCsG
|
||||
AQUFBwEBBFkwVzAkBggrBgEFBQcwAYYYaHR0cHM6Ly93d3cuZXhhbXBsZS5jb20v
|
||||
MC8GCCsGAQUFBzAChiNodHRwczovL3d3dy5leGFtcGxlLmNvbS9MZWFmLUNBLmNl
|
||||
cjAdBgNVHQ4EFgQUFqJIf6+UCGHOPxco7gu0ABOZmiQwCgYIKoZIzj0EAwIDSAAw
|
||||
RQIgZZkmdAu7vY6hgfFFVkPwnF/7uKr7sBd18R7ZfucVrDYCIQCN8ieOUwHZbisC
|
||||
Q5F0bmA+Atvba6GW+Gu9K5sfACdEMQ==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIB71Owd1hTS6T9kNtK2pgbfL/bCuEpM+3aHMquGhJJejoAoGCCqGSM49
|
||||
AwEHoUQDQgAEEvKLd+Kd3aPOhE7LFHRQYTYQdR63u5UdtUcmE443vsTPPIRpF+86
|
||||
64YxEBtcVyPjLDtcX8JfpzTySJpxudvmjA==
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,14 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICJTCCAcqgAwIBAgIUKg1NxeDzrOBL1u7mC0CNQ9+qDNgwCgYIKoZIzj0EAwIw
|
||||
HzEdMBsGA1UEAwwUQ1BPIFN1YiBOZXh0IENBIDIgVjEwHhcNMjUwNjEyMTMzNDAx
|
||||
WhcNNDQwODExMTMzNDAxWjAYMRYwFAYDVQQDDA1ERVBpb25peExlYWZBMFkwEwYH
|
||||
KoZIzj0CAQYIKoZIzj0DAQcDQgAEEvKLd+Kd3aPOhE7LFHRQYTYQdR63u5UdtUcm
|
||||
E443vsTPPIRpF+8664YxEBtcVyPjLDtcX8JfpzTySJpxudvmjKOB6jCB5zAfBgNV
|
||||
HSMEGDAWgBR2E1GkzArGFjN/mYT0p+hlnPNY9zAPBgNVHRMECDAGAQH/AgEBMA4G
|
||||
A1UdDwEB/wQEAwIBBjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwZQYI
|
||||
KwYBBQUHAQEEWTBXMCQGCCsGAQUFBzABhhhodHRwczovL3d3dy5leGFtcGxlLmNv
|
||||
bS8wLwYIKwYBBQUHMAKGI2h0dHBzOi8vd3d3LmV4YW1wbGUuY29tL0xlYWYtQ0Eu
|
||||
Y2VyMB0GA1UdDgQWBBS69kEneMseD8T1X3U5sBt2wx/eIjAKBggqhkjOPQQDAgNJ
|
||||
ADBGAiEAqQD6cffwNrvS0RvY/LkL5aTzPp6iJGDQ3by3qqzd/m8CIQCvKsGoyB8U
|
||||
MpVqnqoOu+kCNhf07XcEYSawtL5VYCkwRA==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIH13t8Z/SV8kObot1c15YsTp/OZgbAw8r6Ns9DzHl2WAoAoGCCqGSM49
|
||||
AwEHoUQDQgAEieno+61njtJx4QY7j6M8eAelAR5AwFLrnP2hG5dGX7EYWsouYp7R
|
||||
6SKuuGxtTIR7w5VU+mnHiSd+wItjJA6sXQ==
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,14 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICJDCCAcqgAwIBAgIUTtaY1jxyTGphoMzE/4Czgxkt/XowCgYIKoZIzj0EAwIw
|
||||
HzEdMBsGA1UEAwwUQ1BPIFN1YiBOZXh0IENBIDIgVjIwHhcNMjUwNjEyMTMzNDEz
|
||||
WhcNNDQwODExMTMzNDEzWjAYMRYwFAYDVQQDDA1ERVBpb25peExlYWZCMFkwEwYH
|
||||
KoZIzj0CAQYIKoZIzj0DAQcDQgAEieno+61njtJx4QY7j6M8eAelAR5AwFLrnP2h
|
||||
G5dGX7EYWsouYp7R6SKuuGxtTIR7w5VU+mnHiSd+wItjJA6sXaOB6jCB5zAfBgNV
|
||||
HSMEGDAWgBS3pelS2jYhYHpxd4AS3pXF049VYzAPBgNVHRMECDAGAQH/AgEBMA4G
|
||||
A1UdDwEB/wQEAwIBBjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwZQYI
|
||||
KwYBBQUHAQEEWTBXMCQGCCsGAQUFBzABhhhodHRwczovL3d3dy5leGFtcGxlLmNv
|
||||
bS8wLwYIKwYBBQUHMAKGI2h0dHBzOi8vd3d3LmV4YW1wbGUuY29tL0xlYWYtQ0Eu
|
||||
Y2VyMB0GA1UdDgQWBBSUSqYHsin+1h5Acqw18D7rrhQkITAKBggqhkjOPQQDAgNI
|
||||
ADBFAiAGRhek1Z2JUU/vuQa9VHFeJ9leP3DVDfIjQkYibIyP/wIhAO/BUNBWwIY0
|
||||
wj1mIDddUxh4MlEMIjT3vsRRk5OrqBKz
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1 @@
|
||||
OCSP_CUSTOM_DATA
|
||||
@@ -0,0 +1,4 @@
|
||||
SHA256
|
||||
82addb4b47026c702b9ed9d482c6e3570bbae9c49b963ec18b0a3523dfb47fe3
|
||||
e9d2a6d245233edbf5a8319b99087313e16307ca29b388373d951b50e93090aa
|
||||
4ed698d63c724c6a61a0ccc4ff80b383192dfd7a
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBAjCBqQIBADBHMREwDwYDVQQDDAhTRUNDQ2VydDEQMA4GA1UECgwHRVZlcmVz
|
||||
dDELMAkGA1UEBhMCREUxEzARBgoJkiaJk/IsZAEZFgNDUE8wWTATBgcqhkjOPQIB
|
||||
BggqhkjOPQMBBwNCAARlxNKadJ0NCSFMfvNd5Y+vExLPqq4q9WsweCR7hnENyAa3
|
||||
VJ6JFkgtm93GIS2ebML/QR3VFWWxCO3+bAK6MswUoAAwCgYIKoZIzj0EAwIDSAAw
|
||||
RQIhAOWltS/gdYqIYndktWPtUdLypfTu59kMNkBOYCgkxq8GAiBW1EG1OeZ56iAB
|
||||
vnu/GEDA0hBBVTV/4SmJB4dKu6gfEQ==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-128-CBC,AB729A472F0970C8348076555BB2FAB2
|
||||
|
||||
A56CcGtbBVUM5NZaG3pxRQBzPX7U6tuk/uraLP8q5ElHGslg1bBKwDNtQqFs1b0H
|
||||
G3Qw2DhlIx1LOIXnNalMlEvWwyMpRqjOPsyxjwwjPcUCp9Bxd6w3KYWuVcXN3SuD
|
||||
TARrzp8XoapdNbk2Eb8JPduYOcs+U5j9KySZfcWfS2E=
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,13 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB4DCCAYagAwIBAgIDB6EgMAoGCCqGSM49BAMCMEgxEjAQBgNVBAMMCVYyR1Jv
|
||||
b3RDQTEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUxEzARBgoJkiaJk/Is
|
||||
ZAEZFgNWMkcwHhcNNzMxMTIzMTI0NTEzWhcNNzQxMTIzMTI0NTEzWjBHMREwDwYD
|
||||
VQQDDAhTRUNDQ2VydDEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUxEzAR
|
||||
BgoJkiaJk/IsZAEZFgNDUE8wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARlxNKa
|
||||
dJ0NCSFMfvNd5Y+vExLPqq4q9WsweCR7hnENyAa3VJ6JFkgtm93GIS2ebML/QR3V
|
||||
FWWxCO3+bAK6MswUo2AwXjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIDiDAd
|
||||
BgNVHQ4EFgQUWiYTE981mb99VHFcgv965AhG5oQwHwYDVR0jBBgwFoAULM49T3Zw
|
||||
XHmTfuClGd3yntJ9wk8wCgYIKoZIzj0EAwIDSAAwRQIgOK0xDHqrklhQJj/llIgq
|
||||
Jxpa5iY9Jpg8hu4LS7g/UWICIQC/acsLgooZ1Z0DAhLepxUTmHwoAJlY3XIVwwu8
|
||||
QuyA9g==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBBDCBqgIBADBIMRIwEAYDVQQDDAlWMkdSb290Q0ExEDAOBgNVBAoMB0VWZXJl
|
||||
c3QxCzAJBgNVBAYTAkRFMRMwEQYKCZImiZPyLGQBGRYDVjJHMFkwEwYHKoZIzj0C
|
||||
AQYIKoZIzj0DAQcDQgAEbdDhKFQmacJwZV1K0PuqHNumOKZTsxRNOyxaRO76+NR/
|
||||
GmwSSeBDiGWZD0KcHA6kQd7GSFQWMQ0m1tX5t87CAaAAMAoGCCqGSM49BAMCA0kA
|
||||
MEYCIQD8wRH3zKKgdCp1169qG72kXflAIE2AupUEDXtQjU9gzwIhALed/4jhovZd
|
||||
GDX7NIXupLXmXZQf14nv2RxZMKMuxW/X
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-128-CBC,C58A94AC0F142EF36F4139972BA6B894
|
||||
|
||||
Wdlblg4YwT65cj2gdbfMfCXzAH/v6VhIPnFa7VQXlBL4Swj+cTxRrsRe+S6EDe2m
|
||||
eyAR8nuvEpoEhhk4o5u6ihEjSAqdjQWzrq3EGRN+1Ms4aG+opzrZyZPv0qYqV1xj
|
||||
yKibyypuPjEu/RW5cGINnQIyn9kjV4g1Nb3pggUmtQQ=
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,12 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBxTCCAWugAwIBAgIDAJxAMAoGCCqGSM49BAMCMEgxEjAQBgNVBAMMCVYyR1Jv
|
||||
b3RDQTEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUxEzARBgoJkiaJk/Is
|
||||
ZAEZFgNWMkcwIBcNNzMxMTIzMTI0NTA5WhgPMjk3MzAzMjUxMjQ1MDlaMEgxEjAQ
|
||||
BgNVBAMMCVYyR1Jvb3RDQTEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUx
|
||||
EzARBgoJkiaJk/IsZAEZFgNWMkcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARt
|
||||
0OEoVCZpwnBlXUrQ+6oc26Y4plOzFE07LFpE7vr41H8abBJJ4EOIZZkPQpwcDqRB
|
||||
3sZIVBYxDSbW1fm3zsIBo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE
|
||||
AwIBBjAdBgNVHQ4EFgQULM49T3ZwXHmTfuClGd3yntJ9wk8wCgYIKoZIzj0EAwID
|
||||
SAAwRQIgTO/bOWw/x7tDV1jxjvfjVqaN/QtXC7spOMHcSBUE7n4CIQDa0kOL2Vis
|
||||
2DO2PXWaZvEKmpY1P1Kjojb+k+ge1BnXyw==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,30 @@
|
||||
[ ca ]
|
||||
default_ca = CA_default # The default ca section
|
||||
|
||||
[ CA_default ]
|
||||
|
||||
dir = . # top dir
|
||||
database = expired_bulk/index.txt # index file.
|
||||
new_certs_dir = $dir/expired_bulk # new certs dir
|
||||
|
||||
certificate = $dir/cert.pem # The CA cert
|
||||
serial = $dir/expired_bulk/serial # serial no file
|
||||
private_key = $dir/cert.key # CA private key
|
||||
RANDFILE = $dir/private/.rand # random number file
|
||||
|
||||
default_days = 365 # how long to certify for
|
||||
default_md = md5 # md to use
|
||||
|
||||
policy = policy_any # default policy
|
||||
email_in_dn = no # Don't add the email into cert DN
|
||||
|
||||
name_opt = ca_default # Subject name display option
|
||||
cert_opt = ca_default # Certificate display option
|
||||
copy_extensions = none # Don't copy extensions from request
|
||||
|
||||
[ policy_any ]
|
||||
countryName = supplied
|
||||
stateOrProvinceName = optional
|
||||
organizationName = optional
|
||||
organizationalUnitName = optional
|
||||
commonName = supplied
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBAjCBqQIBADBHMREwDwYDVQQDDAhTRUNDQ2VydDEQMA4GA1UECgwHRVZlcmVz
|
||||
dDELMAkGA1UEBhMCREUxEzARBgoJkiaJk/IsZAEZFgNDUE8wWTATBgcqhkjOPQIB
|
||||
BggqhkjOPQMBBwNCAASyun0cfxUIIGFWEc8MkdVVvQlfzPPDqjO6tbSogEvT79Vd
|
||||
+vKkAFHM/sjZwwVteOIswBLC03QN5GuwSOnoPtI9oAAwCgYIKoZIzj0EAwIDSAAw
|
||||
RQIhAKp82SmThGq04FGShXtzydwmCm7W7l9yBqjLL/0+Si9aAiBFqreBoS7lvniy
|
||||
R7tRgnrqIek8Yd/bSRodZSG/HQyUtQ==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-128-CBC,9B7B1D26EAB889668AD829E722BB9CD1
|
||||
|
||||
goWq5y9cvQZvW6j6Ne+ACUd1+VbSUZj4EbcZBTf9h2mkiLN/NtCf/FSLmTpyco6Q
|
||||
Lfrnaz0HbJV+8NNHotyOEqiGYJkm+rQr1tGw6zv6rRDCQOwtWLhwV8bbo3ZElk+X
|
||||
Fy7/uuVuKFDfBvvHuQJFyQBinLRBhDdWU64a0rB68WE=
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,13 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB5TCCAYqgAwIBAgIDAMNQMAoGCCqGSM49BAMCMEgxEjAQBgNVBAMMCVYyR1Jv
|
||||
b3RDQTEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUxEzARBgoJkiaJk/Is
|
||||
ZAEZFgNWMkcwIhgPMjEyMzExMjMxMjMzMTZaGA8yMTI0MTEyMjEyMzMxNlowRzER
|
||||
MA8GA1UEAwwIU0VDQ0NlcnQxEDAOBgNVBAoMB0VWZXJlc3QxCzAJBgNVBAYTAkRF
|
||||
MRMwEQYKCZImiZPyLGQBGRYDQ1BPMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
|
||||
srp9HH8VCCBhVhHPDJHVVb0JX8zzw6ozurW0qIBL0+/VXfrypABRzP7I2cMFbXji
|
||||
LMASwtN0DeRrsEjp6D7SPaNgMF4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMC
|
||||
A4gwHQYDVR0OBBYEFKVZrBNwlKDQUS+yPKXI+zg2k6WLMB8GA1UdIwQYMBaAFP32
|
||||
NXYHCHWxVUbsKZeyLcP9twwFMAoGCCqGSM49BAMCA0kAMEYCIQDDTiywDU34zIKE
|
||||
MVShaXA53sF9/9wtDtWoKgDuG2WA6AIhAN+ab34CVF5++P3zDaHZ4uYHj/5S+vCN
|
||||
p78XvoeGnSG3
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-128-CBC,7AB01F370701C906A96B511640DC7E1F
|
||||
|
||||
zq9LdsWtB/QquUTP+aEBzrkdkmXEuMRTD4Wq62g1Ic+9rCbOTqn46CGjj40k3i0W
|
||||
VjaRddZ/jgNgAO3PpdLIpI5Lu4wTqFRPNebm0mzAOt+HeAeUvipA3OIaeAy1CAJ4
|
||||
d6wA2JPMyAIfeZbG/pwzrzqxdlqEzJy2ZNfMJ0nqtcA=
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -0,0 +1,12 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBxDCCAWqgAwIBAgICMDkwCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJVjJHUm9v
|
||||
dENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk
|
||||
ARkWA1YyRzAgFw0yMzExMjMxMjAzMDNaGA8zMDIzMDMyNjEyMDMwM1owSDESMBAG
|
||||
A1UEAwwJVjJHUm9vdENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET
|
||||
MBEGCgmSJomT8ixkARkWA1YyRzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHRc
|
||||
wtH/nAaONX07z9F/zvKkrFlsabTXPFybrADcI+EdigFs970aakxWabIyfDeTccxO
|
||||
u9HxvpvqBml/FXtG2OmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD
|
||||
AgEGMB0GA1UdDgQWBBT99jV2Bwh1sVVG7CmXsi3D/bcMBTAKBggqhkjOPQQDAgNI
|
||||
ADBFAiEArTdHTcGWNyYKa/aS0h8PNOaSgk5XYvXQjsZVri+KC8oCIGdSld8XrgBm
|
||||
xdMwvu0rqdMjqNnQJ2kTDEWCPRz9fbPN
|
||||
-----END CERTIFICATE-----
|
||||
110
tools/EVerest-main/lib/everest/evse_security/tests/generate_test_certs.sh
Executable file
110
tools/EVerest-main/lib/everest/evse_security/tests/generate_test_certs.sh
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
|
||||
CERT_PATH="./certs"
|
||||
CSR_PATH="./csr"
|
||||
|
||||
EC_CURVE=prime256v1
|
||||
SYMMETRIC_CIPHER=-aes-128-cbc
|
||||
password="123456"
|
||||
|
||||
CA_CSMS_PATH="$CERT_PATH/ca/csms"
|
||||
CA_CSO_PATH="$CERT_PATH/ca/cso"
|
||||
CA_V2G_PATH="$CERT_PATH/ca/v2g"
|
||||
CA_MO_PATH="$CERT_PATH/ca/mo"
|
||||
CA_INVALID_PATH="$CERT_PATH/ca/invalid"
|
||||
|
||||
CLIENT_CSMS_PATH="$CERT_PATH/client/csms"
|
||||
CLIENT_CSO_PATH="$CERT_PATH/client/cso"
|
||||
CLIENT_V2G_PATH="$CERT_PATH/client/v2g"
|
||||
CLIENT_MO_PATH="$CERT_PATH/client/mo"
|
||||
CLIENT_INVALID_PATH="$CERT_PATH/client/invalid"
|
||||
VALIDITY=3650
|
||||
|
||||
TO_BE_INSTALLED_PATH="$CERT_PATH/to_be_installed"
|
||||
|
||||
mkdir -p "$CERT_PATH"
|
||||
mkdir -p "$CSR_PATH"
|
||||
mkdir -p "$CA_CSMS_PATH"
|
||||
mkdir -p "$CA_CSO_PATH"
|
||||
mkdir -p "$CA_V2G_PATH"
|
||||
mkdir -p "$CA_MO_PATH"
|
||||
mkdir -p "$CLIENT_CSMS_PATH"
|
||||
mkdir -p "$CLIENT_CSO_PATH"
|
||||
mkdir -p "$CLIENT_V2G_PATH"
|
||||
mkdir -p "$CLIENT_MO_PATH"
|
||||
mkdir -p "$CLIENT_INVALID_PATH"
|
||||
mkdir -p "$TO_BE_INSTALLED_PATH"
|
||||
|
||||
function create_certificate() {
|
||||
# Args:
|
||||
# $1: name of the certificate (without the .pem extension)
|
||||
# $2: directory to install the certificate and private key into
|
||||
# $3: openssl config file for the certificate
|
||||
# $4: serial number for the certificate
|
||||
# $5: CA certificate file. If this is missing, we will create a self-signed certificate.
|
||||
# $6: CA private key file. Likewise omit this to create a self-signed certificate.
|
||||
|
||||
local name="$1"
|
||||
local install_dir="$2"
|
||||
local config="$3"
|
||||
local serial_num="$4"
|
||||
local signed_by_cert="$5"
|
||||
local signed_by_key="$6"
|
||||
|
||||
openssl ecparam -genkey -name "$EC_CURVE" | openssl ec "$SYMMETRIC_CIPHER" -passout pass:"$password" -out "${install_dir}/${name}.key"
|
||||
|
||||
if [ -z $signed_by_cert ]
|
||||
then
|
||||
openssl req -new -key "${install_dir}/${name}.key" -passin pass:"$password" -config "configs/${config}" -out "${CSR_PATH}/${name}.csr"
|
||||
openssl x509 -req -in "${CSR_PATH}/${name}.csr" -extfile "configs/${config}" -extensions ext -signkey "${install_dir}/${name}.key" -passin pass:"$password" $SHA -set_serial "${serial_num}" -out "${install_dir}/${name}.pem" -days "$VALIDITY"
|
||||
else
|
||||
openssl req -new -key "${install_dir}/${name}.key" -passin pass:"$password" -config "configs/${config}" -out "${CSR_PATH}/${name}.csr"
|
||||
openssl x509 -req -in "${CSR_PATH}/${name}.csr" -extfile "configs/${config}" -extensions ext -CA "${signed_by_cert}" -CAkey "${signed_by_key}" -passin pass:"$password" -set_serial "${serial_num}" -out "${install_dir}/${name}.pem" -days "$VALIDITY"
|
||||
fi
|
||||
}
|
||||
|
||||
# V2G root CA
|
||||
create_certificate V2G_ROOT_CA "${CA_V2G_PATH}" v2gRootCACert.cnf 12345
|
||||
# Second V2G root CA
|
||||
create_certificate V2G_ROOT_CA_NEW "${CA_V2G_PATH}" v2gRootCACert.cnf 12349
|
||||
# Sub-CA 1
|
||||
create_certificate CPO_SUB_CA1 "${CA_CSMS_PATH}" cpoSubCA1Cert.cnf 12346 "${CA_V2G_PATH}/V2G_ROOT_CA.pem" "${CA_V2G_PATH}/V2G_ROOT_CA.key"
|
||||
# Sub-CA 2
|
||||
create_certificate CPO_SUB_CA2 "${CA_CSMS_PATH}" cpoSubCA2Cert.cnf 12347 "${CA_CSMS_PATH}/CPO_SUB_CA1.pem" "${CA_CSMS_PATH}/CPO_SUB_CA1.key"
|
||||
# Chargepoint leaf
|
||||
create_certificate SECC_LEAF "${CLIENT_CSO_PATH}" seccLeafCert.cnf 12348 "${CA_CSMS_PATH}/CPO_SUB_CA2.pem" "${CA_CSMS_PATH}/CPO_SUB_CA2.key"
|
||||
# Invalid self-signed CSMS cert
|
||||
create_certificate INVALID_CSMS "${CLIENT_INVALID_PATH}" v2gRootCACert.cnf 12345
|
||||
|
||||
# create cert chain bundles in the V2G root ca and chargepoint leaf dirs
|
||||
cat "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" "$CA_V2G_PATH/V2G_ROOT_CA.pem" > "$CA_V2G_PATH/V2G_CA_BUNDLE.pem"
|
||||
cat "$CLIENT_CSO_PATH/SECC_LEAF.pem" "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" > "$CLIENT_CSO_PATH/CPO_CERT_CHAIN.pem"
|
||||
|
||||
cp "$CLIENT_CSO_PATH/SECC_LEAF.key" "$CLIENT_CSMS_PATH/CSMS_LEAF.key"
|
||||
|
||||
# assume CSO and CSMS are same authority
|
||||
cp -r $CA_CSMS_PATH/* $CA_CSO_PATH
|
||||
cp "$CLIENT_CSO_PATH/SECC_LEAF.pem" "$CLIENT_CSMS_PATH/CSMS_LEAF.pem"
|
||||
|
||||
# MO root CA
|
||||
create_certificate MO_ROOT_CA "${CA_MO_PATH}" MORootCACert.cnf 32345
|
||||
# MO Sub-CA 1
|
||||
create_certificate MO_SUB_CA1 "${CA_MO_PATH}" MOSubCA1Cert.cnf 32346 "${CA_MO_PATH}/MO_ROOT_CA.pem" "${CA_MO_PATH}/MO_ROOT_CA.key"
|
||||
# MO Sub-CA 2
|
||||
create_certificate MO_SUB_CA2 "${CA_MO_PATH}" MOSubCA2Cert.cnf 32347 "${CA_MO_PATH}/MO_SUB_CA1.pem" "${CA_MO_PATH}/MO_SUB_CA1.key"
|
||||
|
||||
# create cert chain bundles in the MO root ca
|
||||
cat "$CA_MO_PATH/MO_SUB_CA2.pem" "$CA_MO_PATH/MO_SUB_CA1.pem" "$CA_MO_PATH/MO_ROOT_CA.pem" > "$CA_MO_PATH/MO_CA_BUNDLE.pem"
|
||||
|
||||
# MO Leaf signed by MO Root
|
||||
create_certificate MO_LEAF "${CLIENT_MO_PATH}" MOLeafCert.cnf 32348 "${CA_MO_PATH}/MO_SUB_CA2.pem" "${CA_MO_PATH}/MO_SUB_CA2.key"
|
||||
|
||||
# MO Leaf signed by V2G Root
|
||||
create_certificate MO_LEAF_V2G "${CLIENT_MO_PATH}" MOLeafCert_V2G.cnf 32349 "${CA_CSMS_PATH}/CPO_SUB_CA2.pem" "${CA_CSMS_PATH}/CPO_SUB_CA2.key"
|
||||
|
||||
# Create certificates used for installation tests
|
||||
create_certificate INSTALL_TEST_ROOT_CA1 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21234
|
||||
create_certificate INSTALL_TEST_ROOT_CA2 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21235
|
||||
create_certificate INSTALL_TEST_ROOT_CA3 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21236
|
||||
create_certificate INSTALL_TEST_ROOT_CA3_SUBCA1 "${TO_BE_INSTALLED_PATH}" install_test_subca1.cnf 21237 "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3.pem" "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3.key"
|
||||
create_certificate INSTALL_TEST_ROOT_CA3_SUBCA2 "${TO_BE_INSTALLED_PATH}" install_test_subca2.cnf 21238 "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3_SUBCA1.pem" "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3_SUBCA1.key"
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
|
||||
CERT_PATH="./certs"
|
||||
CSR_PATH="./csr"
|
||||
|
||||
EC_CURVE=prime256v1
|
||||
SYMMETRIC_CIPHER=-aes-128-cbc
|
||||
password="123456"
|
||||
|
||||
CA_CSMS_PATH="$CERT_PATH/ca/csms"
|
||||
CA_CSO_PATH="$CERT_PATH/ca/cso"
|
||||
CA_V2G_PATH="$CERT_PATH/ca/v2g"
|
||||
CA_MO_PATH="$CERT_PATH/ca/mo"
|
||||
CA_INVALID_PATH="$CERT_PATH/ca/invalid"
|
||||
|
||||
CLIENT_CSMS_PATH="$CERT_PATH/client/csms"
|
||||
CLIENT_CSO_PATH="$CERT_PATH/client/cso"
|
||||
CLIENT_V2G_PATH="$CERT_PATH/client/v2g"
|
||||
CLIENT_INVALID_PATH="$CERT_PATH/client/invalid"
|
||||
VALIDITY=3650
|
||||
|
||||
TO_BE_INSTALLED_PATH="$CERT_PATH/to_be_installed"
|
||||
|
||||
mkdir -p "$CERT_PATH"
|
||||
mkdir -p "$CSR_PATH"
|
||||
mkdir -p "$CA_CSMS_PATH"
|
||||
mkdir -p "$CA_CSO_PATH"
|
||||
mkdir -p "$CA_V2G_PATH"
|
||||
mkdir -p "$CA_MO_PATH"
|
||||
mkdir -p "$CLIENT_CSMS_PATH"
|
||||
mkdir -p "$CLIENT_CSO_PATH"
|
||||
mkdir -p "$CLIENT_V2G_PATH"
|
||||
mkdir -p "$CLIENT_INVALID_PATH"
|
||||
mkdir -p "$TO_BE_INSTALLED_PATH"
|
||||
|
||||
function create_certificate() {
|
||||
# Args:
|
||||
# $1: name of the certificate (without the .pem extension)
|
||||
# $2: directory to install the certificate and private key into
|
||||
# $3: openssl config file for the certificate
|
||||
# $4: serial number for the certificate
|
||||
# $5: CA certificate file. If this is missing, we will create a self-signed certificate.
|
||||
# $6: CA private key file. Likewise omit this to create a self-signed certificate.
|
||||
|
||||
local name="$1"
|
||||
local install_dir="$2"
|
||||
local config="$3"
|
||||
local serial_num="$4"
|
||||
local signed_by_cert="$5"
|
||||
local signed_by_key="$6"
|
||||
|
||||
openssl ecparam -genkey -name "$EC_CURVE" | openssl ec "$SYMMETRIC_CIPHER" -passout pass:"$password" -out "${install_dir}/${name}.key"
|
||||
|
||||
if [ -z $signed_by_cert ]
|
||||
then
|
||||
openssl req -new -key "${install_dir}/${name}.key" -passin pass:"$password" -config "configs/${config}" -out "${CSR_PATH}/${name}.csr"
|
||||
openssl x509 -req -in "${CSR_PATH}/${name}.csr" -extfile "configs/${config}" -extensions ext -signkey "${install_dir}/${name}.key" -passin pass:"$password" $SHA -set_serial "${serial_num}" -out "${install_dir}/${name}.pem" -days "$VALIDITY"
|
||||
else
|
||||
openssl req -new -key "${install_dir}/${name}.key" -passin pass:"$password" -config "configs/${config}" -out "${CSR_PATH}/${name}.csr"
|
||||
openssl x509 -req -in "${CSR_PATH}/${name}.csr" -extfile "configs/${config}" -extensions ext -CA "${signed_by_cert}" -CAkey "${signed_by_key}" -passin pass:"$password" -set_serial "${serial_num}" -out "${install_dir}/${name}.pem" -days "$VALIDITY"
|
||||
fi
|
||||
}
|
||||
|
||||
# V2G root CA
|
||||
create_certificate V2G_ROOT_CA "${CA_V2G_PATH}" v2gRootCACert.cnf 12345
|
||||
# Second V2G root CA
|
||||
create_certificate V2G_ROOT_CA_NEW "${CA_V2G_PATH}" v2gRootCACert.cnf 12349
|
||||
# Sub-CA 1
|
||||
create_certificate CPO_SUB_CA1 "${CA_CSMS_PATH}" cpoSubCA1Cert.cnf 12346 "${CA_V2G_PATH}/V2G_ROOT_CA.pem" "${CA_V2G_PATH}/V2G_ROOT_CA.key"
|
||||
# Sub-CA 2
|
||||
create_certificate CPO_SUB_CA2 "${CA_CSMS_PATH}" cpoSubCA2Cert.cnf 12347 "${CA_CSMS_PATH}/CPO_SUB_CA1.pem" "${CA_CSMS_PATH}/CPO_SUB_CA1.key"
|
||||
# Chargepoint leaf
|
||||
create_certificate SECC_LEAF "${CLIENT_CSO_PATH}" seccLeafCert.cnf 12348 "${CA_CSMS_PATH}/CPO_SUB_CA2.pem" "${CA_CSMS_PATH}/CPO_SUB_CA2.key"
|
||||
# Alternate chargepoint leaf
|
||||
create_certificate SECC_LEAF_GRIDSYNC "${CLIENT_CSO_PATH}" seccLeafCert_Alternate.cnf 12349 "${CA_CSMS_PATH}/CPO_SUB_CA2.pem" "${CA_CSMS_PATH}/CPO_SUB_CA2.key"
|
||||
# Invalid self-signed CSMS cert
|
||||
create_certificate INVALID_CSMS "${CLIENT_INVALID_PATH}" v2gRootCACert.cnf 12345
|
||||
|
||||
# create cert chain bundles in the V2G root ca and chargepoint leaf dirs
|
||||
cat "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" "$CA_V2G_PATH/V2G_ROOT_CA.pem" > "$CA_V2G_PATH/V2G_CA_BUNDLE.pem"
|
||||
cat "$CLIENT_CSO_PATH/SECC_LEAF.pem" "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" > "$CLIENT_CSO_PATH/CPO_CERT_CHAIN.pem"
|
||||
|
||||
cp "$CLIENT_CSO_PATH/SECC_LEAF.key" "$CLIENT_CSMS_PATH/CSMS_LEAF.key"
|
||||
|
||||
# assume CSO and CSMS are same authority
|
||||
cp -r $CA_CSMS_PATH/* $CA_CSO_PATH
|
||||
cp "$CLIENT_CSO_PATH/SECC_LEAF.pem" "$CLIENT_CSMS_PATH/CSMS_LEAF.pem"
|
||||
|
||||
# empty MO bundle
|
||||
touch "$CA_MO_PATH/MO_CA_BUNDLE.pem"
|
||||
|
||||
# Create certificates used for installation tests
|
||||
create_certificate INSTALL_TEST_ROOT_CA1 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21234
|
||||
create_certificate INSTALL_TEST_ROOT_CA2 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21235
|
||||
create_certificate INSTALL_TEST_ROOT_CA3 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21236
|
||||
create_certificate INSTALL_TEST_ROOT_CA3_SUBCA1 "${TO_BE_INSTALLED_PATH}" install_test_subca1.cnf 21237 "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3.pem" "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3.key"
|
||||
create_certificate INSTALL_TEST_ROOT_CA3_SUBCA2 "${TO_BE_INSTALLED_PATH}" install_test_subca2.cnf 21238 "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3_SUBCA1.pem" "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3_SUBCA1.key"
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
|
||||
CERT_PATH="./certs"
|
||||
CSR_PATH="./csr"
|
||||
|
||||
EC_CURVE=prime256v1
|
||||
SYMMETRIC_CIPHER=-aes-128-cbc
|
||||
password="123456"
|
||||
|
||||
CA_CSMS_PATH="$CERT_PATH/ca/csms"
|
||||
CA_CSO_PATH="$CERT_PATH/ca/cso"
|
||||
CA_V2G_PATH="$CERT_PATH/ca/v2g"
|
||||
CA_MO_PATH="$CERT_PATH/ca/mo"
|
||||
CA_INVALID_PATH="$CERT_PATH/ca/invalid"
|
||||
|
||||
CLIENT_CSMS_PATH="$CERT_PATH/client/csms"
|
||||
CLIENT_CSO_PATH="$CERT_PATH/client/cso"
|
||||
CLIENT_V2G_PATH="$CERT_PATH/client/v2g"
|
||||
CLIENT_INVALID_PATH="$CERT_PATH/client/invalid"
|
||||
VALIDITY=3650
|
||||
|
||||
TO_BE_INSTALLED_PATH="$CERT_PATH/to_be_installed"
|
||||
|
||||
mkdir -p "$CERT_PATH"
|
||||
mkdir -p "$CSR_PATH"
|
||||
mkdir -p "$CA_CSMS_PATH"
|
||||
mkdir -p "$CA_CSO_PATH"
|
||||
mkdir -p "$CA_V2G_PATH"
|
||||
mkdir -p "$CA_MO_PATH"
|
||||
mkdir -p "$CLIENT_CSMS_PATH"
|
||||
mkdir -p "$CLIENT_CSO_PATH"
|
||||
mkdir -p "$CLIENT_V2G_PATH"
|
||||
mkdir -p "$CLIENT_INVALID_PATH"
|
||||
mkdir -p "$TO_BE_INSTALLED_PATH"
|
||||
|
||||
function create_certificate() {
|
||||
# Args:
|
||||
# $1: name of the certificate (without the .pem extension)
|
||||
# $2: directory to install the certificate and private key into
|
||||
# $3: openssl config file for the certificate
|
||||
# $4: serial number for the certificate
|
||||
# $5: CA certificate file. If this is missing, we will create a self-signed certificate.
|
||||
# $6: CA private key file. Likewise omit this to create a self-signed certificate.
|
||||
|
||||
local name="$1"
|
||||
local install_dir="$2"
|
||||
local config="$3"
|
||||
local serial_num="$4"
|
||||
local signed_by_cert="$5"
|
||||
local signed_by_key="$6"
|
||||
|
||||
openssl ecparam -genkey -name "$EC_CURVE" | openssl ec "$SYMMETRIC_CIPHER" -passout pass:"$password" -out "${install_dir}/${name}.key"
|
||||
|
||||
if [ -z $signed_by_cert ]
|
||||
then
|
||||
openssl req -new -key "${install_dir}/${name}.key" -passin pass:"$password" -config "configs/${config}" -out "${CSR_PATH}/${name}.csr"
|
||||
openssl x509 -req -in "${CSR_PATH}/${name}.csr" -extfile "configs/${config}" -extensions ext -signkey "${install_dir}/${name}.key" -passin pass:"$password" $SHA -set_serial "${serial_num}" -out "${install_dir}/${name}.pem" -days "$VALIDITY"
|
||||
else
|
||||
openssl req -new -key "${install_dir}/${name}.key" -passin pass:"$password" -config "configs/${config}" -out "${CSR_PATH}/${name}.csr"
|
||||
openssl x509 -req -in "${CSR_PATH}/${name}.csr" -extfile "configs/${config}" -extensions ext -CA "${signed_by_cert}" -CAkey "${signed_by_key}" -passin pass:"$password" -set_serial "${serial_num}" -out "${install_dir}/${name}.pem" -days "$VALIDITY"
|
||||
fi
|
||||
}
|
||||
|
||||
# V2G root CA
|
||||
create_certificate V2G_ROOT_CA "${CA_V2G_PATH}" v2gRootCACert.cnf 12345
|
||||
# Second V2G root CA
|
||||
create_certificate V2G_ROOT_CA_NEW "${CA_V2G_PATH}" v2gRootCACert.cnf 12349
|
||||
# Sub-CA 1
|
||||
create_certificate CPO_SUB_CA1 "${CA_CSMS_PATH}" cpoSubCA1Cert.cnf 12346 "${CA_V2G_PATH}/V2G_ROOT_CA.pem" "${CA_V2G_PATH}/V2G_ROOT_CA.key"
|
||||
# Sub-CA 2
|
||||
create_certificate CPO_SUB_CA2 "${CA_CSMS_PATH}" cpoSubCA2Cert.cnf 12347 "${CA_CSMS_PATH}/CPO_SUB_CA1.pem" "${CA_CSMS_PATH}/CPO_SUB_CA1.key"
|
||||
# Chargepoint leaf
|
||||
create_certificate SECC_LEAF "${CLIENT_CSO_PATH}" seccLeafCert.cnf 12348 "${CA_CSMS_PATH}/CPO_SUB_CA2.pem" "${CA_CSMS_PATH}/CPO_SUB_CA2.key"
|
||||
# Invalid self-signed CSMS cert
|
||||
create_certificate INVALID_CSMS "${CLIENT_INVALID_PATH}" v2gRootCACert.cnf 12345
|
||||
|
||||
# V2G alternate root CA
|
||||
create_certificate V2G_ROOT_GRIDSYNC_CA "${CA_V2G_PATH}" v2gRootCACert_Alternate.cnf 12345
|
||||
# Alternate chargepoint leaf
|
||||
create_certificate SECC_LEAF_GRIDSYNC "${CLIENT_CSMS_PATH}" seccLeafCert_Alternate.cnf 12348 "${CA_V2G_PATH}/V2G_ROOT_GRIDSYNC_CA.pem" "${CA_V2G_PATH}/V2G_ROOT_GRIDSYNC_CA.key"
|
||||
|
||||
# create cert chain bundles in the V2G root ca and chargepoint leaf dirs
|
||||
cat "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" "$CA_V2G_PATH/V2G_ROOT_CA.pem" "$CA_V2G_PATH/V2G_ROOT_GRIDSYNC_CA.pem" > "$CA_V2G_PATH/V2G_CA_BUNDLE.pem"
|
||||
cat "$CLIENT_CSO_PATH/SECC_LEAF.pem" "$CA_CSMS_PATH/CPO_SUB_CA2.pem" "$CA_CSMS_PATH/CPO_SUB_CA1.pem" > "$CLIENT_CSO_PATH/CPO_CERT_CHAIN.pem"
|
||||
|
||||
cp "$CLIENT_CSO_PATH/SECC_LEAF.key" "$CLIENT_CSMS_PATH/CSMS_LEAF.key"
|
||||
|
||||
# assume CSO and CSMS are same authority
|
||||
cp -r $CA_CSMS_PATH/* $CA_CSO_PATH
|
||||
cp "$CLIENT_CSO_PATH/SECC_LEAF.pem" "$CLIENT_CSMS_PATH/CSMS_LEAF.pem"
|
||||
|
||||
# empty MO bundle
|
||||
touch "$CA_MO_PATH/MO_CA_BUNDLE.pem"
|
||||
|
||||
# Create certificates used for installation tests
|
||||
create_certificate INSTALL_TEST_ROOT_CA1 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21234
|
||||
create_certificate INSTALL_TEST_ROOT_CA2 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21235
|
||||
create_certificate INSTALL_TEST_ROOT_CA3 "${TO_BE_INSTALLED_PATH}" install_test.cnf 21236
|
||||
create_certificate INSTALL_TEST_ROOT_CA3_SUBCA1 "${TO_BE_INSTALLED_PATH}" install_test_subca1.cnf 21237 "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3.pem" "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3.key"
|
||||
create_certificate INSTALL_TEST_ROOT_CA3_SUBCA2 "${TO_BE_INSTALLED_PATH}" install_test_subca2.cnf 21238 "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3_SUBCA1.pem" "${TO_BE_INSTALLED_PATH}/INSTALL_TEST_ROOT_CA3_SUBCA1.key"
|
||||
@@ -0,0 +1,78 @@
|
||||
openssl_conf = openssl_init
|
||||
|
||||
[openssl_init]
|
||||
providers = provider_section
|
||||
|
||||
[provider_section]
|
||||
default = default_section
|
||||
tpm2 = tpm2_section
|
||||
base = base_section
|
||||
|
||||
[default_section]
|
||||
activate = 1
|
||||
|
||||
[tpm2_section]
|
||||
activate = 1
|
||||
|
||||
[base_section]
|
||||
activate = 1
|
||||
|
||||
[tpm2tss_section]
|
||||
engine_id = tpm2tss
|
||||
dynamic_path = /usr/lib/engines-3/libtpm2tss.so
|
||||
init = 1
|
||||
|
||||
[req_root]
|
||||
distinguished_name = req_dn_root
|
||||
utf8 = yes
|
||||
prompt = no
|
||||
req_extensions = v3_root
|
||||
|
||||
[req_ca]
|
||||
distinguished_name = req_dn_ca
|
||||
utf8 = yes
|
||||
prompt = no
|
||||
req_extensions = v3_ca
|
||||
|
||||
[req_server]
|
||||
distinguished_name = req_dn_server
|
||||
utf8 = yes
|
||||
prompt = no
|
||||
req_extensions = v3_server
|
||||
|
||||
[req_dn_root]
|
||||
C = GB
|
||||
O = Pionix
|
||||
L = London
|
||||
CN = Root Trust Anchor
|
||||
|
||||
[req_dn_ca]
|
||||
C = GB
|
||||
O = Pionix
|
||||
L = London
|
||||
CN = Intermediate CA
|
||||
|
||||
[req_dn_server]
|
||||
C = GB
|
||||
O = Pionix
|
||||
L = London
|
||||
CN = 00000000
|
||||
|
||||
[v3_root]
|
||||
subjectKeyIdentifier=hash
|
||||
authorityKeyIdentifier=keyid:always,issuer:always
|
||||
basicConstraints = critical, CA:true, pathlen:2
|
||||
keyUsage = keyCertSign, cRLSign
|
||||
|
||||
[v3_ca]
|
||||
subjectKeyIdentifier=hash
|
||||
authorityKeyIdentifier=keyid:always,issuer:always
|
||||
basicConstraints = critical, CA:true
|
||||
keyUsage = keyCertSign, cRLSign
|
||||
|
||||
[v3_server]
|
||||
subjectKeyIdentifier=hash
|
||||
authorityKeyIdentifier=keyid:always,issuer:always
|
||||
keyUsage = digitalSignature, keyEncipherment, keyAgreement
|
||||
extendedKeyUsage = serverAuth, clientAuth
|
||||
subjectAltName = IP:192.168.240.1, DNS:pionix.com
|
||||
@@ -0,0 +1,182 @@
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <evse_security/crypto/openssl/openssl_crypto_supplier.hpp>
|
||||
#include <optional>
|
||||
|
||||
// #define OUTPUT_CSR
|
||||
|
||||
using namespace evse_security;
|
||||
|
||||
namespace {
|
||||
|
||||
static std::string getFile(const std::string name) {
|
||||
std::ifstream file(name);
|
||||
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
class OpenSSLSupplierTest : public testing::Test {
|
||||
protected:
|
||||
static void SetUpTestSuite() {
|
||||
std::system("./create-pki.sh");
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(OpenSSLSupplierTest, generate_key_RSA_TPM20) {
|
||||
KeyGenerationInfo info = {
|
||||
CryptoKeyType::RSA_TPM20, false, std::nullopt, std::nullopt, std::nullopt,
|
||||
};
|
||||
KeyHandle_ptr key;
|
||||
auto res = OpenSSLSupplier::generate_key(info, key);
|
||||
ASSERT_TRUE(res);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTest, generate_key_RSA_3072) {
|
||||
KeyGenerationInfo info = {
|
||||
CryptoKeyType::RSA_3072, false, std::nullopt, std::nullopt, std::nullopt,
|
||||
};
|
||||
KeyHandle_ptr key;
|
||||
auto res = OpenSSLSupplier::generate_key(info, key);
|
||||
ASSERT_TRUE(res);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTest, generate_key_EC_prime256v1) {
|
||||
KeyGenerationInfo info = {
|
||||
CryptoKeyType::EC_prime256v1, false, std::nullopt, std::nullopt, std::nullopt,
|
||||
};
|
||||
KeyHandle_ptr key;
|
||||
auto res = OpenSSLSupplier::generate_key(info, key);
|
||||
ASSERT_TRUE(res);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTest, generate_key_EC_EC_secp384r1) {
|
||||
KeyGenerationInfo info = {
|
||||
CryptoKeyType::EC_secp384r1, false, std::nullopt, std::nullopt, std::nullopt,
|
||||
};
|
||||
KeyHandle_ptr key;
|
||||
auto res = OpenSSLSupplier::generate_key(info, key);
|
||||
ASSERT_TRUE(res);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTest, load_certificates) {
|
||||
auto file = getFile("pki/cert_path.pem");
|
||||
auto res = OpenSSLSupplier::load_certificates(file, EncodingFormat::PEM);
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTest, x509_check_private_key) {
|
||||
auto cert_leaf = getFile("pki/server_cert.pem");
|
||||
auto res_leaf = OpenSSLSupplier::load_certificates(cert_leaf, EncodingFormat::PEM);
|
||||
auto cert = res_leaf[0].get();
|
||||
auto key = getFile("pki/server_priv.pem");
|
||||
auto res = OpenSSLSupplier::x509_check_private_key(cert, key, std::nullopt);
|
||||
ASSERT_TRUE(res == KeyValidationResult::Valid);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTest, x509_verify_certificate_chain) {
|
||||
auto cert_path = getFile("pki/cert_path.pem");
|
||||
auto cert_leaf = getFile("pki/server_cert.pem");
|
||||
|
||||
auto res_path = OpenSSLSupplier::load_certificates(cert_path, EncodingFormat::PEM);
|
||||
auto res_leaf = OpenSSLSupplier::load_certificates(cert_leaf, EncodingFormat::PEM);
|
||||
|
||||
std::vector<X509Handle*> parents;
|
||||
std::vector<X509Handle*> empty_untrusted;
|
||||
|
||||
for (auto& i : res_path) {
|
||||
parents.push_back(i.get());
|
||||
}
|
||||
|
||||
auto res = OpenSSLSupplier::x509_verify_certificate_chain(res_leaf[0].get(), parents, empty_untrusted, true,
|
||||
std::nullopt, "pki/root_cert.pem");
|
||||
ASSERT_EQ(res, CertificateValidationResult::Valid);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTest, x509_generate_csr) {
|
||||
std::string csr;
|
||||
CertificateSigningRequestInfo csr_info = {
|
||||
0,
|
||||
"UK",
|
||||
"Pionix",
|
||||
"0123456789",
|
||||
.dns_name = std::nullopt,
|
||||
.ip_address = std::nullopt,
|
||||
{CryptoKeyType::EC_prime256v1, false, std::nullopt, "pki/csr_key.pem", std::nullopt}};
|
||||
auto res = OpenSSLSupplier::x509_generate_csr(csr_info, csr);
|
||||
ASSERT_EQ(res, CertificateSignRequestResult::Valid);
|
||||
|
||||
std::ofstream out("csr.pem");
|
||||
out << csr;
|
||||
out.close();
|
||||
|
||||
ASSERT_GT(csr.size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTest, x509_generate_csr_dns) {
|
||||
std::string csr;
|
||||
CertificateSigningRequestInfo csr_info = {
|
||||
0,
|
||||
"UK",
|
||||
"Pionix",
|
||||
"0123456789",
|
||||
.dns_name = "cs.pionix.de",
|
||||
.ip_address = std::nullopt,
|
||||
{CryptoKeyType::EC_prime256v1, false, std::nullopt, "pki/csr_key.pem", std::nullopt}};
|
||||
auto res = OpenSSLSupplier::x509_generate_csr(csr_info, csr);
|
||||
ASSERT_EQ(res, CertificateSignRequestResult::Valid);
|
||||
|
||||
#ifdef OUTPUT_CSR
|
||||
std::ofstream out("csr_dns.pem");
|
||||
out << csr;
|
||||
out.close();
|
||||
#endif
|
||||
|
||||
ASSERT_GT(csr.size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTest, x509_generate_csr_ip) {
|
||||
std::string csr;
|
||||
CertificateSigningRequestInfo csr_info = {
|
||||
0,
|
||||
"UK",
|
||||
"Pionix",
|
||||
"0123456789",
|
||||
.dns_name = std::nullopt,
|
||||
.ip_address = "127.0.0.1",
|
||||
{CryptoKeyType::EC_prime256v1, false, std::nullopt, "pki/csr_key.pem", std::nullopt}};
|
||||
auto res = OpenSSLSupplier::x509_generate_csr(csr_info, csr);
|
||||
ASSERT_EQ(res, CertificateSignRequestResult::Valid);
|
||||
|
||||
#ifdef OUTPUT_CSR
|
||||
std::ofstream out("csr_ip.pem");
|
||||
out << csr;
|
||||
out.close();
|
||||
#endif
|
||||
|
||||
ASSERT_GT(csr.size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTest, x509_generate_csr_dns_ip) {
|
||||
std::string csr;
|
||||
CertificateSigningRequestInfo csr_info = {
|
||||
0,
|
||||
"UK",
|
||||
"Pionix",
|
||||
"0123456789",
|
||||
.dns_name = "cs.pionix.de",
|
||||
.ip_address = "127.0.0.1",
|
||||
{CryptoKeyType::EC_prime256v1, false, std::nullopt, "pki/csr_key.pem", std::nullopt}};
|
||||
auto res = OpenSSLSupplier::x509_generate_csr(csr_info, csr);
|
||||
ASSERT_EQ(res, CertificateSignRequestResult::Valid);
|
||||
|
||||
#ifdef OUTPUT_CSR
|
||||
std::ofstream out("csr_dns_ip.pem");
|
||||
out << csr;
|
||||
out.close();
|
||||
#endif
|
||||
|
||||
ASSERT_GT(csr.size(), 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,153 @@
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <gtest/gtest.h>
|
||||
#include <iostream>
|
||||
|
||||
#include <evse_security/crypto/openssl/openssl_crypto_supplier.hpp>
|
||||
#include <evse_security/crypto/openssl/openssl_provider.hpp>
|
||||
|
||||
using namespace evse_security;
|
||||
|
||||
namespace {
|
||||
|
||||
static std::string getFile(const std::string name) {
|
||||
std::ifstream file(name);
|
||||
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
class OpenSSLSupplierTpmTest : public testing::Test {
|
||||
protected:
|
||||
static void SetUpTestSuite() {
|
||||
std::system("./create-pki.sh tpm");
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(OpenSSLSupplierTpmTest, supports_provider_tpm) {
|
||||
OpenSSLProvider::cleanup();
|
||||
ASSERT_FALSE(OpenSSLProvider::supports_provider_tpm());
|
||||
// calculates
|
||||
OpenSSLProvider provider;
|
||||
// returns cached
|
||||
ASSERT_TRUE(OpenSSLProvider::supports_provider_tpm());
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTpmTest, supports_provider_tpm_key_creation) {
|
||||
OpenSSLProvider::cleanup();
|
||||
ASSERT_FALSE(OpenSSLProvider::supports_provider_tpm());
|
||||
// should calculate
|
||||
ASSERT_TRUE(OpenSSLSupplier::supports_tpm_key_creation());
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTpmTest, generate_key_RSA_TPM20) {
|
||||
KeyGenerationInfo info = {
|
||||
CryptoKeyType::RSA_TPM20, true, std::nullopt, std::nullopt, std::nullopt,
|
||||
};
|
||||
KeyHandle_ptr key;
|
||||
auto res = OpenSSLSupplier::generate_key(info, key);
|
||||
ASSERT_TRUE(res);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTpmTest, generate_key_RSA_3072) {
|
||||
// Enable this test manually only if your platform supports 3072 TPM keys
|
||||
GTEST_SKIP() << "Skipping TPM2.0 GEN_RSA_3072 test since it is a non-spec value"
|
||||
" which probably will not be supported on many platforms!";
|
||||
|
||||
KeyGenerationInfo info = {
|
||||
CryptoKeyType::RSA_3072, true, std::nullopt, std::nullopt, std::nullopt,
|
||||
};
|
||||
KeyHandle_ptr key;
|
||||
auto res = OpenSSLSupplier::generate_key(info, key);
|
||||
ASSERT_TRUE(res);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTpmTest, generate_key_EC_prime256v1) {
|
||||
KeyGenerationInfo info = {
|
||||
CryptoKeyType::EC_prime256v1, true, std::nullopt, std::nullopt, std::nullopt,
|
||||
};
|
||||
KeyHandle_ptr key;
|
||||
auto res = OpenSSLSupplier::generate_key(info, key);
|
||||
ASSERT_TRUE(res);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTpmTest, generate_key_EC_EC_secp384r1) {
|
||||
KeyGenerationInfo info = {
|
||||
CryptoKeyType::EC_secp384r1, true, std::nullopt, std::nullopt, std::nullopt,
|
||||
};
|
||||
KeyHandle_ptr key;
|
||||
auto res = OpenSSLSupplier::generate_key(info, key);
|
||||
ASSERT_TRUE(res);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTpmTest, load_certificates) {
|
||||
auto file = getFile("tpm_pki/cert_path.pem");
|
||||
auto res = OpenSSLSupplier::load_certificates(file, EncodingFormat::PEM);
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTpmTest, x509_check_private_key) {
|
||||
auto cert_leaf = getFile("tpm_pki/server_cert.pem");
|
||||
auto res_leaf = OpenSSLSupplier::load_certificates(cert_leaf, EncodingFormat::PEM);
|
||||
auto cert = res_leaf[0].get();
|
||||
auto key = getFile("tpm_pki/server_priv.pem");
|
||||
auto res = OpenSSLSupplier::x509_check_private_key(cert, key, std::nullopt);
|
||||
ASSERT_EQ(res, KeyValidationResult::Valid);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTpmTest, x509_verify_certificate_chain) {
|
||||
auto cert_path = getFile("tpm_pki/cert_path.pem");
|
||||
auto cert_leaf = getFile("tpm_pki/server_cert.pem");
|
||||
|
||||
auto res_path = OpenSSLSupplier::load_certificates(cert_path, EncodingFormat::PEM);
|
||||
auto res_leaf = OpenSSLSupplier::load_certificates(cert_leaf, EncodingFormat::PEM);
|
||||
|
||||
std::vector<X509Handle*> parents;
|
||||
|
||||
for (auto& i : res_path) {
|
||||
parents.push_back(i.get());
|
||||
}
|
||||
|
||||
auto res = OpenSSLSupplier::x509_verify_certificate_chain(res_leaf[0].get(), parents, {}, true, std::nullopt,
|
||||
"tpm_pki/root_cert.pem");
|
||||
ASSERT_EQ(res, CertificateValidationResult::Valid);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTpmTest, x509_generate_csr) {
|
||||
std::string csr;
|
||||
CertificateSigningRequestInfo csr_info = {
|
||||
0,
|
||||
"UK",
|
||||
"Pionix",
|
||||
"0123456789",
|
||||
.dns_name = std::nullopt,
|
||||
.ip_address = std::nullopt,
|
||||
{CryptoKeyType::EC_prime256v1, true, std::nullopt, "tpm_pki/csr_key.tkey", std::nullopt}};
|
||||
|
||||
// std::cout << "tpm2 pre: " << OSSL_PROVIDER_available(nullptr, "tpm2") << std::endl;
|
||||
// std::cout << "base pre: " << OSSL_PROVIDER_available(nullptr, "base") << std::endl;
|
||||
auto res = OpenSSLSupplier::x509_generate_csr(csr_info, csr);
|
||||
// std::cout << "tpm2 post: " << OSSL_PROVIDER_available(nullptr, "tpm2") << std::endl;
|
||||
// std::cout << "base post: " << OSSL_PROVIDER_available(nullptr, "base") << std::endl;
|
||||
|
||||
ASSERT_EQ(res, CertificateSignRequestResult::Valid);
|
||||
ASSERT_GT(csr.size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(OpenSSLSupplierTpmTest, x509_generate_csr2) {
|
||||
std::string csr;
|
||||
CertificateSigningRequestInfo csr_info = {
|
||||
0,
|
||||
"UK",
|
||||
"Pionix",
|
||||
"0123456789",
|
||||
.dns_name = std::nullopt,
|
||||
.ip_address = std::nullopt,
|
||||
{CryptoKeyType::RSA_TPM20, true, std::nullopt, "tpm_pki/csr_key.tkey", std::nullopt}};
|
||||
|
||||
auto res = OpenSSLSupplier::x509_generate_csr(csr_info, csr);
|
||||
|
||||
ASSERT_EQ(res, CertificateSignRequestResult::Valid);
|
||||
ASSERT_GT(csr.size(), 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
1450
tools/EVerest-main/lib/everest/evse_security/tests/tests.cpp
Normal file
1450
tools/EVerest-main/lib/everest/evse_security/tests/tests.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user