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:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View 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 }

View File

@@ -0,0 +1,3 @@
* @pietfried @hikinggrass @james-ctc
/.github/ @pietfried @hikinggrass

View File

@@ -0,0 +1,8 @@
.*
!.clang-format
!.clang-tidy
!.ci
!.github
!.gitignore
build*
clang-tidy-*

View 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

View 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",
],
)

View 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()

View 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.

View File

@@ -0,0 +1,117 @@
# libevse-security
![Github Actions](https://github.com/EVerest/libevse-security/actions/workflows/build_and_test.yml/badge.svg)
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.

View File

@@ -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._

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
add_subdirectory(evse_security)

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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 issuers 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIJ9RbIOPOVCNRhrcq6Fw/3qWw6J00lF/yT7FdrSXCuhzoAoGCCqGSM49
AwEHoUQDQgAEQplOIWUtl6KOnRhM9OQRu7TawKd0SAExZwztsJChemlIXEJ9D5dc
K0/+rKjpTgHoDg9LdluA+tv9nmeeyiX8pQ==
-----END EC PRIVATE KEY-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIB71Owd1hTS6T9kNtK2pgbfL/bCuEpM+3aHMquGhJJejoAoGCCqGSM49
AwEHoUQDQgAEEvKLd+Kd3aPOhE7LFHRQYTYQdR63u5UdtUcmE443vsTPPIRpF+86
64YxEBtcVyPjLDtcX8JfpzTySJpxudvmjA==
-----END EC PRIVATE KEY-----

View File

@@ -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-----

View File

@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIH13t8Z/SV8kObot1c15YsTp/OZgbAw8r6Ns9DzHl2WAoAoGCCqGSM49
AwEHoUQDQgAEieno+61njtJx4QY7j6M8eAelAR5AwFLrnP2hG5dGX7EYWsouYp7R
6SKuuGxtTIR7w5VU+mnHiSd+wItjJA6sXQ==
-----END EC PRIVATE KEY-----

View File

@@ -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-----

View File

@@ -0,0 +1,4 @@
SHA256
82addb4b47026c702b9ed9d482c6e3570bbae9c49b963ec18b0a3523dfb47fe3
e9d2a6d245233edbf5a8319b99087313e16307ca29b388373d951b50e93090aa
4ed698d63c724c6a61a0ccc4ff80b383192dfd7a

View File

@@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBAjCBqQIBADBHMREwDwYDVQQDDAhTRUNDQ2VydDEQMA4GA1UECgwHRVZlcmVz
dDELMAkGA1UEBhMCREUxEzARBgoJkiaJk/IsZAEZFgNDUE8wWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAARlxNKadJ0NCSFMfvNd5Y+vExLPqq4q9WsweCR7hnENyAa3
VJ6JFkgtm93GIS2ebML/QR3VFWWxCO3+bAK6MswUoAAwCgYIKoZIzj0EAwIDSAAw
RQIhAOWltS/gdYqIYndktWPtUdLypfTu59kMNkBOYCgkxq8GAiBW1EG1OeZ56iAB
vnu/GEDA0hBBVTV/4SmJB4dKu6gfEQ==
-----END CERTIFICATE REQUEST-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBBDCBqgIBADBIMRIwEAYDVQQDDAlWMkdSb290Q0ExEDAOBgNVBAoMB0VWZXJl
c3QxCzAJBgNVBAYTAkRFMRMwEQYKCZImiZPyLGQBGRYDVjJHMFkwEwYHKoZIzj0C
AQYIKoZIzj0DAQcDQgAEbdDhKFQmacJwZV1K0PuqHNumOKZTsxRNOyxaRO76+NR/
GmwSSeBDiGWZD0KcHA6kQd7GSFQWMQ0m1tX5t87CAaAAMAoGCCqGSM49BAMCA0kA
MEYCIQD8wRH3zKKgdCp1169qG72kXflAIE2AupUEDXtQjU9gzwIhALed/4jhovZd
GDX7NIXupLXmXZQf14nv2RxZMKMuxW/X
-----END CERTIFICATE REQUEST-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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

View File

@@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBAjCBqQIBADBHMREwDwYDVQQDDAhTRUNDQ2VydDEQMA4GA1UECgwHRVZlcmVz
dDELMAkGA1UEBhMCREUxEzARBgoJkiaJk/IsZAEZFgNDUE8wWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAASyun0cfxUIIGFWEc8MkdVVvQlfzPPDqjO6tbSogEvT79Vd
+vKkAFHM/sjZwwVteOIswBLC03QN5GuwSOnoPtI9oAAwCgYIKoZIzj0EAwIDSAAw
RQIhAKp82SmThGq04FGShXtzydwmCm7W7l9yBqjLL/0+Si9aAiBFqreBoS7lvniy
R7tRgnrqIek8Yd/bSRodZSG/HQyUtQ==
-----END CERTIFICATE REQUEST-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff