From 27763249d96d58220f0a94430af805d4e866703f Mon Sep 17 00:00:00 2001 From: James Chapman Date: Thu, 27 Jun 2024 13:57:28 +0100 Subject: [PATCH 1/3] feat: Added OpenSSL server that supports TLS certificate status request feat: added optional OpenSSL server feat: integration of OpenSSL into EvseV2G feat: added cmake integration with optional OpenSSL feat: added OpenSSL sha256 and ECDSA functions + ISO test vectors feat: added and tested EXI signatures feat: added base64 encode/decode feat: OpenSSL or MbedTLS for EvseV2G cmake -DEVEREST_CORE_BUILD_TESTING=ON -DUSING_MBED_TLS=OFF -GNinja .. default is to use MBED TLS fix: clang format missmatch fix: missing dependencies fix: link issue fix: updated to support new error logging framework fix: updated to support new libevse-security intrefaces feat: remove use of mbedtls_base64_encode and mbedtls_base64_decode when building for OpenSSL OpenSSL base64 routines used when USING_MBED_TLS=OFF mbedTLS base64 routines used when USING_MBED_TLS=ON fix: remove duplicate include fix: removed typo in filename fix: SIL testing fixes fix: corrected typo in filename fix: add structure to EvseV2G use subdirectories to help collect common functionality and provide structure to the module. OpenSSL TLS moved to common area since it is not tied to EvseV2G. fix: addressing PR review comments fix: moved EvseManager changes to separate PR fix: rebase against main with new libcbv2g fix: remove TLS tests from CI - fail for unknown reason fix: Update README.md fix: corrected unit test executable names fix: updated code owners for the tls directory fix: removed debugging output to cout fix: added log handler to remove dependency on EVLOG fix: updates to try and get tests to run in CI fix: some codacy issues Signed-off-by: James Chapman Adding missing Signed-off-by: Sebastian Lukas --- .github/CODEOWNERS | 1 + dependencies.yaml | 2 +- lib/staging/CMakeLists.txt | 1 + lib/staging/tls/CMakeLists.txt | 29 + ...updates-to-support-status_request_v2.patch | 139 ++ lib/staging/tls/openssl-patch.md | 98 ++ lib/staging/tls/openssl_conv.cpp | 28 + lib/staging/tls/openssl_conv.hpp | 18 + lib/staging/tls/openssl_util.cpp | 523 ++++++ lib/staging/tls/openssl_util.hpp | 295 ++++ lib/staging/tls/tests/CMakeLists.txt | 114 ++ lib/staging/tls/tests/README.md | 43 + lib/staging/tls/tests/crypto_test.cpp | 42 + lib/staging/tls/tests/gtest_main.cpp | 23 + lib/staging/tls/tests/openssl_util_test.cpp | 381 +++++ lib/staging/tls/tests/patched_test.cpp | 366 +++++ lib/staging/tls/tests/pki/.gitignore | 1 + lib/staging/tls/tests/pki/iso_pkey.asn1 | 11 + lib/staging/tls/tests/pki/ocsp_response.der | Bin 0 -> 279 bytes lib/staging/tls/tests/pki/openssl-pki.conf | 143 ++ lib/staging/tls/tests/pki/pki.sh | 63 + lib/staging/tls/tests/tls_client_main.cpp | 104 ++ lib/staging/tls/tests/tls_main.cpp | 99 ++ lib/staging/tls/tests/tls_test.cpp | 47 + lib/staging/tls/tls.cpp | 1435 +++++++++++++++++ lib/staging/tls/tls.hpp | 609 +++++++ lib/staging/util/EnumFlags.hpp | 56 + modules/EvseV2G/CMakeLists.txt | 54 +- modules/EvseV2G/EvseV2G.cpp | 45 +- modules/EvseV2G/EvseV2G.hpp | 6 + .../EvseV2G/charger/ISO15118_chargerImpl.hpp | 2 +- .../EvseV2G/{ => connection}/connection.cpp | 81 +- .../EvseV2G/{ => connection}/connection.hpp | 35 +- modules/EvseV2G/connection/tls_connection.cpp | 263 +++ modules/EvseV2G/connection/tls_connection.hpp | 49 + modules/EvseV2G/crypto/crypto_common.hpp | 19 + modules/EvseV2G/crypto/crypto_mbedtls.cpp | 428 +++++ modules/EvseV2G/crypto/crypto_mbedtls.hpp | 143 ++ modules/EvseV2G/crypto/crypto_openssl.cpp | 209 +++ modules/EvseV2G/crypto/crypto_openssl.hpp | 102 ++ modules/EvseV2G/iso_server.cpp | 477 ++---- modules/EvseV2G/tests/CMakeLists.txt | 88 + .../tests/ISO15118_chargerImplStub.hpp | 86 + modules/EvseV2G/tests/README.md | 51 + .../EvseV2G/tests/evse_securityIntfStub.hpp | 52 + modules/EvseV2G/tests/log.cpp | 16 + modules/EvseV2G/tests/openssl_test.cpp | 156 ++ modules/EvseV2G/tests/requirement.cpp | 12 + modules/EvseV2G/tests/v2g_main.cpp | 167 ++ modules/EvseV2G/v2g.hpp | 57 +- modules/EvseV2G/v2g_ctx.cpp | 12 +- modules/EvseV2G/v2g_server.cpp | 32 +- 52 files changed, 6884 insertions(+), 429 deletions(-) create mode 100644 lib/staging/tls/CMakeLists.txt create mode 100644 lib/staging/tls/openssl-3.0.8-feat-updates-to-support-status_request_v2.patch create mode 100644 lib/staging/tls/openssl-patch.md create mode 100644 lib/staging/tls/openssl_conv.cpp create mode 100644 lib/staging/tls/openssl_conv.hpp create mode 100644 lib/staging/tls/openssl_util.cpp create mode 100644 lib/staging/tls/openssl_util.hpp create mode 100644 lib/staging/tls/tests/CMakeLists.txt create mode 100644 lib/staging/tls/tests/README.md create mode 100644 lib/staging/tls/tests/crypto_test.cpp create mode 100644 lib/staging/tls/tests/gtest_main.cpp create mode 100644 lib/staging/tls/tests/openssl_util_test.cpp create mode 100644 lib/staging/tls/tests/patched_test.cpp create mode 100644 lib/staging/tls/tests/pki/.gitignore create mode 100644 lib/staging/tls/tests/pki/iso_pkey.asn1 create mode 100644 lib/staging/tls/tests/pki/ocsp_response.der create mode 100644 lib/staging/tls/tests/pki/openssl-pki.conf create mode 100755 lib/staging/tls/tests/pki/pki.sh create mode 100644 lib/staging/tls/tests/tls_client_main.cpp create mode 100644 lib/staging/tls/tests/tls_main.cpp create mode 100644 lib/staging/tls/tests/tls_test.cpp create mode 100644 lib/staging/tls/tls.cpp create mode 100644 lib/staging/tls/tls.hpp create mode 100644 lib/staging/util/EnumFlags.hpp rename modules/EvseV2G/{ => connection}/connection.cpp (95%) rename modules/EvseV2G/{ => connection}/connection.hpp (69%) create mode 100644 modules/EvseV2G/connection/tls_connection.cpp create mode 100644 modules/EvseV2G/connection/tls_connection.hpp create mode 100644 modules/EvseV2G/crypto/crypto_common.hpp create mode 100644 modules/EvseV2G/crypto/crypto_mbedtls.cpp create mode 100644 modules/EvseV2G/crypto/crypto_mbedtls.hpp create mode 100644 modules/EvseV2G/crypto/crypto_openssl.cpp create mode 100644 modules/EvseV2G/crypto/crypto_openssl.hpp create mode 100644 modules/EvseV2G/tests/CMakeLists.txt create mode 100644 modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp create mode 100644 modules/EvseV2G/tests/README.md create mode 100644 modules/EvseV2G/tests/evse_securityIntfStub.hpp create mode 100644 modules/EvseV2G/tests/log.cpp create mode 100644 modules/EvseV2G/tests/openssl_test.cpp create mode 100644 modules/EvseV2G/tests/requirement.cpp create mode 100644 modules/EvseV2G/tests/v2g_main.cpp diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 12c758927..d43ddb76b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,7 @@ /lib/staging/gpio/ @corneliusclaussen @hikinggrass @hikinggrass /lib/staging/ocpp/ @hikinggrass @pietfried @corneliusclaussen /lib/staging/slac/ @a-w50 @corneliusclaussen @SebaLukas +/lib/staging/tls/ @james-ctc @AssemblyJohn @corneliusclaussen @SebaLukas # modules /modules/API/ @hikinggrass @pietfried @corneliusclaussen diff --git a/dependencies.yaml b/dependencies.yaml index 3840b6141..d980384a8 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -54,7 +54,7 @@ libcurl: # and would otherwise be overwritten by the version used there libevse-security: git: https://github.com/EVerest/libevse-security.git - git_tag: v0.7.0 + git_tag: b140c17b0a5eaf09b60035605ed8aeb84627eb78 cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBEVSE_SECURITY" # OCPP diff --git a/lib/staging/CMakeLists.txt b/lib/staging/CMakeLists.txt index f186caf24..4caf81493 100644 --- a/lib/staging/CMakeLists.txt +++ b/lib/staging/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(can_dpm1000) add_subdirectory(evse_security) +add_subdirectory(tls) if(EVEREST_DEPENDENCY_ENABLED_LIBSLAC AND EVEREST_DEPENDENCY_ENABLED_LIBFSM) add_subdirectory(slac) endif() diff --git a/lib/staging/tls/CMakeLists.txt b/lib/staging/tls/CMakeLists.txt new file mode 100644 index 000000000..5fb832a7b --- /dev/null +++ b/lib/staging/tls/CMakeLists.txt @@ -0,0 +1,29 @@ +add_library(tls STATIC) +add_library(everest::tls ALIAS tls) + +find_package(OpenSSL 3) + +target_sources(tls + PRIVATE + openssl_conv.cpp + openssl_util.cpp + tls.cpp +) + +target_include_directories(tls + PUBLIC + $ + $ +) + +target_link_libraries(tls + PUBLIC + OpenSSL::SSL + OpenSSL::Crypto + everest::evse_security + everest::framework +) + +if(EVEREST_CORE_BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/lib/staging/tls/openssl-3.0.8-feat-updates-to-support-status_request_v2.patch b/lib/staging/tls/openssl-3.0.8-feat-updates-to-support-status_request_v2.patch new file mode 100644 index 000000000..49f380646 --- /dev/null +++ b/lib/staging/tls/openssl-3.0.8-feat-updates-to-support-status_request_v2.patch @@ -0,0 +1,139 @@ +From 92125584f2fe87023cbfe96bba06358111ed8c13 Mon Sep 17 00:00:00 2001 +From: James Chapman +Date: Fri, 21 Jun 2024 10:29:44 +0100 +Subject: [PATCH 1/1] feat: updates to support status_request_v2 + +Signed-off-by: James Chapman +--- + include/openssl/ssl.h.in | 2 ++ + include/openssl/tls1.h | 7 +++++++ + ssl/s3_lib.c | 8 ++++++++ + ssl/statem/extensions_clnt.c | 3 ++- + ssl/statem/extensions_srvr.c | 4 ++++ + ssl/statem/statem_clnt.c | 3 ++- + 6 files changed, 25 insertions(+), 2 deletions(-) + +diff --git a/include/openssl/ssl.h.in b/include/openssl/ssl.h.in +index 105b4a4a3c..b29f65fbfa 100644 +--- a/include/openssl/ssl.h.in ++++ b/include/openssl/ssl.h.in +@@ -1251,6 +1251,8 @@ DECLARE_PEM_rw(SSL_SESSION, SSL_SESSION) + # define SSL_CTRL_SET_TLSEXT_STATUS_REQ_IDS 69 + # define SSL_CTRL_GET_TLSEXT_STATUS_REQ_OCSP_RESP 70 + # define SSL_CTRL_SET_TLSEXT_STATUS_REQ_OCSP_RESP 71 ++# define SSL_CTRL_GET_TLSEXT_STATUS_EXPECTED 270 ++# define SSL_CTRL_SET_TLSEXT_STATUS_EXPECTED 271 + # ifndef OPENSSL_NO_DEPRECATED_3_0 + # define SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB 72 + # endif +diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h +index d6e9331fa1..f0a8413703 100644 +--- a/include/openssl/tls1.h ++++ b/include/openssl/tls1.h +@@ -160,6 +160,7 @@ extern "C" { + # define TLSEXT_NAMETYPE_host_name 0 + /* status request value from RFC3546 */ + # define TLSEXT_STATUSTYPE_ocsp 1 ++# define TLSEXT_STATUSTYPE_ocsp_multi 2 + + /* ECPointFormat values from RFC4492 */ + # define TLSEXT_ECPOINTFORMAT_first 0 +@@ -291,6 +292,12 @@ __owur int SSL_check_chain(SSL *s, X509 *x, EVP_PKEY *pk, STACK_OF(X509) *chain) + # define SSL_set_tlsext_status_ocsp_resp(ssl, arg, arglen) \ + SSL_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_REQ_OCSP_RESP,arglen,arg) + ++# define SSL_get_tlsext_status_expected(ssl) \ ++ SSL_ctrl(ssl,SSL_CTRL_GET_TLSEXT_STATUS_EXPECTED,0,NULL) ++ ++# define SSL_set_tlsext_status_expected(ssl, arg) \ ++ SSL_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_EXPECTED,arg,NULL) ++ + # define SSL_CTX_set_tlsext_servername_callback(ctx, cb) \ + SSL_CTX_callback_ctrl(ctx,SSL_CTRL_SET_TLSEXT_SERVERNAME_CB,\ + (void (*)(void))cb) +diff --git a/ssl/s3_lib.c b/ssl/s3_lib.c +index 78d4f04056..ede3a56f2f 100644 +--- a/ssl/s3_lib.c ++++ b/ssl/s3_lib.c +@@ -3556,6 +3556,14 @@ long ssl3_ctrl(SSL *s, int cmd, long larg, void *parg) + ret = 1; + break; + ++ case SSL_CTRL_GET_TLSEXT_STATUS_EXPECTED: ++ return (long)s->ext.status_expected; ++ ++ case SSL_CTRL_SET_TLSEXT_STATUS_EXPECTED: ++ s->ext.status_expected = larg; ++ ret = 1; ++ break; ++ + case SSL_CTRL_CHAIN: + if (larg) + return ssl_cert_set1_chain(s, NULL, (STACK_OF(X509) *)parg); +diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c +index 842be0722b..b9d5493e72 100644 +--- a/ssl/statem/extensions_clnt.c ++++ b/ssl/statem/extensions_clnt.c +@@ -8,6 +8,7 @@ + */ + + #include ++#include + #include "../ssl_local.h" + #include "internal/cryptlib.h" + #include "statem_local.h" +@@ -1397,7 +1398,7 @@ int tls_parse_stoc_status_request(SSL *s, PACKET *pkt, unsigned int context, + * MUST only be sent if we've requested a status + * request message. In TLS <= 1.2 it must also be empty. + */ +- if (s->ext.status_type != TLSEXT_STATUSTYPE_ocsp) { ++ if ((s->ext.status_type != TLSEXT_STATUSTYPE_ocsp) && (s->ext.status_type != TLSEXT_STATUSTYPE_ocsp_multi)) { + SSLfatal(s, SSL_AD_UNSUPPORTED_EXTENSION, SSL_R_BAD_EXTENSION); + return 0; + } +diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c +index 16765a5a5b..7fb67937bf 100644 +--- a/ssl/statem/extensions_srvr.c ++++ b/ssl/statem/extensions_srvr.c +@@ -8,6 +8,7 @@ + */ + + #include ++#include + #include "../ssl_local.h" + #include "statem_local.h" + #include "internal/cryptlib.h" +@@ -1421,6 +1422,9 @@ EXT_RETURN tls_construct_stoc_status_request(SSL *s, WPACKET *pkt, + if (!s->ext.status_expected) + return EXT_RETURN_NOT_SENT; + ++ if (s->ext.status_type == TLSEXT_STATUSTYPE_ocsp_multi) ++ return EXT_RETURN_NOT_SENT; ++ + if (SSL_IS_TLS13(s) && chainidx != 0) + return EXT_RETURN_NOT_SENT; + +diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c +index 3cd1ee2d3d..29a07bd413 100644 +--- a/ssl/statem/statem_clnt.c ++++ b/ssl/statem/statem_clnt.c +@@ -9,6 +9,7 @@ + * https://www.openssl.org/source/license.html + */ + ++#include + #include + #include + #include +@@ -2636,7 +2637,7 @@ int tls_process_cert_status_body(SSL *s, PACKET *pkt) + unsigned int type; + + if (!PACKET_get_1(pkt, &type) +- || type != TLSEXT_STATUSTYPE_ocsp) { ++ || (type != TLSEXT_STATUSTYPE_ocsp) && (type != TLSEXT_STATUSTYPE_ocsp_multi)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_UNSUPPORTED_STATUS_TYPE); + return 0; + } +-- +2.34.1 + diff --git a/lib/staging/tls/openssl-patch.md b/lib/staging/tls/openssl-patch.md new file mode 100644 index 000000000..c5477b6e4 --- /dev/null +++ b/lib/staging/tls/openssl-patch.md @@ -0,0 +1,98 @@ + +# OpenSSL 3.0.8 patch + +The file `openssl-3.0.8-feat-updates-to-support-status_request_v2.patch` is a +patch to OpenSSL 3.0.8 to support the `status_request_v2` TLS extension defined +in [RFC 6961](https://datatracker.ietf.org/doc/html/rfc6961). + +## Apply the patch + +Assuming `openssl-3.0.8-feat-updates-to-support-status_request_v2.patch` is in +the current directory: + +```sh +$ git clone --branch openssl-3.0.8 https://github.com/openssl/openssl.git +$ cd openssl +$ patch -p1 < ../openssl-3.0.8-feat-updates-to-support-status_request_v2.patch +$ ./Configure +$ make +$ sudo make install +``` + +The patch can also be added to `SRC_URI` in a yocto bbappend file +`openssl_3.0.8.bbappend`: + +```bitbake +SRC_URI:append = " file://openssl-3.0.8-feat-updates-to-support-status_request_v2.patch" +``` + +## Notes + +The patch is designed to be a minimal change so that `status_request_v2` can be +supported with the emphasis on TLS server support. TLS client support exists to +facilitate testing. + +`status_request_v2` is deprecated for TLS 1.3 and must not be used. The code +ignores `status_request_v2` extensions when TLS 1.3 has been negotiated. + +When a client requests `status_request` and `status_request_v2` then +`status_request_v2` is used and `status_request` ignored. + +## Implementation + +`status_request_v2` is implemented in `tls.cpp` and relies on OCSP responses +being available in separate files that are associated with the server +certificate and chain. + +The patch defines `TLSEXT_STATUSTYPE_ocsp_multi` which is used in `tls.cpp` to +detect a patched version of OpenSSL. + +### OpenSSL + +OpenSSL contains a framework for adding handlers for TLS extensions that are not +natively handled. `status_request` is supported and the same mechanism is used +to to build the `status_request_v2` response. + +Unfortunately both `status_request` and `status_request_v2` add an additional +TLS handshake record `Certificate Status` containing the OCSP responses rather +than including them as part of the extension. The OpenSSL extension framework +doesn't provide a mechanism to add a `Certificate Status` record. + +The solution is to reuse the support for `status_request` and provide the +`status_request_v2` data for the `Certificate Status` record in application +code. + +The patch adds the additional status type `TLSEXT_STATUSTYPE_ocsp_multi` for use +with `SSL_set_tlsext_status_type()` and updates checks on `ext.status_type` so +that it isn't rejected. + +Additional functions `SSL_get_tlsext_status_expected()` and +`SSL_set_tlsext_status_expected()` are added so that application code can +indicate to OpenSSL that the `Certificate Status` record needs to be added. + +`SSL_set_tlsext_status_ocsp_resp()` is used by both `status_request` and +`status_request_v2` to populate the response. + +An early `Client Hello` handler is used to detect `status_request` and +`status_request_v2` extensions so that the `status_request` handler can ignore +the request (unless TLS 1.3 had been negotiated). + +### OcspCache + +Contains a digest method that produces a digest of a certificate. This digest +is paired with the OCSP response filename which provides the association used +in the OCSP cache. + +When responding to a `status_request_v2` the server iterates through the server +certificates and builds the response including the cached OCSP response for each +certificate where available. + +## Testing + +The primary testing has been performed using `Wireshark` to ensure that the +`Server Hello` and `Certificate Status` records are correctly formed. + +There is a googletest test suite `patched_test` that checks operation via the +OpenSSL APIs but it isn't able to check the handshake records directly. + +There are a test TLS server and client that can be used to check operation. diff --git a/lib/staging/tls/openssl_conv.cpp b/lib/staging/tls/openssl_conv.cpp new file mode 100644 index 000000000..9deebea8b --- /dev/null +++ b/lib/staging/tls/openssl_conv.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include + +#include +#include +#include + +using evse_security::X509Handle_ptr; +using evse_security::X509HandleOpenSSL; +using evse_security::X509Wrapper; + +namespace openssl::conversions { + +X509Handle_ptr to_X509Handle_ptr(x509_st* cert) { + X509Handle_ptr ptr; + if (X509_up_ref(cert) == 1) { + ptr = std::make_unique(cert); + } + return ptr; +} + +X509Wrapper to_X509Wrapper(x509_st* cert) { + return X509Wrapper(to_X509Handle_ptr(cert)); +} + +} // namespace openssl::conversions diff --git a/lib/staging/tls/openssl_conv.hpp b/lib/staging/tls/openssl_conv.hpp new file mode 100644 index 000000000..82322535a --- /dev/null +++ b/lib/staging/tls/openssl_conv.hpp @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef OPENSSL_CONV_HPP_ +#define OPENSSL_CONV_HPP_ + +#include +#include +#include + +namespace openssl::conversions { + +evse_security::X509Handle_ptr to_X509Handle_ptr(x509_st* cert); +evse_security::X509Wrapper to_X509Wrapper(x509_st* cert); + +} // namespace openssl::conversions + +#endif // OPENSSL_CONV_HPP_ diff --git a/lib/staging/tls/openssl_util.cpp b/lib/staging/tls/openssl_util.cpp new file mode 100644 index 000000000..15e8cdbe2 --- /dev/null +++ b/lib/staging/tls/openssl_util.cpp @@ -0,0 +1,523 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "openssl_util.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +openssl::log_handler_t s_log_handler{nullptr}; + +int add_error_str(const char* str, std::size_t len, void* u) { + assert(u != nullptr); + auto* list = reinterpret_cast(u); + *list += '\n' + std::string(str, len); + return 0; +} +} // namespace + +namespace openssl { + +void log(log_level_t level, const std::string& str) { + std::string messages = {str}; + ERR_print_errors_cb(&add_error_str, &messages); + if (s_log_handler == nullptr) { + std::cerr << messages << std::endl; + } else { + s_log_handler(level, messages); + } +} + +log_handler_t set_log_handler(log_handler_t handler) { + const auto tmp = s_log_handler; + s_log_handler = handler; + return tmp; +} + +} // namespace openssl + +namespace { + +void DER_Signature_free(std::uint8_t* ptr) { + OPENSSL_free(ptr); +} + +template bool sha_impl(const void* data, std::size_t len, DIGEST& digest, const EVP_MD* HASH) { + std::array buffer{}; + unsigned int digestlen{0}; + const auto res = EVP_Digest(data, len, buffer.data(), &digestlen, HASH, nullptr); + if (res == 1) { + if (digestlen == digest.size()) { + std::memcpy(digest.data(), buffer.data(), digest.size()); + } else { + openssl::log_error("EVP_Digest - size"); + } + } else { + openssl::log_error("EVP_Digest"); + } + return res == 1; +} + +template bool sha(const void* data, std::size_t len, DIGEST& digest); + +template <> bool sha(const void* data, std::size_t len, openssl::sha_256_digest_t& digest) { + return sha_impl(data, len, digest, EVP_sha256()); +} +template <> bool sha(const void* data, std::size_t len, openssl::sha_384_digest_t& digest) { + return sha_impl(data, len, digest, EVP_sha384()); +} +template <> bool sha(const void* data, std::size_t len, openssl::sha_512_digest_t& digest) { + return sha_impl(data, len, digest, EVP_sha512()); +} + +} // namespace + +namespace openssl { + +bool sign(evp_pkey_st* pkey, bn_t& r, bn_t& s, const sha_256_digest_t& digest) { + bool bRes{false}; + std::array signature{}; + auto len = signature.size(); + bRes = sign(pkey, signature.data(), len, digest.data(), sha_256_digest_size); + if (bRes) { + bRes = signature_to_bn(r, s, signature.data(), len); + } + return bRes; +} + +bool sign(EVP_PKEY* pkey, unsigned char* sig, std::size_t& siglen, const unsigned char* tbs, std::size_t tbslen) { + bool bRes{true}; + + auto* ctx = EVP_PKEY_CTX_new(pkey, nullptr); + if (ctx == nullptr) { + log_error("EVP_PKEY_CTX_new"); + bRes = false; + } + if (bRes && (EVP_PKEY_sign_init(ctx) != 1)) { + log_error("EVP_PKEY_sign_init"); + bRes = false; + } + if (bRes && (EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha256()) != 1)) { + log_error("EVP_PKEY_CTX_set_signature_md"); + bRes = false; + } + if (bRes) { + // calculate signature size + std::size_t length{0}; + if (EVP_PKEY_sign(ctx, nullptr, &length, tbs, tbslen) != 1) { + log_error("EVP_PKEY_sign - length"); + bRes = false; + } else if (siglen < length) { + log_error("EVP_PKEY_sign - length too small: " + std::to_string(length)); + bRes = false; + } + if (bRes) { + const auto res = EVP_PKEY_sign(ctx, sig, &siglen, tbs, tbslen); + if (res != 1) { + log_error("EVP_PKEY_sign" + std::to_string(res)); + bRes = false; + } + } + } + EVP_PKEY_CTX_free(ctx); + return bRes; +} + +bool verify(evp_pkey_st* pkey, const bn_t& r, const bn_t& s, const sha_256_digest_t& digest) { + return verify(pkey, r.data(), s.data(), digest); +} + +bool verify(evp_pkey_st* pkey, const std::uint8_t* r, const std::uint8_t* s, const sha_256_digest_t& digest) { + bool bRes{false}; + auto [signature, len] = bn_to_signature(r, s); + if ((signature != nullptr) && (len > 0)) { + bRes = verify(pkey, signature.get(), len, digest.data(), sha_256_digest_size); + } + return bRes; +} + +bool verify(EVP_PKEY* pkey, const unsigned char* sig, std::size_t siglen, const unsigned char* tbs, + std::size_t tbslen) { + bool bRes{true}; + + auto* ctx = EVP_PKEY_CTX_new(pkey, nullptr); + if (ctx == nullptr) { + log_error("EVP_PKEY_CTX_new"); + bRes = false; + } + if (bRes && (EVP_PKEY_verify_init(ctx) != 1)) { + log_error("EVP_PKEY_verify_init"); + bRes = false; + } + if (bRes && (EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha256()) != 1)) { + log_error("EVP_PKEY_CTX_set_signature_md"); + bRes = false; + } + if (bRes) { + const auto res = EVP_PKEY_verify(ctx, sig, siglen, tbs, tbslen); + if (res != 1) { + log_error("EVP_PKEY_verify: " + std::to_string(res)); + bRes = false; + } + } + EVP_PKEY_CTX_free(ctx); + return bRes; +} + +bool sha_256(const void* data, std::size_t len, sha_256_digest_t& digest) { + return sha(data, len, digest); +} + +bool sha_384(const void* data, std::size_t len, sha_384_digest_t& digest) { + return sha(data, len, digest); +} + +bool sha_512(const void* data, std::size_t len, sha_512_digest_t& digest) { + return sha(data, len, digest); +} + +std::vector base64_decode(const char* text, std::size_t len) { + assert(text != nullptr); + assert(len > 0); + + // remove \n + auto input = std::make_unique(len); + std::size_t input_len{0}; + + for (std::size_t i = 0; i < len; i++) { + const auto item = text[i]; + if (item != '\n') { + input.get()[input_len++] = item; + } + } + + auto* b64 = BIO_new(BIO_f_base64()); + auto* mem = BIO_new_mem_buf(input.get(), static_cast(input_len)); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + BIO_push(b64, mem); + + std::size_t output_len{0}; + int read_len{0}; + std::array buffer{}; + std::vector result(len); + + while ((read_len = BIO_read(b64, buffer.data(), buffer.size())) > 0) { + if ((output_len + read_len) <= result.size()) { + std::memcpy(&result[output_len], buffer.data(), read_len); + output_len += static_cast(read_len); + } else { + // decoded data is larger than the input - can't happen! + output_len = 0; + break; + } + } + + result.resize(output_len); + BIO_free_all(b64); + return result; +} + +bool base64_decode(const char* text, std::size_t len, std::uint8_t* out_data, std::size_t& out_len) { + assert(out_data != nullptr); + + bool bResult = false; + auto res = base64_decode(text, len); + if ((res.size() > 0) && (res.size() <= out_len)) { + std::memcpy(out_data, res.data(), res.size()); + out_len = res.size(); + bResult = true; + } + return bResult; +} + +std::string base64_encode(const std::uint8_t* data, std::size_t len, bool newLine) { + assert(data != nullptr); + assert(len > 0); + + auto* b64 = BIO_new(BIO_f_base64()); + auto* mem = BIO_new(BIO_s_mem()); + BIO_push(b64, mem); + if (!newLine) { + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + } + BIO_write(b64, data, static_cast(len)); + BIO_flush(b64); + + char* ptr{nullptr}; + const auto size = BIO_get_mem_data(mem, &ptr); + + std::string result(ptr, size); + BIO_free_all(b64); + return result; +} + +std::tuple bn_to_signature(const bn_t& r, const bn_t& s) { + return bn_to_signature(r.data(), s.data()); +}; + +std::tuple bn_to_signature(const std::uint8_t* r, const std::uint8_t* s) { + std::uint8_t* sig_p{nullptr}; + std::size_t signature_len{0}; + BIGNUM* rbn{nullptr}; + BIGNUM* sbn{nullptr}; + + auto* signature = ECDSA_SIG_new(); + if (signature == nullptr) { + log_error("ECDSA_SIG_new"); + } else { + rbn = BN_bin2bn(r, signature_n_size, nullptr); + sbn = BN_bin2bn(s, signature_n_size, nullptr); + } + + if (rbn != nullptr && sbn != nullptr) { + if (ECDSA_SIG_set0(signature, rbn, sbn) == 1) { + /* Set these to NULL since they are now owned by obj */ + rbn = sbn = nullptr; + signature_len = i2d_ECDSA_SIG(signature, &sig_p); + if (signature_len == 0) { + log_error("i2d_ECDSA_SIG"); + } + } else { + log_error("ECDSA_SIG_set0"); + } + } + + BN_free(rbn); + BN_free(sbn); + ECDSA_SIG_free(signature); + return {DER_Signature_ptr(sig_p, &DER_Signature_free), signature_len}; +}; + +bool signature_to_bn(bn_t& r, bn_t& s, const std::uint8_t* sig_p, std::size_t len) { + bool bRes{false}; + + auto* signature = d2i_ECDSA_SIG(nullptr, &sig_p, static_cast(len)); + if (signature == nullptr) { + log_error("d2i_ECDSA_SIG"); + } else { + const auto* rbn = ECDSA_SIG_get0_r(signature); + const auto* sbn = ECDSA_SIG_get0_s(signature); + + bRes = BN_bn2binpad(rbn, r.data(), static_cast(r.size())) != -1; + bRes = bRes && BN_bn2binpad(sbn, s.data(), static_cast(s.size())) != -1; + if (!bRes) { + log_error("BN_bn2binpad"); + } + } + + ECDSA_SIG_free(signature); + return bRes; +}; + +std::vector load_certificates(const char* filename) { + assert(filename != nullptr); + + std::vector result{}; + auto* store = OSSL_STORE_open(filename, UI_null(), nullptr, nullptr, nullptr); + + if (store != nullptr) { + while (OSSL_STORE_eof(store) != 1) { + auto* info = OSSL_STORE_load(store); + + if (info != nullptr) { + if (OSSL_STORE_error(store) == 1) { + log_error("OSSL_STORE_load"); + } else { + const auto type = OSSL_STORE_INFO_get_type(info); + + if (type == OSSL_STORE_INFO_CERT) { + // get a copy of the certificate + auto cert = OSSL_STORE_INFO_get1_CERT(info); + result.push_back({cert, &X509_free}); + } + } + } + + OSSL_STORE_INFO_free(info); + } + } + + OSSL_STORE_close(store); + return result; +} + +std::string certificate_to_pem(const x509_st* cert) { + assert(cert != nullptr); + + auto* mem = BIO_new(BIO_s_mem()); + std::string result; + if (PEM_write_bio_X509(mem, cert) != 1) { + log_error("PEM_write_bio_X509"); + } else { + BIO_flush(mem); + + char* ptr{nullptr}; + const auto size = BIO_get_mem_data(mem, &ptr); + + result = std::string(ptr, size); + } + BIO_free(mem); + return result; +} + +Certificate_ptr der_to_certificate(const std::uint8_t* der, std::size_t len) { + Certificate_ptr result{nullptr, nullptr}; + const auto* ptr = der; + auto* cert = d2i_X509(nullptr, &ptr, static_cast(len)); + if (cert == nullptr) { + log_error("d2i_X509"); + } else { + result = Certificate_ptr{cert, &X509_free}; + } + return result; +} + +verify_result_t verify_certificate(const x509_st* cert, const CertificateList& trust_anchors, + const CertificateList& untrusted) { + verify_result_t result = verify_result_t::verified; + auto* store_ctx = X509_STORE_CTX_new(); + auto* ta_store = X509_STORE_new(); + auto* chain = sk_X509_new_null(); + X509* target{nullptr}; + + if (store_ctx == nullptr) { + log_error("X509_STORE_CTX_new"); + result = verify_result_t::OtherError; + } + + if (ta_store == nullptr) { + log_error("X509_STORE_new"); + result = verify_result_t::OtherError; + } + + if (chain == nullptr) { + log_error("sk_X509_new_null"); + result = verify_result_t::OtherError; + } + + if (cert != nullptr) { + target = X509_dup(cert); + if (target == nullptr) { + log_error("X509_dup"); + result = verify_result_t::OtherError; + } + } + + if (result == verify_result_t::verified) { + result = verify_result_t::OtherError; + + for (const auto& i : trust_anchors) { + if (X509_STORE_add_cert(ta_store, i.get()) != 1) { + log_error("X509_STORE_add_cert"); + } + } + + for (const auto& j : untrusted) { + if (X509_add_cert(chain, j.get(), X509_ADD_FLAG_UP_REF | X509_ADD_FLAG_NO_DUP | X509_ADD_FLAG_NO_SS) != 1) { + log_error("X509_add_cert"); + } + } + + if (X509_STORE_CTX_init(store_ctx, ta_store, target, chain) != 1) { + log_error("X509_STORE_CTX_init"); + } else { + if (X509_STORE_CTX_verify(store_ctx) != 1) { + const auto err = X509_STORE_CTX_get_error(store_ctx); + if (err != X509_V_OK) { + log_error("X509_STORE_CTX_verify (" + std::to_string(X509_STORE_CTX_get_error_depth(store_ctx)) + + ") " + X509_verify_cert_error_string(err)); + } + + switch (err) { + case X509_V_ERR_CERT_CHAIN_TOO_LONG: + case X509_V_ERR_CERT_SIGNATURE_FAILURE: + case X509_V_ERR_CERT_UNTRUSTED: + case X509_V_ERR_PATH_LENGTH_EXCEEDED: + case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN: + case X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE: + case X509_V_ERR_UNSPECIFIED: + result = verify_result_t::CertChainError; + break; + case X509_V_ERR_CERT_HAS_EXPIRED: + case X509_V_ERR_CERT_NOT_YET_VALID: + result = verify_result_t::CertificateExpired; + break; + case X509_V_ERR_CERT_REVOKED: + result = verify_result_t::CertificateRevoked; + break; + case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: + case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: + result = verify_result_t::NoCertificateAvailable; + break; + default: + break; + } + } else { + result = verify_result_t::verified; + } + } + } + + X509_STORE_CTX_free(store_ctx); + X509_STORE_free(ta_store); + sk_X509_pop_free(chain, X509_free); + X509_free(target); + return result; +} + +std::map certificate_subject(const x509_st* cert) { + assert(cert != nullptr); + std::map result; + + // DO NOT FREE - internal pointers to certificate + const auto* subject = X509_get_subject_name(cert); + if (subject != nullptr) { + for (int i = 0; i < X509_NAME_entry_count(subject); i++) { + const auto* name_entry = X509_NAME_get_entry(subject, i); + if (name_entry != nullptr) { + const auto* object = X509_NAME_ENTRY_get_object(name_entry); + const auto* data = X509_NAME_ENTRY_get_data(name_entry); + if ((object != nullptr) && (data != nullptr)) { + std::string name(OBJ_nid2sn(OBJ_obj2nid(object))); + std::string value(reinterpret_cast(ASN1_STRING_get0_data(data)), + ASN1_STRING_length(data)); + result[name] = value; + } + } + } + } + + return result; +} + +PKey_ptr certificate_public_key(x509_st* cert) { + PKey_ptr result{nullptr, nullptr}; + auto* pkey = X509_get_pubkey(cert); + if (pkey == nullptr) { + log_error("X509_get_pubkey"); + } else { + result = PKey_ptr(pkey, &EVP_PKEY_free); + } + return result; +} + +} // namespace openssl diff --git a/lib/staging/tls/openssl_util.hpp b/lib/staging/tls/openssl_util.hpp new file mode 100644 index 000000000..661a36bef --- /dev/null +++ b/lib/staging/tls/openssl_util.hpp @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef OPENSSL_UTIL_HPP_ +#define OPENSSL_UTIL_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct evp_pkey_st; +struct x509_st; + +namespace openssl { + +enum class verify_result_t : std::uint8_t { + verified, + CertChainError, + CertificateExpired, + CertificateRevoked, + NoCertificateAvailable, + OtherError, +}; + +constexpr std::size_t signature_size = 64; +constexpr std::size_t signature_n_size = 32; +constexpr std::size_t signature_der_size = 128; +constexpr std::size_t sha_256_digest_size = 32; +constexpr std::size_t sha_384_digest_size = 48; +constexpr std::size_t sha_512_digest_size = 64; + +enum class digest_alg_t : std::uint8_t { + sha256, + sha384, + sha512, +}; + +using sha_256_digest_t = std::array; +using sha_384_digest_t = std::array; +using sha_512_digest_t = std::array; +using bn_t = std::array; +using bn_const_t = std::array; + +using Certificate_ptr = std::unique_ptr; +using CertificateList = std::vector; +using DER_Signature_ptr = std::unique_ptr; +using PKey_ptr = std::unique_ptr; + +/** + * \brief sign using ECDSA on curve secp256r1/prime256v1/P-256 of a SHA 256 digest + * \param[in] pkey the private key + * \param[out] r the R component of the signature as a BIGNUM + * \param[out] s the S component of the signature as a BIGNUM + * \param[out] digest the SHA256 digest to sign + * \return true when successful + */ +bool sign(evp_pkey_st* pkey, bn_t& r, bn_t& s, const sha_256_digest_t& digest); + +/** + * \brief sign using ECDSA on curve secp256r1/prime256v1/P-256 of a SHA 256 digest + * \param[in] pkey the private key + * \param[out] sig the buffer where the DER encoded signature will be placed + * \param[inout] siglen the size of the signature buffer, updated to be the size of the signature + * \param[in] tbs a pointer to the SHA256 digest + * \param[in] tbslen the size of the SHA256 digest + * \return true when successful + */ +bool sign(evp_pkey_st* pkey, unsigned char* sig, std::size_t& siglen, const unsigned char* tbs, std::size_t tbslen); + +/** + * \brief verify a signature against a SHA 256 digest using ECDSA on curve secp256r1/prime256v1/P-256 + * \param[in] pkey the public key + * \param[in] r the R component of the signature as a BIGNUM + * \param[in] s the S component of the signature as a BIGNUM + * \param[in] digest the SHA256 digest to sign + * \return true when successful + */ +bool verify(evp_pkey_st* pkey, const bn_t& r, const bn_t& s, const sha_256_digest_t& digest); + +/** + * \brief verify a signature against a SHA 256 digest using ECDSA on curve secp256r1/prime256v1/P-256 + * \param[in] pkey the public key + * \param[in] r the R component of the signature as a BIGNUM (0-padded 32 bytes) + * \param[in] s the S component of the signature as a BIGNUM (0-padded 32 bytes) + * \param[in] digest the SHA256 digest to sign + * \return true when successful + */ +bool verify(evp_pkey_st* pkey, const std::uint8_t* r, const std::uint8_t* s, const sha_256_digest_t& digest); + +/** + * \brief verify a signature against a SHA 256 digest using ECDSA on curve secp256r1/prime256v1/P-256 + * \param[in] pkey the public key + * \param[in] sig the DER encoded signature + * \param[in] siglen the size of the DER encoded signature + * \param[in] tbs a pointer to the SHA256 digest + * \param[in] tbslen the size of the SHA256 digest + * \return true when successful + */ +bool verify(evp_pkey_st* pkey, const unsigned char* sig, std::size_t siglen, const unsigned char* tbs, + std::size_t tbslen); + +/** + * \brief calculate the SHA256 digest over an array of bytes + * \param[in] data the start of the data + * \param[in] len the length of the data + * \param[out] the SHA256 digest + * \return true on success + */ +bool sha_256(const void* data, std::size_t len, sha_256_digest_t& digest); + +/** + * \brief calculate the SHA384 digest over an array of bytes + * \param[in] data the start of the data + * \param[in] len the length of the data + * \param[out] the SHA384 digest + * \return true on success + */ +bool sha_384(const void* data, std::size_t len, sha_384_digest_t& digest); + +/** + * \brief calculate the SHA512 digest over an array of bytes + * \param[in] data the start of the data + * \param[in] len the length of the data + * \param[out] the SHA512 digest + * \return true on success + */ +bool sha_512(const void* data, std::size_t len, sha_512_digest_t& digest); + +/** + * \brief decode a base64 string into it's binary form + * \param[in] text the base64 string (does not need to be \0 terminated) + * \param[in] len the length of the string (excluding any terminating \0) + * \return binary array or empty on error + */ +std::vector base64_decode(const char* text, std::size_t len); + +/** + * \brief decode a base64 string into it's binary form + * \param[in] text the base64 string (does not need to be \0 terminated) + * \param[in] len the length of the string (excluding any terminating \0) + * \param[out] out_data where to place the decoded data + * \param[inout] out_len the size of out_data, updated to be the length of the decoded data + * \return true on success + */ +bool base64_decode(const char* text, std::size_t len, std::uint8_t* out_data, std::size_t& out_len); + +/** + * \brief encode data into a base64 text string + * \param[in] data the data to encode + * \param[in] len the length of the data + * \param[in] newLine when true add a \n to break the result into multiple lines + * \return base64 string or empty on error + */ +std::string base64_encode(const std::uint8_t* data, std::size_t len, bool newLine); + +/** + * \brief encode data into a base64 text string + * \param[in] data the data to encode + * \param[in] len the length of the data + * \return base64 string or empty on error + * \note the return string doesn't include line breaks + */ +inline std::string base64_encode(const std::uint8_t* data, std::size_t len) { + return base64_encode(data, len, false); +} + +/** + * \brief zero a structure + * \param mem the structure to zero + */ +template constexpr void zero(T& mem) { + std::memset(mem.data(), 0, mem.size()); +} + +/** + * \brief convert R, S BIGNUM to DER signature + * \param[in] r the BIGNUM R component of the signature + * \param[in] s the BIGNUM S component of the signature + * \return The DER signature and it's length + */ +std::tuple bn_to_signature(const bn_t& r, const bn_t& s); + +/** + * \brief convert R, S BIGNUM to DER signature + * \param[in] r the BIGNUM R component of the signature (0-padded 32 bytes) + * \param[in] s the BIGNUM S component of the signature (0-padded 32 bytes) + * \return The DER signature and it's length + */ +std::tuple bn_to_signature(const std::uint8_t* r, const std::uint8_t* s); + +/** + * \brief convert DER signature into BIGNUM R and S components + * \param[out] r the BIGNUM R component of the signature + * \param[out] s the BIGNUM S component of the signature + * \param[in] sig_p a pointer to the DER encoded signature + * \param[in] len the length of the DER encoded signature + * \return true when successful + */ +bool signature_to_bn(openssl::bn_t& r, openssl::bn_t& s, const std::uint8_t* sig_p, std::size_t len); + +/** + * \brief load any PEM encoded certificates from a file + * \param[in] filename + * \return a list of 0 or more certificates + */ +CertificateList load_certificates(const char* filename); + +/** + * \brief convert a certificate to a PEM string + * \param[in] cert the certificate + * \return the PEM string or empty on error + */ +std::string certificate_to_pem(const x509_st* cert); + +/** + * \brief parse a DER (ASN.1) encoded certificate + * \param[in] der a pointer to the DER encoded certificate + * \param[in] len the length of the DER encoded certificate + * \return the certificate or empty unique_ptr on error + */ +Certificate_ptr der_to_certificate(const std::uint8_t* der, std::size_t len); + +/** + * \brief verify a certificate against a certificate chain and trust anchors + * \param[in] cert the certificate to verify - when nullptr the certificate must + * be the first certificate in the untrusted list + * \param[in] trust_anchors a list of trust anchors. Must not contain any + * intermediate CAs + * \param[in] untrusted intermediate CAs needed to form a chain from the leaf + * certificate to one of the supplied trust anchors + */ +verify_result_t verify_certificate(const x509_st* cert, const CertificateList& trust_anchors, + const CertificateList& untrusted); + +/** + * \brief extract the certificate subject as a dictionary of name/value pairs + * \param cert the certificate + * \return dictionary of the (short name, value) pairs + * \note short name examples "CN" for CommonName "OU" for OrganizationalUnit + * "C" for Country ... + */ +std::map certificate_subject(const x509_st* cert); + +/** + * \brief extract the subject public key from the certificate + * \param[in] cert the certificate + * \return a unique_ptr holding the key or empty on error + */ +PKey_ptr certificate_public_key(x509_st* cert); + +enum class log_level_t : std::uint8_t { + debug, + warning, + error, +}; + +/** + * \brief log an OpenSSL event + * \param[in] level the event level + * \param[in] str string to display + * \note any OpenSSL error is displayed after the string + */ +void log(log_level_t level, const std::string& str); + +static inline void log_error(const std::string& str) { + log(log_level_t::error, str); +} + +static inline void log_warning(const std::string& str) { + log(log_level_t::warning, str); +} + +static inline void log_debug(const std::string& str) { + log(log_level_t::debug, str); +} + +using log_handler_t = void (*)(log_level_t level, const std::string& err); + +/** + * \brief set log handler function + * \param[in] handler a pointer to the function + * \return the pointer to the previous handler or nullptr + * where there is no previous handler + */ +log_handler_t set_log_handler(log_handler_t handler); + +} // namespace openssl + +#endif // OPENSSL_UTIL_HPP_ diff --git a/lib/staging/tls/tests/CMakeLists.txt b/lib/staging/tls/tests/CMakeLists.txt new file mode 100644 index 000000000..263902667 --- /dev/null +++ b/lib/staging/tls/tests/CMakeLists.txt @@ -0,0 +1,114 @@ +find_package(OpenSSL 3) + +set(TLS_GTEST_NAME tls_test) +add_executable(${TLS_GTEST_NAME}) + +target_include_directories(${TLS_GTEST_NAME} PRIVATE + . .. ../../util +) + +target_compile_definitions(${TLS_GTEST_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${TLS_GTEST_NAME} PRIVATE + gtest_main.cpp + crypto_test.cpp + openssl_util_test.cpp + tls_test.cpp + ../openssl_conv.cpp + ../openssl_util.cpp + ../tls.cpp +) + +target_link_libraries(${TLS_GTEST_NAME} PRIVATE + GTest::gtest + OpenSSL::SSL + OpenSSL::Crypto + everest::evse_security +) + +set(TLS_MAIN_NAME tls_server) +add_executable(${TLS_MAIN_NAME}) + +target_include_directories(${TLS_MAIN_NAME} PRIVATE + . .. ../../util +) + +target_compile_definitions(${TLS_MAIN_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${TLS_MAIN_NAME} PRIVATE + tls_main.cpp + ../openssl_util.cpp + ../tls.cpp +) + +target_link_libraries(${TLS_MAIN_NAME} PRIVATE + OpenSSL::SSL + OpenSSL::Crypto +) + +set(TLS_CLIENT_NAME tls_client) +add_executable(${TLS_CLIENT_NAME}) + +target_include_directories(${TLS_CLIENT_NAME} PRIVATE + . .. ../../util +) + +target_compile_definitions(${TLS_CLIENT_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${TLS_CLIENT_NAME} PRIVATE + tls_client_main.cpp + ../openssl_util.cpp + ../tls.cpp +) + +target_link_libraries(${TLS_CLIENT_NAME} PRIVATE + OpenSSL::SSL + OpenSSL::Crypto +) + +set(TLS_PATCH_NAME patched_test) +add_executable(${TLS_PATCH_NAME}) + +target_include_directories(${TLS_PATCH_NAME} PRIVATE + . .. ../../util +) + +target_compile_definitions(${TLS_PATCH_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${TLS_PATCH_NAME} PRIVATE + patched_test.cpp + ../openssl_util.cpp + ../tls.cpp +) + +target_link_libraries(${TLS_PATCH_NAME} PRIVATE + GTest::gtest_main + OpenSSL::SSL + OpenSSL::Crypto +) + +install( + FILES + pki/iso_pkey.asn1 + pki/openssl-pki.conf + pki/ocsp_response.der + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" +) + +install( + PROGRAMS + pki/pki.sh + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" +) + +# tests don't run successfully in CI pipeline +# pki.sh not installed or run from wrong directory +# add_test(${TLS_GTEST_NAME} ${TLS_GTEST_NAME}) diff --git a/lib/staging/tls/tests/README.md b/lib/staging/tls/tests/README.md new file mode 100644 index 000000000..e9b25ad0c --- /dev/null +++ b/lib/staging/tls/tests/README.md @@ -0,0 +1,43 @@ + +# Tests + +Building tests: + +```sh +$ cd everest-core +$ mkdir build +$ cd build +$ cmake -GNinja -DEVEREST_CORE_BUILD_TESTING=ON .. +$ ninja install +``` + +`touch release.json` may be needed if it hasn't been created +(then re-run `ninja install`). + +## Unit tests + +- `./tls_test` and `./patched_test` +- automatically runs `pki.sh` +- run from the directory containing the executable + +## Standalone server + +- Run `pki.sh` to build the test certificates and keys +- use openssl_s_client to make test connections +- run from the directory containing the executable + +### Standalone TLS server + +Tests the Server class in isolation. + +- `./tls_server` +- connects to IPv4 and IPv6 +- only one connection at a time +- gracefully terminates after 30 seconds +- `valgrind` can be used to check memory allocations (should be none) +- requires client certificate and supports `status_request` extension +- s_client echos back what is typed + +```sh +openssl s_client -connect localhost:8444 -verify 2 -CAfile server_root_cert.pem -cert client_cert.pem -cert_chain client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname evse.pionix.de -status +``` diff --git a/lib/staging/tls/tests/crypto_test.cpp b/lib/staging/tls/tests/crypto_test.cpp new file mode 100644 index 000000000..b3b024576 --- /dev/null +++ b/lib/staging/tls/tests/crypto_test.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "gtest/gtest.h" + +#include +#include + +#include +#include + +#include +#include + +namespace { + +using evse_security::HashAlgorithm; +using evse_security::X509CertificateHierarchy; +using evse_security::X509Wrapper; +using openssl::load_certificates; +using openssl::conversions::to_X509Wrapper; + +TEST(evseSecurity, certificateHash) { + auto chain = load_certificates("client_chain.pem"); + ASSERT_GT(chain.size(), 0); + + std::vector certs; + + for (const auto& cert : chain) { + certs.push_back(to_X509Wrapper(cert.get())); + } + + for (std::uint8_t i = 0; i < certs.size() - 1; i++) { + SCOPED_TRACE("i=" + std::to_string(i)); + const auto& cert = certs[i]; + const auto& issuer = certs[i + 1]; + const auto resA = cert.get_certificate_hash_data(issuer); + EXPECT_EQ(resA.hash_algorithm, HashAlgorithm::SHA256); + } +} + +} // namespace diff --git a/lib/staging/tls/tests/gtest_main.cpp b/lib/staging/tls/tests/gtest_main.cpp new file mode 100644 index 000000000..477e18bba --- /dev/null +++ b/lib/staging/tls/tests/gtest_main.cpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include +#include +#include +#include + +#include + +int main(int argc, char** argv) { + // create test certificates and keys + if (std::system("./pki.sh") != 0) { + std::cerr << "Problem creating test certificates and keys" << std::endl; + char buf[PATH_MAX]; + if (getcwd(&buf[0], sizeof(buf)) != nullptr) { + std::cerr << "./pki.sh not found in " << buf << std::endl; + } + return 1; + } + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/lib/staging/tls/tests/openssl_util_test.cpp b/lib/staging/tls/tests/openssl_util_test.cpp new file mode 100644 index 000000000..970f863d5 --- /dev/null +++ b/lib/staging/tls/tests/openssl_util_test.cpp @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include + +namespace { + +template constexpr void setCharacters(T& dest, const std::string& s) { + dest.charactersLen = s.size(); + std::memcpy(&dest.characters[0], s.c_str(), s.size()); +} + +template constexpr void setBytes(T& dest, const std::uint8_t* b, std::size_t len) { + dest.bytesLen = len; + std::memcpy(&dest.bytes[0], b, len); +} + +struct test_vectors_t { + const char* input; + const std::uint8_t digest[32]; +}; + +constexpr std::uint8_t sign_test[] = {0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, + 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}; + +constexpr test_vectors_t sha_256_test[] = { + {"", {0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}}, + {"abc", {0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, + 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}}}; + +// Test vectors from ISO 15118-2 Section J.2 +// checked okay (see iso_priv.pem) +constexpr std::uint8_t iso_private_key[] = {0xb9, 0x13, 0x49, 0x63, 0xf5, 0x1c, 0x44, 0x14, 0x73, 0x84, 0x35, + 0x05, 0x7f, 0x97, 0xbb, 0xf1, 0x01, 0x0c, 0xab, 0xcb, 0x8d, 0xbd, + 0xe9, 0xc5, 0xd4, 0x81, 0x38, 0x39, 0x6a, 0xa9, 0x4b, 0x9d}; +// checked okay (see iso_priv.pem) +constexpr std::uint8_t iso_public_key[] = {0x43, 0xe4, 0xfc, 0x4c, 0xcb, 0x64, 0x39, 0x04, 0x27, 0x9c, 0x7a, 0x5e, 0x65, + 0x76, 0xb3, 0x23, 0xe5, 0x5e, 0xc7, 0x9f, 0xf0, 0xe5, 0xa4, 0x05, 0x6e, 0x33, + 0x40, 0x84, 0xcb, 0xc3, 0x36, 0xff, 0x46, 0xe4, 0x4c, 0x1a, 0xdd, 0xf6, 0x91, + 0x62, 0xe5, 0x19, 0x2c, 0x2a, 0x83, 0xfc, 0x2b, 0xca, 0x9d, 0x8f, 0x46, 0xec, + 0xf4, 0xb7, 0x80, 0x67, 0xc2, 0x47, 0x6f, 0x6b, 0x3f, 0x34, 0x60, 0x0e}; + +// EXI AuthorizationReq: checked okay (hash computes correctly) +constexpr std::uint8_t iso_exi_a[] = {0x80, 0x04, 0x01, 0x52, 0x51, 0x0c, 0x40, 0x82, 0x9b, 0x7b, 0x6b, 0x29, 0x02, + 0x93, 0x0b, 0x73, 0x23, 0x7b, 0x69, 0x02, 0x23, 0x0b, 0xa3, 0x09, 0xe8}; + +// checked okay +constexpr std::uint8_t iso_exi_a_hash[] = {0xd1, 0xb5, 0xe0, 0x3d, 0x00, 0x65, 0xbe, 0xe5, 0x6b, 0x31, 0x79, + 0x84, 0x45, 0x30, 0x51, 0xeb, 0x54, 0xca, 0x18, 0xfc, 0x0e, 0x09, + 0x16, 0x17, 0x4f, 0x8b, 0x3c, 0x77, 0xa9, 0x8f, 0x4a, 0xa9}; + +// EXI AuthorizationReq signature block: checked okay (hash computes correctly) +constexpr std::uint8_t iso_exi_b[] = { + 0x80, 0x81, 0x12, 0xb4, 0x3a, 0x3a, 0x38, 0x1d, 0x17, 0x97, 0xbb, 0xbb, 0xbb, 0x97, 0x3b, 0x99, 0x97, 0x37, 0xb9, + 0x33, 0x97, 0xaa, 0x29, 0x17, 0xb1, 0xb0, 0xb7, 0x37, 0xb7, 0x34, 0xb1, 0xb0, 0xb6, 0x16, 0xb2, 0xbc, 0x34, 0x97, + 0xa1, 0xab, 0x43, 0xa3, 0xa3, 0x81, 0xd1, 0x79, 0x7b, 0xbb, 0xbb, 0xb9, 0x73, 0xb9, 0x99, 0x73, 0x7b, 0x93, 0x39, + 0x79, 0x91, 0x81, 0x81, 0x89, 0x79, 0x81, 0xa1, 0x7b, 0xc3, 0x6b, 0x63, 0x23, 0x9b, 0x4b, 0x39, 0x6b, 0x6b, 0x7b, + 0x93, 0x29, 0x1b, 0x2b, 0x1b, 0x23, 0x9b, 0x09, 0x6b, 0x9b, 0x43, 0x09, 0x91, 0xa9, 0xb2, 0x20, 0x62, 0x34, 0x94, + 0x43, 0x10, 0x25, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72, + 0x67, 0x2f, 0x54, 0x52, 0x2f, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x2d, 0x65, 0x78, 0x69, 0x2f, + 0x48, 0x52, 0xd0, 0xe8, 0xe8, 0xe0, 0x74, 0x5e, 0x5e, 0xee, 0xee, 0xee, 0x5c, 0xee, 0x66, 0x5c, 0xde, 0xe4, 0xce, + 0x5e, 0x64, 0x60, 0x60, 0x62, 0x5e, 0x60, 0x68, 0x5e, 0xf0, 0xda, 0xd8, 0xca, 0xdc, 0xc6, 0x46, 0xe6, 0xd0, 0xc2, + 0x64, 0x6a, 0x6c, 0x84, 0x1a, 0x36, 0xbc, 0x07, 0xa0, 0x0c, 0xb7, 0xdc, 0xad, 0x66, 0x2f, 0x30, 0x88, 0xa6, 0x0a, + 0x3d, 0x6a, 0x99, 0x43, 0x1f, 0x81, 0xc1, 0x22, 0xc2, 0xe9, 0xf1, 0x67, 0x8e, 0xf5, 0x31, 0xe9, 0x55, 0x23, 0x70}; + +// checked okay +constexpr std::uint8_t iso_exi_b_hash[] = {0xa4, 0xe9, 0x03, 0xe1, 0x82, 0x43, 0x04, 0x1b, 0x55, 0x4e, 0x11, + 0x64, 0x7e, 0x10, 0x1e, 0xd2, 0x5f, 0xc9, 0xf2, 0x15, 0x2a, 0xf4, + 0x67, 0x40, 0x14, 0xfe, 0x2a, 0xde, 0xac, 0x1e, 0x1c, 0xf7}; + +// checked okay (verifies iso_exi_b_hash with iso_priv.pem) +constexpr std::uint8_t iso_exi_sig[] = {0x4c, 0x8f, 0x20, 0xc1, 0x40, 0x0b, 0xa6, 0x76, 0x06, 0xaa, 0x48, 0x11, 0x57, + 0x2a, 0x2f, 0x1a, 0xd3, 0xc1, 0x50, 0x89, 0xd9, 0x54, 0x20, 0x36, 0x34, 0x30, + 0xbb, 0x26, 0xb4, 0x9d, 0xb1, 0x04, 0xf0, 0x8d, 0xfa, 0x8b, 0xf8, 0x05, 0x5e, + 0x63, 0xa4, 0xb7, 0x5a, 0x8d, 0x31, 0x69, 0x20, 0x6f, 0xa8, 0xd5, 0x43, 0x08, + 0xba, 0x58, 0xf0, 0x56, 0x6b, 0x96, 0xba, 0xf6, 0x92, 0xce, 0x59, 0x50}; + +const char iso_exi_a_hash_b64[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk="; +const char iso_exi_a_hash_b64_nl[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=\n"; + +const char iso_exi_sig_b64[] = + "TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBvqNVDCLpY8FZrlrr2ks5ZUA=="; +const char iso_exi_sig_b64_nl[] = + "TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBv\nqNVDCLpY8FZrlrr2ks5ZUA==\n"; + +TEST(util, removeHyphen) { + const std::string expected{"UKSWI123456791A"}; + std::string cert_emaid{"UKSWI123456791A"}; + + EXPECT_EQ(cert_emaid, expected); + cert_emaid.erase(std::remove(cert_emaid.begin(), cert_emaid.end(), '-'), cert_emaid.end()); + EXPECT_EQ(cert_emaid, expected); + + cert_emaid = std::string{"-UKSWI-123456791-A-"}; + cert_emaid.erase(std::remove(cert_emaid.begin(), cert_emaid.end(), '-'), cert_emaid.end()); + EXPECT_EQ(cert_emaid, expected); +} + +TEST(openssl, base64Encode) { + auto res = openssl::base64_encode(&iso_exi_a_hash[0], sizeof(iso_exi_a_hash)); + EXPECT_EQ(res, iso_exi_a_hash_b64); + res = openssl::base64_encode(&iso_exi_sig[0], sizeof(iso_exi_sig)); + EXPECT_EQ(res, iso_exi_sig_b64); +} + +TEST(openssl, base64EncodeNl) { + auto res = openssl::base64_encode(&iso_exi_a_hash[0], sizeof(iso_exi_a_hash), true); + EXPECT_EQ(res, iso_exi_a_hash_b64_nl); + res = openssl::base64_encode(&iso_exi_sig[0], sizeof(iso_exi_sig), true); + EXPECT_EQ(res, iso_exi_sig_b64_nl); +} + +TEST(openssl, base64Decode) { + auto res = openssl::base64_decode(&iso_exi_a_hash_b64[0], sizeof(iso_exi_a_hash_b64)); + ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash)); + EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0); + res = openssl::base64_decode(&iso_exi_sig_b64[0], sizeof(iso_exi_sig_b64)); + ASSERT_EQ(res.size(), sizeof(iso_exi_sig)); + EXPECT_EQ(std::memcmp(res.data(), &iso_exi_sig[0], res.size()), 0); + + std::array buffer{}; + std::size_t buffer_len = buffer.size(); + + EXPECT_TRUE(openssl::base64_decode(&iso_exi_a_hash_b64[0], sizeof(iso_exi_a_hash_b64), buffer.data(), buffer_len)); + ASSERT_EQ(buffer_len, sizeof(iso_exi_a_hash)); + EXPECT_EQ(std::memcmp(buffer.data(), &iso_exi_a_hash[0], buffer_len), 0); +} + +TEST(openssl, base64DecodeNl) { + auto res = openssl::base64_decode(&iso_exi_a_hash_b64_nl[0], sizeof(iso_exi_a_hash_b64_nl)); + ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash)); + EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0); + res = openssl::base64_decode(&iso_exi_sig_b64_nl[0], sizeof(iso_exi_sig_b64_nl)); + ASSERT_EQ(res.size(), sizeof(iso_exi_sig)); + EXPECT_EQ(std::memcmp(res.data(), &iso_exi_sig[0], res.size()), 0); + + std::array buffer{}; + std::size_t buffer_len = buffer.size(); + + EXPECT_TRUE( + openssl::base64_decode(&iso_exi_a_hash_b64_nl[0], sizeof(iso_exi_a_hash_b64_nl), buffer.data(), buffer_len)); + ASSERT_EQ(buffer_len, sizeof(iso_exi_a_hash)); + EXPECT_EQ(std::memcmp(buffer.data(), &iso_exi_a_hash[0], buffer_len), 0); +} + +TEST(openssl, sha256) { + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(sha_256_test[0].input, 0, digest)); + EXPECT_EQ(std::memcmp(digest.data(), &sha_256_test[0].digest[0], 32), 0); + EXPECT_TRUE(openssl::sha_256(sha_256_test[1].input, 3, digest)); + EXPECT_EQ(std::memcmp(digest.data(), &sha_256_test[1].digest[0], 32), 0); +} + +TEST(openssl, sha256Exi) { + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(&iso_exi_a[0], sizeof(iso_exi_a), digest)); + EXPECT_EQ(std::memcmp(digest.data(), &iso_exi_a_hash[0], 32), 0); + + EXPECT_TRUE(openssl::sha_256(&iso_exi_b[0], sizeof(iso_exi_b), digest)); + EXPECT_EQ(std::memcmp(digest.data(), &iso_exi_b_hash[0], 32), 0); +} + +TEST(openssl, signVerify) { + auto* bio = BIO_new_file("server_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + + std::array sig_der{}; + std::size_t sig_der_len{sig_der.size()}; + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(&sign_test[0], openssl::sha_256_digest_size, digest)); + + EXPECT_TRUE(openssl::sign(pkey, sig_der.data(), sig_der_len, digest.data(), digest.size())); + EXPECT_TRUE(openssl::verify(pkey, sig_der.data(), sig_der_len, digest.data(), digest.size())); + + BIO_free(bio); + EVP_PKEY_free(pkey); +} + +TEST(openssl, signVerifyBn) { + auto* bio = BIO_new_file("server_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + + openssl::bn_t r; + openssl::bn_t s; + + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(&sign_test[0], openssl::sha_256_digest_size, digest)); + + EXPECT_TRUE(openssl::sign(pkey, r, s, digest)); + EXPECT_TRUE(openssl::verify(pkey, r, s, digest)); + + BIO_free(bio); + EVP_PKEY_free(pkey); +} + +TEST(openssl, signVerifyMix) { + auto* bio = BIO_new_file("server_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + + std::array sig_der; + std::size_t sig_der_len{sig_der.size()}; + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(&sign_test[0], openssl::sha_256_digest_size, digest)); + + EXPECT_TRUE(openssl::sign(pkey, sig_der.data(), sig_der_len, digest.data(), digest.size())); + + openssl::bn_t r; + openssl::bn_t s; + EXPECT_TRUE(openssl::signature_to_bn(r, s, sig_der.data(), sig_der_len)); + EXPECT_TRUE(openssl::verify(pkey, r, s, digest)); + + BIO_free(bio); + EVP_PKEY_free(pkey); +} + +TEST(openssl, signVerifyFail) { + auto bio = BIO_new_file("server_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + BIO_free(bio); + + bio = BIO_new_file("client_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey_inv = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + BIO_free(bio); + + std::array sig_der; + std::size_t sig_der_len{sig_der.size()}; + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(&sign_test[0], openssl::sha_256_digest_size, digest)); + + EXPECT_TRUE(openssl::sign(pkey, sig_der.data(), sig_der_len, digest.data(), digest.size())); + EXPECT_FALSE(openssl::verify(pkey_inv, sig_der.data(), sig_der_len, digest.data(), digest.size())); + + EVP_PKEY_free(pkey); + EVP_PKEY_free(pkey_inv); +} + +TEST(openssl, verifyIso) { + auto* bio = BIO_new_file("iso_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + BIO_free(bio); + + auto [sig, siglen] = openssl::bn_to_signature(&iso_exi_sig[0], &iso_exi_sig[32]); + EXPECT_TRUE(openssl::verify(pkey, sig.get(), siglen, &iso_exi_b_hash[0], sizeof(iso_exi_b_hash))); + EVP_PKEY_free(pkey); +} + +TEST(certificateLoad, single) { + auto certs = ::openssl::load_certificates("server_cert.pem"); + EXPECT_EQ(certs.size(), 1); +} + +TEST(certificateLoad, chain) { + auto certs = ::openssl::load_certificates("server_chain.pem"); + EXPECT_EQ(certs.size(), 2); +} + +TEST(certificateLoad, key) { + auto certs = ::openssl::load_certificates("server_priv.pem"); + EXPECT_EQ(certs.size(), 0); +} + +TEST(certificate, toPem) { + auto certs = ::openssl::load_certificates("client_ca_cert.pem"); + ASSERT_EQ(certs.size(), 1); + auto pem = ::openssl::certificate_to_pem(certs[0].get()); + EXPECT_FALSE(pem.empty()); + // std::cout << pem << std::endl; +} + +TEST(certificate, verify) { + auto client = ::openssl::load_certificates("client_cert.pem"); + auto chain = ::openssl::load_certificates("client_chain.pem"); + auto root = ::openssl::load_certificates("client_root_cert.pem"); + + ASSERT_EQ(client.size(), 1); + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + EXPECT_EQ(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::verified); +} + +TEST(certificate, verifyRemoveClientFromChain) { + auto client = ::openssl::load_certificates("client_cert.pem"); + auto chain = ::openssl::load_certificates("client_chain.pem"); + auto root = ::openssl::load_certificates("client_root_cert.pem"); + + ASSERT_EQ(client.size(), 1); + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + // client certificate is 1st in the list + openssl::CertificateList new_chain; + for (auto itt = std::next(chain.begin()); itt != chain.end(); itt++) { + new_chain.push_back(std::move(*itt)); + } + + EXPECT_EQ(::openssl::verify_certificate(client[0].get(), root, new_chain), openssl::verify_result_t::verified); +} + +TEST(certificate, verifyNoClient) { + // client certificate is in the chain + auto chain = ::openssl::load_certificates("client_chain.pem"); + auto root = ::openssl::load_certificates("client_root_cert.pem"); + + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + EXPECT_EQ(::openssl::verify_certificate(nullptr, root, chain), openssl::verify_result_t::verified); +} + +TEST(certificate, verifyFailWrongClient) { + auto client = ::openssl::load_certificates("server_cert.pem"); + auto chain = ::openssl::load_certificates("client_chain.pem"); + auto root = ::openssl::load_certificates("client_root_cert.pem"); + + ASSERT_EQ(client.size(), 1); + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::verified); +} + +TEST(certificate, verifyFailWrongRoot) { + auto client = ::openssl::load_certificates("client_cert.pem"); + auto chain = ::openssl::load_certificates("client_chain.pem"); + auto root = ::openssl::load_certificates("server_root_cert.pem"); + + ASSERT_EQ(client.size(), 1); + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::verified); +} + +TEST(certificate, verifyFailWrongChain) { + auto client = ::openssl::load_certificates("client_cert.pem"); + auto chain = ::openssl::load_certificates("server_chain.pem"); + auto root = ::openssl::load_certificates("client_root_cert.pem"); + + ASSERT_EQ(client.size(), 1); + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::verified); +} + +TEST(certificate, subjectName) { + auto chain = ::openssl::load_certificates("client_chain.pem"); + EXPECT_GT(chain.size(), 0); + + for (const auto& cert : chain) { + auto subject = ::openssl::certificate_subject(cert.get()); + EXPECT_GT(subject.size(), 0); +#if 0 + for (const auto& itt : subject) { + std::cout << itt.first << ": " << itt.second << std::endl; + } +#endif + } +} + +} // namespace diff --git a/lib/staging/tls/tests/patched_test.cpp b/lib/staging/tls/tests/patched_test.cpp new file mode 100644 index 000000000..119533569 --- /dev/null +++ b/lib/staging/tls/tests/patched_test.cpp @@ -0,0 +1,366 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +/** + * \file testing patched version of OpenSSL + * + * These tests will only pass on a patched version of OpenSSL. + * (they should compile and run fine with some test failures) + * + * It is recommended to also run tests alongside Wireshark + * e.g. `./patched_test --gtest_filter=OcspTest.TLS12` + * to check that the Server Hello record is correctly formed: + * - no status_request or status_request_v2 then no Certificate Status record + * - status_request or status_request_v2 then there is a Certificate Status record + * - never both status_request and status_request_v2 + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// ---------------------------------------------------------------------------- +// set up code + +void log_handler(openssl::log_level_t level, const std::string& str) { + switch (level) { + case openssl::log_level_t::debug: + std::cout << "DEBUG: " << str << std::endl; + break; + case openssl::log_level_t::warning: + std::cout << "WARN: " << str << std::endl; + break; + case openssl::log_level_t::error: + std::cerr << "ERROR: " << str << std::endl; + break; + default: + std::cerr << "Unknown: " << str << std::endl; + break; + } +} + +struct ClientTest : public tls::Client { + enum class flags_t { + status_request_cb, + status_request, + status_request_v2, + connected, + last = connected, + }; + util::AtomicEnumFlags flags; + + void reset() { + flags.reset(); + } + + virtual int status_request_cb(tls::Ssl* ctx) { + /* + * This callback is called when status_request or status_request_v2 extensions + * were present in the Client Hello. It doesn't mean that the extension is in + * the Server Hello SSL_get_tlsext_status_ocsp_resp() returns -1 in that case + */ + const unsigned char* response{nullptr}; + const auto total_length = SSL_get_tlsext_status_ocsp_resp(ctx, &response); +#if 0 + // could set a different flag to spot no extension + if (total_length != -1) { + // -1 is the extension isn't present + flags.set(flags_t::status_request_cb); + } +#else + flags.set(flags_t::status_request_cb); +#endif + if ((response != nullptr) && (total_length > 0)) { + switch (response[0]) { + case 0x30: + flags.set(flags_t::status_request); + break; + case 0x00: + flags.set(flags_t::status_request_v2); + break; + default: + break; + } + } + return 1; + // return tls::Client::status_request_cb(ctx); + } +}; + +void handler(std::shared_ptr& con) { + if (con->accept()) { + std::uint32_t count{0}; + std::array buffer{}; + bool bExit = false; + while (!bExit) { + std::size_t readbytes = 0; + std::size_t writebytes = 0; + + switch (con->read(buffer.data(), buffer.size(), readbytes)) { + case tls::Connection::result_t::success: + switch (con->write(buffer.data(), readbytes, writebytes)) { + case tls::Connection::result_t::success: + break; + case tls::Connection::result_t::timeout: + case tls::Connection::result_t::error: + default: + bExit = true; + break; + } + break; + case tls::Connection::result_t::timeout: + count++; + if (count > 10) { + bExit = true; + } + break; + case tls::Connection::result_t::error: + default: + bExit = true; + break; + } + } + con->shutdown(); + } +} + +void run_server(tls::Server& server) { + server.serve(&handler); +} + +class OcspTest : public testing::Test { +protected: + using flags_t = ClientTest::flags_t; + + tls::Server server; + tls::Server::config_t server_config; + std::thread server_thread; + ClientTest client; + tls::Client::config_t client_config; + + static void SetUpTestSuite() { + openssl::set_log_handler(log_handler); + struct sigaction action; + std::memset(&action, 0, sizeof(action)); + action.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &action, nullptr); + if (std::system("./pki.sh > /dev/null") != 0) { + std::cerr << "Problem creating test certificates and keys" << std::endl; + char buf[PATH_MAX]; + if (getcwd(&buf[0], sizeof(buf)) != nullptr) { + std::cerr << "./pki.sh not found in " << buf << std::endl; + } + exit(1); + } + } + + void SetUp() override { + server_config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + // server_config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; + server_config.ciphersuites = ""; + server_config.certificate_chain_file = "server_chain.pem"; + server_config.private_key_file = "server_priv.pem"; + // server_config.verify_locations_file = "client_root_cert.pem"; + server_config.ocsp_response_files = {"ocsp_response.der", "ocsp_response.der"}; + server_config.host = "localhost"; + server_config.service = "8444"; + server_config.ipv6_only = false; + server_config.verify_client = false; + server_config.io_timeout_ms = 100; + + client_config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + // client_config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; + // client_config.certificate_chain_file = "client_chain.pem"; + // client_config.private_key_file = "client_priv.pem"; + client_config.verify_locations_file = "server_root_cert.pem"; + client_config.io_timeout_ms = 100; + client_config.verify_server = false; + client_config.status_request = false; + client_config.status_request_v2 = false; + client.reset(); + } + + void TearDown() override { + server.stop(); + server.wait_stopped(); + if (server_thread.joinable()) { + server_thread.join(); + } + } + + void start() { + if (server.init(server_config)) { + server_thread = std::thread(&run_server, std::ref(server)); + server.wait_running(); + } + } + + void connect() { + client.init(client_config); + client.reset(); + // localhost works in some cases but not in the CI pipeline for IPv6 + // use ip6-localhost + auto connection = client.connect("localhost", "8444", false); + if (connection) { + if (connection->connect()) { + set(ClientTest::flags_t::connected); + connection->shutdown(); + } + } + } + + void set(flags_t flag) { + client.flags.set(flag); + } + + [[nodiscard]] bool is_set(flags_t flag) const { + return client.flags.is_set(flag); + } + + [[nodiscard]] bool is_reset(flags_t flag) const { + return client.flags.is_reset(flag); + } +}; + +// ---------------------------------------------------------------------------- +// The tests + +TEST_F(OcspTest, NonBlocking) { + // test shouldn't hang + start(); +} + +TEST_F(OcspTest, NonBlockingConnect) { + // test shouldn't hang + start(); + connect(); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_reset(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); +} + +TEST_F(OcspTest, TLS12) { + // test using TLS 1.2 + start(); + connect(); + // no status requested + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_reset(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = true; + connect(); + // status_request only + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_set(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = false; + client_config.status_request_v2 = true; + connect(); + // status_request_v2 only + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_set(flags_t::status_request_v2)); + + client_config.status_request = true; + connect(); + // status_request and status_request_v2 + // status_request_v2 is preferred over status_request + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_set(flags_t::status_request_v2)); +} + +TEST_F(OcspTest, TLS13) { + // test using TLS 1.3 + // there shouldn't be status_request_v2 responses + // TLS 1.3 still supports status_request however it is handled differently + // (which is handled within the OpenSSL API) + server_config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; + start(); + connect(); + // no status requested + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_reset(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = true; + connect(); + // status_request only + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_set(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = false; + client_config.status_request_v2 = true; + connect(); + // status_request_v2 only - ignored by server + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = true; + connect(); + // status_request and status_request_v2 + // status_request_v2 is ignored by server and status_request used + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_set(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); +} + +TEST_F(OcspTest, NoOcspFiles) { + // test using TLS 1.2 + server_config.ocsp_response_files.clear(); + + start(); + connect(); + // no status requested + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_reset(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = true; + connect(); + // status_request only + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = false; + client_config.status_request_v2 = true; + connect(); + // status_request_v2 only + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = true; + connect(); + // status_request and status_request_v2 + // status_request_v2 is preferred over status_request + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); +} + +} // namespace diff --git a/lib/staging/tls/tests/pki/.gitignore b/lib/staging/tls/tests/pki/.gitignore new file mode 100644 index 000000000..cfaad7611 --- /dev/null +++ b/lib/staging/tls/tests/pki/.gitignore @@ -0,0 +1 @@ +*.pem diff --git a/lib/staging/tls/tests/pki/iso_pkey.asn1 b/lib/staging/tls/tests/pki/iso_pkey.asn1 new file mode 100644 index 000000000..4d657c3b4 --- /dev/null +++ b/lib/staging/tls/tests/pki/iso_pkey.asn1 @@ -0,0 +1,11 @@ +asn1=SEQ:pkcs8c +[pkcs8c] +ver=INT:0 +algid=SEQ:algid +data=OCTWRAP,SEQ:sec1 +[algid] +alg=OID:id-ecPublicKey +parm=OID:prime256v1 +[sec1] +ver=INT:1 +privkey=FORMAT:HEX,OCT:b9134963f51c4414738435057f97bbf1010cabcb8dbde9c5d48138396aa94b9d diff --git a/lib/staging/tls/tests/pki/ocsp_response.der b/lib/staging/tls/tests/pki/ocsp_response.der new file mode 100644 index 0000000000000000000000000000000000000000..c79ef5c78fe05b7b64a5a89acb21874e8362cccc GIT binary patch literal 279 zcmXqLVie|LWLVI|$YapN$ic>`&Bn;e%5K2O$kO=Bpz*swWMW`qXklP#U}#_(Wl(HTXy9qU$;PV9$IK+f%D^J>F^08a z#@9+`_JAcZuQE=uvUkPvVmFORfIsI=RW`@{O=d=mH*#vUZEs*evcbdzXh+lnL70${ zsev(A$bgHDL#xf>oGlA86SD_{fh&_j{IhA@|5k61zL&n%wZ?8|ai?ODu-DV8wiQc$ v>~dXb(#NF8Fu`7>ddj44d#;{+66I$lnDhG1yH}~RwzBf>iB@`NqF)aHadlxp literal 0 HcmV?d00001 diff --git a/lib/staging/tls/tests/pki/openssl-pki.conf b/lib/staging/tls/tests/pki/openssl-pki.conf new file mode 100644 index 000000000..90147e4d8 --- /dev/null +++ b/lib/staging/tls/tests/pki/openssl-pki.conf @@ -0,0 +1,143 @@ +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 + +# server section +# ============== +[req_server_root] +distinguished_name = req_dn_server_root +utf8 = yes +prompt = no +req_extensions = v3_server_root + +[req_server_ca] +distinguished_name = req_dn_server_ca +utf8 = yes +prompt = no +req_extensions = v3_server_ca + +[req_server] +distinguished_name = req_dn_server +utf8 = yes +prompt = no +req_extensions = v3_server + +[req_dn_server_root] +C = GB +O = Pionix +L = London +CN = Root Trust Anchor + +[req_dn_server_ca] +C = GB +O = Pionix +L = London +CN = Intermediate CA + +[req_dn_server] +C = GB +O = Pionix +L = London +CN = 00000000 + +[req_dn_client] +C = GB +O = Pionix +L = London +CN = 12345678 + +[v3_server_root] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always +basicConstraints = critical, CA:true, pathlen:2 +keyUsage = keyCertSign, cRLSign + +[v3_server_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.245.1, DNS:evse.pionix.de + +# client section +# ============== +[req_client] +distinguished_name = req_dn_client +utf8 = yes +prompt = no +req_extensions = v3_client + +[req_client_root] +distinguished_name = req_dn_client_root +utf8 = yes +prompt = no +req_extensions = v3_client_root + +[req_client_ca] +distinguished_name = req_dn_client_ca +utf8 = yes +prompt = no +req_extensions = v3_client_ca + +[req_server] +distinguished_name = req_dn_server +utf8 = yes +prompt = no +req_extensions = v3_server + +[req_dn_client_root] +C = DE +O = Pionix +L = Frankfurt +CN = Root Trust Anchor + +[req_dn_client_ca] +C = DE +O = Pionix +L = Frankfurt +CN = Intermediate CA + +[req_dn_client] +C = DE +O = Pionix +L = Frankfurt +CN = 12345678 + +[v3_client_root] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always +basicConstraints = critical, CA:true, pathlen:2 +keyUsage = keyCertSign, cRLSign + +[v3_client_ca] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always +basicConstraints = critical, CA:true +keyUsage = keyCertSign, cRLSign + +[v3_client] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always +keyUsage = digitalSignature, keyEncipherment, keyAgreement +extendedKeyUsage = clientAuth diff --git a/lib/staging/tls/tests/pki/pki.sh b/lib/staging/tls/tests/pki/pki.sh new file mode 100755 index 000000000..feec62e92 --- /dev/null +++ b/lib/staging/tls/tests/pki/pki.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +cfg=openssl-pki.conf + +server_root_priv=server_root_priv.pem +server_ca_priv=server_ca_priv.pem +server_priv=server_priv.pem + +server_root_cert=server_root_cert.pem +server_ca_cert=server_ca_cert.pem +server_cert=server_cert.pem +server_chain=server_chain.pem + +client_root_priv=client_root_priv.pem +client_ca_priv=client_ca_priv.pem +client_priv=client_priv.pem + +client_root_cert=client_root_cert.pem +client_ca_cert=client_ca_cert.pem +client_cert=client_cert.pem +client_chain=client_chain.pem + +# generate keys +for i in ${server_root_priv} ${server_ca_priv} ${server_priv} \ + ${client_root_priv} ${client_ca_priv} ${client_priv} +do + openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out $i + chmod 644 $i +done + +export OPENSSL_CONF=${cfg} + +echo "Generate server root" +openssl req \ + -config ${cfg} -x509 -section req_server_root -extensions v3_server_root \ + -key ${server_root_priv} -out ${server_root_cert} +echo "Generate server ca" +openssl req \ + -config ${cfg} -x509 -section req_server_ca -extensions v3_server_ca \ + -key ${server_ca_priv} -CA ${server_root_cert} -CAkey ${server_root_priv} -out ${server_ca_cert} +echo "Generate server" +openssl req \ + -config ${cfg} -x509 -section req_server -extensions v3_server \ + -key ${server_priv} -CA ${server_ca_cert} -CAkey ${server_ca_priv} -out ${server_cert} +cat ${server_cert} ${server_ca_cert} > ${server_chain} + +echo "Generate client root" +openssl req \ + -config ${cfg} -x509 -section req_client_root -extensions v3_client_root \ + -key ${client_root_priv} -out ${client_root_cert} +echo "Generate client ca" +openssl req \ + -config ${cfg} -x509 -section req_client_ca -extensions v3_client_ca \ + -key ${client_ca_priv} -CA ${client_root_cert} -CAkey ${client_root_priv} -out ${client_ca_cert} +echo "Generate client" +openssl req \ + -config ${cfg} -x509 -section req_client -extensions v3_client \ + -key ${client_priv} -CA ${client_ca_cert} -CAkey ${client_ca_priv} -out ${client_cert} + +cat ${client_cert} ${client_ca_cert} > ${client_chain} + +# convert iso key to PEM +openssl asn1parse -genconf iso_pkey.asn1 -noout -out -| openssl pkey -inform der -out iso_priv.pem diff --git a/lib/staging/tls/tests/tls_client_main.cpp b/lib/staging/tls/tests/tls_client_main.cpp new file mode 100644 index 000000000..0baa90713 --- /dev/null +++ b/lib/staging/tls/tests/tls_client_main.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include + +#include +#include +#include +#include + +using namespace std::chrono_literals; + +namespace { +const char* short_opts = "h123"; +bool use_tls1_3{false}; +bool use_status_request{false}; +bool use_status_request_v2{false}; + +void parse_options(int argc, char** argv) { + int c; + + while ((c = getopt(argc, argv, short_opts)) != -1) { + switch (c) { + break; + case '1': + use_status_request = true; + break; + case '2': + use_status_request_v2 = true; + break; + case '3': + use_tls1_3 = true; + break; + case 'h': + case '?': + std::cout << "Usage: " << argv[0] << " [-1|-2|-3]" << std::endl; + std::cout << " -1 request status_request" << std::endl; + std::cout << " -2 request status_request_v2" << std::endl; + std::cout << " -3 use TLS 1.3 (TLS 1.2 otherwise)" << std::endl; + exit(1); + break; + default: + exit(2); + } + } +} +} // namespace + +int main(int argc, char** argv) { + parse_options(argc, argv); + + tls::Client client; + tls::Client::config_t config; + + if (use_tls1_3) { + config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; + std::cout << "use_tls1_3 true" << std::endl; + } else { + config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + config.ciphersuites = ""; // No TLS1.3 + std::cout << "use_tls1_3 false" << std::endl; + } + + config.certificate_chain_file = "client_chain.pem"; + config.private_key_file = "client_priv.pem"; + config.verify_locations_file = "server_root_cert.pem"; + config.io_timeout_ms = 500; + config.verify_server = false; + + if (use_status_request) { + config.status_request = true; + std::cout << "use_status_request true" << std::endl; + } else { + config.status_request = false; + std::cout << "use_status_request false" << std::endl; + } + + if (use_status_request_v2) { + config.status_request_v2 = true; + std::cout << "use_status_request_v2 true" << std::endl; + } else { + config.status_request_v2 = false; + std::cout << "use_status_request_v2 false" << std::endl; + } + + client.init(config); + + // localhost works in some cases but not in the CI pipeline + auto connection = client.connect("ip6-localhost", "8444", true); + if (connection) { + if (connection->connect()) { + std::array buffer{}; + std::size_t readbytes = 0; + std::cout << "about to read" << std::endl; + const auto res = connection->read(buffer.data(), buffer.size(), readbytes); + std::cout << (int)res << std::endl; + std::this_thread::sleep_for(1s); + connection->shutdown(); + } + } + + return 0; +} diff --git a/lib/staging/tls/tests/tls_main.cpp b/lib/staging/tls/tests/tls_main.cpp new file mode 100644 index 000000000..d6734f38d --- /dev/null +++ b/lib/staging/tls/tests/tls_main.cpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +/* + * testing options + * openssl s_client -connect localhost:8444 -verify 2 -CAfile server_root_cert.pem -cert client_cert.pem -cert_chain + * client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname evse.pionix.de -status + */ + +#include + +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +void handle_connection(std::shared_ptr& con) { + std::cout << "Connection" << std::endl; + if (con->accept()) { + std::uint32_t count{0}; + std::array buffer{}; + bool bExit = false; + while (!bExit) { + std::size_t readbytes = 0; + std::size_t writebytes = 0; + + switch (con->read(buffer.data(), buffer.size(), readbytes)) { + case tls::Connection::result_t::success: + switch (con->write(buffer.data(), readbytes, writebytes)) { + case tls::Connection::result_t::success: + break; + case tls::Connection::result_t::timeout: + case tls::Connection::result_t::error: + default: + bExit = true; + break; + } + break; + case tls::Connection::result_t::timeout: + count++; + if (count > 10) { + bExit = true; + } + break; + case tls::Connection::result_t::error: + default: + bExit = true; + break; + } + } + + con->shutdown(); + } + std::cout << "Connection closed" << std::endl; +} + +int main() { + tls::Server server; + tls::Server::config_t config; + +#if 0 + config.cipher_list = + "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384"; + config.ciphersuites = "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"; +#else + // config.cipher_list = "ECDHE-ECDSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256"; + config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; + // config.ciphersuites = ""; +#endif + config.certificate_chain_file = "server_chain.pem"; + config.private_key_file = "server_priv.pem"; + config.verify_locations_file = "client_root_cert.pem"; + // config.ocsp_response_files = {"ocsp_response.der", nullptr}; + config.ocsp_response_files = {"ocsp_response.der", "ocsp_response.der"}; + config.service = "8444"; + config.ipv6_only = false; + config.verify_client = true; + config.io_timeout_ms = 1000; + + std::thread stop([&server]() { + std::this_thread::sleep_for(30s); + server.stop(); + }); + + server.init(config); + server.wait_stopped(); + + // server.serve(&handle_connection); + server.serve([](auto con) { handle_connection(con); }); + server.wait_stopped(); + + stop.join(); + + return 0; +} diff --git a/lib/staging/tls/tests/tls_test.cpp b/lib/staging/tls/tests/tls_test.cpp new file mode 100644 index 000000000..2e3df0590 --- /dev/null +++ b/lib/staging/tls/tests/tls_test.cpp @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include +#include + +#include + +std::string to_string(const openssl::sha_256_digest_t& digest) { + std::stringstream string_stream; + string_stream << std::hex; + + for (int idx = 0; idx < digest.size(); ++idx) + string_stream << std::setw(2) << std::setfill('0') << (int)digest[idx]; + + return string_stream.str(); +} + +namespace { + +TEST(OcspCache, initEmpty) { + tls::OcspCache cache; + openssl::sha_256_digest_t digest{}; + auto res = cache.lookup(digest); + EXPECT_EQ(res.get(), nullptr); +} + +TEST(OcspCache, init) { + tls::OcspCache cache; + + auto chain = openssl::load_certificates("client_chain.pem"); + std::vector entries; + + openssl::sha_256_digest_t digest{}; + for (const auto& cert : chain) { + ASSERT_TRUE(tls::OcspCache::digest(digest, cert.get())); + // std::cout << "digest: " << to_string(digest) << std::endl; + entries.emplace_back(digest, "ocsp_response.der"); + } + + EXPECT_TRUE(cache.load(entries)); + // std::cout << "digest: " << to_string(digest) << std::endl; + auto res = cache.lookup(digest); + EXPECT_NE(res.get(), nullptr); +} + +} // namespace diff --git a/lib/staging/tls/tls.cpp b/lib/staging/tls/tls.cpp new file mode 100644 index 000000000..7ae9a13dd --- /dev/null +++ b/lib/staging/tls/tls.cpp @@ -0,0 +1,1435 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "tls.hpp" +#include "openssl_util.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef TLSEXT_STATUSTYPE_ocsp_multi +#define OPENSSL_PATCHED +#endif + +namespace std { +template <> class default_delete { +public: + void operator()(SSL* ptr) const { + ::SSL_free(ptr); + } +}; +template <> class default_delete { +public: + void operator()(SSL_CTX* ptr) const { + ::SSL_CTX_free(ptr); + } +}; +} // namespace std + +using ::openssl::log_error; + +namespace { + +/** + * \brief convert a big endian 3 byte (24 bit) unsigned value to uint32 + * \param[in] ptr the pointer to the most significant byte + * \return the interpreted value + */ +constexpr std::uint32_t uint24(const std::uint8_t* ptr) { + return (static_cast(ptr[0]) << 16U) | (static_cast(ptr[1]) << 8U) | + static_cast(ptr[2]); +} + +/** + * \brief convert a uint32 to big endian 3 byte (24 bit) value + * \param[in] ptr the pointer to the most significant byte + * \param[in] value the 24 bit value + */ +constexpr void uint24(std::uint8_t* ptr, std::uint32_t value) { + ptr[0] = (value >> 16U) & 0xffU; + ptr[1] = (value >> 8U) & 0xffU; + ptr[2] = value & 0xffU; +} + +// see https://datatracker.ietf.org/doc/html/rfc6961 +constexpr int TLSEXT_TYPE_status_request_v2 = 17; + +std::string to_string(const openssl::sha_256_digest_t& digest) { + std::stringstream string_stream; + string_stream << std::hex; + for (const auto& c : digest) { + string_stream << std::setw(2) << std::setfill('0') << static_cast(c); + } + return string_stream.str(); +} + +constexpr std::uint32_t c_shutdown_timeout_ms = 5000; // 5 seconds + +enum class ssl_error_t : std::uint8_t { + error, + error_ssl, + error_syscall, + none, + want_accept, + want_async, + want_async_job, + want_connect, + want_hello_cb, + want_read, + want_write, + want_x509_lookup, + zero_return, + timeout, // not an OpenSSL result +}; + +constexpr ssl_error_t convert(const int err) { + ssl_error_t res{ssl_error_t::error}; + switch (err) { + case SSL_ERROR_NONE: + res = ssl_error_t::none; + break; + case SSL_ERROR_ZERO_RETURN: + res = ssl_error_t::zero_return; + break; + case SSL_ERROR_WANT_READ: + res = ssl_error_t::want_read; + break; + case SSL_ERROR_WANT_WRITE: + res = ssl_error_t::want_write; + break; + case SSL_ERROR_WANT_CONNECT: + res = ssl_error_t::want_connect; + break; + case SSL_ERROR_WANT_ACCEPT: + res = ssl_error_t::want_accept; + break; + case SSL_ERROR_WANT_X509_LOOKUP: + res = ssl_error_t::want_x509_lookup; + break; + case SSL_ERROR_WANT_ASYNC: + res = ssl_error_t::want_async; + break; + case SSL_ERROR_WANT_ASYNC_JOB: + res = ssl_error_t::want_async_job; + break; + case SSL_ERROR_WANT_CLIENT_HELLO_CB: + res = ssl_error_t::want_hello_cb; + break; + case SSL_ERROR_SYSCALL: + res = ssl_error_t::error_syscall; + break; + case SSL_ERROR_SSL: + res = ssl_error_t::error_ssl; + break; + default: + log_error(std::string("Unexpected SSL_get_error: ") + std::to_string(static_cast(res))); + break; + }; + return res; +} + +enum class ssl_result_t : std::uint8_t { + error, + error_syscall, + success, + closed, + timeout, +}; + +constexpr ssl_result_t convert(ssl_error_t err) { + switch (err) { + case ssl_error_t::none: + return ssl_result_t::success; + case ssl_error_t::timeout: + return ssl_result_t::timeout; + case ssl_error_t::error_syscall: + case ssl_error_t::error_ssl: + return ssl_result_t::error_syscall; + case ssl_error_t::zero_return: + return ssl_result_t::closed; + case ssl_error_t::error: + case ssl_error_t::want_accept: + case ssl_error_t::want_async: + case ssl_error_t::want_async_job: + case ssl_error_t::want_connect: + case ssl_error_t::want_hello_cb: + case ssl_error_t::want_read: + case ssl_error_t::want_write: + case ssl_error_t::want_x509_lookup: + default: + return ssl_result_t::error; + } +} + +constexpr tls::Connection::result_t convert(ssl_result_t err) { + switch (err) { + case ssl_result_t::success: + return tls::Connection::result_t::success; + case ssl_result_t::timeout: + return tls::Connection::result_t::timeout; + case ssl_result_t::closed: + case ssl_result_t::error: + case ssl_result_t::error_syscall: + default: + return tls::Connection::result_t::error; + } +} + +int wait_for(int soc, bool forWrite, std::int32_t timeout_ms) { + std::int16_t event = POLLIN; + if (forWrite) { + event = POLLOUT; + } + std::array fds = {{{soc, event, 0}}}; + int poll_res{0}; + + for (;;) { + poll_res = poll(fds.data(), fds.size(), timeout_ms); + if (poll_res == -1) { + if (errno != EINTR) { + log_error(std::string("wait_for poll: ") + std::to_string(errno)); + break; + } + } + // timeout or event(s) + break; + } + + return poll_res; +} + +[[nodiscard]] ssl_result_t ssl_read(SSL* ctx, std::byte* buf, std::size_t num, std::size_t& readbytes, + std::int32_t timeout_ms); +[[nodiscard]] ssl_result_t ssl_write(SSL* ctx, const std::byte* buf, std::size_t num, std::size_t& writebytes, + std::int32_t timeout_ms); +[[nodiscard]] ssl_result_t ssl_accept(SSL* ctx, std::int32_t timeout_ms); +[[nodiscard]] ssl_result_t ssl_connect(SSL* ctx, std::int32_t timeout_ms); +void ssl_shutdown(SSL* ctx, std::int32_t timeout_ms); + +bool process_result(SSL* ctx, const std::string& operation, const int res, ssl_error_t& result, + std::int32_t timeout_ms) { + bool bLoop = false; + + if (res <= 0) { + const auto sslerr_raw = SSL_get_error(ctx, res); + result = convert(sslerr_raw); + switch (result) { + case ssl_error_t::none: + case ssl_error_t::zero_return: + break; + case ssl_error_t::want_accept: + case ssl_error_t::want_connect: + case ssl_error_t::want_read: + case ssl_error_t::want_write: + if (wait_for(SSL_get_fd(ctx), result == ssl_error_t::want_write, timeout_ms) > 0) { + bLoop = true; + } + result = ssl_error_t::timeout; + break; + case ssl_error_t::error_syscall: + if (errno != 0) { + log_error(operation + "SSL_ERROR_SYSCALL " + std::to_string(errno)); + } + break; + case ssl_error_t::error: + case ssl_error_t::error_ssl: + case ssl_error_t::want_async: + case ssl_error_t::want_async_job: + case ssl_error_t::want_hello_cb: + case ssl_error_t::want_x509_lookup: + default: + log_error(operation + std::to_string(res) + " " + std::to_string(sslerr_raw)); + break; + } + } else { + result = ssl_error_t::none; + } + + return bLoop; +} + +ssl_result_t ssl_read(SSL* ctx, std::byte* buf, const std::size_t num, std::size_t& readbytes, + std::int32_t timeout_ms) { + ssl_error_t result = ssl_error_t::error; + bool bLoop = ctx != nullptr; + while (bLoop) { + const auto res = SSL_read_ex(ctx, buf, num, &readbytes); + bLoop = process_result(ctx, "SSL_read: ", res, result, timeout_ms); + } + return convert(result); +}; + +ssl_result_t ssl_write(SSL* ctx, const std::byte* buf, const std::size_t num, std::size_t& writebytes, + std::int32_t timeout_ms) { + ssl_error_t result = ssl_error_t::error; + bool bLoop = ctx != nullptr; + while (bLoop) { + const auto res = SSL_write_ex(ctx, buf, num, &writebytes); + bLoop = process_result(ctx, "SSL_write: ", res, result, timeout_ms); + } + return convert(result); +} + +ssl_result_t ssl_accept(SSL* ctx, std::int32_t timeout_ms) { + ssl_error_t result = ssl_error_t::error; + bool bLoop = ctx != nullptr; + while (bLoop) { + const auto res = SSL_accept(ctx); + // 0 is handshake not successful + // < 0 is other error + bLoop = process_result(ctx, "SSL_accept: ", res, result, timeout_ms); + } + return convert(result); +} + +ssl_result_t ssl_connect(SSL* ctx, std::int32_t timeout_ms) { + ssl_error_t result = ssl_error_t::error; + bool bLoop = ctx != nullptr; + + while (bLoop) { + const auto res = SSL_connect(ctx); + // 0 is handshake not successful + // < 0 is other error + bLoop = process_result(ctx, "SSL_connect: ", res, result, timeout_ms); + } + return convert(result); +} + +void ssl_shutdown(SSL* ctx, std::int32_t timeout_ms) { + ssl_error_t result = ssl_error_t::error; + bool bLoop = ctx != nullptr; + while (bLoop) { + const auto res = SSL_shutdown(ctx); + bLoop = process_result(ctx, "SSL_shutdown: ", res, result, timeout_ms); + } +} + +bool configure_ssl_ctx(SSL_CTX* ctx, const char* ciphersuites, const char* cipher_list, + const char* certificate_chain_file, const char* private_key_file, + const char* private_key_password) { + bool bRes{true}; + + if (ctx == nullptr) { + log_error("server_init::SSL_CTX_new"); + bRes = false; + } else { + if (SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 0) { + log_error("SSL_CTX_set_min_proto_version"); + bRes = false; + } + if ((ciphersuites != nullptr) && (ciphersuites[0] == '\0')) { + // no cipher suites configured - don't use TLS 1.3 + // nullptr means use the defaults + if (SSL_CTX_set_max_proto_version(ctx, TLS1_2_VERSION) == 0) { + log_error("SSL_CTX_set_max_proto_version"); + bRes = false; + } + } + if (cipher_list != nullptr) { + if (SSL_CTX_set_cipher_list(ctx, cipher_list) == 0) { + log_error("SSL_CTX_set_cipher_list"); + bRes = false; + } + } + if (ciphersuites != nullptr) { + if (SSL_CTX_set_ciphersuites(ctx, ciphersuites) == 0) { + log_error("SSL_CTX_set_ciphersuites"); + bRes = false; + } + } + + if (certificate_chain_file != nullptr) { + if (SSL_CTX_use_certificate_chain_file(ctx, certificate_chain_file) != 1) { + log_error("SSL_CTX_use_certificate_chain_file"); + bRes = false; + } + } + + if (private_key_file != nullptr) { + // the password callback uses a non-const argument + void* pass_ptr{nullptr}; + std::string pass_str; + if (private_key_password != nullptr) { + pass_str = private_key_password; + pass_ptr = pass_str.data(); + } + SSL_CTX_set_default_passwd_cb_userdata(ctx, pass_ptr); + + if (SSL_CTX_use_PrivateKey_file(ctx, private_key_file, SSL_FILETYPE_PEM) != 1) { + log_error("SSL_CTX_use_PrivateKey_file"); + bRes = false; + } + if (SSL_CTX_check_private_key(ctx) != 1) { + log_error("SSL_CTX_check_private_key"); + bRes = false; + } + } + } + + return bRes; +} + +OCSP_RESPONSE* load_ocsp(const char* filename) { + // update the cache + OCSP_RESPONSE* resp{nullptr}; + + if (filename != nullptr) { + + BIO* bio_file = BIO_new_file(filename, "r"); + if (bio_file == nullptr) { + log_error(std::string("BIO_new_file: ") + filename); + } else { + resp = d2i_OCSP_RESPONSE_bio(bio_file, nullptr); + BIO_free(bio_file); + } + + if (resp == nullptr) { + log_error("d2i_OCSP_RESPONSE_bio"); + } + } + + return resp; +} + +} // namespace + +namespace tls { + +using SSL_ptr = std::unique_ptr; +using SSL_CTX_ptr = std::unique_ptr; +using OCSP_RESPONSE_ptr = std::shared_ptr; + +struct connection_ctx { + SSL_ptr ctx; + BIO* soc_bio{nullptr}; + int soc{0}; +}; + +struct ocsp_cache_ctx { + std::map cache; +}; + +struct server_ctx { + SSL_CTX_ptr ctx; +}; + +struct client_ctx { + SSL_CTX_ptr ctx; +}; + +// ---------------------------------------------------------------------------- +// OcspCache +OcspCache::OcspCache() : m_context(std::make_unique()) { +} + +OcspCache::~OcspCache() = default; + +bool OcspCache::load(const std::vector& filenames) { + assert(m_context != nullptr); + + bool bResult{true}; + + if (filenames.empty()) { + // clear the cache + std::lock_guard lock(mux); + m_context->cache.clear(); + } else { + std::map updates; + for (const auto& entry : filenames) { + const auto& digest = std::get(entry); + const auto* filename = std::get(entry); + + OCSP_RESPONSE* resp{nullptr}; + + if (filename != nullptr) { + resp = load_ocsp(filename); + if (resp == nullptr) { + bResult = false; + } + } + + if (resp != nullptr) { + updates[digest] = std::shared_ptr(resp, &::OCSP_RESPONSE_free); + } + } + + { + std::lock_guard lock(mux); + m_context->cache.swap(updates); + } + } + + return bResult; +} + +std::shared_ptr OcspCache::lookup(const openssl::sha_256_digest_t& digest) { + assert(m_context != nullptr); + + std::shared_ptr resp; + std::lock_guard lock(mux); + if (const auto itt = m_context->cache.find(digest); itt != m_context->cache.end()) { + resp = itt->second; + } else { + log_error("OcspCache::lookup: not in cache: " + to_string(digest)); + } + + return resp; +} + +bool OcspCache::digest(openssl::sha_256_digest_t& digest, const x509_st* cert) { + assert(cert != nullptr); + + bool bResult{false}; + const ASN1_BIT_STRING* signature{nullptr}; + const X509_ALGOR* alg{nullptr}; + X509_get0_signature(&signature, &alg, cert); + if (signature != nullptr) { + unsigned char* data{nullptr}; + const auto len = i2d_ASN1_BIT_STRING(signature, &data); + if (len > 0) { + bResult = openssl::sha_256(data, len, digest); + } + OPENSSL_free(data); + } + + return bResult; +} + +// ---------------------------------------------------------------------------- +// CertificateStatusRequestV2 + +bool CertificateStatusRequestV2::set_ocsp_response(const openssl::sha_256_digest_t& digest, SSL* ctx) { + bool bResult{false}; + auto response = m_cache.lookup(digest); + if (response) { + unsigned char* der{nullptr}; + auto len = i2d_OCSP_RESPONSE(response.get(), &der); + if (len > 0) { + bResult = SSL_set_tlsext_status_ocsp_resp(ctx, der, len) == 1; + if (bResult) { + SSL_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp); + } else { + log_error((std::string("SSL_set_tlsext_status_ocsp_resp"))); + OPENSSL_free(der); + } + } + } + return bResult; +} + +int CertificateStatusRequestV2::status_request_cb(SSL* ctx, void* object) { + // returns: + // - SSL_TLSEXT_ERR_OK response to client via SSL_set_tlsext_status_ocsp_resp + // - SSL_TLSEXT_ERR_NOACK no response to client + // - SSL_TLSEXT_ERR_ALERT_FATAL abort connection + bool bSet{false}; + bool tls_1_3{false}; + int result = SSL_TLSEXT_ERR_NOACK; + openssl::sha_256_digest_t digest{}; + + if (ctx != nullptr) { + const auto* cert = SSL_get_certificate(ctx); + bSet = OcspCache::digest(digest, cert); + } + + const auto* session = SSL_get0_session(ctx); + if (session != nullptr) { + tls_1_3 = SSL_SESSION_get_protocol_version(session) == TLS1_3_VERSION; + } + + if (!tls_1_3) { + auto* connection = reinterpret_cast(SSL_get_app_data(ctx)); + if (connection != nullptr) { + /* + * if there is a status_request_v2 then don't provide a status_request response + * unless this is TLS 1.3 where status_request_v2 is deprecated (not to be used) + */ + if (connection->has_status_request_v2()) { + bSet = false; + result = SSL_TLSEXT_ERR_NOACK; + } + } + } + + auto* ptr = reinterpret_cast(object); + if (bSet && (ptr != nullptr)) { + if (ptr->set_ocsp_response(digest, ctx)) { + result = SSL_TLSEXT_ERR_OK; + } + } + return result; +} + +bool CertificateStatusRequestV2::set_ocsp_v2_response(const std::vector& digests, SSL* ctx) { + /* + * There is no response in the extension. An additional handshake message is + * sent after the certificate (certificate status) that includes the + * actual response. + */ + + /* + * s->ext.status_expected, set to 1 to include the certificate status message + * s->ext.status_type, ocsp(1), ocsp_multi(2) + * s->ext.ocsp.resp, set by SSL_set_tlsext_status_ocsp_resp + * s->ext.ocsp.resp_len, set by SSL_set_tlsext_status_ocsp_resp + */ + + bool bResult{false}; + +#ifdef OPENSSL_PATCHED + if (ctx != nullptr) { + std::vector> response_list; + std::size_t total_size{0}; + + for (const auto& digest : digests) { + auto response = m_cache.lookup(digest); + if (response) { + unsigned char* der{nullptr}; + auto len = i2d_OCSP_RESPONSE(response.get(), &der); + if (len > 0) { + const std::size_t adjusted_len = len + 3; + total_size += adjusted_len; + // prefix the length of the DER encoded OCSP response + auto* der_len = static_cast(OPENSSL_malloc(adjusted_len)); + if (der_len != nullptr) { + uint24(der_len, len); + std::memcpy(&der_len[3], der, len); + response_list.emplace_back(adjusted_len, der_len); + } + OPENSSL_free(der); + } + } + } + + // don't include the extension when there are no OCSP responses + if (total_size > 0) { + std::size_t resp_len = total_size; + auto* resp = static_cast(OPENSSL_malloc(resp_len)); + if (resp == nullptr) { + resp_len = 0; + } else { + std::size_t idx{0}; + + for (auto& entry : response_list) { + auto len = entry.first; + auto* der = entry.second; + std::memcpy(&resp[idx], der, len); + OPENSSL_free(der); + idx += len; + } + } + + // SSL_set_tlsext_status_ocsp_resp sets the correct overall length + bResult = SSL_set_tlsext_status_ocsp_resp(ctx, resp, resp_len) == 1; + if (bResult) { + SSL_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp_multi); + SSL_set_tlsext_status_expected(ctx, 1); + } else { + log_error((std::string("SSL_set_tlsext_status_ocsp_resp"))); + } + } + } +#endif // OPENSSL_PATCHED + + return bResult; +} + +int CertificateStatusRequestV2::status_request_v2_add(Ssl* ctx, unsigned int ext_type, unsigned int context, + const unsigned char** out, std::size_t* outlen, Certificate* cert, + std::size_t chainidx, int* alert, void* object) { + /* + * return values: + * - fatal, abort handshake and sent TLS Alert: result = -1 and *alert = alert value + * - do not include extension: result = 0 + * - include extension: result = 1 + */ + + *out = nullptr; + *outlen = 0; + + int result = 0; + +#ifdef OPENSSL_PATCHED + openssl::sha_256_digest_t digest{}; + std::vector digest_chain; + + if (ctx != nullptr) { + const auto* cert = SSL_get_certificate(ctx); + const auto* name = X509_get_subject_name(cert); + if (OcspCache::digest(digest, cert)) { + digest_chain.push_back(digest); + } + + STACK_OF(X509) * chain{nullptr}; + + if (SSL_get0_chain_certs(ctx, &chain) != 1) { + log_error((std::string("SSL_get0_chain_certs"))); + } else { + const auto num = sk_X509_num(chain); + for (std::size_t i = 0; i < num; i++) { + cert = sk_X509_value(chain, i); + name = X509_get_subject_name(cert); + if (OcspCache::digest(digest, cert)) { + digest_chain.push_back(digest); + } + } + } + } + + auto* ptr = reinterpret_cast(object); + if (!digest_chain.empty() && (ptr != nullptr)) { + if (ptr->set_ocsp_v2_response(digest_chain, ctx)) { + result = 1; + } + } +#endif // OPENSSL_PATCHED + + return result; +} + +void CertificateStatusRequestV2::status_request_v2_free(Ssl* ctx, unsigned int ext_type, unsigned int context, + const unsigned char* out, void* object) { + OPENSSL_free(const_cast(out)); +} + +int CertificateStatusRequestV2::status_request_v2_cb(Ssl* ctx, unsigned int ext_type, unsigned int context, + const unsigned char* data, std::size_t datalen, X509* cert, + std::size_t chainidx, int* alert, void* object) { + /* + * return values: + * - fatal, abort handshake and sent TLS Alert: result = 0 or negative and *alert = alert value + * - success: result = 1 + */ + + // TODO(james-ctc): check requested type std, or multi + return 1; +} + +int CertificateStatusRequestV2::client_hello_cb(Ssl* ctx, int* alert, void* object) { + /* + * return values: + * - fatal, abort handshake and sent TLS Alert: result = 0 or negative and *alert = alert value + * - success: result = 1 + */ + + auto* connection = reinterpret_cast(SSL_get_app_data(ctx)); + if (connection != nullptr) { + int* extensions{nullptr}; + std::size_t length{0}; + if (SSL_client_hello_get1_extensions_present(ctx, &extensions, &length) == 1) { + for (std::size_t i = 0; i < length; i++) { + if (extensions[i] == TLSEXT_TYPE_status_request) { + connection->status_request_received(); + } else if (extensions[i] == TLSEXT_TYPE_status_request_v2) { + connection->status_request_v2_received(); + } + } + OPENSSL_free(extensions); + } + } + return 1; +} + +// ---------------------------------------------------------------------------- +// Connection represents a TLS connection + +Connection::Connection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms) : + m_context(std::make_unique()), m_ip(ip_in), m_service(service_in), m_timeout_ms(timeout_ms) { + m_context->ctx = SSL_ptr(SSL_new(ctx)); + m_context->soc = soc; + + if (m_context->ctx == nullptr) { + log_error("Connection::SSL_new"); + } else { + m_context->soc_bio = BIO_new_socket(soc, BIO_CLOSE); + } + + if (m_context->soc_bio == nullptr) { + log_error("Connection::BIO_new_socket"); + } +} + +Connection::~Connection() = default; + +Connection::result_t Connection::read(std::byte* buf, std::size_t num, std::size_t& readbytes) { + assert(m_context != nullptr); + ssl_result_t result{ssl_result_t::error}; + if (m_state == state_t::connected) { + result = ssl_read(m_context->ctx.get(), buf, num, readbytes, m_timeout_ms); + switch (result) { + case ssl_result_t::success: + case ssl_result_t::timeout: + break; + case ssl_result_t::error_syscall: + m_state = state_t::fault; + break; + case ssl_result_t::closed: + shutdown(); + break; + case ssl_result_t::error: + default: + shutdown(); + m_state = state_t::fault; + break; + } + } + return convert(result); +} + +Connection::result_t Connection::write(const std::byte* buf, std::size_t num, std::size_t& writebytes) { + assert(m_context != nullptr); + ssl_result_t result{ssl_result_t::error}; + if (m_state == state_t::connected) { + result = ssl_write(m_context->ctx.get(), buf, num, writebytes, m_timeout_ms); + switch (result) { + case ssl_result_t::success: + case ssl_result_t::timeout: + break; + case ssl_result_t::error_syscall: + m_state = state_t::fault; + break; + case ssl_result_t::closed: + shutdown(); + break; + case ssl_result_t::error: + default: + shutdown(); + m_state = state_t::fault; + break; + } + } + return convert(result); +} + +void Connection::shutdown() { + assert(m_context != nullptr); + if (m_state == state_t::connected) { + ssl_shutdown(m_context->ctx.get(), c_shutdown_timeout_ms); + m_state = state_t::closed; + } +} + +int Connection::socket() const { + return m_context->soc; +} + +// ---------------------------------------------------------------------------- +// ServerConnection represents a TLS server connection + +std::uint32_t ServerConnection::m_count{0}; +std::mutex ServerConnection::m_cv_mutex; +std::condition_variable ServerConnection::m_cv; + +ServerConnection::ServerConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, + std::int32_t timeout_ms) : + Connection(ctx, soc, ip_in, service_in, timeout_ms) { + { + std::lock_guard lock(m_cv_mutex); + m_count++; + } + if (m_context->soc_bio != nullptr) { + // BIO_free is handled when SSL_free is done (SSL_ptr) + SSL_set_bio(m_context->ctx.get(), m_context->soc_bio, m_context->soc_bio); + SSL_set_accept_state(m_context->ctx.get()); + SSL_set_app_data(m_context->ctx.get(), this); + } +} + +ServerConnection::~ServerConnection() { + { + std::lock_guard lock(m_cv_mutex); + m_count--; + } + m_cv.notify_all(); +}; + +bool ServerConnection::accept() { + assert(m_context != nullptr); + ssl_result_t result{ssl_result_t::error}; + if (m_state == state_t::idle) { + result = ssl_accept(m_context->ctx.get(), m_timeout_ms); + switch (result) { + case ssl_result_t::success: + m_state = state_t::connected; + break; + case ssl_result_t::error_syscall: + m_state = state_t::fault; + break; + case ssl_result_t::closed: + shutdown(); + break; + case ssl_result_t::error: + default: + shutdown(); + m_state = state_t::fault; + break; + } + } + return result == ssl_result_t::success; +} + +void ServerConnection::wait_all_closed() { + std::unique_lock lock(m_cv_mutex); + m_cv.wait(lock, [] { return m_count == 0; }); + lock.unlock(); +} + +// ---------------------------------------------------------------------------- +// ClientConnection represents a TLS server connection + +ClientConnection::ClientConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, + std::int32_t timeout_ms) : + Connection(ctx, soc, ip_in, service_in, timeout_ms) { + if (m_context->soc_bio != nullptr) { + BIO_set_nbio(m_context->soc_bio, 1); + // BIO_free is handled when SSL_free is done (SSL_ptr) + SSL_set_bio(m_context->ctx.get(), m_context->soc_bio, m_context->soc_bio); + SSL_set_connect_state(m_context->ctx.get()); + } +} + +ClientConnection::~ClientConnection() = default; + +bool ClientConnection::connect() { + assert(m_context != nullptr); + ssl_result_t result{ssl_result_t::error}; + if (m_state == state_t::idle) { + result = ssl_connect(m_context->ctx.get(), m_timeout_ms); + switch (result) { + case ssl_result_t::success: + m_state = state_t::connected; + break; + case ssl_result_t::error_syscall: + m_state = state_t::fault; + break; + case ssl_result_t::closed: + shutdown(); + break; + case ssl_result_t::error: + default: + shutdown(); + m_state = state_t::fault; + break; + } + } + return result == ssl_result_t::success; +} + +// ---------------------------------------------------------------------------- +// TLS Server + +Server::Server() : m_context(std::make_unique()), m_status_request_v2(m_cache) { +} + +Server::~Server() { + stop(); + wait_stopped(); +} + +bool Server::init_socket(const config_t& cfg) { + bool bRes = false; + if (cfg.socket == INVALID_SOCKET) { + BIO_ADDRINFO* addrinfo{nullptr}; + + bRes = BIO_lookup_ex(cfg.host, cfg.service, BIO_LOOKUP_SERVER, AF_UNSPEC, SOCK_STREAM, IPPROTO_TCP, + &addrinfo) != 0; + + if (!bRes) { + log_error("init_socket::BIO_lookup_ex"); + } else { + const auto sock_family = BIO_ADDRINFO_family(addrinfo); + const auto sock_type = BIO_ADDRINFO_socktype(addrinfo); + const auto sock_protocol = BIO_ADDRINFO_protocol(addrinfo); + const auto* sock_address = BIO_ADDRINFO_address(addrinfo); + int sock_options{BIO_SOCK_REUSEADDR | BIO_SOCK_NONBLOCK}; + if (cfg.ipv6_only) { + sock_options = BIO_SOCK_REUSEADDR | BIO_SOCK_V6_ONLY | BIO_SOCK_NONBLOCK; + } + + m_socket = BIO_socket(sock_family, sock_type, sock_protocol, 0); + + if (m_socket == INVALID_SOCKET) { + log_error("init_socket::BIO_socket"); + } else { + bRes = BIO_listen(m_socket, sock_address, sock_options) != 0; + if (!bRes) { + log_error("init_socket::BIO_listen"); + BIO_closesocket(m_socket); + m_socket = INVALID_SOCKET; + } + } + } + + BIO_ADDRINFO_free(addrinfo); + } else { + // the code that sets cfg.socket is responsible for + // all socket initialisation + m_socket = cfg.socket; + bRes = true; + } + + return bRes; +} + +bool Server::init_ssl(const config_t& cfg) { + assert(m_context != nullptr); + + // TODO(james-ctc) TPM2 support + + const SSL_METHOD* method = TLS_server_method(); + auto* ctx = SSL_CTX_new(method); + auto bRes = configure_ssl_ctx(ctx, cfg.ciphersuites, cfg.cipher_list, cfg.certificate_chain_file, + cfg.private_key_file, cfg.private_key_password); + if (bRes) { + int mode = SSL_VERIFY_NONE; + + if (cfg.verify_client) { + mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + if (SSL_CTX_load_verify_locations(ctx, cfg.verify_locations_file, cfg.verify_locations_path) != 1) { + log_error("SSL_CTX_load_verify_locations"); + } + } else { + if (SSL_CTX_set_default_verify_paths(ctx) != 1) { + log_error("SSL_CTX_set_default_verify_paths"); + bRes = false; + } + } + + SSL_CTX_set_verify(ctx, mode, nullptr); + SSL_CTX_set_client_hello_cb(ctx, &CertificateStatusRequestV2::client_hello_cb, nullptr); + + if (SSL_CTX_set_tlsext_status_cb(ctx, &CertificateStatusRequestV2::status_request_cb) != 1) { + log_error("SSL_CTX_set_tlsext_status_cb"); + bRes = false; + } + + if (SSL_CTX_set_tlsext_status_arg(ctx, &m_status_request_v2) != 1) { + log_error("SSL_CTX_set_tlsext_status_arg"); + bRes = false; + } + + constexpr int context = SSL_EXT_TLS_ONLY | SSL_EXT_TLS1_2_AND_BELOW_ONLY | SSL_EXT_IGNORE_ON_RESUMPTION | + SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO; + if (SSL_CTX_add_custom_ext(ctx, TLSEXT_TYPE_status_request_v2, context, + &CertificateStatusRequestV2::status_request_v2_add, + &CertificateStatusRequestV2::status_request_v2_free, &m_status_request_v2, + &CertificateStatusRequestV2::status_request_v2_cb, nullptr) != 1) { + log_error("SSL_CTX_add_custom_ext"); + bRes = false; + } + } + + if (!bRes) { + SSL_CTX_free(ctx); + ctx = nullptr; + } + + m_context->ctx = SSL_CTX_ptr(ctx); + return ctx != nullptr; +} + +bool Server::init(const config_t& cfg) { + std::lock_guard lock(m_mutex); + (void)update_ocsp(cfg); + m_timeout_ms = cfg.io_timeout_ms; + bool bRes = init_ssl(cfg); + bRes = bRes && init_socket(cfg); + m_state = state_t::init; + return bRes; +} + +bool Server::update_ocsp(const config_t& cfg) { + std::vector entries; + auto chain = openssl::load_certificates(cfg.certificate_chain_file); + bool bRes = chain.size() == cfg.ocsp_response_files.size(); + + if (bRes) { + for (std::size_t i = 0; i < chain.size(); i++) { + const auto& file = cfg.ocsp_response_files[i]; + const auto& cert = chain[i]; + + if (file != nullptr) { + openssl::sha_256_digest_t digest{}; + if (OcspCache::digest(digest, cert.get())) { + entries.emplace_back(digest, file); + } + } + } + + bRes = m_cache.load(entries); + } else { + log_error(std::string("update_ocsp: ocsp files != ") + std::to_string(chain.size())); + } + + return bRes; +} + +bool Server::serve(const std::function& ctx)>& handler) { + assert(m_context != nullptr); + // prevent init() or server() being called while serve is running + std::lock_guard lock(m_mutex); + bool bRes = false; + { + std::lock_guard lock(m_cv_mutex); + m_running = true; + } + m_cv.notify_all(); + + if (m_socket != INVALID_SOCKET) { + m_exit = false; + m_state = state_t::running; + while (!m_exit) { + auto* peer = BIO_ADDR_new(); + if (peer == nullptr) { + log_error("serve::BIO_ADDR_new"); + m_exit = true; + } else { + int soc{INVALID_SOCKET}; + while ((soc < 0) && !m_exit) { + auto poll_res = wait_for(m_socket, false, m_timeout_ms); + if (poll_res == -1) { + log_error(std::string("Server::serve poll: ") + std::to_string(errno)); + m_exit = true; + } else if (poll_res == 0) { + // timeout + } else { + soc = BIO_accept_ex(m_socket, peer, BIO_SOCK_NONBLOCK); + if (BIO_sock_should_retry(soc) == 0) { + break; + } + } + }; + + if (m_exit) { + if (soc >= 0) { + BIO_closesocket(soc); + } + } else { + if (soc < 0) { + log_error("serve::BIO_accept_ex"); + } else { + auto* ip = BIO_ADDR_hostname_string(peer, 1); + auto* service = BIO_ADDR_service_string(peer, 1); + auto connection = + std::make_shared(m_context->ctx.get(), soc, ip, service, m_timeout_ms); + handler(connection); + OPENSSL_free(ip); + OPENSSL_free(service); + } + } + } + + BIO_ADDR_free(peer); + } + + BIO_closesocket(m_socket); + m_socket = INVALID_SOCKET; + bRes = true; + m_state = state_t::stopped; + } + + { + std::lock_guard lock(m_cv_mutex); + m_running = false; + } + m_cv.notify_all(); + return bRes; +} + +void Server::stop() { + m_exit = true; +} + +void Server::wait_running() { + std::unique_lock lock(m_cv_mutex); + m_cv.wait(lock, [this] { return m_running; }); + lock.unlock(); +} + +void Server::wait_stopped() { + std::unique_lock lock(m_cv_mutex); + m_cv.wait(lock, [this] { return !m_running; }); + lock.unlock(); +} + +// ---------------------------------------------------------------------------- +// Client + +Client::Client() : m_context(std::make_unique()) { +} + +Client::~Client() = default; + +bool Client::print_ocsp_response(FILE* stream, const unsigned char*& response, std::size_t length) { + OCSP_RESPONSE* ocsp{nullptr}; + + if (response != nullptr) { + ocsp = d2i_OCSP_RESPONSE(nullptr, &response, static_cast(length)); + if (ocsp == nullptr) { + std::cerr << "d2i_OCSP_RESPONSE: decode error" << std::endl; + } else { + BIO* bio_out = BIO_new_fp(stream, BIO_NOCLOSE); + OCSP_RESPONSE_print(bio_out, ocsp, 0); + OCSP_RESPONSE_free(ocsp); + BIO_free(bio_out); + } + } + + return ocsp != nullptr; +} + +bool Client::init(const config_t& cfg) { + assert(m_context != nullptr); + + // TODO(james-ctc) TPM2 support + + const SSL_METHOD* method = TLS_client_method(); + auto* ctx = SSL_CTX_new(method); + auto bRes = configure_ssl_ctx(ctx, cfg.ciphersuites, cfg.cipher_list, cfg.certificate_chain_file, + cfg.private_key_file, cfg.private_key_password); + if (bRes) { + int mode = SSL_VERIFY_NONE; + + if (cfg.verify_server) { + mode = SSL_VERIFY_PEER; + if (SSL_CTX_load_verify_locations(ctx, cfg.verify_locations_file, cfg.verify_locations_path) != 1) { + log_error("SSL_CTX_load_verify_locations"); + } + } else { + if (SSL_CTX_set_default_verify_paths(ctx) != 1) { + log_error("SSL_CTX_set_default_verify_paths"); + bRes = false; + } + } + + SSL_CTX_set_verify(ctx, mode, nullptr); + + if (cfg.status_request) { + if (SSL_CTX_set_tlsext_status_cb(ctx, &Client::status_request_v2_multi_cb) != 1) { + log_error("SSL_CTX_set_tlsext_status_cb"); + bRes = false; + } + if (SSL_CTX_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp) != 1) { + log_error("SSL_CTX_set_tlsext_status_type"); + bRes = false; + } + } + + if (cfg.status_request_v2) { + constexpr int context = SSL_EXT_TLS_ONLY | SSL_EXT_TLS1_2_AND_BELOW_ONLY | SSL_EXT_IGNORE_ON_RESUMPTION | + SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO; + if (SSL_CTX_add_custom_ext(ctx, TLSEXT_TYPE_status_request_v2, context, &Client::status_request_v2_add, + nullptr, nullptr, &Client::status_request_v2_cb, this) != 1) { + log_error("SSL_CTX_add_custom_ext"); + bRes = false; + } + if (SSL_CTX_set_tlsext_status_cb(ctx, &Client::status_request_v2_multi_cb) != 1) { + log_error("SSL_CTX_set_tlsext_status_cb"); + bRes = false; + } + } + + if (cfg.status_request || cfg.status_request_v2) { + if (SSL_CTX_set_tlsext_status_arg(ctx, this) != 1) { + log_error("SSL_CTX_set_tlsext_status_arg"); + bRes = false; + } + } + } + + if (bRes) { + m_context->ctx = SSL_CTX_ptr(ctx); + m_state = state_t::init; + } else { + SSL_CTX_free(ctx); + ctx = nullptr; + } + + return ctx != nullptr; +} + +std::unique_ptr Client::connect(const char* host, const char* service, bool ipv6_only) { + BIO_ADDRINFO* addrinfo{nullptr}; + std::unique_ptr result; + + const int family = (ipv6_only) ? AF_INET6 : AF_UNSPEC; + bool bRes = BIO_lookup_ex(host, service, BIO_LOOKUP_CLIENT, family, SOCK_STREAM, IPPROTO_TCP, &addrinfo) != 0; + + if (!bRes) { + log_error("connect::BIO_lookup_ex"); + } else { + const auto sock_family = BIO_ADDRINFO_family(addrinfo); + const auto sock_type = BIO_ADDRINFO_socktype(addrinfo); + const auto sock_protocol = BIO_ADDRINFO_protocol(addrinfo); + const auto* sock_address = BIO_ADDRINFO_address(addrinfo); + + // set non-blocking after a successful connection + // using BIO_SOCK_NONBLOCK on connect is problematic + // int sock_options{BIO_SOCK_NONBLOCK}; + + auto socket = BIO_socket(sock_family, sock_type, sock_protocol, 0); + + if (socket == INVALID_SOCKET) { + log_error("connect::BIO_socket"); + } else { + if (BIO_connect(socket, sock_address, 0) != 1) { + log_error("connect::BIO_connect"); + } else { + result = std::make_unique(m_context->ctx.get(), socket, host, service, m_timeout_ms); + } + } + } + + BIO_ADDRINFO_free(addrinfo); + return result; +} + +int Client::status_request_cb(Ssl* ctx) { + /* + * This callback is called when status_request or status_request_v2 extensions + * were present in the Client Hello. It doesn't mean that the extension is in + * the Server Hello SSL_get_tlsext_status_ocsp_resp() returns -1 in that case + */ + + /* + * The callback when used on the client side should return + * a negative value on error, + * 0 if the response is not acceptable (in which case the handshake will fail), or + * a positive value if it is acceptable. + */ + + int result{1}; + + const unsigned char* response{nullptr}; + const auto total_length = SSL_get_tlsext_status_ocsp_resp(ctx, &response); + // length == -1 on no response and response will be nullptr + + if ((response != nullptr) && (total_length > 0)) { + // there is a response + + if (response[0] == 0x30) { + // not a multi response + if (!print_ocsp_response(stdout, response, total_length)) { + result = 0; + } + } else { + // multiple responses + auto remaining{total_length}; + const unsigned char* ptr{response}; + + while (remaining > 0) { + bool b_okay = remaining > 3; + std::uint32_t len{0}; + + if (b_okay) { + len = uint24(ptr); + remaining -= len + 3; + b_okay = remaining >= 0; + } + + if (b_okay) { + ptr += 3; + b_okay = print_ocsp_response(stdout, ptr, len); + } + + if (!b_okay) { + result = 0; + remaining = -1; + } + } + } + } + + return result; +} + +int Client::status_request_v2_multi_cb(Ssl* ctx, void* object) { + /* + * This callback is called when status_request or status_request_v2 extensions + * were present in the Client Hello. It doesn't mean that the extension is in + * the Server Hello SSL_get_tlsext_status_ocsp_resp() returns -1 in that case + */ + + /* + * The callback when used on the client side should return + * a negative value on error, + * 0 if the response is not acceptable (in which case the handshake will fail), or + * a positive value if it is acceptable. + */ + + auto* client_ptr = reinterpret_cast(object); + + int result{1}; + if (client_ptr != nullptr) { + result = client_ptr->status_request_cb(ctx); + } else { + log_error("Client::status_request_v2_multi_cb missing Client *"); + } + return result; +} + +int Client::status_request_v2_add(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char** out, + std::size_t* outlen, Certificate* cert, std::size_t chainidx, int* alert, + void* object) { + if (context == SSL_EXT_CLIENT_HELLO) { + /* + * CertificateStatusRequestListV2: + * 0x0007 struct CertificateStatusRequestItemV2 + length + * 0x02 CertificateStatusType - OCSP multi + * 0x0004 request_length (uint 16) + * 0x0000 struct ResponderID list + length + * 0x0000 struct Extensions + length + */ + // don't use constexpr + static const std::uint8_t asn1[] = {0x00, 0x07, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00}; + *out = &asn1[0]; + *outlen = sizeof(asn1); +#ifdef OPENSSL_PATCHED + /* + * ensure client callback is called - SSL_set_tlsext_status_type() needs to have a value + * TLSEXT_STATUSTYPE_ocsp_multi for status_request_v2, or + * TLSEXT_STATUSTYPE_ocsp for status_request and status_request_v2 + */ + + if (SSL_get_tlsext_status_type(ctx) != TLSEXT_STATUSTYPE_ocsp) { + SSL_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp_multi); + } +#endif // OPENSSL_PATCHED + return 1; + } + return 0; +} + +int Client::status_request_v2_cb(SSL* ctx, unsigned int ext_type, unsigned int context, const unsigned char* data, + std::size_t datalen, X509* cert, std::size_t chainidx, int* alert, void* object) { +#ifdef OPENSSL_PATCHED + SSL_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp_multi); + SSL_set_tlsext_status_expected(ctx, 1); +#endif // OPENSSL_PATCHED + + return 1; +} + +} // namespace tls diff --git a/lib/staging/tls/tls.hpp b/lib/staging/tls/tls.hpp new file mode 100644 index 000000000..f77a88bb6 --- /dev/null +++ b/lib/staging/tls/tls.hpp @@ -0,0 +1,609 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef TLS_HPP_ +#define TLS_HPP_ + +#include "openssl_util.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +struct ocsp_response_st; +struct ssl_ctx_st; +struct ssl_st; +struct x509_st; + +namespace tls { + +constexpr int INVALID_SOCKET{-1}; + +using Certificate = struct ::x509_st; +using OcspResponse = struct ::ocsp_response_st; +using Ssl = struct ::ssl_st; +using SslContext = struct ::ssl_ctx_st; + +struct connection_ctx; +struct ocsp_cache_ctx; +struct server_ctx; +struct client_ctx; + +// ---------------------------------------------------------------------------- +// Cache of OCSP responses for status_request and status_request_v2 extensions + +/** + * \brief cache of OCSP responses + * \note responses can be updated at any time via load() + */ +class OcspCache { +public: + using ocsp_entry_t = std::tuple; + +private: + std::unique_ptr m_context; + std::mutex mux; //!< protects the cached OCSP responses + +public: + OcspCache(); + OcspCache(const OcspCache&) = delete; + OcspCache(OcspCache&&) = delete; + OcspCache& operator=(const OcspCache&) = delete; + OcspCache& operator=(OcspCache&&) = delete; + ~OcspCache(); + + bool load(const std::vector& filenames); + std::shared_ptr lookup(const openssl::sha_256_digest_t& digest); + static bool digest(openssl::sha_256_digest_t& digest, const x509_st* cert); +}; + +// ---------------------------------------------------------------------------- +// TLS handshake extension status_request amd status_request_v2 support + +/** + * \brief TLS status_request and status_request_v2 support + */ +class CertificateStatusRequestV2 { +private: + OcspCache& m_cache; + +public: + explicit CertificateStatusRequestV2(OcspCache& cache) : m_cache(cache) { + } + CertificateStatusRequestV2() = delete; + CertificateStatusRequestV2(const CertificateStatusRequestV2&) = delete; + CertificateStatusRequestV2(CertificateStatusRequestV2&&) = delete; + CertificateStatusRequestV2& operator=(const CertificateStatusRequestV2&) = delete; + CertificateStatusRequestV2& operator=(CertificateStatusRequestV2&&) = delete; + ~CertificateStatusRequestV2() = default; + + /** + * \brief set the OCSP reponse for the SSL context + * \param[in] digest the certificate requested + * \param[in] ctx the connection context + * \return true on success + * \return for status_request extension + */ + bool set_ocsp_response(const openssl::sha_256_digest_t& digest, Ssl* ctx); + + /** + * \brief the OpenSSL callback for the status_request extension + * \param[in] ctx the connection context + * \param[in] object the instance of a CertificateStatusRequest + * \return SSL_TLSEXT_ERR_OK on success and SSL_TLSEXT_ERR_NOACK on error + */ + static int status_request_cb(Ssl* ctx, void* object); + + /** + * \brief set the OCSP reponse for the SSL context + * \param[in] digest the certificate requested + * \param[in] ctx the connection context + * \return true on success + * \return for status_request_v2 extension + */ + bool set_ocsp_v2_response(const std::vector& digests, Ssl* ctx); + + /** + * \brief add status_request_v2 extension to server hello + * \param[in] ctx the connection context + * \param[in] ext_type the TLS extension + * \param[in] context the extension context flags + * \param[in] out pointer to the extension data + * \param[in] outlen size of extension data + * \param[in] cert certificate + * \param[in] chainidx certificate chain index + * \param[in] alert the alert to send on error + * \param[in] object the instance of a CertificateStatusRequestV2 + * \return success = 1, do not include = 0, error == -1 + */ + static int status_request_v2_add(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char** out, + std::size_t* outlen, Certificate* cert, std::size_t chainidx, int* alert, + void* object); + + /** + * \brief free status_request_v2 extension added to server hello + * \param[in] ctx the connection context + * \param[in] ext_type the TLS extension + * \param[in] context the extension context flags + * \param[in] out pointer to the extension data + * \param[in] object the instance of a CertificateStatusRequestV2 + */ + static void status_request_v2_free(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char* out, + void* object); + + /** + * \brief the OpenSSL callback for the status_request_v2 extension + * \param[in] ctx the connection context + * \param[in] ext_type the TLS extension + * \param[in] context the extension context flags + * \param[in] data pointer to the extension data + * \param[in] datalen size of extension data + * \param[in] cert certificate + * \param[in] chainidx certificate chain index + * \param[in] alert the alert to send on error + * \param[in] object the instance of a CertificateStatusRequestV2 + * \return success = 1, error = zero or negative + */ + static int status_request_v2_cb(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char* data, + std::size_t datalen, Certificate* cert, std::size_t chainidx, int* alert, + void* object); + + /** + * \brief the OpenSSL callback for the client hello record + * \param[in] ctx the connection context + * \param[in] alert the alert to send on error + * \param[in] object the instance of a CertificateStatusRequestV2 + * \return success = 1, error = zero or negative + * + * This callback has early access to the extensions requested by the client. + * It is used to determine whether status_request and status_request_v2 + * have been requested so that status_request_v2 can take priority. + */ + static int client_hello_cb(Ssl* ctx, int* alert, void* object); +}; + +// ---------------------------------------------------------------------------- +// Connection represents a TLS connection + +/** + * \brief class representing a TLS connection + */ +class Connection { +public: + /** + * \brief connection state + */ + enum class state_t : std::uint8_t { + idle, //!< no connection in progress + connected, //!< active connection + closed, //!< connection has closed + fault, //!< connection has faulted + }; + + enum class result_t : std::uint8_t { + success, + error, + timeout, + }; + +protected: + std::unique_ptr m_context; + state_t m_state{state_t::idle}; + std::string m_ip; + std::string m_service; + std::int32_t m_timeout_ms; + +public: + Connection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms); + Connection() = delete; + Connection(const Connection&) = delete; + Connection(Connection&&) = delete; + Connection& operator=(const Connection&) = delete; + Connection& operator=(Connection&&) = delete; + ~Connection(); + + /** + * \brief read bytes from the TLS connection + * \param[out] buf pointer to output buffer + * \param[in] num size of output buffer + * \param[out] readBytes number of received bytes. May be less than num + * when there has been a timeout + * \return success, error, or timeout. On error the connection will have been closed + */ + [[nodiscard]] result_t read(std::byte* buf, std::size_t num, std::size_t& readbytes); + + /** + * \brief write bytes to the TLS connection + * \param[in] buf pointer to input buffer + * \param[in] num size of input buffer + * \param[out] writeBytes number of sent bytes. May be less than num + * when there has been a timeout + * \return success, error, or timeout. On error the connection will have been closed + */ + [[nodiscard]] result_t write(const std::byte* buf, std::size_t num, std::size_t& writebytes); + + /** + * \brief close the TLS connection + */ + void shutdown(); + + /** + * IP address of the connection's peer + */ + [[nodiscard]] const std::string& ip_address() const { + return m_ip; + } + + /** + * Service (port number) of the connection's peer + */ + [[nodiscard]] const std::string& service() const { + return m_service; + } + + /** + * \brief return the current state + * \return the current state + */ + [[nodiscard]] state_t state() const { + return m_state; + } + + /** + * \brief obtain the underlying socket for use with poll or select + * \returns the underlying socket or INVALID_SOCKET on error + */ + [[nodiscard]] int socket() const; +}; + +/** + * \brief class representing a TLS connection + */ +class ServerConnection : public Connection { +private: + static std::uint32_t m_count; + static std::mutex m_cv_mutex; + static std::condition_variable m_cv; + + enum class flags_t : std::uint8_t { + status_request, + status_request_v2, + last = status_request_v2, + }; + + util::AtomicEnumFlags flags; + +public: + ServerConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms); + ServerConnection() = delete; + ServerConnection(const ServerConnection&) = delete; + ServerConnection(ServerConnection&&) = delete; + ServerConnection& operator=(const ServerConnection&) = delete; + ServerConnection& operator=(ServerConnection&&) = delete; + ~ServerConnection(); + + void status_request_received() { + flags.set(flags_t::status_request); + } + void status_request_v2_received() { + flags.set(flags_t::status_request_v2); + } + [[nodiscard]] bool has_status_request() const { + return flags.is_set(flags_t::status_request); + } + [[nodiscard]] bool has_status_request_v2() const { + return flags.is_set(flags_t::status_request_v2); + } + + /** + * \brief accept the incoming connection and run the TLS handshake + * \return true when the TLS connection has been established + */ + [[nodiscard]] bool accept(); + + /** + * \brief wait for all connections to be closed + */ + static void wait_all_closed(); + + /** + * \brief return number of active connections (indicative only) + * \return number of active connections + */ + static std::uint32_t active_connections() { + return m_count; + } +}; + +/** + * \brief class representing a TLS connection + */ +class ClientConnection : public Connection { +public: + ClientConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms); + ClientConnection() = delete; + ClientConnection(const ClientConnection&) = delete; + ClientConnection(ClientConnection&&) = delete; + ClientConnection& operator=(const ClientConnection&) = delete; + ClientConnection& operator=(ClientConnection&&) = delete; + ~ClientConnection(); + + /** + * \brief run the TLS handshake + * \return true when the TLS connection has been established + */ + [[nodiscard]] bool connect(); +}; + +// ---------------------------------------------------------------------------- +// TLS server + +/** + * \brief represents a TLS server + * + * The TLS server does not make use of any threads. This is to support multiple + * use cases giving developers the option on how best to support multiple + * incoming connections. + * + * Example code in tests has some examples on how this can be done. + * + * One option is to have a server thread calling Server.serve() with the supplied + * handler creating a new connection thread when a connection arrives. + * + * For unit tests the connection handler can perform the test directly which + * has the advantage of preventing new connections from being accepted. + * + * Another option is for the connection handler to add the new connection to a list + * which is serviced by an event handler - i.e. one thread could manage all connections. + */ +class Server { +public: + /** + * \brief server state + */ + enum class state_t : std::uint8_t { + need_init, //!< not initialised yet + init, //!< initialised but not running + running, //!< waiting for connections + stopped, //!< stopped + }; + + struct config_t { + const char* cipher_list{nullptr}; // nullptr means use default + const char* ciphersuites{nullptr}; // nullptr means use default, "" disables TSL 1.3 + const char* certificate_chain_file{nullptr}; + const char* private_key_file{nullptr}; + const char* private_key_password{nullptr}; + const char* verify_locations_file{nullptr}; // for client certificate + const char* verify_locations_path{nullptr}; // for client certificate + const char* host{nullptr}; // see BIO_lookup_ex() + const char* service{nullptr}; // TLS port number + std::vector ocsp_response_files; // in certificate chain order + int socket{INVALID_SOCKET}; // use this specific socket - bypasses socket setup in init_socket() when set + std::int32_t io_timeout_ms{-1}; // socket timeout in milliseconds + bool ipv6_only{true}; + bool verify_client{true}; + }; + +private: + std::unique_ptr m_context; + int m_socket{INVALID_SOCKET}; + bool m_running{false}; + std::int32_t m_timeout_ms{-1}; + std::atomic_bool m_exit{false}; + std::atomic m_state{state_t::need_init}; + std::mutex m_mutex; + std::mutex m_cv_mutex; + std::condition_variable m_cv; + OcspCache m_cache; + CertificateStatusRequestV2 m_status_request_v2; + + /** + * \brief initialise the server socket + * \param[in] cfg server configuration + * \return true on success + */ + bool init_socket(const config_t& cfg); + + /** + * \brief initialise TLS configuration + * \param[in] cfg server configuration + * \return true on success + */ + bool init_ssl(const config_t& cfg); + +public: + Server(); + Server(const Server&) = delete; + Server(Server&&) = delete; + Server& operator=(const Server&) = delete; + Server& operator=(Server&&) = delete; + ~Server(); + + /** + * \brief initialise the server socket and TLS configuration + * \param[in] cfg server configuration + * \return true on success + * \note when the server certificate and key change then the server needs + * to be stopped, initialised and start serving. + */ + bool init(const config_t& cfg); + + /** + * \brief update the OCSP cache + * \param[in] cfg server configuration + * \return true on success + * \note used to update OCSP caches + */ + bool update_ocsp(const config_t& cfg); + + /** + * \brief wait for incomming connections + * \param[in] handler called when there is a new connection + * \return false when there was an error listening for connections + * \note this is a blocking call that will not return until stop() has been called. + */ + bool serve(const std::function& ctx)>& handler); + + /** + * \brief stop listening for new connections + * \note returns immediately + */ + void stop(); + + /** + * \brief wait for server to start listening for connections + * \note blocks until server is running + */ + void wait_running(); + + /** + * \brief wait for server to stop + * \note blocks until server has stopped + */ + void wait_stopped(); + + /** + * \brief return the current server state (indicative only) + * \return current server state + */ + [[nodiscard]] state_t state() const { + return m_state; + } +}; + +// ---------------------------------------------------------------------------- +// TLS client + +/** + * \brief represents a TLS client + */ +class Client { +public: + /** + * \brief client state + */ + enum class state_t : std::uint8_t { + need_init, //!< not initialised yet + init, //!< initialised but not connected + connected, //!< connected to server + stopped, //!< stopped + }; + + struct config_t { + const char* cipher_list{nullptr}; // nullptr means use default + const char* ciphersuites{nullptr}; // nullptr means use default, "" disables TSL 1.3 + const char* certificate_chain_file{nullptr}; + const char* private_key_file{nullptr}; + const char* private_key_password{nullptr}; + const char* verify_locations_file{nullptr}; // for client certificate + const char* verify_locations_path{nullptr}; // for client certificate + std::int32_t io_timeout_ms{-1}; // socket timeout in milliseconds + bool verify_server{true}; + bool status_request{false}; + bool status_request_v2{false}; + }; + +private: + std::unique_ptr m_context; + std::int32_t m_timeout_ms{-1}; + std::atomic m_state{state_t::need_init}; + +public: + Client(); + Client(const Client&) = delete; + Client(Client&&) = delete; + Client& operator=(const Client&) = delete; + Client& operator=(Client&&) = delete; + virtual ~Client(); + + static bool print_ocsp_response(FILE* stream, const unsigned char*& response, std::size_t length); + + /** + * \brief initialise the client socket and TLS configuration + * \param[in] cfg server configuration + * \return true on success + * \note when the server certificate and key change then the client needs + * to be stopped, initialised and start serving. + */ + bool init(const config_t& cfg); + + /** + * \brief connect to server + * \param[in] host host to connect to + * \param[in] service port to connect to + * \param[in] ipv6_only false - also support IPv4 + * \return a connection pointer (nullptr on error) + */ + std::unique_ptr connect(const char* host, const char* service, bool ipv6_only); + + /** + * \brief return the current server state (indicative only) + * \return current server state + */ + [[nodiscard]] state_t state() const { + return m_state; + } + + /** + * \brief the OpenSSL callback for the status_request and status_request_v2 extensions + * \param[in] ctx the connection context + * \return SSL_TLSEXT_ERR_OK on success and SSL_TLSEXT_ERR_NOACK on error + */ + virtual int status_request_cb(Ssl* ctx); + + /** + * \brief the OpenSSL callback for the status_request_v2 extension + * \param[in] ctx the connection context + * \param[in] object the instance of a CertificateStatusRequest + * \return SSL_TLSEXT_ERR_OK on success and SSL_TLSEXT_ERR_NOACK on error + */ + static int status_request_v2_multi_cb(Ssl* ctx, void* object); + + /** + * \brief add status_request_v2 extension to client hello + * \param[in] ctx the connection context + * \param[in] ext_type the TLS extension + * \param[in] context the extension context flags + * \param[in] out pointer to the extension data + * \param[in] outlen size of extension data + * \param[in] cert certificate + * \param[in] chainidx certificate chain index + * \param[in] alert the alert to send on error + * \param[in] object the instance of a CertificateStatusRequestV2 + * \return success = 1, do not include = 0, error == -1 + */ + static int status_request_v2_add(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char** out, + std::size_t* outlen, Certificate* cert, std::size_t chainidx, int* alert, + void* object); + + /** + * \brief the OpenSSL callback for the status_request_v2 extension + * \param[in] ctx the connection context + * \param[in] ext_type the TLS extension + * \param[in] context the extension context flags + * \param[in] data pointer to the extension data + * \param[in] datalen size of extension data + * \param[in] cert certificate + * \param[in] chainidx certificate chain index + * \param[in] alert the alert to send on error + * \param[in] object the instance of a CertificateStatusRequestV2 + * \return success = 1, error = zero or negative + */ + static int status_request_v2_cb(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char* data, + std::size_t datalen, Certificate* cert, std::size_t chainidx, int* alert, + void* object); +}; + +} // namespace tls + +#endif // TLS_HPP_ diff --git a/lib/staging/util/EnumFlags.hpp b/lib/staging/util/EnumFlags.hpp new file mode 100644 index 000000000..af892001e --- /dev/null +++ b/lib/staging/util/EnumFlags.hpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef ENUMFLAGS_HPP +#define ENUMFLAGS_HPP + +#include +#include +#include + +namespace util { + +template struct AtomicEnumFlags { + static_assert(std::is_enum() == true, "Not enum"); + static_assert(std::is_integral() == true, "Not integer"); + static_assert((sizeof(B) * 8) >= static_cast(T::last) + 1, "Underlying flag type too small"); + std::atomic _value{0ULL}; + + constexpr std::size_t bit(const T& flag) const { + return 1ULL << static_cast>(flag); + } + + constexpr void set(const T& flag, bool value) { + if (value) { + set(flag); + } else { + reset(flag); + } + } + + constexpr void set(const T& flag) { + _value |= bit(flag); + } + + constexpr void reset(const T& flag) { + _value &= ~bit(flag); + } + + constexpr void reset() { + _value = 0ULL; + } + + [[nodiscard]] constexpr bool all_reset() const { + return _value == 0ULL; + } + + constexpr bool is_set(const T& flag) const { + return (_value & bit(flag)) != 0; + } + + constexpr bool is_reset(const T& flag) const { + return (_value & bit(flag)) == 0; + } +}; + +} // namespace util +#endif diff --git a/modules/EvseV2G/CMakeLists.txt b/modules/EvseV2G/CMakeLists.txt index f2756c830..a2fed2d58 100644 --- a/modules/EvseV2G/CMakeLists.txt +++ b/modules/EvseV2G/CMakeLists.txt @@ -9,6 +9,13 @@ ev_setup_cpp_module() # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 # insert your custom targets and additional config variables here +option(USING_MBED_TLS "Use MbedTLS for V2G" ON) + +if(USING_MBED_TLS) +target_compile_definitions(${MODULE_NAME} PRIVATE + EVEREST_MBED_TLS +) +endif() # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 target_sources(${MODULE_NAME} @@ -23,13 +30,15 @@ find_package(PkgConfig REQUIRED) # search for libevent.pc pkg_search_module(EVENT REQUIRED libevent) +target_include_directories(${MODULE_NAME} PRIVATE + crypto + connection +) + target_link_libraries(${MODULE_NAME} PUBLIC ${EVENT_LIBRARIES} -levent -lpthread -levent_pthreads) target_link_libraries(${MODULE_NAME} PRIVATE - mbedcrypto - mbedtls - mbedx509 cbv2g::din cbv2g::iso2 cbv2g::tp @@ -37,13 +46,44 @@ target_link_libraries(${MODULE_NAME} target_sources(${MODULE_NAME} PRIVATE - "v2g_ctx.cpp" + "connection/connection.cpp" + "iso_server.cpp" + "din_server.cpp" "log.cpp" "sdp.cpp" - "connection.cpp" "tools.cpp" + "v2g_ctx.cpp" "v2g_server.cpp" - "iso_server.cpp" - "din_server.cpp" ) + +if(USING_MBED_TLS) +# needed for header file enum definition +target_include_directories(${MODULE_NAME} PRIVATE + ../../lib/staging/tls +) +target_link_libraries(${MODULE_NAME} + PRIVATE + mbedcrypto + mbedtls + mbedx509 +) +target_sources(${MODULE_NAME} + PRIVATE + "crypto/crypto_mbedtls.cpp" +) +else() +target_link_libraries(${MODULE_NAME} + PRIVATE + everest::tls +) +target_sources(${MODULE_NAME} + PRIVATE + "crypto/crypto_openssl.cpp" + "connection/tls_connection.cpp" +) +endif() + +if(EVEREST_CORE_BUILD_TESTING) + add_subdirectory(tests) +endif() # ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/EvseV2G/EvseV2G.cpp b/modules/EvseV2G/EvseV2G.cpp index 98c6a35f4..67e4b77b1 100644 --- a/modules/EvseV2G/EvseV2G.cpp +++ b/modules/EvseV2G/EvseV2G.cpp @@ -3,22 +3,51 @@ // Copyright (C) 2022-2023 Contributors to EVerest #include "EvseV2G.hpp" #include "connection.hpp" +#include "everest/logging.hpp" #include "log.hpp" #include "sdp.hpp" -struct v2g_context* v2g_ctx = NULL; +#ifndef EVEREST_MBED_TLS +#include +namespace { +void log_handler(openssl::log_level_t level, const std::string& str) { + switch (level) { + case openssl::log_level_t::debug: + // ignore debug logs + break; + case openssl::log_level_t::warning: + EVLOG_warning << str; + break; + case openssl::log_level_t::error: + default: + EVLOG_error << str; + break; + } +} +} // namespace +#endif // EVEREST_MBED_TLS + +struct v2g_context* v2g_ctx = nullptr; namespace module { void EvseV2G::init() { - int rv = 0; /* create v2g context */ v2g_ctx = v2g_ctx_create(&(*p_charger), &(*r_security)); - if (v2g_ctx == NULL) + if (v2g_ctx == nullptr) return; +#ifndef EVEREST_MBED_TLS + (void)openssl::set_log_handler(log_handler); + v2g_ctx->tls_server = &tls_server; +#endif // EVEREST_MBED_TLS + invoke_init(*p_charger); +} + +void EvseV2G::ready() { + int rv = 0; dlog(DLOG_LEVEL_DEBUG, "Starting SDP responder"); @@ -42,14 +71,6 @@ void EvseV2G::init() { goto err_out; } - return; -err_out: - v2g_ctx_free(v2g_ctx); -} - -void EvseV2G::ready() { - int rv = 0; - invoke_ready(*p_charger); rv = sdp_listen(v2g_ctx); @@ -59,6 +80,8 @@ void EvseV2G::ready() { goto err_out; } + return; + err_out: v2g_ctx_free(v2g_ctx); } diff --git a/modules/EvseV2G/EvseV2G.hpp b/modules/EvseV2G/EvseV2G.hpp index 7835f8bb0..851dd5d6e 100644 --- a/modules/EvseV2G/EvseV2G.hpp +++ b/modules/EvseV2G/EvseV2G.hpp @@ -19,6 +19,9 @@ // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 // insert your custom include headers here #include "v2g_ctx.hpp" +#ifndef EVEREST_MBED_TLS +#include +#endif // EVEREST_MBED_TLS // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 namespace module { @@ -70,6 +73,9 @@ class EvseV2G : public Everest::ModuleBase { // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 // insert your private definitions here +#ifndef EVEREST_MBED_TLS + tls::Server tls_server; +#endif // EVEREST_MBED_TLS // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 }; diff --git a/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp b/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp index 20cf7b89a..aebd98e02 100644 --- a/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp +++ b/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp @@ -10,7 +10,7 @@ #include -#include "../EvseV2G.hpp" +#include "EvseV2G.hpp" // ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 #include "v2g.hpp" diff --git a/modules/EvseV2G/connection.cpp b/modules/EvseV2G/connection/connection.cpp similarity index 95% rename from modules/EvseV2G/connection.cpp rename to modules/EvseV2G/connection/connection.cpp index 7a933bd22..ca110a5d5 100644 --- a/modules/EvseV2G/connection.cpp +++ b/modules/EvseV2G/connection/connection.cpp @@ -4,21 +4,18 @@ #include "connection.hpp" #include "log.hpp" +#include "tls_connection.hpp" #include "tools.hpp" #include "v2g_server.hpp" #include +#include #include #include #include #include #include #include -#include -#include -#include -#include -#include #include #include #include @@ -29,15 +26,20 @@ #include #include -#ifndef SYSCONFDIR -#define SYSCONFDIR "/etc" -#endif +#ifdef EVEREST_MBED_TLS +#include +#include +#include +#include +#include +#endif // EVEREST_MBED_TLS #define DEFAULT_SOCKET_BACKLOG 3 #define DEFAULT_TCP_PORT 61341 #define DEFAULT_TLS_PORT 64109 #define ERROR_SESSION_ALREADY_STARTED 2 +#ifdef EVEREST_MBED_TLS #define MBEDTLS_DEBUG_LEVEL_VERBOSE 4 #define MBEDTLS_DEBUG_LEVEL_NO_DEBUG 0 @@ -69,6 +71,7 @@ static const int v2g_ssl_allowed_hashes[] = { MBEDTLS_MD_SHA256, MBEDTLS_MD_SHA224, #endif MBEDTLS_MD_SHA1, MBEDTLS_MD_NONE}; +#endif // EVEREST_MBED_TLS /*! * \brief connection_create_socket This function creates a tcp/tls socket @@ -134,7 +137,8 @@ static int connection_create_socket(struct sockaddr_in6* sockaddr) { return s; } -static int connection_ssl_initialize(void) { +static int connection_ssl_initialize() { +#ifdef EVEREST_MBED_TLS unsigned char random_data[64]; int rv; @@ -168,6 +172,7 @@ static int connection_ssl_initialize(void) { #if defined(MBEDTLS_SSL_CACHE_C) mbedtls_ssl_cache_init(&cache); #endif +#endif // EVEREST_MBED_TLS return 0; } @@ -180,7 +185,8 @@ static int connection_ssl_initialize(void) { */ int check_interface(struct v2g_context* v2g_ctx) { - struct ipv6_mreq mreq = {{0}, 0}; + struct ipv6_mreq mreq = {}; + std::memset(&mreq, 0, sizeof(mreq)); if (strcmp(v2g_ctx->if_name, "auto") == 0) { v2g_ctx->if_name = choose_first_ipv6_interface(); @@ -192,7 +198,7 @@ int check_interface(struct v2g_context* v2g_ctx) { return -1; } - return (v2g_ctx->if_name == NULL) ? -1 : 0; + return (v2g_ctx->if_name == nullptr) ? -1 : 0; } /*! @@ -207,7 +213,7 @@ int connection_init(struct v2g_context* v2g_ctx) { if (v2g_ctx->tls_security != TLS_SECURITY_FORCE) { v2g_ctx->local_tcp_addr = static_cast(calloc(1, sizeof(*v2g_ctx->local_tcp_addr))); - if (v2g_ctx->local_tcp_addr == NULL) { + if (v2g_ctx->local_tcp_addr == nullptr) { dlog(DLOG_LEVEL_ERROR, "Failed to allocate memory for TCP address"); return -1; } @@ -254,7 +260,7 @@ int connection_init(struct v2g_context* v2g_ctx) { sleep(1); continue; } - if (inet_ntop(AF_INET6, &v2g_ctx->local_tcp_addr->sin6_addr, buffer, sizeof(buffer)) != NULL) { + if (inet_ntop(AF_INET6, &v2g_ctx->local_tcp_addr->sin6_addr, buffer, sizeof(buffer)) != nullptr) { dlog(DLOG_LEVEL_INFO, "TCP server on %s is listening on port [%s%%%" PRIu32 "]:%" PRIu16, v2g_ctx->if_name, buffer, v2g_ctx->local_tcp_addr->sin6_scope_id, ntohs(v2g_ctx->local_tcp_addr->sin6_port)); @@ -282,7 +288,7 @@ int connection_init(struct v2g_context* v2g_ctx) { continue; } - if (inet_ntop(AF_INET6, &v2g_ctx->local_tls_addr->sin6_addr, buffer, sizeof(buffer)) != NULL) { + if (inet_ntop(AF_INET6, &v2g_ctx->local_tls_addr->sin6_addr, buffer, sizeof(buffer)) != nullptr) { dlog(DLOG_LEVEL_INFO, "TLS server on %s is listening on port [%s%%%" PRIu32 "]:%" PRIu16, v2g_ctx->if_name, buffer, v2g_ctx->local_tls_addr->sin6_scope_id, ntohs(v2g_ctx->local_tls_addr->sin6_port)); @@ -295,6 +301,12 @@ int connection_init(struct v2g_context* v2g_ctx) { /* Sockets should be ready, leave the loop */ break; } + +#ifndef EVEREST_MBED_TLS + if (v2g_ctx->local_tls_addr) { + return tls::connection_init(v2g_ctx); + } +#endif // EVEREST_MBED_TLS return 0; } @@ -304,7 +316,7 @@ int connection_init(struct v2g_context* v2g_ctx) { * \param ctx is the V2G context. * \return Returns \c true if a timeout has occured, otherwise \c false */ -static bool is_sequence_timeout(struct timespec ts_start, struct v2g_context* ctx) { +bool is_sequence_timeout(struct timespec ts_start, struct v2g_context* ctx) { struct timespec ts_current; int sequence_timeout = V2G_SEQUENCE_TIMEOUT_60S; @@ -340,6 +352,7 @@ ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t int num_of_bytes; if (conn->is_tls_connection) { +#ifdef EVEREST_MBED_TLS num_of_bytes = mbedtls_ssl_read(&conn->conn.ssl.ssl_context, &buf[bytes_read], count - bytes_read); if (num_of_bytes == MBEDTLS_ERR_SSL_WANT_READ || num_of_bytes == MBEDTLS_ERR_SSL_WANT_WRITE || @@ -356,6 +369,10 @@ ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t return -1; } +#else + dlog(DLOG_LEVEL_ERROR, "mbedtls_ssl_read() not configured"); + return -1; +#endif // EVEREST_MBED_TLS } else { /* use select for timeout handling */ struct timeval tv; @@ -367,7 +384,7 @@ ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t tv.tv_sec = conn->ctx->network_read_timeout / 1000; tv.tv_usec = (conn->ctx->network_read_timeout % 1000) * 1000; - num_of_bytes = select(conn->conn.socket_fd + 1, &read_fds, NULL, NULL, &tv); + num_of_bytes = select(conn->conn.socket_fd + 1, &read_fds, nullptr, nullptr, &tv); if (num_of_bytes == -1) { if (errno == EINTR) @@ -422,6 +439,7 @@ ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, size_t int num_of_bytes; if (conn->is_tls_connection) { +#ifdef EVEREST_MBED_TLS num_of_bytes = mbedtls_ssl_write(&conn->conn.ssl.ssl_context, &buf[bytes_written], count - bytes_written); if (num_of_bytes == MBEDTLS_ERR_SSL_WANT_READ || num_of_bytes == MBEDTLS_ERR_SSL_WANT_WRITE) @@ -429,7 +447,10 @@ ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, size_t if (num_of_bytes < 0) return -1; - +#else + dlog(DLOG_LEVEL_ERROR, "mbedtls_ssl_write() not configured"); + return -1; // shouldn't be using this function +#endif // EVEREST_MBED_TLS } else { num_of_bytes = (int)write(conn->conn.socket_fd, &buf[bytes_written], count - bytes_written); @@ -455,7 +476,7 @@ ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, size_t * \brief connection_teardown This function must be called on connection teardown. * \param conn is the V2G connection context */ -static void connection_teardown(struct v2g_connection* conn) { +void connection_teardown(struct v2g_connection* conn) { if (conn->ctx->session.is_charging == true) { conn->ctx->p_charger->publish_currentDemand_Finished(nullptr); @@ -470,7 +491,7 @@ static void connection_teardown(struct v2g_connection* conn) { v2g_ctx_init_charging_session(conn->ctx, true); /* stop timer */ - stop_timer(&conn->ctx->com_setup_timeout, NULL, conn->ctx); + stop_timer(&conn->ctx->com_setup_timeout, nullptr, conn->ctx); /* print dlink status */ switch (conn->dlink_action) { @@ -488,6 +509,7 @@ static void connection_teardown(struct v2g_connection* conn) { } } +#ifdef EVEREST_MBED_TLS static bool connection_init_tls(struct v2g_context* ctx) { int rv; @@ -520,7 +542,7 @@ static bool connection_init_tls(struct v2g_context* ctx) { mbedtls_pk_init(&ctx->evse_tls_crt_key[idx]); } - if (ctx->evseTlsCrt == NULL || ctx->evse_tls_crt_key == NULL) { + if (ctx->evseTlsCrt == nullptr || ctx->evse_tls_crt_key == nullptr) { dlog(DLOG_LEVEL_ERROR, "Failed to allocate memory!"); goto error_out; } @@ -673,6 +695,7 @@ static void ssl_key_log_debug_callback(void* ACtx, int ALevel, const char* AFile send_udp_message(); ctx->udp_buffer = {}; } +#endif // EVEREST_MBED_TLS /** * This is the 'main' function of a thread, which handles a TCP connection. @@ -719,13 +742,14 @@ static void* connection_handle_tcp(void* data) { free(conn); - return NULL; + return nullptr; } /** * This is the 'main' function of a thread, which handles a TLS connection. */ static void* connection_handle_tls(void* data) { +#ifdef EVEREST_MBED_TLS struct v2g_connection* conn = static_cast(data); struct v2g_context* v2g_ctx = conn->ctx; mbedtls_ssl_config* ssl_config = conn->conn.ssl.ssl_config; @@ -905,8 +929,9 @@ static void* connection_handle_tls(void* data) { connection_teardown(conn); free(conn); +#endif // EVEREST_MBED_TLS - return NULL; + return nullptr; } static void* connection_server(void* data) { @@ -939,14 +964,21 @@ static void* connection_server(void* data) { /* setup common stuff */ conn->ctx = ctx; + conn->read = &connection_read; + conn->write = &connection_write; /* if this thread is the TLS thread, then connections are TLS secured; * return code is non-zero if equal so align it */ +#ifdef EVEREST_MBED_TLS conn->is_tls_connection = !!pthread_equal(pthread_self(), ctx->tls_thread); +#else + conn->is_tls_connection = false; +#endif // EVEREST_MBED_TLS /* wait for an incoming connection */ if (conn->is_tls_connection) { +#ifdef EVEREST_MBED_TLS conn->conn.ssl.ssl_config = &ctx->ssl_config; /* at the moment, this is simply resetting the fd to -1; kept for upwards compatibility */ @@ -957,6 +989,7 @@ static void* connection_server(void* data) { dlog(DLOG_LEVEL_ERROR, "Accept(tls) failed: %s", strerror(errno)); continue; } +#endif // EVEREST_MBED_TLS } else { conn->conn.socket_fd = accept(ctx->tcp_socket, (struct sockaddr*)&addr, &addrlen); if (conn->conn.socket_fd == -1) { @@ -1010,7 +1043,11 @@ int connection_start_servers(struct v2g_context* ctx) { } if (ctx->tls_socket.fd != -1) { +#ifdef EVEREST_MBED_TLS rv = pthread_create(&ctx->tls_thread, NULL, connection_server, ctx); +#else + rv = tls::connection_start_server(ctx); +#endif // EVEREST_MBED_TLS if (rv != 0) { if (tcp_started) { pthread_cancel(ctx->tcp_thread); diff --git a/modules/EvseV2G/connection.hpp b/modules/EvseV2G/connection/connection.hpp similarity index 69% rename from modules/EvseV2G/connection.hpp rename to modules/EvseV2G/connection/connection.hpp index cbfb6de37..336183555 100644 --- a/modules/EvseV2G/connection.hpp +++ b/modules/EvseV2G/connection/connection.hpp @@ -5,15 +5,40 @@ #ifndef CONNECTION_H #define CONNECTION_H -#include "v2g_ctx.hpp" - +#include #include -#include +#include "v2g_ctx.hpp" + +/*! + * \brief initialise TCP/TLS connections + * \param ctx the V2G context + * \return 0 on success + */ int connection_init(struct v2g_context* ctx); + +/*! + * \brief start TCP/TLS servers + * \param ctx the V2G context + * \return 0 on success + */ int connection_start_servers(struct v2g_context* ctx); int create_udp_socket(const uint16_t udp_port, const char* interface_name); +/*! + * \brief check for V2G message sequence timeout + * \param ts_start start time + * \param ctx the V2G context + * \return true on timeout + */ +bool is_sequence_timeout(struct timespec ts_start, struct v2g_context* ctx); + +/*! + * \brief actions to take on connection close + * \param conn v2g connection context + */ +void connection_teardown(struct v2g_connection* conn); + /*! * \brief connection_read This abstracts a read from the connection socket, so that higher level functions * are not required to distinguish between TCP and TLS connections. @@ -22,7 +47,7 @@ int create_udp_socket(const uint16_t udp_port, const char* interface_name); * \param count number of read bytes. * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and * -2 for closed connection */ -ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t count); +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, std::size_t count); /*! * \brief connection_write This abstracts a write to the connection socket, so that higher level functions @@ -32,6 +57,6 @@ ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t * \param count size of the buffer * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and * -2 for closed connection */ -ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, size_t count); +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count); #endif /* CONNECTION_H */ diff --git a/modules/EvseV2G/connection/tls_connection.cpp b/modules/EvseV2G/connection/tls_connection.cpp new file mode 100644 index 000000000..bbe3ba4a5 --- /dev/null +++ b/modules/EvseV2G/connection/tls_connection.cpp @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "tls_connection.hpp" +#include "connection.hpp" +#include "log.hpp" +#include "v2g.hpp" +#include "v2g_server.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef EVEREST_MBED_TLS +namespace tls { +int connection_init(struct v2g_context* ctx) { + return -1; +} +int connection_start_servers(struct v2g_context* ctx) { + return -1; +} +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, std::size_t count) { + return -1; +} +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count) { + return -1; +} +} // namespace tls + +#else // EVEREST_MBED_TLS + +namespace { + +void process_connection_thread(std::shared_ptr con, struct v2g_context* ctx) { + assert(con != nullptr); + assert(ctx != nullptr); + + openssl::PKey_ptr contract_public_key{nullptr, nullptr}; + auto connection = std::make_unique(); + connection->ctx = ctx; + connection->is_tls_connection = true; + connection->read = &tls::connection_read; + connection->write = &tls::connection_write; + connection->tls_connection = con.get(); + connection->pubkey = &contract_public_key; + + dlog(DLOG_LEVEL_INFO, "Incoming TLS connection"); + + if (con->accept()) { + // TODO(james-ctc) v2g_ctx->tls_key_logging + + if (ctx->state == 0) { + const auto rv = ::v2g_handle_connection(connection.get()); + dlog(DLOG_LEVEL_INFO, "v2g_dispatch_connection exited with %d", rv); + } else { + dlog(DLOG_LEVEL_INFO, "%s", "Closing tls-connection. v2g-session is already running"); + } + + con->shutdown(); + } + + ::connection_teardown(connection.get()); +} + +void handle_new_connection_cb(std::shared_ptr con, struct v2g_context* ctx) { + assert(con != nullptr); + assert(ctx != nullptr); + // create a thread to process this connection + try { + std::thread connection_loop(process_connection_thread, con, ctx); + connection_loop.detach(); + } catch (const std::system_error&) { + // unable to start thread + dlog(DLOG_LEVEL_ERROR, "pthread_create() failed: %s", strerror(errno)); + con->shutdown(); + } +} + +void server_loop_thread(struct v2g_context* ctx) { + assert(ctx != nullptr); + assert(ctx->tls_server != nullptr); + const auto res = ctx->tls_server->serve([ctx](auto con) { handle_new_connection_cb(con, ctx); }); + if (!res) { + dlog(DLOG_LEVEL_ERROR, "tls::Server failed to serve"); + } +} + +} // namespace + +namespace tls { + +int connection_init(struct v2g_context* ctx) { + assert(ctx != nullptr); + assert(ctx->tls_server != nullptr); + assert(ctx->r_security != nullptr); + + using types::evse_security::CaCertificateType; + using types::evse_security::EncodingFormat; + using types::evse_security::GetCertificateInfoStatus; + using types::evse_security::LeafCertificateType; + + /* + * libevse-security checks for an optional password and when one + * isn't set is uses an empty string as the password rather than nullptr. + * hence private keys are always encrypted. + */ + + tls::Server::config_t config; + bool bResult = false; + + config.cipher_list = "ECDHE-ECDSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256"; + config.ciphersuites = ""; // disable TLS 1.3 + config.verify_client = false; // contract certificate managed in-band in 15118-2 + + // information from libevse-security + const auto cert_info = + ctx->r_security->call_get_leaf_certificate_info(LeafCertificateType::V2G, EncodingFormat::PEM, false); + if (cert_info.status != GetCertificateInfoStatus::Accepted) { + dlog(DLOG_LEVEL_ERROR, "Failed to read cert_info! Not Accepted"); + } else { + if (cert_info.info) { + const auto& info = cert_info.info.value(); + const auto cert_path = info.certificate.value_or(""); + const auto key_path = info.key; + + // workaround (see above libevse-security comment) + const auto key_password = info.password.value_or(""); + + config.certificate_chain_file = cert_path.c_str(); + config.private_key_file = key_path.c_str(); + config.private_key_password = key_password.c_str(); + + if (info.ocsp) { + for (const auto& ocsp : info.ocsp.value()) { + const char* file{nullptr}; + if (ocsp.ocsp_path) { + file = ocsp.ocsp_path.value().c_str(); + } + config.ocsp_response_files.push_back(file); + } + } + + // use the existing configured socket + config.socket = ctx->tls_socket.fd; + config.io_timeout_ms = static_cast(ctx->network_read_timeout_tls); + + ctx->tls_server->stop(); + ctx->tls_server->wait_stopped(); + bResult = ctx->tls_server->init(config); + } else { + dlog(DLOG_LEVEL_ERROR, "Failed to read cert_info! Empty response"); + } + } + + return (bResult) ? 0 : -1; +} + +int connection_start_server(struct v2g_context* ctx) { + assert(ctx != nullptr); + assert(ctx->tls_server != nullptr); + + // only starts the TLS server + + int res = 0; + try { + ctx->tls_server->stop(); + ctx->tls_server->wait_stopped(); + std::thread serve_loop(server_loop_thread, ctx); + serve_loop.detach(); + ctx->tls_server->wait_running(); + } catch (const std::system_error&) { + // unable to start thread (caller logs failure) + res = -1; + } + return res; +} + +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, const std::size_t count) { + assert(conn != nullptr); + assert(conn->tls_connection != nullptr); + + ssize_t result{0}; + std::size_t bytes_read{0}; + timespec ts_start{}; + + if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == -1) { + dlog(DLOG_LEVEL_ERROR, "clock_gettime(ts_start) failed: %s", strerror(errno)); + result = -1; + } + + while ((bytes_read < count) && (result >= 0)) { + const std::size_t remaining = count - bytes_read; + std::size_t bytes_in{0}; + auto* ptr = reinterpret_cast(&buf[bytes_read]); + + switch (conn->tls_connection->read(ptr, remaining, bytes_in)) { + case tls::Connection::result_t::success: + bytes_read += bytes_in; + break; + case tls::Connection::result_t::timeout: + break; + case tls::Connection::result_t::error: + default: + result = -1; + break; + } + + if (conn->ctx->is_connection_terminated) { + dlog(DLOG_LEVEL_ERROR, "Reading from tcp-socket aborted"); + conn->tls_connection->shutdown(); + result = -2; + } + + if (::is_sequence_timeout(ts_start, conn->ctx)) { + break; + } + } + + return (result < 0) ? result : static_cast(bytes_read); +} + +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count) { + assert(conn != nullptr); + assert(conn->tls_connection != nullptr); + + ssize_t result{0}; + std::size_t bytes_written{0}; + + while ((bytes_written < count) && (result >= 0)) { + const std::size_t remaining = count - bytes_written; + std::size_t bytes_out{0}; + const auto* ptr = reinterpret_cast(&buf[bytes_written]); + + switch (conn->tls_connection->write(ptr, remaining, bytes_out)) { + case tls::Connection::result_t::success: + bytes_written += bytes_out; + break; + case tls::Connection::result_t::timeout: + break; + case tls::Connection::result_t::error: + default: + result = -1; + break; + } + } + + if ((result == -1) && (conn->tls_connection->state() == tls::Connection::state_t::closed)) { + // if the connection has closed - return the number of bytes sent + result = 0; + } + + return (result < 0) ? result : static_cast(bytes_written); +} + +} // namespace tls + +#endif // EVEREST_MBED_TLS diff --git a/modules/EvseV2G/connection/tls_connection.hpp b/modules/EvseV2G/connection/tls_connection.hpp new file mode 100644 index 000000000..d7bacdb87 --- /dev/null +++ b/modules/EvseV2G/connection/tls_connection.hpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef TLS_CONNECTION_HPP_ +#define TLS_CONNECTION_HPP_ + +#include +#include + +struct v2g_context; +struct v2g_connection; + +namespace tls { + +/*! + * \param ctx v2g connection context + * \return returns 0 on succss and -1 on error + */ +int connection_init(struct v2g_context* ctx); + +/*! + * \param ctx v2g connection context + * \return returns 0 on succss and -1 on error + */ +int connection_start_server(struct v2g_context* ctx); + +/*! + * \brief connection_read This abstracts a read from the connection socket, so that higher level functions + * are not required to distinguish between TCP and TLS connections. + * \param conn v2g connection context + * \param buf buffer to store received message sequence. + * \param count number of read bytes. + * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and + * -2 for closed connection */ +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, std::size_t count); + +/*! + * \brief connection_write This abstracts a write to the connection socket, so that higher level functions + * are not required to distinguish between TCP and TLS connections. + * \param conn v2g connection context + * \param buf buffer to store received message sequence. + * \param count size of the buffer + * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and + * -2 for closed connection */ +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count); + +} // namespace tls + +#endif // TLS_CONNECTION_HPP_ diff --git a/modules/EvseV2G/crypto/crypto_common.hpp b/modules/EvseV2G/crypto/crypto_common.hpp new file mode 100644 index 000000000..1e4f74567 --- /dev/null +++ b/modules/EvseV2G/crypto/crypto_common.hpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef CRTYPTO_COMMON_HPP_ +#define CRTYPTO_COMMON_HPP_ + +#include + +#include + +namespace crypto { + +using verify_result_t = openssl::verify_result_t; + +constexpr std::size_t MAX_EXI_SIZE = 8192; + +} // namespace crypto + +#endif // CRTYPTO_COMMON_HPP_ diff --git a/modules/EvseV2G/crypto/crypto_mbedtls.cpp b/modules/EvseV2G/crypto/crypto_mbedtls.cpp new file mode 100644 index 000000000..5f1b96872 --- /dev/null +++ b/modules/EvseV2G/crypto/crypto_mbedtls.cpp @@ -0,0 +1,428 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 chargebyte GmbH +// Copyright (C) 2023 Contributors to EVerest + +#include +#include + +#include "crypto_mbedtls.hpp" +#include "iso_server.hpp" +#include "log.hpp" + +#include +#include //for V2GTP_HEADER_LENGTHs +#include +#include +#include + +#include +#include +#include // To extract the emaid +#include + +namespace { + +constexpr std::size_t DIGEST_SIZE = 32; +constexpr std::size_t MQTT_MAX_PAYLOAD_SIZE = 268435455; +constexpr std::size_t MAX_EMAID_LEN = 18; + +bool getSubjectData(const mbedtls_x509_name* ASubject, const char* AAttrName, const mbedtls_asn1_buf** AVal); +int debug_verify_cert(void* data, mbedtls_x509_crt* crt, int depth, uint32_t* flags); +void printMbedVerifyErrorCode(int AErr, uint32_t AFlags); +bool base64_decode(const char* text, std::size_t len, std::uint8_t* out_data, std::size_t& out_len); +std::string base64_encode(const std::uint8_t* data, std::size_t len, bool newLine); + +bool getSubjectData(const mbedtls_x509_name* ASubject, const char* AAttrName, const mbedtls_asn1_buf** AVal) { + const char* attrName = NULL; + + while (NULL != ASubject) { + if ((0 == mbedtls_oid_get_attr_short_name(&ASubject->oid, &attrName)) && (0 == strcmp(attrName, AAttrName))) { + + *AVal = &ASubject->val; + return true; + } else { + ASubject = ASubject->next; + } + } + *AVal = NULL; + return false; +} + +/*! + * \brief debug_verify_cert Function is from https://github.com/aws/aws-iot-device-sdk-embedded-C/blob + * /master/platform/linux/mbedtls/network_mbedtls_wrapper.c to debug certificate verification + * \param data + * \param crt + * \param depth + * \param flags + * \return + */ +int debug_verify_cert(void* data, mbedtls_x509_crt* crt, int depth, uint32_t* flags) { + char buf[1024]; + ((void)data); + + dlog(DLOG_LEVEL_INFO, "\nVerify requested for (Depth %d):\n", depth); + mbedtls_x509_crt_info(buf, sizeof(buf) - 1, "", crt); + dlog(DLOG_LEVEL_INFO, "%s", buf); + + if ((*flags) == 0) + dlog(DLOG_LEVEL_INFO, " This certificate has no flags\n"); + else { + mbedtls_x509_crt_verify_info(buf, sizeof(buf), " ! ", *flags); + dlog(DLOG_LEVEL_INFO, "%s\n", buf); + } + + return (0); +} + +/*! + * \brief printMbedVerifyErrorCode This functions prints the mbedTls specific error code. + * \param AErr is the return value of the mbed verify function + * \param AFlags includes the flags of the verification. + */ +void printMbedVerifyErrorCode(int AErr, uint32_t AFlags) { + dlog(DLOG_LEVEL_ERROR, "Failed to verify certificate (err: 0x%08x flags: 0x%08x)", AErr, AFlags); + if (AErr == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { + if (AFlags & MBEDTLS_X509_BADCERT_EXPIRED) + dlog(DLOG_LEVEL_ERROR, "CERT_EXPIRED"); + else if (AFlags & MBEDTLS_X509_BADCERT_REVOKED) + dlog(DLOG_LEVEL_ERROR, "CERT_REVOKED"); + else if (AFlags & MBEDTLS_X509_BADCERT_CN_MISMATCH) + dlog(DLOG_LEVEL_ERROR, "CN_MISMATCH"); + else if (AFlags & MBEDTLS_X509_BADCERT_NOT_TRUSTED) + dlog(DLOG_LEVEL_ERROR, "CERT_NOT_TRUSTED"); + else if (AFlags & MBEDTLS_X509_BADCRL_NOT_TRUSTED) + dlog(DLOG_LEVEL_ERROR, "CRL_NOT_TRUSTED"); + else if (AFlags & MBEDTLS_X509_BADCRL_EXPIRED) + dlog(DLOG_LEVEL_ERROR, "CRL_EXPIRED"); + else if (AFlags & MBEDTLS_X509_BADCERT_MISSING) + dlog(DLOG_LEVEL_ERROR, "CERT_MISSING"); + else if (AFlags & MBEDTLS_X509_BADCERT_SKIP_VERIFY) + dlog(DLOG_LEVEL_ERROR, "SKIP_VERIFY"); + else if (AFlags & MBEDTLS_X509_BADCERT_OTHER) + dlog(DLOG_LEVEL_ERROR, "CERT_OTHER"); + else if (AFlags & MBEDTLS_X509_BADCERT_FUTURE) + dlog(DLOG_LEVEL_ERROR, "CERT_FUTURE"); + else if (AFlags & MBEDTLS_X509_BADCRL_FUTURE) + dlog(DLOG_LEVEL_ERROR, "CRL_FUTURE"); + else if (AFlags & MBEDTLS_X509_BADCERT_KEY_USAGE) + dlog(DLOG_LEVEL_ERROR, "KEY_USAGE"); + else if (AFlags & MBEDTLS_X509_BADCERT_EXT_KEY_USAGE) + dlog(DLOG_LEVEL_ERROR, "EXT_KEY_USAGE"); + else if (AFlags & MBEDTLS_X509_BADCERT_NS_CERT_TYPE) + dlog(DLOG_LEVEL_ERROR, "NS_CERT_TYPE"); + else if (AFlags & MBEDTLS_X509_BADCERT_BAD_MD) + dlog(DLOG_LEVEL_ERROR, "BAD_MD"); + else if (AFlags & MBEDTLS_X509_BADCERT_BAD_PK) + dlog(DLOG_LEVEL_ERROR, "BAD_PK"); + else if (AFlags & MBEDTLS_X509_BADCERT_BAD_KEY) + dlog(DLOG_LEVEL_ERROR, "BAD_KEY"); + else if (AFlags & MBEDTLS_X509_BADCRL_BAD_MD) + dlog(DLOG_LEVEL_ERROR, "CRL_BAD_MD"); + else if (AFlags & MBEDTLS_X509_BADCRL_BAD_PK) + dlog(DLOG_LEVEL_ERROR, "CRL_BAD_PK"); + else if (AFlags & MBEDTLS_X509_BADCRL_BAD_KEY) + dlog(DLOG_LEVEL_ERROR, "CRL_BAD_KEY"); + } +} + +bool base64_decode(const char* text, std::size_t len, std::uint8_t* out_data, std::size_t& out_len) { + bool bResult = true; + const std::size_t dlen = out_len; + const auto rv = mbedtls_base64_decode(out_data, dlen, &out_len, reinterpret_cast(text), len); + if (rv != 0) { + std::array strerr{}; + mbedtls_strerror(rv, strerr.data(), strerr.size()); + dlog(DLOG_LEVEL_ERROR, "Failed to decode base64 stream (-0x%04x) %s", rv, strerr); + bResult = false; + } + return bResult; +} + +std::string base64_encode(const std::uint8_t* data, std::size_t len, bool /* newLine */) { + std::string result; + std::size_t olen{0}; + mbedtls_base64_encode(nullptr, 0, &olen, data, len); + + if (MQTT_MAX_PAYLOAD_SIZE < olen) { + dlog(DLOG_LEVEL_ERROR, "Mqtt payload size exceeded!"); + } else { + result = std::string(olen, '\0'); + + if ((mbedtls_base64_encode(reinterpret_cast(result.data()), result.size(), &olen, data, len) != + 0)) { + result.clear(); + dlog(DLOG_LEVEL_ERROR, "Unable to base64 encode"); + } + } + + return result; +} + +} // namespace + +namespace crypto::mbedtls { + +/*! + * \brief check_iso2_signature This function validates the ISO signature + * \param iso2_signature is the signature of the ISO EXI fragment + * \param public_key is the public key to validate the signature against the ISO EXI fragment + * \param iso2_exi_fragment iso2_exi_fragment is the ISO EXI fragment + */ +bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, mbedtls_ecdsa_context& public_key, + struct iso2_exiFragment* iso2_exi_fragment) { + /** Digest check **/ + int err = 0; + const struct iso2_SignatureType* sig = iso2_signature; + unsigned char buf[MAX_EXI_SIZE]; + const struct iso2_ReferenceType* req_ref = &sig->SignedInfo.Reference.array[0]; + exi_bitstream_t stream; + exi_bitstream_init(&stream, buf, MAX_EXI_SIZE, 0, NULL); + uint8_t digest[DIGEST_SIZE]; + err = encode_iso2_exiFragment(&stream, iso2_exi_fragment); + if (err != 0) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode fragment, error code = %d", err); + return false; + } + uint32_t frag_data_len = exi_bitstream_get_length(&stream); + mbedtls_sha256(buf, frag_data_len, digest, 0); + + if (req_ref->DigestValue.bytesLen != DIGEST_SIZE) { + dlog(DLOG_LEVEL_ERROR, "Invalid digest length %u in signature", req_ref->DigestValue.bytesLen); + return false; + } + + if (memcmp(req_ref->DigestValue.bytes, digest, DIGEST_SIZE) != 0) { + dlog(DLOG_LEVEL_ERROR, "Invalid digest in signature"); + return false; + } + + /** Validate signature **/ + struct iso2_xmldsigFragment sig_fragment; + init_iso2_xmldsigFragment(&sig_fragment); + sig_fragment.SignedInfo_isUsed = 1; + sig_fragment.SignedInfo = sig->SignedInfo; + + /** \req [V2G2-771] Don't use following fields */ + sig_fragment.SignedInfo.Id_isUsed = 0; + sig_fragment.SignedInfo.CanonicalizationMethod.ANY_isUsed = 0; + sig_fragment.SignedInfo.SignatureMethod.HMACOutputLength_isUsed = 0; + sig_fragment.SignedInfo.SignatureMethod.ANY_isUsed = 0; + for (auto* ref = sig_fragment.SignedInfo.Reference.array; + ref != (sig_fragment.SignedInfo.Reference.array + sig_fragment.SignedInfo.Reference.arrayLen); ++ref) { + ref->Type_isUsed = 0; + ref->Transforms.Transform.ANY_isUsed = 0; + ref->Transforms.Transform.XPath_isUsed = 0; + ref->DigestMethod.ANY_isUsed = 0; + } + + stream.byte_pos = 0; + stream.bit_count = 0; + err = encode_iso2_xmldsigFragment(&stream, &sig_fragment); + + if (err != 0) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode XML signature fragment, error code = %d", err); + return false; + } + uint32_t sign_info_fragmen_len = exi_bitstream_get_length(&stream); + + /* Hash the signature */ + mbedtls_sha256(buf, sign_info_fragmen_len, digest, 0); + + /* Validate the ecdsa signature using the public key */ + if (sig->SignatureValue.CONTENT.bytesLen == 0) { + dlog(DLOG_LEVEL_ERROR, "Signature len is invalid (%i)", sig->SignatureValue.CONTENT.bytesLen); + return false; + } + + /* Init mbedtls parameter */ + mbedtls_ecp_group ecp_group; + mbedtls_ecp_group_init(&ecp_group); + + mbedtls_mpi mpi_r; + mbedtls_mpi_init(&mpi_r); + mbedtls_mpi mpi_s; + mbedtls_mpi_init(&mpi_s); + + mbedtls_mpi_read_binary(&mpi_r, (const unsigned char*)&sig->SignatureValue.CONTENT.bytes[0], + sig->SignatureValue.CONTENT.bytesLen / 2); + mbedtls_mpi_read_binary( + &mpi_s, (const unsigned char*)&sig->SignatureValue.CONTENT.bytes[sig->SignatureValue.CONTENT.bytesLen / 2], + sig->SignatureValue.CONTENT.bytesLen / 2); + + err = mbedtls_ecp_group_load(&ecp_group, MBEDTLS_ECP_DP_SECP256R1); + + if (err == 0) { + err = mbedtls_ecdsa_verify(&ecp_group, static_cast(digest), 32, &public_key.Q, &mpi_r, + &mpi_s); + } + + mbedtls_ecp_group_free(&ecp_group); + mbedtls_mpi_free(&mpi_r); + mbedtls_mpi_free(&mpi_s); + + if (err != 0) { + char error_buf[100]; + mbedtls_strerror(err, error_buf, sizeof(error_buf)); + dlog(DLOG_LEVEL_ERROR, "Invalid signature, error code = -0x%08x, %s", err, error_buf); + return false; + } + + return true; +} + +bool load_contract_root_cert(Certificate_ptr& contract_root_crt, const char* V2G_file_path, const char* MO_file_path) { + int rv = 0; + + if ((rv = mbedtls_x509_crt_parse_file(contract_root_crt.get(), MO_file_path)) != 0) { + char strerr[256]; + mbedtls_strerror(rv, strerr, sizeof(strerr)); + dlog(DLOG_LEVEL_WARNING, "Unable to parse MO (%s) root certificate. (err: -0x%04x - %s)", MO_file_path, -rv, + strerr); + dlog(DLOG_LEVEL_INFO, "Attempting to parse V2G root certificate.."); + + if ((rv = mbedtls_x509_crt_parse_file(contract_root_crt.get(), V2G_file_path)) != 0) { + mbedtls_strerror(rv, strerr, sizeof(strerr)); + dlog(DLOG_LEVEL_ERROR, "Unable to parse V2G (%s) root certificate. (err: -0x%04x - %s)", V2G_file_path, -rv, + strerr); + } + } + + return (rv != 0) ? false : true; +} + +void free_connection_crypto_data(v2g_connection* conn) { + mbedtls_ecdsa_free(&conn->ctx->session.contract.pubkey); + mbedtls_ecdsa_init(&conn->ctx->session.contract.pubkey); +} + +int load_certificate(Certificate_ptr& crt, const std::uint8_t* bytes, std::uint16_t bytesLen) { + int err{-1}; + + // Parse contract leaf certificate + if (bytesLen != 0) { + err = mbedtls_x509_crt_parse(crt.get(), bytes, bytesLen); + if (err != 0) { + char strerr[256]; + mbedtls_strerror(err, strerr, std::string(strerr).size()); + dlog(DLOG_LEVEL_ERROR, "handle_payment_detail: invalid certificate received in req: %s", strerr); + } + + } else { + dlog(DLOG_LEVEL_ERROR, "No certificate received!"); + } + + return err; +} + +int parse_contract_certificate(Certificate_ptr& crt, const std::uint8_t* bytes, std::size_t bytesLen) { + auto err = mbedtls_x509_crt_parse(crt.get(), bytes, bytesLen); + + if (err != 0) { + char strerr[256]; + mbedtls_strerror(err, strerr, std::string(strerr).size()); + dlog(DLOG_LEVEL_ERROR, "handle_payment_detail: invalid certificate received in req: %s", strerr); + } + + return err; +} + +/*! + * \brief getEmaidFromContractCert This function extracts the emaid from an subject of a contract certificate. + * \param ASubject is the subject of the contract certificate. + * \return Returns the length of the extracted emaid. + */ +std::string getEmaidFromContractCert(const Certificate_ptr& crt) { + const mbedtls_x509_name* ASubject = &crt.get()->subject; + const mbedtls_asn1_buf* val = NULL; + std::size_t certEmaidLen = 0; + char certEmaid[MAX_EMAID_LEN + 1]; + + std::string result; + if (true == getSubjectData(ASubject, "CN", &val)) { + /* Check the emaid length within the certificate */ + certEmaidLen = MAX_EMAID_LEN < val->len ? 0 : val->len; + strncpy(certEmaid, reinterpret_cast(val->p), certEmaidLen); + certEmaid[certEmaidLen] = '\0'; + result = std::string{&certEmaid[0], certEmaidLen}; + } else { + dlog(DLOG_LEVEL_WARNING, "No CN found in subject of the contract certificate"); + } + + return result; +} + +std::string chain_to_pem(const Certificate_ptr& contract_crt, const void* /* chain */) { + const mbedtls_x509_crt* crt = contract_crt.get(); + std::string contract_cert_chain_pem; + + while (crt != nullptr && crt->version != 0) { + const auto pem = certificate_to_pem(crt); + if (pem.empty()) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode certificate chain"); + break; + } + contract_cert_chain_pem.append(pem); + crt = crt->next; + } + + return contract_cert_chain_pem; +} + +verify_result_t verify_certificate(Certificate_ptr& contract_crt, const void* chain, const char* v2g_root_cert_path, + const char* mo_root_cert_path, bool debugMode) { + Certificate_ptr contract_root_crt; + uint32_t flags; + verify_result_t result{verify_result_t::verified}; + + /* Load supported V2G/MO root certificates */ + if (load_contract_root_cert(contract_root_crt, v2g_root_cert_path, mo_root_cert_path)) { + // === Verify the retrieved contract ECDSA key against the root cert === + const int err = mbedtls_x509_crt_verify(contract_crt.get(), contract_root_crt.get(), NULL, NULL, &flags, + (debugMode) ? debug_verify_cert : NULL, NULL); + if (err != 0) { + printMbedVerifyErrorCode(err, flags); + dlog(DLOG_LEVEL_ERROR, "Validation of the contract certificate failed!"); + if ((err == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) && (flags & MBEDTLS_X509_BADCERT_EXPIRED)) { + result = verify_result_t::CertificateExpired; + } else if ((err == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) && (flags & MBEDTLS_X509_BADCERT_REVOKED)) { + result = verify_result_t::CertificateRevoked; + } else { + result = verify_result_t::CertChainError; + } + } + + } else { + result = verify_result_t::NoCertificateAvailable; + } + + return result; +} + +int get_public_key(mbedtls_ecdsa_context* pubkey, mbedtls_pk_context& pk) { + // Convert the public key in the certificate to a mbed TLS ECDSA public key + // This also verifies that it's an ECDSA key and not an RSA key + int err = mbedtls_ecdsa_from_keypair(pubkey, mbedtls_pk_ec(pk)); + if (err != 0) { + char strerr[256]; + mbedtls_strerror(err, strerr, std::string(strerr).size()); + dlog(DLOG_LEVEL_ERROR, "Could not retrieve ecdsa public key from certificate keypair: %s", strerr); + } + + return err; +} + +std::string certificate_to_pem(const mbedtls_x509_crt* crt) { + std::string result; + auto cert_b64 = base64_encode(crt->raw.p, crt->raw.len, true); + if (cert_b64.empty()) { + dlog(DLOG_LEVEL_ERROR, "Unable to convert certificate to PEM"); + } else { + result.append("-----BEGIN CERTIFICATE-----\n"); + result.append(cert_b64); + result.append("\n-----END CERTIFICATE-----\n"); + } + return result; +} + +} // namespace crypto::mbedtls diff --git a/modules/EvseV2G/crypto/crypto_mbedtls.hpp b/modules/EvseV2G/crypto/crypto_mbedtls.hpp new file mode 100644 index 000000000..e96fab4b8 --- /dev/null +++ b/modules/EvseV2G/crypto/crypto_mbedtls.hpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef CRYPTO_MBEDTLS_HPP_ +#define CRYPTO_MBEDTLS_HPP_ + +#include +#include + +#include "crypto_common.hpp" +#include "v2g.hpp" + +/** + * \file Mbed TLS implementation + */ + +struct mbedtls_x509_crt; + +namespace crypto::mbedtls { + +/** + * \brief Wrapper around a mbedtls_x509_crt to ensure it is freed properly + */ +class Certificate_ptr { +private: + mbedtls_x509_crt cert = {}; + +public: + Certificate_ptr() { + mbedtls_x509_crt_init(&cert); + } + ~Certificate_ptr() { + mbedtls_x509_crt_free(&cert); + } + + Certificate_ptr(const Certificate_ptr&) = delete; + Certificate_ptr(Certificate_ptr&&) = delete; + Certificate_ptr& operator=(const Certificate_ptr&) = delete; + Certificate_ptr& operator=(Certificate_ptr&&) = delete; + + [[nodiscard]] mbedtls_x509_crt* get() { + return &cert; + } + + [[nodiscard]] const mbedtls_x509_crt* get() const { + return &cert; + } + + explicit operator mbedtls_x509_crt*() { + return &cert; + } +}; + +/** + * \brief check the signature of a signed 15118 message + * \param iso2_signature the signature to check + * \param public_key the public key from the contract certificate + * \param iso2_exi_fragment the signed data + * \return true when the signature is valid + */ +bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, mbedtls_ecdsa_context& public_key, + struct iso2_exiFragment* iso2_exi_fragment); + +/** + * \brief load the trust anchor for the contract certificate. + * Use the mobility operator certificate if it exists otherwise + * the V2G certificate + * \param contract_root_crt the retrieved trust anchor + * \param V2G_file_path the file containing the V2G trust anchor (PEM format) + * \param MO_file_path the file containing the mobility operator trust anchor (PEM format) + * \return true when a certificate was found + */ +bool load_contract_root_cert(Certificate_ptr& contract_root_crt, const char* V2G_file_path, const char* MO_file_path); + +/** + * \brief clear public key from previous connection + * \param conn the V2G connection data + */ +void free_connection_crypto_data(v2g_connection* conn); + +/** + * \brief load a contract certificate's certification path certificate from the V2G message as DER bytes + * \param crt the certificate path certificates (this certificate is added to the list) + * \param bytes the DER (ASN.1) X509v3 certificate in the V2G message + * \param bytesLen the length of the DER encoded certificate + * \return 0 when certificate successfully loaded + */ +int load_certificate(Certificate_ptr& crt, const std::uint8_t* bytes, std::uint16_t bytesLen); + +/** + * \brief load the contract certificate from the V2G message as DER bytes + * \param crt the certificate + * \param bytes the DER (ASN.1) X509v3 certificate in the V2G message + * \param bytesLen the length of the DER encoded certificate + * \return 0 when certificate successfully loaded + */ +int parse_contract_certificate(Certificate_ptr& crt, const std::uint8_t* bytes, std::size_t bytesLen); + +/** + * \brief get the EMAID from the certificate (CommonName from the SubjectName) + * \param crt the certificate + * \return the EMAD or empty on error + */ +std::string getEmaidFromContractCert(const Certificate_ptr& crt); + +/** + * \brief convert a list of certificates into a PEM string starting with the contract certificate + * \param contract_crt the contract certificate (when not the first certificate in the chain) + * \param chain the certification path chain (might include the contract certificate as the first item) + * \return PEM string or empty on error + */ +std::string chain_to_pem(const Certificate_ptr& contract_crt, const void* chain); + +/** + * \brief verify certification path of the contract certificate through to a trust anchor + * \param contract_crt (single certificate or chain with the contract certificate as the first item) + * \param chain intermediate certificates (may be nullptr) + * \param v2g_root_cert_path V2G trust anchor file name + * \param mo_root_cert_path mobility operator trust anchor file name + * \param debugMode additional information on verification failures + * \result a subset of possible verification failures where known or 'verified' on success + */ +verify_result_t verify_certificate(Certificate_ptr& contract_crt, const void* chain, const char* v2g_root_cert_path, + const char* mo_root_cert_path, bool debugMode); + +/** + * \brief convert certificate public key into a suable form + * \param pubkey the certificate public key + * \param pk the key in a form usable for checking signatures + * \return 0 when key successfully loaded + */ +int get_public_key(mbedtls_ecdsa_context* pubkey, mbedtls_pk_context& pk); + +/** + * \brief convert a certificate into a PEM string + * \param crt the certificate + * \return the PEM string or empty on error + */ +std::string certificate_to_pem(const mbedtls_x509_crt* crt); + +} // namespace crypto::mbedtls + +#endif // CRYPTO_MBEDTLS_HPP_ diff --git a/modules/EvseV2G/crypto/crypto_openssl.cpp b/modules/EvseV2G/crypto/crypto_openssl.cpp new file mode 100644 index 000000000..a142ba0c9 --- /dev/null +++ b/modules/EvseV2G/crypto/crypto_openssl.cpp @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include +#include + +#include "crypto_openssl.hpp" +#include "iso_server.hpp" +#include "log.hpp" + +#include +#include //for V2GTP_HEADER_LENGTHs +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace crypto ::openssl { +using ::openssl::bn_const_t; +using ::openssl::bn_t; +using ::openssl::log_error; +using ::openssl::sha_256; +using ::openssl::sha_256_digest_t; +using ::openssl::verify; + +bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, EVP_PKEY* pkey, + struct iso2_exiFragment* iso2_exi_fragment) { + assert(pkey != nullptr); + assert(iso2_signature != nullptr); + assert(iso2_exi_fragment != nullptr); + + bool bRes{true}; + + // signature information + const struct iso2_ReferenceType* req_ref = &iso2_signature->SignedInfo.Reference.array[0]; + const auto signature_len = iso2_signature->SignatureValue.CONTENT.bytesLen; + const auto* signature = &iso2_signature->SignatureValue.CONTENT.bytes[0]; + + // build data to check signature against + std::array exi_buffer{}; + exi_bitstream_t stream; + exi_bitstream_init(&stream, exi_buffer.data(), MAX_EXI_SIZE, 0, NULL); + + auto err = encode_iso2_exiFragment(&stream, iso2_exi_fragment); + if (err != 0) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode fragment, error code = %d", err); + bRes = false; + } + + sha_256_digest_t digest; + + // calculate hash of data + if (bRes) { + const auto frag_data_len = exi_bitstream_get_length(&stream); + bRes = sha_256(exi_buffer.data(), frag_data_len, digest); + } + + // check hash matches the value in the message + if (bRes) { + if (req_ref->DigestValue.bytesLen != digest.size()) { + dlog(DLOG_LEVEL_ERROR, "Invalid digest length %u in signature", req_ref->DigestValue.bytesLen); + bRes = false; + } + } + if (bRes) { + if (std::memcmp(req_ref->DigestValue.bytes, digest.data(), digest.size()) != 0) { + dlog(DLOG_LEVEL_ERROR, "Invalid digest in signature"); + bRes = false; + } + } + + // verify the signature + if (bRes) { + struct iso2_xmldsigFragment sig_fragment {}; + init_iso2_xmldsigFragment(&sig_fragment); + sig_fragment.SignedInfo_isUsed = 1; + sig_fragment.SignedInfo = iso2_signature->SignedInfo; + + /** \req [V2G2-771] Don't use following fields */ + sig_fragment.SignedInfo.Id_isUsed = 0; + sig_fragment.SignedInfo.CanonicalizationMethod.ANY_isUsed = 0; + sig_fragment.SignedInfo.SignatureMethod.HMACOutputLength_isUsed = 0; + sig_fragment.SignedInfo.SignatureMethod.ANY_isUsed = 0; + for (auto* ref = sig_fragment.SignedInfo.Reference.array; + ref != (sig_fragment.SignedInfo.Reference.array + sig_fragment.SignedInfo.Reference.arrayLen); ++ref) { + ref->Type_isUsed = 0; + ref->Transforms.Transform.ANY_isUsed = 0; + ref->Transforms.Transform.XPath_isUsed = 0; + ref->DigestMethod.ANY_isUsed = 0; + } + + stream.byte_pos = 0; + stream.bit_count = 0; + err = encode_iso2_xmldsigFragment(&stream, &sig_fragment); + + if (err != 0) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode XML signature fragment, error code = %d", err); + bRes = false; + } + } + + if (bRes) { + // hash again (different data) buffer_pos has been updated ... + const auto frag_data_len = exi_bitstream_get_length(&stream); + bRes = sha_256(exi_buffer.data(), frag_data_len, digest); + } + + if (bRes) { + /* Validate the ecdsa signature using the public key */ + if (signature_len != ::openssl::signature_size) { + dlog(DLOG_LEVEL_ERROR, "Signature len is invalid (%i)", signature_len); + bRes = false; + } + } + + if (bRes) { + const std::uint8_t* r = &signature[0]; + const std::uint8_t* s = &signature[32]; + bRes = verify(pkey, r, s, digest); + } + + return bRes; +} + +bool load_contract_root_cert(::openssl::CertificateList& trust_anchors, const char* V2G_file_path, + const char* MO_file_path) { + // note the file(s) may contain more than one certificate (hence must be PEM) + // try MO_file_path first then fallback to V2G_file_path + + trust_anchors.clear(); + trust_anchors = ::openssl::load_certificates(MO_file_path); + if (trust_anchors.empty()) { + log_error("Unable to load MO root(s)"); + trust_anchors = ::openssl::load_certificates(V2G_file_path); + if (trust_anchors.empty()) { + log_error("Unable to load V2G root(s)"); + } + } + + return !trust_anchors.empty(); +} + +int load_certificate(::openssl::CertificateList* chain, const std::uint8_t* bytes, std::uint16_t bytesLen) { + assert(chain != nullptr); + int result{-1}; + + auto tmp_cert = ::openssl::der_to_certificate(bytes, bytesLen); + if (tmp_cert != nullptr) { + chain->push_back(std::move(tmp_cert)); + result = 0; + } + + return result; +} + +int parse_contract_certificate(::openssl::Certificate_ptr& crt, const std::uint8_t* buf, std::size_t buflen) { + crt = ::openssl::der_to_certificate(buf, buflen); + return (crt == nullptr) ? -1 : 0; +} + +std::string getEmaidFromContractCert(const ::openssl::Certificate_ptr& crt) { + std::string cert_emaid; + const auto subject = ::openssl::certificate_subject(crt.get()); + if (auto itt = subject.find("CN"); itt != subject.end()) { + cert_emaid = itt->second; + } + + return cert_emaid; +} + +std::string chain_to_pem(const ::openssl::Certificate_ptr& cert, const ::openssl::CertificateList* chain) { + assert(chain != nullptr); + + std::string contract_cert_chain_pem(::openssl::certificate_to_pem(cert.get())); + for (const auto& crt : *chain) { + const auto pem = ::openssl::certificate_to_pem(crt.get()); + if (pem.empty()) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode certificate chain"); + break; + } + contract_cert_chain_pem.append(pem); + } + + return contract_cert_chain_pem; +} + +verify_result_t verify_certificate(const ::openssl::Certificate_ptr& cert, const ::openssl::CertificateList* chain, + const char* v2g_root_cert_path, const char* mo_root_cert_path, + bool /* debugMode */) { + assert(chain != nullptr); + + verify_result_t result{verify_result_t::verified}; + ::openssl::CertificateList trust_anchors; + + if (!load_contract_root_cert(trust_anchors, v2g_root_cert_path, mo_root_cert_path)) { + result = verify_result_t::NoCertificateAvailable; + } else { + result = ::openssl::verify_certificate(cert.get(), trust_anchors, *chain); + } + + return result; +} + +} // namespace crypto::openssl diff --git a/modules/EvseV2G/crypto/crypto_openssl.hpp b/modules/EvseV2G/crypto/crypto_openssl.hpp new file mode 100644 index 000000000..a65b56401 --- /dev/null +++ b/modules/EvseV2G/crypto/crypto_openssl.hpp @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef CRYPTO_OPENSSL_HPP_ +#define CRYPTO_OPENSSL_HPP_ + +#include +#include + +#include "crypto_common.hpp" +#include + +/** + * \file OpenSSL implementation + */ + +struct evp_pkey_st; +struct iso2_SignatureType; +struct iso2_exiFragment; +struct x509_st; +struct v2g_connection; + +namespace crypto::openssl { + +/** + * \brief check the signature of a signed 15118 message + * \param iso2_signature the signature to check + * \param public_key the public key from the contract certificate + * \param iso2_exi_fragment the signed data + * \return true when the signature is valid + */ +bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, evp_pkey_st* pkey, + struct iso2_exiFragment* iso2_exi_fragment); + +/** + * \brief load the trust anchor for the contract certificate. + * Use the mobility operator certificate if it exists otherwise + * the V2G certificate + * \param contract_root_crt the retrieved trust anchor + * \param V2G_file_path the file containing the V2G trust anchor (PEM format) + * \param MO_file_path the file containing the mobility operator trust anchor (PEM format) + * \return true when a certificate was found + */ +bool load_contract_root_cert(::openssl::CertificateList& trust_anchors, const char* V2G_file_path, + const char* MO_file_path); + +/** + * \brief clear certificate and public key from previous connection + * \param conn the V2G connection data + * \note not needed for the OpenSSL implementation + */ +constexpr void free_connection_crypto_data(v2g_connection* conn) { +} + +/** + * \brief load a contract certificate's certification path certificate from the V2G message as DER bytes + * \param chain the certificate path certificates (this certificate is added to the list) + * \param bytes the DER (ASN.1) X509v3 certificate in the V2G message + * \param bytesLen the length of the DER encoded certificate + * \return 0 when certificate successfully loaded + */ +int load_certificate(::openssl::CertificateList* chain, const std::uint8_t* bytes, std::uint16_t bytesLen); + +/** + * \brief load the contract certificate from the V2G message as DER bytes + * \param crt the certificate + * \param bytes the DER (ASN.1) X509v3 certificate in the V2G message + * \param bytesLen the length of the DER encoded certificate + * \return 0 when certificate successfully loaded + */ +int parse_contract_certificate(::openssl::Certificate_ptr& crt, const std::uint8_t* buf, std::size_t buflen); + +/** + * \brief get the EMAID from the certificate (CommonName from the SubjectName) + * \param crt the certificate + * \return the EMAD or empty on error + */ +std::string getEmaidFromContractCert(const ::openssl::Certificate_ptr& crt); + +/** + * \brief convert a list of certificates into a PEM string starting with the contract certificate + * \param contract_crt the contract certificate (when not the first certificate in the chain) + * \param chain the certification path chain (might include the contract certificate as the first item) + * \return PEM string or empty on error + */ +std::string chain_to_pem(const ::openssl::Certificate_ptr& cert, const ::openssl::CertificateList* chain); + +/** + * \brief verify certification path of the contract certificate through to a trust anchor + * \param contract_crt (single certificate or chain with the contract certificate as the first item) + * \param chain intermediate certificates (may be nullptr) + * \param v2g_root_cert_path V2G trust anchor file name + * \param mo_root_cert_path mobility operator trust anchor file name + * \param debugMode additional information on verification failures + * \result a subset of possible verification failures where known or 'verified' on success + */ +verify_result_t verify_certificate(const ::openssl::Certificate_ptr& cert, const ::openssl::CertificateList* chain, + const char* v2g_root_cert_path, const char* mo_root_cert_path, bool debugMode); + +} // namespace crypto::openssl + +#endif // CRYPTO_OPENSSL_HPP_ diff --git a/modules/EvseV2G/iso_server.cpp b/modules/EvseV2G/iso_server.cpp index 659c6c8a0..11c2c2c58 100644 --- a/modules/EvseV2G/iso_server.cpp +++ b/modules/EvseV2G/iso_server.cpp @@ -10,13 +10,22 @@ #include #include #include +#include +#include +#include + +#ifdef EVEREST_MBED_TLS +#include "crypto/crypto_mbedtls.hpp" +using namespace crypto::mbedtls; #include #include #include /* To extract the emaid */ #include -#include -#include -#include +#else +#include "crypto/crypto_openssl.hpp" +using namespace openssl; +using namespace crypto::openssl; +#endif // EVEREST_MBED_TLS #include "iso_server.hpp" #include "log.hpp" @@ -24,17 +33,12 @@ #include "v2g_ctx.hpp" #include "v2g_server.hpp" -#define MAX_EXI_SIZE 8192 -#define DIGEST_SIZE 32 #define MQTT_MAX_PAYLOAD_SIZE 268435455 #define V2G_SECC_MSG_CERTINSTALL_TIME 4500 -#define MAX_EXI_SIZE 8192 -#define MAX_CERT_SIZE 800 // [V2G2-010] -#define MAX_EMAID_LEN 18 #define GEN_CHALLENGE_SIZE 16 -const uint16_t SAE_V2H = 28472; -const uint16_t SAE_V2G = 28473; +constexpr uint16_t SAE_V2H = 28472; +constexpr uint16_t SAE_V2G = 28473; /*! * \brief iso_validate_state This function checks whether the received message is expected and valid at this @@ -110,213 +114,6 @@ static v2g_event iso_validate_response_code(iso2_responseCodeType* const v2g_res return next_event; } -static bool load_contract_root_cert(mbedtls_x509_crt* contract_root_crt, const char* V2G_file_path, - const char* MO_file_path) { - int rv = 0; - - if ((rv = mbedtls_x509_crt_parse_file(contract_root_crt, MO_file_path)) != 0) { - char strerr[256]; - mbedtls_strerror(rv, strerr, sizeof(strerr)); - dlog(DLOG_LEVEL_WARNING, "Unable to parse MO (%s) root certificate. (err: -0x%04x - %s)", MO_file_path, -rv, - strerr); - dlog(DLOG_LEVEL_INFO, "Attempting to parse V2G root certificate.."); - - if ((rv = mbedtls_x509_crt_parse_file(contract_root_crt, V2G_file_path)) != 0) { - mbedtls_strerror(rv, strerr, sizeof(strerr)); - dlog(DLOG_LEVEL_ERROR, "Unable to parse V2G (%s) root certificate. (err: -0x%04x - %s)", V2G_file_path, -rv, - strerr); - } - } - - return (rv != 0) ? false : true; -} - -/*! - * \brief debug_verify_cert Function is from https://github.com/aws/aws-iot-device-sdk-embedded-C/blob - * /master/platform/linux/mbedtls/network_mbedtls_wrapper.c to debug certificate verification - * \param data - * \param crt - * \param depth - * \param flags - * \return - */ -static int debug_verify_cert(void* data, mbedtls_x509_crt* crt, int depth, uint32_t* flags) { - char buf[1024]; - ((void)data); - - dlog(DLOG_LEVEL_INFO, "\nVerify requested for (Depth %d):\n", depth); - mbedtls_x509_crt_info(buf, sizeof(buf) - 1, "", crt); - dlog(DLOG_LEVEL_INFO, "%s", buf); - - if ((*flags) == 0) - dlog(DLOG_LEVEL_INFO, " This certificate has no flags\n"); - else { - mbedtls_x509_crt_verify_info(buf, sizeof(buf), " ! ", *flags); - dlog(DLOG_LEVEL_INFO, "%s\n", buf); - } - - return (0); -} - -/*! - * \brief printMbedVerifyErrorCode This functions prints the mbedTls specific error code. - * \param AErr is the return value of the mbed verify function - * \param AFlags includes the flags of the verification. - */ -static void printMbedVerifyErrorCode(int AErr, uint32_t AFlags) { - dlog(DLOG_LEVEL_ERROR, "Failed to verify certificate (err: 0x%08x flags: 0x%08x)", AErr, AFlags); - if (AErr == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { - if (AFlags & MBEDTLS_X509_BADCERT_EXPIRED) - dlog(DLOG_LEVEL_ERROR, "CERT_EXPIRED"); - else if (AFlags & MBEDTLS_X509_BADCERT_REVOKED) - dlog(DLOG_LEVEL_ERROR, "CERT_REVOKED"); - else if (AFlags & MBEDTLS_X509_BADCERT_CN_MISMATCH) - dlog(DLOG_LEVEL_ERROR, "CN_MISMATCH"); - else if (AFlags & MBEDTLS_X509_BADCERT_NOT_TRUSTED) - dlog(DLOG_LEVEL_ERROR, "CERT_NOT_TRUSTED"); - else if (AFlags & MBEDTLS_X509_BADCRL_NOT_TRUSTED) - dlog(DLOG_LEVEL_ERROR, "CRL_NOT_TRUSTED"); - else if (AFlags & MBEDTLS_X509_BADCRL_EXPIRED) - dlog(DLOG_LEVEL_ERROR, "CRL_EXPIRED"); - else if (AFlags & MBEDTLS_X509_BADCERT_MISSING) - dlog(DLOG_LEVEL_ERROR, "CERT_MISSING"); - else if (AFlags & MBEDTLS_X509_BADCERT_SKIP_VERIFY) - dlog(DLOG_LEVEL_ERROR, "SKIP_VERIFY"); - else if (AFlags & MBEDTLS_X509_BADCERT_OTHER) - dlog(DLOG_LEVEL_ERROR, "CERT_OTHER"); - else if (AFlags & MBEDTLS_X509_BADCERT_FUTURE) - dlog(DLOG_LEVEL_ERROR, "CERT_FUTURE"); - else if (AFlags & MBEDTLS_X509_BADCRL_FUTURE) - dlog(DLOG_LEVEL_ERROR, "CRL_FUTURE"); - else if (AFlags & MBEDTLS_X509_BADCERT_KEY_USAGE) - dlog(DLOG_LEVEL_ERROR, "KEY_USAGE"); - else if (AFlags & MBEDTLS_X509_BADCERT_EXT_KEY_USAGE) - dlog(DLOG_LEVEL_ERROR, "EXT_KEY_USAGE"); - else if (AFlags & MBEDTLS_X509_BADCERT_NS_CERT_TYPE) - dlog(DLOG_LEVEL_ERROR, "NS_CERT_TYPE"); - else if (AFlags & MBEDTLS_X509_BADCERT_BAD_MD) - dlog(DLOG_LEVEL_ERROR, "BAD_MD"); - else if (AFlags & MBEDTLS_X509_BADCERT_BAD_PK) - dlog(DLOG_LEVEL_ERROR, "BAD_PK"); - else if (AFlags & MBEDTLS_X509_BADCERT_BAD_KEY) - dlog(DLOG_LEVEL_ERROR, "BAD_KEY"); - else if (AFlags & MBEDTLS_X509_BADCRL_BAD_MD) - dlog(DLOG_LEVEL_ERROR, "CRL_BAD_MD"); - else if (AFlags & MBEDTLS_X509_BADCRL_BAD_PK) - dlog(DLOG_LEVEL_ERROR, "CRL_BAD_PK"); - else if (AFlags & MBEDTLS_X509_BADCRL_BAD_KEY) - dlog(DLOG_LEVEL_ERROR, "CRL_BAD_KEY"); - } -} - -/*! - * \brief check_iso2_signature This function validates the ISO signature - * \param iso2_signature is the signature of the ISO EXI fragment - * \param public_key is the public key to validate the signature against the ISO EXI fragment - * \param iso2_exi_fragment iso2_exi_fragment is the ISO EXI fragment - */ -static bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, mbedtls_ecdsa_context* public_key, - struct iso2_exiFragment* iso2_exi_fragment) { - /** Digest check **/ - int err = 0; - const struct iso2_SignatureType* sig = iso2_signature; - unsigned char buf[MAX_EXI_SIZE]; - const struct iso2_ReferenceType* req_ref = &sig->SignedInfo.Reference.array[0]; - exi_bitstream_t stream; - exi_bitstream_init(&stream, buf, MAX_EXI_SIZE, 0, NULL); - uint8_t digest[DIGEST_SIZE]; - err = encode_iso2_exiFragment(&stream, iso2_exi_fragment); - if (err != 0) { - dlog(DLOG_LEVEL_ERROR, "Unable to encode fragment, error code = %d", err); - return false; - } - uint32_t frag_data_len = exi_bitstream_get_length(&stream); - mbedtls_sha256(buf, frag_data_len, digest, 0); - - if (req_ref->DigestValue.bytesLen != DIGEST_SIZE) { - dlog(DLOG_LEVEL_ERROR, "Invalid digest length %u in signature", req_ref->DigestValue.bytesLen); - return false; - } - - if (memcmp(req_ref->DigestValue.bytes, digest, DIGEST_SIZE) != 0) { - dlog(DLOG_LEVEL_ERROR, "Invalid digest in signature"); - return false; - } - - /** Validate signature **/ - struct iso2_xmldsigFragment sig_fragment; - init_iso2_xmldsigFragment(&sig_fragment); - sig_fragment.SignedInfo_isUsed = 1; - sig_fragment.SignedInfo = sig->SignedInfo; - - /** \req [V2G2-771] Don't use following fields */ - sig_fragment.SignedInfo.Id_isUsed = 0; - sig_fragment.SignedInfo.CanonicalizationMethod.ANY_isUsed = 0; - sig_fragment.SignedInfo.SignatureMethod.HMACOutputLength_isUsed = 0; - sig_fragment.SignedInfo.SignatureMethod.ANY_isUsed = 0; - for (auto* ref = sig_fragment.SignedInfo.Reference.array; - ref != (sig_fragment.SignedInfo.Reference.array + sig_fragment.SignedInfo.Reference.arrayLen); ++ref) { - ref->Type_isUsed = 0; - ref->Transforms.Transform.ANY_isUsed = 0; - ref->Transforms.Transform.XPath_isUsed = 0; - ref->DigestMethod.ANY_isUsed = 0; - } - - stream.byte_pos = 0; - stream.bit_count = 0; - err = encode_iso2_xmldsigFragment(&stream, &sig_fragment); - - if (err != 0) { - dlog(DLOG_LEVEL_ERROR, "Unable to encode XML signature fragment, error code = %d", err); - return false; - } - uint32_t sign_info_fragmen_len = exi_bitstream_get_length(&stream); - - /* Hash the signature */ - mbedtls_sha256(buf, sign_info_fragmen_len, digest, 0); - - /* Validate the ecdsa signature using the public key */ - if (sig->SignatureValue.CONTENT.bytesLen == 0) { - dlog(DLOG_LEVEL_ERROR, "Signature len is invalid (%i)", sig->SignatureValue.CONTENT.bytesLen); - return false; - } - - /* Init mbedtls parameter */ - mbedtls_ecp_group ecp_group; - mbedtls_ecp_group_init(&ecp_group); - - mbedtls_mpi mpi_r; - mbedtls_mpi_init(&mpi_r); - mbedtls_mpi mpi_s; - mbedtls_mpi_init(&mpi_s); - - mbedtls_mpi_read_binary(&mpi_r, (const unsigned char*)&sig->SignatureValue.CONTENT.bytes[0], - sig->SignatureValue.CONTENT.bytesLen / 2); - mbedtls_mpi_read_binary( - &mpi_s, (const unsigned char*)&sig->SignatureValue.CONTENT.bytes[sig->SignatureValue.CONTENT.bytesLen / 2], - sig->SignatureValue.CONTENT.bytesLen / 2); - - err = mbedtls_ecp_group_load(&ecp_group, MBEDTLS_ECP_DP_SECP256R1); - - if (err == 0) { - err = mbedtls_ecdsa_verify(&ecp_group, static_cast(digest), 32, &public_key->Q, &mpi_r, - &mpi_s); - } - - mbedtls_ecp_group_free(&ecp_group); - mbedtls_mpi_free(&mpi_r); - mbedtls_mpi_free(&mpi_s); - - if (err != 0) { - char error_buf[100]; - mbedtls_strerror(err, error_buf, sizeof(error_buf)); - dlog(DLOG_LEVEL_ERROR, "Invalid signature, error code = -0x%08x, %s", err, error_buf); - return false; - } - - return true; -} - /*! * \brief populate_ac_evse_status This function configures the evse_status struct * \param ctx is the V2G context @@ -429,64 +226,6 @@ static void publish_DC_EVStatusType(struct v2g_context* ctx, const struct iso2_D } } -static bool getSubjectData(const mbedtls_x509_name* ASubject, const char* AAttrName, const mbedtls_asn1_buf** AVal) { - const char* attrName = NULL; - - while (NULL != ASubject) { - if ((0 == mbedtls_oid_get_attr_short_name(&ASubject->oid, &attrName)) && (0 == strcmp(attrName, AAttrName))) { - - *AVal = &ASubject->val; - return true; - } else { - ASubject = ASubject->next; - } - } - *AVal = NULL; - return false; -} - -/*! - * \brief getEmaidFromContractCert This function extracts the emaid from an subject of a contract certificate. - * \param ASubject is the subject of the contract certificate. - * \param AEmaid is the buffer for the extracted emaid. The extracted string is null terminated. - * \param AEmaidLen is length of the AEmaid buffer. - * \return Returns the length of the extracted emaid. - */ -static size_t getEmaidFromContractCert(const mbedtls_x509_name* ASubject, char* AEmaid, uint8_t AEmaidLen) { - - const mbedtls_asn1_buf* val = NULL; - size_t certEmaidLen = 0; - char certEmaid[MAX_EMAID_LEN + 1]; - - if (true == getSubjectData(ASubject, "CN", &val)) { - /* Check the emaid length within the certificate */ - certEmaidLen = MAX_EMAID_LEN < val->len ? 0 : val->len; - strncpy(certEmaid, reinterpret_cast(val->p), certEmaidLen); - certEmaid[certEmaidLen] = '\0'; - - /* Filter '-' character */ - certEmaidLen = 0; - for (uint8_t idx = 0; certEmaid[idx] != '\0'; idx++) { - if ('-' != certEmaid[idx]) { - certEmaid[certEmaidLen++] = certEmaid[idx]; - } - } - certEmaid[certEmaidLen] = '\0'; - - if (certEmaidLen > (AEmaidLen - 1)) { - dlog(DLOG_LEVEL_WARNING, "emaid buffer is too small (%i received, expected %i)", certEmaidLen, - AEmaidLen - 1); - return 0; - } else { - strcpy(AEmaid, certEmaid); - } - } else { - dlog(DLOG_LEVEL_WARNING, "No CN found in subject of the contract certificate"); - } - - return certEmaidLen; -} - static auto get_emergency_status_code(const struct v2g_context* ctx, uint8_t phase_type) { if (ctx->intl_emergency_shutdown) return iso2_DC_EVSEStatusCodeType_EVSE_EmergencyShutdown; @@ -719,6 +458,7 @@ static bool publish_iso_certificate_installation_exi_req(struct v2g_context* ctx size_t olen; types::iso15118_charger::Request_Exi_Stream_Schema certificate_request; +#ifdef EVEREST_MBED_TLS /* Parse contract leaf certificate */ mbedtls_base64_encode(NULL, 0, &olen, static_cast(AExiBuffer), AExiBufferSize); @@ -736,8 +476,19 @@ static bool publish_iso_certificate_installation_exi_req(struct v2g_context* ctx dlog(DLOG_LEVEL_ERROR, "Unable to encode contract leaf certificate"); goto exit; } - certificate_request.exiRequest = std::string(reinterpret_cast(base64Buffer), olen); +#else + certificate_request.exiRequest = openssl::base64_encode(AExiBuffer, AExiBufferSize); + if (certificate_request.exiRequest.size() > MQTT_MAX_PAYLOAD_SIZE) { + dlog(DLOG_LEVEL_ERROR, "Mqtt payload size exceeded!"); + return false; + } + if (certificate_request.exiRequest.size() == 0) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode contract leaf certificate"); + return false; + } +#endif // EVEREST_MBED_TLS + certificate_request.iso15118SchemaVersion = ISO_15118_2013_MSG_DEF; certificate_request.certificateAction = types::iso15118_charger::CertificateActionEnum::Install; ctx->p_charger->publish_Certificate_Request(certificate_request); @@ -1090,46 +841,40 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { int err; // === For the contract certificate, the certificate chain should be checked === - conn->ctx->session.contract.valid_crt = false; - if (conn->ctx->session.iso_selected_payment_option == iso2_paymentOptionType_Contract) { // Free old stuff if it exists - mbedtls_x509_crt_free(&conn->ctx->session.contract.crt); - mbedtls_ecdsa_free(&conn->ctx->session.contract.pubkey); - - mbedtls_x509_crt_init(&conn->ctx->session.contract.crt); - mbedtls_ecdsa_init(&conn->ctx->session.contract.pubkey); + free_connection_crypto_data(conn); // Parse contract leaf certificate + +#ifdef EVEREST_MBED_TLS + Certificate_ptr contract_crt; + const void* chain{nullptr}; +#else + Certificate_ptr contract_crt{nullptr, nullptr}; + CertificateList chain{}; +#endif // EVEREST_MBED_TLS + if (req->ContractSignatureCertChain.Certificate.bytesLen != 0) { - err = mbedtls_x509_crt_parse(&conn->ctx->session.contract.crt, - req->ContractSignatureCertChain.Certificate.bytes, - req->ContractSignatureCertChain.Certificate.bytesLen); + err = parse_contract_certificate(contract_crt, req->ContractSignatureCertChain.Certificate.bytes, + req->ContractSignatureCertChain.Certificate.bytesLen); } else { dlog(DLOG_LEVEL_ERROR, "No certificate received!"); res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; goto error_out; } - /* Check the received emaid against the certificate emaid. */ - char cert_emaid[iso2_eMAID_CHARACTER_SIZE]; - getEmaidFromContractCert(&conn->ctx->session.contract.crt.subject, cert_emaid, sizeof(cert_emaid)); + auto cert_emaid = getEmaidFromContractCert(contract_crt); + std::string req_emaid{&req->eMAID.characters[0], req->eMAID.charactersLen}; /* Filter '-' character */ - uint8_t emaid_len = 0; - for (uint8_t idx = 0; idx < req->eMAID.charactersLen; idx++) { - if ('-' != req->eMAID.characters[idx]) { - req->eMAID.characters[emaid_len++] = req->eMAID.characters[idx]; - } - } - - req->eMAID.charactersLen = emaid_len; - req->eMAID.characters[emaid_len] = '\0'; + cert_emaid.erase(std::remove(cert_emaid.begin(), cert_emaid.end(), '-'), cert_emaid.end()); + req_emaid.erase(std::remove(req_emaid.begin(), req_emaid.end(), '-'), req_emaid.end()); - dlog(DLOG_LEVEL_TRACE, "emaid-v2g: %s emaid-cert: %s", req->eMAID.characters, cert_emaid); + dlog(DLOG_LEVEL_TRACE, "emaid-v2g: %s emaid-cert: %s", req_emaid.c_str(), cert_emaid.c_str()); - if ((std::string(cert_emaid).size() != req->eMAID.charactersLen) || - (0 != strncasecmp(req->eMAID.characters, cert_emaid, req->eMAID.charactersLen))) { + if ((req_emaid.size() != cert_emaid.size()) || + (strncasecmp(req_emaid.c_str(), cert_emaid.c_str(), req_emaid.size()) != 0)) { dlog(DLOG_LEVEL_ERROR, "emaid of the contract certificate doesn't match with the received v2g-emaid"); res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; goto error_out; @@ -1138,65 +883,49 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { if (err != 0) { memset(res, 0, sizeof(*res)); res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; - char strerr[256]; - mbedtls_strerror(err, strerr, std::string(strerr).size()); - dlog(DLOG_LEVEL_ERROR, "handle_payment_detail: invalid certificate received in req: %s", strerr); goto error_out; } // Convert the public key in the certificate to a mbed TLS ECDSA public key // This also verifies that it's an ECDSA key and not an RSA key - err = mbedtls_ecdsa_from_keypair(&conn->ctx->session.contract.pubkey, - mbedtls_pk_ec(conn->ctx->session.contract.crt.pk)); + +#ifdef EVEREST_MBED_TLS + err = get_public_key(&conn->ctx->session.contract.pubkey, contract_crt.get()->pk); +#else + assert(conn->pubkey != nullptr); + *conn->pubkey = certificate_public_key(contract_crt.get()); + err = (*conn->pubkey == nullptr) ? -1 : 0; +#endif // EVEREST_MBED_TLS + if (err != 0) { memset(res, 0, sizeof(*res)); res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; - char strerr[256]; - mbedtls_strerror(err, strerr, std::string(strerr).size()); - dlog(DLOG_LEVEL_ERROR, "Could not retrieve ecdsa public key from certificate keypair: %s", strerr); goto error_out; } // Parse contract sub certificates if (req->ContractSignatureCertChain.SubCertificates_isUsed == 1) { for (int i = 0; i < req->ContractSignatureCertChain.SubCertificates.Certificate.arrayLen; i++) { - err = mbedtls_x509_crt_parse( - &conn->ctx->session.contract.crt, - req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytes, - req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytesLen); - +#ifdef EVEREST_MBED_TLS + err = load_certificate(contract_crt, + req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytes, + req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytesLen); +#else + err = + load_certificate(&chain, req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytes, + req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytesLen); +#endif // EVEREST_MBED_TLS if (err != 0) { - char strerr[256]; - mbedtls_strerror(err, strerr, std::string(strerr).size()); - dlog(DLOG_LEVEL_ERROR, "handle_payment_detail: invalid sub-certificate received in req: %s", - strerr); + res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; goto error_out; } } } // initialize contract cert chain to retrieve ocsp request data - std::string contract_cert_chain_pem = ""; - // Save the certificate chain in a variable in PEM format to publish it - mbedtls_x509_crt* crt = &conn->ctx->session.contract.crt; - unsigned char* base64Buffer = NULL; - size_t olen; - - while (crt != nullptr && crt->version != 0) { - mbedtls_base64_encode(NULL, 0, &olen, crt->raw.p, crt->raw.len); - base64Buffer = static_cast(malloc(olen)); - if ((base64Buffer == NULL) || - ((mbedtls_base64_encode(base64Buffer, olen, &olen, crt->raw.p, crt->raw.len)) != 0)) { - dlog(DLOG_LEVEL_ERROR, "Unable to encode certificate chain"); - break; - } - contract_cert_chain_pem.append("-----BEGIN CERTIFICATE-----\n"); - contract_cert_chain_pem.append(std::string(reinterpret_cast(base64Buffer), olen)); - contract_cert_chain_pem.append("\n-----END CERTIFICATE-----\n"); - free(base64Buffer); - crt = crt->next; - } + // Save the certificate chain in a variable in PEM format to publish it + std::string contract_cert_chain_pem = chain_to_pem(contract_crt, &chain); std::optional> iso15118_certificate_hash_data; @@ -1206,36 +935,41 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { conn->ctx->r_security->call_get_verify_file(types::evse_security::CaCertificateType::V2G); std::string mo_root_cert_path = conn->ctx->r_security->call_get_verify_file(types::evse_security::CaCertificateType::MO); - mbedtls_x509_crt contract_root_crt; - mbedtls_x509_crt_init(&contract_root_crt); - uint32_t flags; - /* Load supported V2G/MO root certificates */ - if (load_contract_root_cert(&contract_root_crt, v2g_root_cert_path.c_str(), mo_root_cert_path.c_str()) == - false) { - memset(res, 0, sizeof(*res)); + crypto::verify_result_t vRes = verify_certificate(contract_crt, &chain, v2g_root_cert_path.c_str(), + mo_root_cert_path.c_str(), conn->ctx->debugMode); + + err = -1; + switch (vRes) { + case crypto::verify_result_t::verified: + err = 0; + break; + case crypto::verify_result_t::CertificateExpired: + res->ResponseCode = iso2_responseCodeType_FAILED_CertificateExpired; + break; + case crypto::verify_result_t::CertificateRevoked: + res->ResponseCode = iso2_responseCodeType_FAILED_CertificateRevoked; + break; + case crypto::verify_result_t::NoCertificateAvailable: res->ResponseCode = iso2_responseCodeType_FAILED_NoCertificateAvailable; - goto error_out; + err = -2; + break; + case crypto::verify_result_t::CertChainError: + default: + res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; + break; } - // === Verify the retrieved contract ECDSA key against the root cert === - err = mbedtls_x509_crt_verify(&conn->ctx->session.contract.crt, &contract_root_crt, NULL, NULL, &flags, - (conn->ctx->debugMode == true) ? debug_verify_cert : NULL, NULL); - if (err != 0) { - printMbedVerifyErrorCode(err, flags); - memset(res, 0, sizeof(*res)); + if (err == -1) { dlog(DLOG_LEVEL_ERROR, "Validation of the contract certificate failed!"); - if ((err == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) && (flags & MBEDTLS_X509_BADCERT_EXPIRED)) { - res->ResponseCode = iso2_responseCodeType_FAILED_CertificateExpired; - } else if ((err == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) && (flags & MBEDTLS_X509_BADCERT_REVOKED)) { - res->ResponseCode = iso2_responseCodeType_FAILED_CertificateRevoked; - } else { - res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; - } // EVSETimeStamp and GenChallenge are mandatory, GenChallenge has fixed size res->EVSETimeStamp = time(NULL); memset(res->GenChallenge.bytes, 0, GEN_CHALLENGE_SIZE); res->GenChallenge.bytesLen = GEN_CHALLENGE_SIZE; + } + + if (err != 0) { + memset(res, 0, sizeof(*res)); goto error_out; } @@ -1249,7 +983,6 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { generate_random_data(&conn->ctx->session.gen_challenge, GEN_CHALLENGE_SIZE); memcpy(res->GenChallenge.bytes, conn->ctx->session.gen_challenge, GEN_CHALLENGE_SIZE); res->GenChallenge.bytesLen = GEN_CHALLENGE_SIZE; - conn->ctx->session.contract.valid_crt = true; // Publish the provided signature certificate chain and eMAID from EVCC // to receive PnC authorization @@ -1320,8 +1053,16 @@ static enum v2g_event handle_iso_authorization(struct v2g_connection* conn) { iso2_fragment.AuthorizationReq_isUsed = 1u; memcpy(&iso2_fragment.AuthorizationReq, req, sizeof(*req)); - if (check_iso2_signature(&conn->exi_in.iso2EXIDocument->V2G_Message.Header.Signature, - &conn->ctx->session.contract.pubkey, &iso2_fragment) == false) { +#ifdef EVEREST_MBED_TLS + const bool bSigRes = check_iso2_signature(&conn->exi_in.iso2EXIDocument->V2G_Message.Header.Signature, + conn->ctx->session.contract.pubkey, &iso2_fragment); +#else + assert(conn->pubkey != nullptr); + const bool bSigRes = check_iso2_signature(&conn->exi_in.iso2EXIDocument->V2G_Message.Header.Signature, + conn->pubkey->get(), &iso2_fragment); +#endif + + if (!bSigRes) { res->ResponseCode = iso2_responseCodeType_FAILED_SignatureError; goto error_out; } @@ -1901,6 +1642,7 @@ static enum v2g_event handle_iso_certificate_installation(struct v2g_connection* if ((conn->ctx->evse_v2g_data.cert_install_res_b64_buffer.empty() == false) && (conn->ctx->evse_v2g_data.cert_install_status == true)) { +#ifdef EVEREST_MBED_TLS size_t buffer_pos = 0; if ((rv = mbedtls_base64_decode( conn->buffer + V2GTP_HEADER_LENGTH, DEFAULT_BUFFER_SIZE, &buffer_pos, @@ -1912,6 +1654,17 @@ static enum v2g_event handle_iso_certificate_installation(struct v2g_connection* goto exit; } conn->stream.byte_pos = buffer_pos; +#else + const auto data = openssl::base64_decode(conn->ctx->evse_v2g_data.cert_install_res_b64_buffer.data(), + conn->ctx->evse_v2g_data.cert_install_res_b64_buffer.size()); + if (data.empty() || (data.size() > DEFAULT_BUFFER_SIZE)) { + dlog(DLOG_LEVEL_ERROR, "Failed to decode base64 stream"); + goto exit; + } else { + std::memcpy(conn->buffer + V2GTP_HEADER_LENGTH, data.data(), data.size()); + conn->stream.byte_pos = data.size(); + } +#endif // EVEREST_MBED_TLS nextEvent = V2G_EVENT_SEND_RECV_EXI_MSG; res->ResponseCode = iso2_responseCodeType_OK; // Is irrelevant but must be valid to serve the internal validation diff --git a/modules/EvseV2G/tests/CMakeLists.txt b/modules/EvseV2G/tests/CMakeLists.txt new file mode 100644 index 000000000..2cc4fc01c --- /dev/null +++ b/modules/EvseV2G/tests/CMakeLists.txt @@ -0,0 +1,88 @@ +get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR) +find_package(libevent) + +set(TLS_GTEST_NAME v2g_openssl_test) +add_executable(${TLS_GTEST_NAME}) + +add_dependencies(${TLS_GTEST_NAME} generate_cpp_files) + +target_include_directories(${TLS_GTEST_NAME} PRIVATE + . .. ../crypto ../../../lib/staging/util + ${GENERATED_INCLUDE_DIR} + ${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME} +) + +target_compile_definitions(${TLS_GTEST_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${TLS_GTEST_NAME} PRIVATE + ../../../lib/staging/tls/tests/gtest_main.cpp + log.cpp + openssl_test.cpp + ../crypto/crypto_openssl.cpp +) + +target_link_libraries(${TLS_GTEST_NAME} PRIVATE + GTest::gtest + cbv2g::din + cbv2g::iso2 + cbv2g::tp + everest::framework + everest::evse_security + everest::tls +) + +set(V2G_MAIN_NAME v2g_server) +add_executable(${V2G_MAIN_NAME}) + +add_dependencies(${V2G_MAIN_NAME} generate_cpp_files) + +target_include_directories(${V2G_MAIN_NAME} PRIVATE + . .. ../connection ../../../tests/include ../../../lib/staging/util + ${GENERATED_INCLUDE_DIR} + ${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME} + ${CMAKE_BINARY_DIR}/generated/include +) + +target_compile_definitions(${V2G_MAIN_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${V2G_MAIN_NAME} PRIVATE + ../connection/connection.cpp + ../connection/tls_connection.cpp + ../tools.cpp + ../v2g_ctx.cpp + log.cpp + requirement.cpp + v2g_main.cpp +) + +target_link_libraries(${V2G_MAIN_NAME} PRIVATE + cbv2g::din + cbv2g::iso2 + cbv2g::tp + everest::log + everest::framework + everest::evse_security + everest::tls + -levent -lpthread -levent_pthreads +) + +install( + FILES + ../../../lib/staging/tls/tests/pki/iso_pkey.asn1 + ../../../lib/staging/tls/tests/pki/openssl-pki.conf + ../../../lib/staging/tls/tests/pki/ocsp_response.der + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" +) + +install( + PROGRAMS + ../../../lib/staging/tls/tests/pki/pki.sh + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" +) + +# runs fine locally, fails in CI +# add_test(${TLS_GTEST_NAME} ${TLS_GTEST_NAME}) diff --git a/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp b/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp new file mode 100644 index 000000000..2d903a3b1 --- /dev/null +++ b/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#ifndef ISO15118_CHARGERIMPLSTUB_H_ +#define ISO15118_CHARGERIMPLSTUB_H_ + +#include + +#include + +//----------------------------------------------------------------------------- +namespace module::stub { + +struct ISO15118_chargerImplStub : public ISO15118_chargerImplBase { +public: + ISO15118_chargerImplStub() : ISO15118_chargerImplBase(nullptr, "EvseV2G"){}; + + virtual void init() { + } + virtual void ready() { + } + + virtual void handle_setup(types::iso15118_charger::EVSEID& evse_id, + std::vector& supported_energy_transfer_modes, + types::iso15118_charger::SAE_J2847_Bidi_Mode& sae_j2847_mode, bool& debug_mode) { + std::cout << "ISO15118_chargerImplBase::handle_setup called" << std::endl; + } + virtual void handle_set_charging_parameters(types::iso15118_charger::SetupPhysicalValues& physical_values) { + std::cout << "ISO15118_chargerImplBase::handle_set_charging_parameters called" << std::endl; + } + virtual void handle_session_setup(std::vector& payment_options, + bool& supported_certificate_service) { + std::cout << "ISO15118_chargerImplBase::handle_session_setup called" << std::endl; + } + virtual void handle_certificate_response(types::iso15118_charger::Response_Exi_Stream_Status& exi_stream_status) { + std::cout << "ISO15118_chargerImplBase::handle_certificate_response called" << std::endl; + } + virtual void handle_authorization_response(types::authorization::AuthorizationStatus& authorization_status, + types::authorization::CertificateStatus& certificate_status) { + std::cout << "ISO15118_chargerImplBase::handle_authorization_response called" << std::endl; + } + virtual void handle_ac_contactor_closed(bool& status) { + std::cout << "ISO15118_chargerImplBase::handle_ac_contactor_closed called" << std::endl; + } + virtual void handle_dlink_ready(bool& value) { + std::cout << "ISO15118_chargerImplBase::handle_dlink_ready called" << std::endl; + } + virtual void handle_cable_check_finished(bool& status) { + std::cout << "ISO15118_chargerImplBase::handle_cable_check_finished called" << std::endl; + } + virtual void handle_receipt_is_required(bool& receipt_required) { + std::cout << "ISO15118_chargerImplBase::handle_receipt_is_required called" << std::endl; + } + virtual void handle_stop_charging(bool& stop) { + std::cout << "ISO15118_chargerImplBase::handle_stop_charging called" << std::endl; + } + virtual void handle_update_ac_max_current(double& max_current) { + std::cout << "ISO15118_chargerImplBase::handle_update_ac_max_current called" << std::endl; + } + virtual void handle_update_dc_maximum_limits(types::iso15118_charger::DC_EVSEMaximumLimits& maximum_limits) { + std::cout << "ISO15118_chargerImplBase::handle_update_dc_maximum_limits called" << std::endl; + } + virtual void handle_update_dc_minimum_limits(types::iso15118_charger::DC_EVSEMinimumLimits& minimum_limits) { + std::cout << "ISO15118_chargerImplBase::handle_update_dc_minimum_limits called" << std::endl; + } + virtual void handle_update_isolation_status(types::iso15118_charger::IsolationStatus& isolation_status) { + std::cout << "ISO15118_chargerImplBase::handle_update_isolation_status called" << std::endl; + } + virtual void + handle_update_dc_present_values(types::iso15118_charger::DC_EVSEPresentVoltage_Current& present_voltage_current) { + std::cout << "ISO15118_chargerImplBase::handle_update_dc_present_values called" << std::endl; + } + virtual void handle_update_meter_info(types::powermeter::Powermeter& powermeter) { + std::cout << "ISO15118_chargerImplBase::handle_update_meter_info called" << std::endl; + } + virtual void handle_send_error(types::iso15118_charger::EvseError& error) { + std::cout << "ISO15118_chargerImplBase::handle_send_error called" << std::endl; + } + virtual void handle_reset_error() { + std::cout << "ISO15118_chargerImplBase::handle_reset_error called" << std::endl; + } +}; + +} // namespace module::stub + +#endif // ISO15118_CHARGERIMPLSTUB_H_ diff --git a/modules/EvseV2G/tests/README.md b/modules/EvseV2G/tests/README.md new file mode 100644 index 000000000..3168e4040 --- /dev/null +++ b/modules/EvseV2G/tests/README.md @@ -0,0 +1,51 @@ + +# Tests + +Building tests: + +```sh +$ cd everest-core +$ mkdir build +$ cd build +$ cmake -GNinja -DEVEREST_CORE_BUILD_TESTING=ON -DUSING_MBED_TLS=OFF .. +$ ninja install +``` + +`touch release.json` may be needed if it hasn't been created +(then re-run `ninja install`). + +## Run EVerest in SIL + +1. start MQTT broker +2. from `build/run-scripts` run `./run-sil-dc.sh` +3. from `build/run-scripts` run `./nodered-sil-dc.sh` +4. open web browser [EVerest Node-RED dashboard](http://localhost:1880/ui/) + +## Unit tests + +- `./v2g_openssl_test` +- automatically runs `pki.sh` +- run from the directory containing the executable + +### Standalone V2G TLS server + +Tests the Server class via the functions in connection.cpp and +tls_connection.cpp. + +- `./v2g_server -i ` +- connects to IPv6 only with a link local address +- requires `boost` library so LD_LIBRARY_PATH may need to be set +- displays the address it is listening on. e.g. + `[fe80::ae91:a1ff:fec9:a947%3]:64109` +- supports multiple connections +- gracefully terminates after 80 seconds +- `valgrind` can be used to check memory allocations + (has some leaks - possibly in v2g_ctx_start_events thread) +- requires client certificate +- s_client echos back what is typed with a delay since V2G has a long timeout + +The connect argument must match what was displayed by `v2g_server` + +```sh +openssl s_client -connect [fe80::ae91:a1ff:fec9:a947%3]:64109 -verify 2 -CAfile server_root_cert.pem -cert client_cert.pem -cert_chain client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname evse.pionix.de -status +``` diff --git a/modules/EvseV2G/tests/evse_securityIntfStub.hpp b/modules/EvseV2G/tests/evse_securityIntfStub.hpp new file mode 100644 index 000000000..d2a388e0a --- /dev/null +++ b/modules/EvseV2G/tests/evse_securityIntfStub.hpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#ifndef EVSE_SECURITYINTFSTUB_H_ +#define EVSE_SECURITYINTFSTUB_H_ + +#include + +#include "ModuleAdapterStub.hpp" +#include "generated/types/evse_security.hpp" +#include "utils/types.hpp" +#include +#include +#include +#include + +//----------------------------------------------------------------------------- +namespace module::stub { + +class evse_securityIntfStub : public ModuleAdapterStub, public evse_securityIntf { +private: + std::map + functions; + +public: + evse_securityIntfStub() : evse_securityIntf(this, Requirement("", 0), "EvseSecurity") { + functions["get_verify_file"] = &evse_securityIntfStub::get_verify_file; + functions["get_leaf_certificate_info"] = &evse_securityIntfStub::get_leaf_certificate_info; + } + + virtual Result call_fn(const Requirement& req, const std::string& str, Parameters args) { + if (auto it = functions.find(str); it != functions.end()) { + return std::invoke(it->second, this, req, args); + } + std::printf("call_fn (%s)\n", str.c_str()); + return std::nullopt; + } + + virtual Result get_verify_file(const Requirement& req, const Parameters& args) { + std::cout << "evse_securityIntf::get_verify_file called" << std::endl; + return ""; + } + + virtual Result get_leaf_certificate_info(const Requirement& req, const Parameters& args) { + std::cout << "evse_securityIntf::get_leaf_certificate_info called" << std::endl; + return ""; + } +}; + +} // namespace module::stub + +#endif // EVSE_SECURITYINTFSTUB_H_ diff --git a/modules/EvseV2G/tests/log.cpp b/modules/EvseV2G/tests/log.cpp new file mode 100644 index 000000000..2b06a3519 --- /dev/null +++ b/modules/EvseV2G/tests/log.cpp @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "log.hpp" + +#include +#include + +void dlog_func(const dloglevel_t loglevel, const char* filename, const int linenumber, const char* functionname, + const char* format, ...) { + va_list ap; + va_start(ap, format); + (void)std::vfprintf(stderr, format, ap); + va_end(ap); + (void)std::fprintf(stderr, "\n"); +} diff --git a/modules/EvseV2G/tests/openssl_test.cpp b/modules/EvseV2G/tests/openssl_test.cpp new file mode 100644 index 000000000..c9e47f934 --- /dev/null +++ b/modules/EvseV2G/tests/openssl_test.cpp @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "crypto_common.hpp" +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include + +#include +#include //for V2GTP_HEADER_LENGTHs +#include +#include +#include + +namespace { + +template constexpr void setCharacters(T& dest, const std::string& s) { + dest.charactersLen = s.size(); + std::memcpy(&dest.characters[0], s.c_str(), s.size()); +} + +template constexpr void setBytes(T& dest, const std::uint8_t* b, std::size_t len) { + dest.bytesLen = len; + std::memcpy(&dest.bytes[0], b, len); +} + +struct test_vectors_t { + const char* input; + const std::uint8_t digest[32]; +}; + +constexpr std::uint8_t sign_test[] = {0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, + 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}; + +constexpr test_vectors_t sha_256_test[] = { + {"", {0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}}, + {"abc", {0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, + 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}}}; + +// Test vectors from ISO 15118-2 Section J.2 +// checked okay (see iso_priv.pem) +constexpr std::uint8_t iso_private_key[] = {0xb9, 0x13, 0x49, 0x63, 0xf5, 0x1c, 0x44, 0x14, 0x73, 0x84, 0x35, + 0x05, 0x7f, 0x97, 0xbb, 0xf1, 0x01, 0x0c, 0xab, 0xcb, 0x8d, 0xbd, + 0xe9, 0xc5, 0xd4, 0x81, 0x38, 0x39, 0x6a, 0xa9, 0x4b, 0x9d}; +// checked okay (see iso_priv.pem) +constexpr std::uint8_t iso_public_key[] = {0x43, 0xe4, 0xfc, 0x4c, 0xcb, 0x64, 0x39, 0x04, 0x27, 0x9c, 0x7a, 0x5e, 0x65, + 0x76, 0xb3, 0x23, 0xe5, 0x5e, 0xc7, 0x9f, 0xf0, 0xe5, 0xa4, 0x05, 0x6e, 0x33, + 0x40, 0x84, 0xcb, 0xc3, 0x36, 0xff, 0x46, 0xe4, 0x4c, 0x1a, 0xdd, 0xf6, 0x91, + 0x62, 0xe5, 0x19, 0x2c, 0x2a, 0x83, 0xfc, 0x2b, 0xca, 0x9d, 0x8f, 0x46, 0xec, + 0xf4, 0xb7, 0x80, 0x67, 0xc2, 0x47, 0x6f, 0x6b, 0x3f, 0x34, 0x60, 0x0e}; + +// EXI AuthorizationReq: checked okay (hash computes correctly) +constexpr std::uint8_t iso_exi_a[] = {0x80, 0x04, 0x01, 0x52, 0x51, 0x0c, 0x40, 0x82, 0x9b, 0x7b, 0x6b, 0x29, 0x02, + 0x93, 0x0b, 0x73, 0x23, 0x7b, 0x69, 0x02, 0x23, 0x0b, 0xa3, 0x09, 0xe8}; + +// checked okay +constexpr std::uint8_t iso_exi_a_hash[] = {0xd1, 0xb5, 0xe0, 0x3d, 0x00, 0x65, 0xbe, 0xe5, 0x6b, 0x31, 0x79, + 0x84, 0x45, 0x30, 0x51, 0xeb, 0x54, 0xca, 0x18, 0xfc, 0x0e, 0x09, + 0x16, 0x17, 0x4f, 0x8b, 0x3c, 0x77, 0xa9, 0x8f, 0x4a, 0xa9}; + +// EXI AuthorizationReq signature block: checked okay (hash computes correctly) +constexpr std::uint8_t iso_exi_b[] = { + 0x80, 0x81, 0x12, 0xb4, 0x3a, 0x3a, 0x38, 0x1d, 0x17, 0x97, 0xbb, 0xbb, 0xbb, 0x97, 0x3b, 0x99, 0x97, 0x37, 0xb9, + 0x33, 0x97, 0xaa, 0x29, 0x17, 0xb1, 0xb0, 0xb7, 0x37, 0xb7, 0x34, 0xb1, 0xb0, 0xb6, 0x16, 0xb2, 0xbc, 0x34, 0x97, + 0xa1, 0xab, 0x43, 0xa3, 0xa3, 0x81, 0xd1, 0x79, 0x7b, 0xbb, 0xbb, 0xb9, 0x73, 0xb9, 0x99, 0x73, 0x7b, 0x93, 0x39, + 0x79, 0x91, 0x81, 0x81, 0x89, 0x79, 0x81, 0xa1, 0x7b, 0xc3, 0x6b, 0x63, 0x23, 0x9b, 0x4b, 0x39, 0x6b, 0x6b, 0x7b, + 0x93, 0x29, 0x1b, 0x2b, 0x1b, 0x23, 0x9b, 0x09, 0x6b, 0x9b, 0x43, 0x09, 0x91, 0xa9, 0xb2, 0x20, 0x62, 0x34, 0x94, + 0x43, 0x10, 0x25, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72, + 0x67, 0x2f, 0x54, 0x52, 0x2f, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x2d, 0x65, 0x78, 0x69, 0x2f, + 0x48, 0x52, 0xd0, 0xe8, 0xe8, 0xe0, 0x74, 0x5e, 0x5e, 0xee, 0xee, 0xee, 0x5c, 0xee, 0x66, 0x5c, 0xde, 0xe4, 0xce, + 0x5e, 0x64, 0x60, 0x60, 0x62, 0x5e, 0x60, 0x68, 0x5e, 0xf0, 0xda, 0xd8, 0xca, 0xdc, 0xc6, 0x46, 0xe6, 0xd0, 0xc2, + 0x64, 0x6a, 0x6c, 0x84, 0x1a, 0x36, 0xbc, 0x07, 0xa0, 0x0c, 0xb7, 0xdc, 0xad, 0x66, 0x2f, 0x30, 0x88, 0xa6, 0x0a, + 0x3d, 0x6a, 0x99, 0x43, 0x1f, 0x81, 0xc1, 0x22, 0xc2, 0xe9, 0xf1, 0x67, 0x8e, 0xf5, 0x31, 0xe9, 0x55, 0x23, 0x70}; + +// checked okay +constexpr std::uint8_t iso_exi_b_hash[] = {0xa4, 0xe9, 0x03, 0xe1, 0x82, 0x43, 0x04, 0x1b, 0x55, 0x4e, 0x11, + 0x64, 0x7e, 0x10, 0x1e, 0xd2, 0x5f, 0xc9, 0xf2, 0x15, 0x2a, 0xf4, + 0x67, 0x40, 0x14, 0xfe, 0x2a, 0xde, 0xac, 0x1e, 0x1c, 0xf7}; + +// checked okay (verifies iso_exi_b_hash with iso_priv.pem) +constexpr std::uint8_t iso_exi_sig[] = {0x4c, 0x8f, 0x20, 0xc1, 0x40, 0x0b, 0xa6, 0x76, 0x06, 0xaa, 0x48, 0x11, 0x57, + 0x2a, 0x2f, 0x1a, 0xd3, 0xc1, 0x50, 0x89, 0xd9, 0x54, 0x20, 0x36, 0x34, 0x30, + 0xbb, 0x26, 0xb4, 0x9d, 0xb1, 0x04, 0xf0, 0x8d, 0xfa, 0x8b, 0xf8, 0x05, 0x5e, + 0x63, 0xa4, 0xb7, 0x5a, 0x8d, 0x31, 0x69, 0x20, 0x6f, 0xa8, 0xd5, 0x43, 0x08, + 0xba, 0x58, 0xf0, 0x56, 0x6b, 0x96, 0xba, 0xf6, 0x92, 0xce, 0x59, 0x50}; + +const char iso_exi_a_hash_b64[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk="; +const char iso_exi_a_hash_b64_nl[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=\n"; + +const char iso_exi_sig_b64[] = + "TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBvqNVDCLpY8FZrlrr2ks5ZUA=="; +const char iso_exi_sig_b64_nl[] = + "TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBv\nqNVDCLpY8FZrlrr2ks5ZUA==\n"; + +TEST(openssl, verifyIso) { + auto* bio = BIO_new_file("iso_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + BIO_free(bio); + + auto [sig, siglen] = openssl::bn_to_signature(&iso_exi_sig[0], &iso_exi_sig[32]); + EXPECT_TRUE(openssl::verify(pkey, sig.get(), siglen, &iso_exi_b_hash[0], sizeof(iso_exi_b_hash))); + EVP_PKEY_free(pkey); +} + +TEST(isoExi, signature) { + // The message is: + // header { SessionID, Signature} + // body { AuthorizationReq } + // the test vector doesn't include the entire encoded message + + auto* bio = BIO_new_file("iso_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + BIO_free(bio); + + // decode the test vector AuthorizationReq + struct iso2_exiFragment exi_a {}; + init_iso2_exiFragment(&exi_a); + init_iso2_AuthorizationReqType(&exi_a.AuthorizationReq); + + exi_bitstream_t stream; + exi_bitstream_init(&stream, const_cast(&iso_exi_a[0]), sizeof(iso_exi_a), 0, nullptr); + EXPECT_EQ(decode_iso2_exiFragment(&stream, &exi_a), 0); + + // manually populate the Signature structure + struct iso2_SignatureType sig {}; + init_iso2_SignatureType(&sig); + + // SignedInfo + setCharacters(sig.SignedInfo.CanonicalizationMethod.Algorithm, "http://www.w3.org/TR/canonical-exi/"); + setCharacters(sig.SignedInfo.SignatureMethod.Algorithm, "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"); + sig.SignedInfo.Reference.arrayLen = 1; + sig.SignedInfo.Reference.array[0].URI_isUsed = 1; + setCharacters(sig.SignedInfo.Reference.array[0].URI, "#ID1"); + sig.SignedInfo.Reference.array[0].Transforms_isUsed = 1; + setCharacters(sig.SignedInfo.Reference.array[0].Transforms.Transform.Algorithm, + "http://www.w3.org/TR/canonical-exi/"); + setCharacters(sig.SignedInfo.Reference.array[0].DigestMethod.Algorithm, "http://www.w3.org/2001/04/xmlenc#sha256"); + setBytes(sig.SignedInfo.Reference.array[0].DigestValue, &iso_exi_a_hash[0], ::openssl::sha_256_digest_size); + // SignatureValue + setBytes(sig.SignatureValue.CONTENT, &iso_exi_sig[0], ::openssl::signature_size); + EXPECT_TRUE(crypto::openssl::check_iso2_signature(&sig, pkey, &exi_a)); + + EVP_PKEY_free(pkey); +} + +} // namespace diff --git a/modules/EvseV2G/tests/requirement.cpp b/modules/EvseV2G/tests/requirement.cpp new file mode 100644 index 000000000..cec9c44a7 --- /dev/null +++ b/modules/EvseV2G/tests/requirement.cpp @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include + +#include "utils/types.hpp" + +Requirement::Requirement(const std::string& requirement_id_, size_t index_) { +} +bool Requirement::operator<(const Requirement& rhs) const { + return true; +} diff --git a/modules/EvseV2G/tests/v2g_main.cpp b/modules/EvseV2G/tests/v2g_main.cpp new file mode 100644 index 000000000..a773dcad2 --- /dev/null +++ b/modules/EvseV2G/tests/v2g_main.cpp @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +/* + * testing options + * openssl s_client -connect [fe80::ae91:a1ff:fec9:a947%3]:64109 -verify 2 -CAfile server_root_cert.pem -cert + * client_cert.pem -cert_chain client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname + * evse.pionix.de -status + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ISO15118_chargerImplStub.hpp" +#include "evse_securityIntfStub.hpp" + +#include +#include +#include + +using namespace std::chrono_literals; + +// needs to be in the global namespace +int v2g_handle_connection(struct v2g_connection* conn) { + assert(conn != nullptr); + assert(conn->read != nullptr); + assert(conn->write != nullptr); + + std::array buffer{}; + bool bExit = false; + while (!bExit) { + const ssize_t readbytes = conn->read(conn, buffer.data(), buffer.size()); + if (readbytes > 0) { + const ssize_t writebytes = conn->write(conn, buffer.data(), readbytes); + if (writebytes <= 0) { + bExit = true; + } + } else if (readbytes < 0) { + bExit = true; + } + } + return 0; +} + +namespace { + +const char* interface; + +void parse_options(int argc, char** argv) { + interface = nullptr; + int c; + + while ((c = getopt(argc, argv, "hi:")) != -1) { + switch (c) { + case 'i': + interface = optarg; + break; + case 'h': + case '?': + std::cout << "Usage: " << argv[0] << " -i " << std::endl; + exit(1); + break; + default: + exit(2); + } + } + + if (interface == nullptr) { + std::cerr << "Error: " << argv[0] << " requires -i " << std::endl; + exit(3); + } +} + +// EvseSecurity "implementation" +struct EvseSecurity : public module::stub::evse_securityIntfStub { + Result get_verify_file(const Requirement& req, const Parameters& args) override { + return "client_root_cert.pem"; + } + + virtual Result get_leaf_certificate_info(const Requirement& req, const Parameters& args) { + // using types::evse_security::CertificateHashDataType; + using types::evse_security::CertificateInfo; + using types::evse_security::CertificateOCSP; + using types::evse_security::GetCertificateInfoResult; + using types::evse_security::GetCertificateInfoStatus; + using types::evse_security::HashAlgorithm; + + CertificateInfo cert_info; + cert_info.key = "server_priv.pem"; + cert_info.certificate = "server_chain.pem"; + cert_info.certificate_count = 2; + cert_info.ocsp = {{ + {HashAlgorithm::SHA256}, + {"ocsp_response.der"}, + }, + { + {HashAlgorithm::SHA256}, + {"ocsp_response.der"}, + }}; + + const GetCertificateInfoResult res = { + GetCertificateInfoStatus::Accepted, + cert_info, + }; + json jres = res; + return jres; + } +}; + +} // namespace + +int main(int argc, char** argv) { + parse_options(argc, argv); + + tls::Server tls_server; + module::stub::ISO15118_chargerImplStub charger; + EvseSecurity security; + + auto* ctx = v2g_ctx_create(&charger, &security); + if (ctx == nullptr) { + std::cerr << "failed to create context" << std::endl; + } else { +#ifndef EVEREST_MBED_TLS + ctx->tls_server = &tls_server; +#endif + ctx->if_name = interface; + ctx->tls_security = TLS_SECURITY_FORCE; + ctx->is_connection_terminated = false; + + std::thread stop([ctx]() { + // there is a 60 second read timeout in connection.cpp + std::this_thread::sleep_for(75s); + std::cout << "shutdown" << std::endl; + ctx->is_connection_terminated = true; + ctx->shutdown = true; + }); + + std::cout << "connection_init" << std::endl; + if (::connection_init(ctx) != 0) { + std::cerr << "connection_init failed" << std::endl; + } else { + std::cout << "connection_init started" << std::endl; + } + + std::cout << "connection_start_servers " << std::endl; + if (::connection_start_servers(ctx) != 0) { + std::cerr << "connection_start_servers failed" << std::endl; + } else { + std::cout << "connection_start_servers started" << std::endl; + } + + stop.join(); + tls::ServerConnection::wait_all_closed(); + + // wait for v2g_ctx_start_events thread to stop + std::this_thread::sleep_for(2s); + v2g_ctx_free(ctx); + } + + return 0; +} diff --git a/modules/EvseV2G/v2g.hpp b/modules/EvseV2G/v2g.hpp index 38d71311f..e0de0b474 100644 --- a/modules/EvseV2G/v2g.hpp +++ b/modules/EvseV2G/v2g.hpp @@ -6,6 +6,13 @@ #include #include + +#include +#include +#include +#include + +#ifdef EVEREST_MBED_TLS #include #include #include @@ -13,21 +20,22 @@ #include #include #include -#include -#include -#include -#include #if MBEDTLS_VERSION_MINOR == 2 #include #else #include #endif +#else +#include +#include +#endif // EVEREST_MBED_TLS + #include #include #include - #include #include + #include #include @@ -170,9 +178,13 @@ struct SAE_Bidi_Data { /** * Abstracts a charging port, i.e. a power outlet in this daemon. + * + * **** NOTE **** + * Be very careful about adding C++ objects since constructors and + * destructors are not called. (see v2g_ctx_create() and calloc) */ struct v2g_context { - volatile int shutdown; + std::atomic_bool shutdown; evse_securityIntf* r_security; ISO15118_chargerImplBase* p_charger; @@ -202,6 +214,7 @@ struct v2g_context { pthread_t tcp_thread; +#ifdef EVEREST_MBED_TLS mbedtls_ssl_config ssl_config; mbedtls_x509_crt* evseTlsCrt; uint8_t num_of_tls_crt; @@ -209,10 +222,17 @@ struct v2g_context { mbedtls_x509_crt v2g_root_crt; mbedtls_net_context tls_socket; keylogDebugCtx tls_log_ctx; - bool tls_key_logging; pthread_t tls_thread; mbedtls_x509_crt mop_root_ca_list; +#else + struct { + int fd; + } tls_socket; + tls::Server* tls_server; +#endif // EVEREST_MBED_TLS + + bool tls_key_logging; pthread_mutex_t mqtt_lock; pthread_cond_t mqtt_cond; @@ -316,11 +336,14 @@ struct v2g_context { types::authorization::CertificateStatus certificate_status; // for PnC bool authorization_rejected; // for PnC +#ifdef EVEREST_MBED_TLS + // needed by iso_server.cpp + // for OpenSSL the key is part of v2g_connection struct { - bool valid_crt; - mbedtls_x509_crt crt; mbedtls_ecdsa_context pubkey; - } contract; // for PnC + } contract; // for PnC +#endif // EVEREST_MBED_TLS + bool renegotiation_required; /* Is set to true if renegotiation is required. Only relevant for ISO */ bool is_charging; /* set to true if ChargeProgress is set to Start */ uint8_t sa_schedule_tuple_id; /* selected SA schedule tuple ID*/ @@ -363,6 +386,8 @@ struct v2g_connection { struct v2g_context* ctx; bool is_tls_connection; + +#ifdef EVEREST_MBED_TLS union { struct { mbedtls_ssl_config* ssl_config; @@ -371,6 +396,18 @@ struct v2g_connection { } ssl; int socket_fd; } conn; +#else + // used for non-TLS connections + struct { + int socket_fd; + } conn; + + tls::Connection* tls_connection; + openssl::PKey_ptr* pubkey; +#endif // EVEREST_MBED_TLS + + ssize_t (*read)(struct v2g_connection* conn, unsigned char* buf, std::size_t count); + ssize_t (*write)(struct v2g_connection* conn, unsigned char* buf, std::size_t count); /* V2GTP EXI encoding/decoding stuff */ uint8_t* buffer; diff --git a/modules/EvseV2G/v2g_ctx.cpp b/modules/EvseV2G/v2g_ctx.cpp index 0d8ec86a0..883ff9108 100644 --- a/modules/EvseV2G/v2g_ctx.cpp +++ b/modules/EvseV2G/v2g_ctx.cpp @@ -2,11 +2,11 @@ // Copyright (C) 2022-2023 chargebyte GmbH // Copyright (C) 2022-2023 Contributors to EVerest +#include +#include #include #include #include -#include -#include #include // sleep #include "log.hpp" @@ -295,6 +295,8 @@ void v2g_ctx_init_charging_values(struct v2g_context* const ctx) { struct v2g_context* v2g_ctx_create(ISO15118_chargerImplBase* p_chargerImplBase, evse_securityIntf* r_security) { struct v2g_context* ctx; + // TODO There are c++ objects within v2g_context and calloc doesn't call initialisers. + // free() will not call destructors ctx = static_cast(calloc(1, sizeof(*ctx))); if (!ctx) return NULL; @@ -308,7 +310,7 @@ struct v2g_context* v2g_ctx_create(ISO15118_chargerImplBase* p_chargerImplBase, ctx->basic_config.evse_ac_current_limit = 0.0f; ctx->local_tcp_addr = NULL; - ctx->local_tcp_addr = NULL; + ctx->local_tls_addr = NULL; ctx->is_dc_charger = true; @@ -323,7 +325,9 @@ struct v2g_context* v2g_ctx_create(ISO15118_chargerImplBase* p_chargerImplBase, ctx->sdp_socket = -1; ctx->tcp_socket = -1; ctx->tls_socket.fd = -1; +#ifdef EVEREST_MBED_TLS memset(&ctx->tls_log_ctx, 0, sizeof(keylogDebugCtx)); +#endif // EVEREST_MBED_TLS ctx->tls_key_logging = false; ctx->debugMode = false; @@ -361,6 +365,7 @@ struct v2g_context* v2g_ctx_create(ISO15118_chargerImplBase* p_chargerImplBase, } static void v2g_ctx_free_tls(struct v2g_context* ctx) { +#ifdef EVEREST_MBED_TLS mbedtls_net_free(&ctx->tls_socket); for (uint8_t idx = 0; idx < ctx->num_of_tls_crt; idx++) { @@ -380,6 +385,7 @@ static void v2g_ctx_free_tls(struct v2g_context* ctx) { fclose(ctx->tls_log_ctx.file); memset(&ctx->tls_log_ctx, 0, sizeof(ctx->tls_log_ctx)); } +#endif // EVEREST_MBED_TLS } void v2g_ctx_free(struct v2g_context* ctx) { diff --git a/modules/EvseV2G/v2g_server.cpp b/modules/EvseV2G/v2g_server.cpp index 6f3da91fc..16b5b83f2 100644 --- a/modules/EvseV2G/v2g_server.cpp +++ b/modules/EvseV2G/v2g_server.cpp @@ -3,14 +3,15 @@ // Copyright (C) 2023 Contributors to EVerest #include "v2g_server.hpp" +#include +#include #include -#include #include #include -#include - +#ifdef EVEREST_MBED_TLS #include +#endif // EVEREST_MBED_TLS #include #include @@ -117,6 +118,9 @@ static void publish_var_V2G_Message(v2g_connection* conn, bool is_req) { tempbuff++; } + std::string EXI_Base64; + +#ifdef EVEREST_MBED_TLS unsigned char* base64_buffer = NULL; size_t base64_buffer_len = 0; mbedtls_base64_encode(NULL, 0, &base64_buffer_len, conn->buffer, (size_t)conn->payload_len + V2GTP_HEADER_LENGTH); @@ -127,10 +131,18 @@ static void publish_var_V2G_Message(v2g_connection* conn, bool is_req) { dlog(DLOG_LEVEL_WARNING, "Unable to base64 encode EXI buffer"); } - v2gMessage.V2G_Message_EXI_Base64 = std::string(reinterpret_cast(base64_buffer), base64_buffer_len); + EXI_Base64 = std::string(reinterpret_cast(base64_buffer), base64_buffer_len); if (base64_buffer != NULL) { free(base64_buffer); } +#else + EXI_Base64 = openssl::base64_encode(conn->buffer, conn->payload_len + V2GTP_HEADER_LENGTH); + if (EXI_Base64.size() == 0) { + dlog(DLOG_LEVEL_WARNING, "Unable to base64 encode EXI buffer"); + } +#endif // EVEREST_MBED_TLS + + v2gMessage.V2G_Message_EXI_Base64 = EXI_Base64; v2gMessage.V2G_Message_ID = get_V2G_Message_ID(conn->ctx->current_v2g_msg, conn->ctx->selected_protocol, is_req); v2gMessage.V2G_Message_EXI_Hex = msg_as_hex_string; conn->ctx->p_charger->publish_V2G_Messages(v2gMessage); @@ -142,10 +154,13 @@ static void publish_var_V2G_Message(v2g_connection* conn, bool is_req) { * \return Returns 0 if the V2G-session was successfully stopped, otherwise -1. */ static int v2g_incoming_v2gtp(struct v2g_connection* conn) { + assert(conn != nullptr); + assert(conn->read != nullptr); + int rv; /* read and process header */ - rv = connection_read(conn, conn->buffer, V2GTP_HEADER_LENGTH); + rv = conn->read(conn, conn->buffer, V2GTP_HEADER_LENGTH); if (rv < 0) { dlog(DLOG_LEVEL_ERROR, "connection_read(header) failed: %s", (rv == -1) ? strerror(errno) : "connection terminated"); @@ -182,7 +197,7 @@ static int v2g_incoming_v2gtp(struct v2g_connection* conn) { return -1; } /* read request */ - rv = connection_read(conn, &conn->buffer[V2GTP_HEADER_LENGTH], conn->payload_len); + rv = conn->read(conn, &conn->buffer[V2GTP_HEADER_LENGTH], conn->payload_len); if (rv < 0) { dlog(DLOG_LEVEL_ERROR, "connection_read(payload) failed: %s", (rv == -1) ? strerror(errno) : "connection terminated"); @@ -205,12 +220,15 @@ static int v2g_incoming_v2gtp(struct v2g_connection* conn) { * \return Returns 0 if the v2g-session was successfully stopped, otherwise -1. */ int v2g_outgoing_v2gtp(struct v2g_connection* conn) { + assert(conn != nullptr); + assert(conn->write != nullptr); + /* fixup/create header */ const auto len = exi_bitstream_get_length(&conn->stream); V2GTP_WriteHeader(conn->buffer, len - V2GTP_HEADER_LENGTH); - if (connection_write(conn, conn->buffer, len) == -1) { + if (conn->write(conn, conn->buffer, len) == -1) { dlog(DLOG_LEVEL_ERROR, "connection_write(header) failed: %s", strerror(errno)); return -1; } From e3faca49e2972e4d33708292a01b8524913ecd59 Mon Sep 17 00:00:00 2001 From: James Chapman Date: Tue, 9 Jul 2024 08:41:11 +0100 Subject: [PATCH 2/3] feat: make OpenSSL the default for 15118 communication with the PEV Signed-off-by: James Chapman --- modules/EvseV2G/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/EvseV2G/CMakeLists.txt b/modules/EvseV2G/CMakeLists.txt index a2fed2d58..da6f883fc 100644 --- a/modules/EvseV2G/CMakeLists.txt +++ b/modules/EvseV2G/CMakeLists.txt @@ -9,7 +9,7 @@ ev_setup_cpp_module() # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 # insert your custom targets and additional config variables here -option(USING_MBED_TLS "Use MbedTLS for V2G" ON) +option(USING_MBED_TLS "Use MbedTLS for V2G" OFF) if(USING_MBED_TLS) target_compile_definitions(${MODULE_NAME} PRIVATE From a7aa4d0301bf855a4fc95513b3a39e90c49e5110 Mon Sep 17 00:00:00 2001 From: James Chapman Date: Tue, 9 Jul 2024 16:32:04 +0100 Subject: [PATCH 3/3] fix: support delayed getting certificates from libevse-security Originally information was fetched from libevse-security every connection. The OpenSSL addition moved away from that so that configuration was checked at module start. The problem occurs when there isn't valid configuration. This change splits config into two sets: 1. config that must exist at module start 2. config that can be obtained at 1st connection TCP socket information is covered in 1. SSL certificates, keys and OCSP resonses are in 2. Signed-off-by: James Chapman --- config/CMakeLists.txt | 3 +- config/config-sil-dc-tls.yaml | 147 ++++++++++++++++++ lib/staging/tls/openssl_util.cpp | 44 +++--- lib/staging/tls/tests/patched_test.cpp | 40 ++++- lib/staging/tls/tests/tls_main.cpp | 2 +- lib/staging/tls/tests/tls_test.cpp | 68 ++++++++ lib/staging/tls/tls.cpp | 141 +++++++++++++---- lib/staging/tls/tls.hpp | 116 ++++++++++---- modules/EvseV2G/connection/tls_connection.cpp | 76 +++++++-- modules/EvseV2G/tests/README.md | 2 +- 10 files changed, 539 insertions(+), 100 deletions(-) create mode 100644 config/config-sil-dc-tls.yaml diff --git a/config/CMakeLists.txt b/config/CMakeLists.txt index f6349c26c..499a2d2dd 100644 --- a/config/CMakeLists.txt +++ b/config/CMakeLists.txt @@ -3,6 +3,7 @@ generate_config_run_script(CONFIG sil-two-evse) generate_config_run_script(CONFIG sil-ocpp) generate_config_run_script(CONFIG sil-ocpp201) generate_config_run_script(CONFIG sil-dc) +generate_config_run_script(CONFIG sil-dc-tls) generate_config_run_script(CONFIG sil-dc-sae-v2g) generate_config_run_script(CONFIG sil-dc-sae-v2h) generate_config_run_script(CONFIG sil-two-evse-dc) @@ -25,7 +26,7 @@ install( install( DIRECTORY "certs" DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}/everest" - FILES_MATCHING PATTERN "*.pem" PATTERN "*.key" PATTERN "*.der" PATTERN "*.txt" PATTERN "*.jks" PATTERN "*.p12" + FILES_MATCHING PATTERN "*.pem" PATTERN "*.key" PATTERN "*.der" PATTERN "*.txt" PATTERN "*.jks" PATTERN "*.p12" ) install( diff --git a/config/config-sil-dc-tls.yaml b/config/config-sil-dc-tls.yaml new file mode 100644 index 000000000..867983a9a --- /dev/null +++ b/config/config-sil-dc-tls.yaml @@ -0,0 +1,147 @@ +active_modules: + iso15118_charger: + module: EvseV2G + config_module: + device: auto + tls_security: force + connections: + security: + - module_id: evse_security + implementation_id: main + iso15118_car: + module: PyEvJosev + config_module: + device: auto + supported_DIN70121: false + supported_ISO15118_2: true + tls_active: true + enforce_tls: true + evse_manager: + module: EvseManager + config_module: + connector_id: 1 + country_code: DE + evse_id: DE*PNX*E12345*1 + evse_id_din: 49A80737A45678 + session_logging: true + session_logging_xml: false + session_logging_path: /tmp/everest-logs + charge_mode: DC + hack_allow_bpt_with_iso2: true + payment_enable_contract: false + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_car_side: + - module_id: powersupply_dc + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + powersupply_DC: + - module_id: powersupply_dc + implementation_id: main + imd: + - module_id: imd + implementation_id: main + powersupply_dc: + module: DCSupplySimulator + yeti_driver: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + imd: + config_implementation: + main: + selftest_success: true + module: IMDSimulator + ev_manager: + module: EvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + dc_target_current: 20 + dc_target_voltage: 400 + connections: + ev_board_support: + - module_id: yeti_driver + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + auth: + module: Auth + config_module: + connection_timeout: 10 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: token_provider + implementation_id: main + token_validator: + - module_id: token_validator + implementation_id: main + evse_manager: + - module_id: evse_manager + implementation_id: evse + token_provider: + module: DummyTokenProvider + config_implementation: + main: + token: TOKEN1 + connections: + evse: + - module_id: evse_manager + implementation_id: evse + token_validator: + module: DummyTokenValidator + config_implementation: + main: + validation_result: Accepted + validation_reason: Token seems valid + sleep: 0.25 + evse_security: + module: EvseSecurity + config_module: + private_key_password: "123456" + energy_manager: + module: EnergyManager + config_module: + schedule_total_duration: 1 + schedule_interval_duration: 60 + debug: false + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: evse_manager + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + api: + module: API + connections: + evse_manager: + - module_id: evse_manager + implementation_id: evse +x-module-layout: {} diff --git a/lib/staging/tls/openssl_util.cpp b/lib/staging/tls/openssl_util.cpp index 15e8cdbe2..a582813bd 100644 --- a/lib/staging/tls/openssl_util.cpp +++ b/lib/staging/tls/openssl_util.cpp @@ -329,34 +329,34 @@ bool signature_to_bn(bn_t& r, bn_t& s, const std::uint8_t* sig_p, std::size_t le }; std::vector load_certificates(const char* filename) { - assert(filename != nullptr); - std::vector result{}; - auto* store = OSSL_STORE_open(filename, UI_null(), nullptr, nullptr, nullptr); - - if (store != nullptr) { - while (OSSL_STORE_eof(store) != 1) { - auto* info = OSSL_STORE_load(store); - - if (info != nullptr) { - if (OSSL_STORE_error(store) == 1) { - log_error("OSSL_STORE_load"); - } else { - const auto type = OSSL_STORE_INFO_get_type(info); - - if (type == OSSL_STORE_INFO_CERT) { - // get a copy of the certificate - auto cert = OSSL_STORE_INFO_get1_CERT(info); - result.push_back({cert, &X509_free}); + + if (filename != nullptr) { + auto* store = OSSL_STORE_open(filename, UI_null(), nullptr, nullptr, nullptr); + if (store != nullptr) { + while (OSSL_STORE_eof(store) != 1) { + auto* info = OSSL_STORE_load(store); + + if (info != nullptr) { + if (OSSL_STORE_error(store) == 1) { + log_error("OSSL_STORE_load"); + } else { + const auto type = OSSL_STORE_INFO_get_type(info); + + if (type == OSSL_STORE_INFO_CERT) { + // get a copy of the certificate + auto cert = OSSL_STORE_INFO_get1_CERT(info); + result.push_back({cert, &X509_free}); + } } } - } - OSSL_STORE_INFO_free(info); + OSSL_STORE_INFO_free(info); + } } - } - OSSL_STORE_close(store); + OSSL_STORE_close(store); + } return result; } diff --git a/lib/staging/tls/tests/patched_test.cpp b/lib/staging/tls/tests/patched_test.cpp index 119533569..525396647 100644 --- a/lib/staging/tls/tests/patched_test.cpp +++ b/lib/staging/tls/tests/patched_test.cpp @@ -172,7 +172,7 @@ class OcspTest : public testing::Test { server_config.service = "8444"; server_config.ipv6_only = false; server_config.verify_client = false; - server_config.io_timeout_ms = 100; + server_config.io_timeout_ms = 500; client_config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; // client_config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; @@ -194,8 +194,10 @@ class OcspTest : public testing::Test { } } - void start() { - if (server.init(server_config)) { + void start(const std::function& init_ssl = nullptr) { + using state_t = tls::Server::state_t; + const auto res = server.init(server_config, init_ssl); + if ((res == state_t::init_complete) || (res == state_t::init_socket)) { server_thread = std::thread(&run_server, std::ref(server)); server.wait_running(); } @@ -228,6 +230,24 @@ class OcspTest : public testing::Test { } }; +bool ssl_init(tls::Server& server) { + std::cout << "ssl_init" << std::endl; + tls::Server::config_t server_config; + server_config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + server_config.ciphersuites = ""; + server_config.certificate_chain_file = "server_chain.pem"; + server_config.private_key_file = "server_priv.pem"; + server_config.ocsp_response_files = {"ocsp_response.der", "ocsp_response.der"}; + server_config.host = "localhost"; + server_config.service = "8444"; + server_config.ipv6_only = false; + server_config.verify_client = false; + server_config.io_timeout_ms = 100; + const auto res = server.update(server_config); + EXPECT_TRUE(res); + return res; +} + // ---------------------------------------------------------------------------- // The tests @@ -246,6 +266,20 @@ TEST_F(OcspTest, NonBlockingConnect) { EXPECT_TRUE(is_reset(flags_t::status_request_v2)); } +TEST_F(OcspTest, delayedConfig) { + // partial config + server_config.certificate_chain_file = nullptr; + server_config.private_key_file = nullptr; + server_config.ocsp_response_files.clear(); + + start(ssl_init); + connect(); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_reset(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); +} + TEST_F(OcspTest, TLS12) { // test using TLS 1.2 start(); diff --git a/lib/staging/tls/tests/tls_main.cpp b/lib/staging/tls/tests/tls_main.cpp index d6734f38d..c521fe010 100644 --- a/lib/staging/tls/tests/tls_main.cpp +++ b/lib/staging/tls/tests/tls_main.cpp @@ -86,7 +86,7 @@ int main() { server.stop(); }); - server.init(config); + server.init(config, nullptr); server.wait_stopped(); // server.serve(&handle_connection); diff --git a/lib/staging/tls/tests/tls_test.cpp b/lib/staging/tls/tests/tls_test.cpp index 2e3df0590..5d6acb500 100644 --- a/lib/staging/tls/tests/tls_test.cpp +++ b/lib/staging/tls/tests/tls_test.cpp @@ -18,6 +18,74 @@ std::string to_string(const openssl::sha_256_digest_t& digest) { namespace { +TEST(strdup, usage) { + // auto* r1 = strdup(nullptr); need to ensure non-nullptr + auto* r2 = strdup(""); + auto* r3 = strdup("hello"); + // free(r1); + free(r2); + free(r3); + free(nullptr); +} + +TEST(string, use) { + // was hoping to use std::string for config, but ... + std::string empty; + std::string space{""}; + std::string value{"something"}; + + EXPECT_TRUE(empty.empty()); + // EXPECT_FALSE(space.empty()); was hoping it would be true + EXPECT_FALSE(value.empty()); + + // EXPECT_EQ(empty.c_str(), nullptr); was hoping it would be nullptr + EXPECT_NE(space.c_str(), nullptr); + EXPECT_NE(value.c_str(), nullptr); +} + +TEST(ConfigItem, test) { + tls::ConfigItem i1; + tls::ConfigItem i2{nullptr}; + tls::ConfigItem i3{"Hello"}; + tls::ConfigItem i4 = nullptr; + tls::ConfigItem i5(nullptr); + tls::ConfigItem i6("Hello"); + + EXPECT_EQ(i1, nullptr); + EXPECT_EQ(i4, nullptr); + EXPECT_EQ(i5, nullptr); + + EXPECT_EQ(i2, i5); + EXPECT_EQ(i3, i6); + + EXPECT_EQ(i1, i2); + EXPECT_NE(i1, i3); + EXPECT_EQ(i1, i5); + EXPECT_NE(i1, i6); + + auto j1(std::move(i3)); + auto j2 = std::move(i6); + EXPECT_EQ(i6, i3); + EXPECT_EQ(j1, j2); + EXPECT_EQ(j1, "Hello"); + EXPECT_NE(j1, i6); + + EXPECT_NE(j1, nullptr); + EXPECT_NE(j2, nullptr); + + EXPECT_EQ(i3, nullptr); + EXPECT_EQ(i6, nullptr); + EXPECT_EQ(i6, i3); + + std::vector j3 = {"one", "two", nullptr}; + EXPECT_EQ(j3[0], "one"); + EXPECT_EQ(j3[1], "two"); + EXPECT_EQ(j3[2], nullptr); + + const char* p = j1; + EXPECT_STREQ(p, "Hello"); +} + TEST(OcspCache, initEmpty) { tls::OcspCache cache; openssl::sha_256_digest_t digest{}; diff --git a/lib/staging/tls/tls.cpp b/lib/staging/tls/tls.cpp index 7ae9a13dd..2220fb721 100644 --- a/lib/staging/tls/tls.cpp +++ b/lib/staging/tls/tls.cpp @@ -49,6 +49,7 @@ template <> class default_delete { } // namespace std using ::openssl::log_error; +using ::openssl::log_warning; namespace { @@ -328,7 +329,7 @@ void ssl_shutdown(SSL* ctx, std::int32_t timeout_ms) { bool configure_ssl_ctx(SSL_CTX* ctx, const char* ciphersuites, const char* cipher_list, const char* certificate_chain_file, const char* private_key_file, - const char* private_key_password) { + const char* private_key_password, bool required) { bool bRes{true}; if (ctx == nullptr) { @@ -365,6 +366,8 @@ bool configure_ssl_ctx(SSL_CTX* ctx, const char* ciphersuites, const char* ciphe log_error("SSL_CTX_use_certificate_chain_file"); bRes = false; } + } else { + bRes = !required; } if (private_key_file != nullptr) { @@ -385,6 +388,8 @@ bool configure_ssl_ctx(SSL_CTX* ctx, const char* ciphersuites, const char* ciphe log_error("SSL_CTX_check_private_key"); bRes = false; } + } else { + bRes = !required; } } @@ -413,10 +418,54 @@ OCSP_RESPONSE* load_ocsp(const char* filename) { return resp; } +constexpr char* dup(const char* value) { + char* res = nullptr; + if (value != nullptr) { + res = strdup(value); + } + return res; +} + } // namespace namespace tls { +ConfigItem::ConfigItem(const char* value) : m_ptr(dup(value)) { +} +ConfigItem& ConfigItem::operator=(const char* value) { + m_ptr = dup(value); + return *this; +} +ConfigItem::ConfigItem(const ConfigItem& obj) : m_ptr(dup(obj.m_ptr)) { +} +ConfigItem& ConfigItem::operator=(const ConfigItem& obj) { + m_ptr = dup(obj.m_ptr); + return *this; +} +ConfigItem::ConfigItem(ConfigItem&& obj) noexcept : m_ptr(obj.m_ptr) { + obj.m_ptr = nullptr; +} +ConfigItem& ConfigItem::operator=(ConfigItem&& obj) noexcept { + m_ptr = obj.m_ptr; + obj.m_ptr = nullptr; + return *this; +} +ConfigItem::~ConfigItem() { + free(m_ptr); + m_ptr = nullptr; +} + +bool ConfigItem::operator==(const char* ptr) const { + bool result{false}; + if (m_ptr == ptr) { + // both nullptr, or both point to the same string + result = true; + } else if ((m_ptr != nullptr) && (ptr != nullptr)) { + result = strcmp(m_ptr, ptr) == 0; + } + return result; +} + using SSL_ptr = std::unique_ptr; using SSL_CTX_ptr = std::unique_ptr; using OCSP_RESPONSE_ptr = std::shared_ptr; @@ -1001,7 +1050,7 @@ bool Server::init_ssl(const config_t& cfg) { const SSL_METHOD* method = TLS_server_method(); auto* ctx = SSL_CTX_new(method); auto bRes = configure_ssl_ctx(ctx, cfg.ciphersuites, cfg.cipher_list, cfg.certificate_chain_file, - cfg.private_key_file, cfg.private_key_password); + cfg.private_key_file, cfg.private_key_password, true); if (bRes) { int mode = SSL_VERIFY_NONE; @@ -1050,56 +1099,81 @@ bool Server::init_ssl(const config_t& cfg) { return ctx != nullptr; } -bool Server::init(const config_t& cfg) { +Server::state_t Server::init(const config_t& cfg, const std::function& init_ssl) { std::lock_guard lock(m_mutex); - (void)update_ocsp(cfg); m_timeout_ms = cfg.io_timeout_ms; - bool bRes = init_ssl(cfg); - bRes = bRes && init_socket(cfg); - m_state = state_t::init; - return bRes; + m_init_callback = init_ssl; + m_state = state_t::init_needed; + if (init_socket(cfg)) { + m_state = state_t::init_socket; + if (update(cfg)) { + m_state = state_t::init_complete; + } + } + return m_state; } -bool Server::update_ocsp(const config_t& cfg) { - std::vector entries; - auto chain = openssl::load_certificates(cfg.certificate_chain_file); - bool bRes = chain.size() == cfg.ocsp_response_files.size(); +bool Server::update(const config_t& cfg) { + bool bRes = init_ssl(cfg); if (bRes) { - for (std::size_t i = 0; i < chain.size(); i++) { - const auto& file = cfg.ocsp_response_files[i]; - const auto& cert = chain[i]; - - if (file != nullptr) { - openssl::sha_256_digest_t digest{}; - if (OcspCache::digest(digest, cert.get())) { - entries.emplace_back(digest, file); + std::vector entries; + auto chain = openssl::load_certificates(cfg.certificate_chain_file); + if (chain.size() == cfg.ocsp_response_files.size()) { + for (std::size_t i = 0; i < chain.size(); i++) { + const auto& file = cfg.ocsp_response_files[i]; + const auto& cert = chain[i]; + + if (file != nullptr) { + openssl::sha_256_digest_t digest{}; + if (OcspCache::digest(digest, cert.get())) { + entries.emplace_back(digest, file); + } } } - } - bRes = m_cache.load(entries); - } else { - log_error(std::string("update_ocsp: ocsp files != ") + std::to_string(chain.size())); + bRes = m_cache.load(entries); + } else { + log_warning(std::string("update_ocsp: ocsp files != ") + std::to_string(chain.size())); + } } return bRes; } -bool Server::serve(const std::function& ctx)>& handler) { +Server::state_t Server::serve(const std::function& ctx)>& handler) { assert(m_context != nullptr); // prevent init() or server() being called while serve is running std::lock_guard lock(m_mutex); bool bRes = false; + + state_t tmp = m_state; + + switch (tmp) { + case state_t::init_socket: + if (m_init_callback != nullptr) { + bRes = m_socket != INVALID_SOCKET; + } + break; + case state_t::init_complete: + bRes = m_socket != INVALID_SOCKET; + break; + case state_t::init_needed: + case state_t::running: + case state_t::stopped: + default: + break; + } + { std::lock_guard lock(m_cv_mutex); m_running = true; } m_cv.notify_all(); - if (m_socket != INVALID_SOCKET) { + if (bRes) { m_exit = false; - m_state = state_t::running; + m_state = (m_state == state_t::init_complete) ? state_t::running : state_t::init_socket; while (!m_exit) { auto* peer = BIO_ADDR_new(); if (peer == nullptr) { @@ -1122,6 +1196,15 @@ bool Server::serve(const std::function& c } }; + if ((soc >= 0) && (m_state == state_t::init_socket)) { + if (m_init_callback(*this)) { + m_state = state_t::running; + } else { + BIO_closesocket(soc); + soc = INVALID_SOCKET; + } + } + if (m_exit) { if (soc >= 0) { BIO_closesocket(soc); @@ -1155,7 +1238,7 @@ bool Server::serve(const std::function& c m_running = false; } m_cv.notify_all(); - return bRes; + return m_state; } void Server::stop() { @@ -1208,7 +1291,7 @@ bool Client::init(const config_t& cfg) { const SSL_METHOD* method = TLS_client_method(); auto* ctx = SSL_CTX_new(method); auto bRes = configure_ssl_ctx(ctx, cfg.ciphersuites, cfg.cipher_list, cfg.certificate_chain_file, - cfg.private_key_file, cfg.private_key_password); + cfg.private_key_file, cfg.private_key_password, false); if (bRes) { int mode = SSL_VERIFY_NONE; diff --git a/lib/staging/tls/tls.hpp b/lib/staging/tls/tls.hpp index f77a88bb6..60f27cb31 100644 --- a/lib/staging/tls/tls.hpp +++ b/lib/staging/tls/tls.hpp @@ -38,6 +38,52 @@ struct ocsp_cache_ctx; struct server_ctx; struct client_ctx; +// ---------------------------------------------------------------------------- +// ConfigItem - store configuration item allowing nullptr + +/** + * \brief class to hold configuration strings, behaves like const char * + * but keeps a copy + * + * unlike std::string this class allows nullptr as a valid setting. + * + * unlike const char * it doesn't have scope issues since it holds + * a copy. + */ +class ConfigItem { +private: + char* m_ptr{nullptr}; + +public: + ConfigItem() = default; + ConfigItem(const char* value); // must not be explicit + ConfigItem& operator=(const char* value); + ConfigItem(const ConfigItem& obj); + ConfigItem& operator=(const ConfigItem& obj); + ConfigItem(ConfigItem&& obj) noexcept; + ConfigItem& operator=(ConfigItem&& obj) noexcept; + + ~ConfigItem(); + + inline operator const char*() const { + return m_ptr; + } + + bool operator==(const char* ptr) const; + + inline bool operator!=(const char* ptr) const { + return !(*this == ptr); + } + + inline bool operator==(const ConfigItem& obj) const { + return *this == obj.m_ptr; + } + + inline bool operator!=(const ConfigItem& obj) const { + return !(*this == obj); + } +}; + // ---------------------------------------------------------------------------- // Cache of OCSP responses for status_request and status_request_v2 extensions @@ -371,23 +417,24 @@ class Server { * \brief server state */ enum class state_t : std::uint8_t { - need_init, //!< not initialised yet - init, //!< initialised but not running - running, //!< waiting for connections - stopped, //!< stopped + init_needed, //!< not initialised yet - call init() + init_socket, //!< TCP listen socket initialised (but not SSL) - call update() + init_complete, //!< initialised but not running - call serve() + running, //!< waiting for connections - fully initialised + stopped, //!< stopped - reinitialisation will be needed }; struct config_t { - const char* cipher_list{nullptr}; // nullptr means use default - const char* ciphersuites{nullptr}; // nullptr means use default, "" disables TSL 1.3 - const char* certificate_chain_file{nullptr}; - const char* private_key_file{nullptr}; - const char* private_key_password{nullptr}; - const char* verify_locations_file{nullptr}; // for client certificate - const char* verify_locations_path{nullptr}; // for client certificate - const char* host{nullptr}; // see BIO_lookup_ex() - const char* service{nullptr}; // TLS port number - std::vector ocsp_response_files; // in certificate chain order + ConfigItem cipher_list{nullptr}; // nullptr means use default + ConfigItem ciphersuites{nullptr}; // nullptr means use default, "" disables TSL 1.3 + ConfigItem certificate_chain_file{nullptr}; + ConfigItem private_key_file{nullptr}; + ConfigItem private_key_password{nullptr}; + ConfigItem verify_locations_file{nullptr}; // for client certificate + ConfigItem verify_locations_path{nullptr}; // for client certificate + ConfigItem host{nullptr}; // see BIO_lookup_ex() + ConfigItem service{nullptr}; // TLS port number + std::vector ocsp_response_files; // in certificate chain order int socket{INVALID_SOCKET}; // use this specific socket - bypasses socket setup in init_socket() when set std::int32_t io_timeout_ms{-1}; // socket timeout in milliseconds bool ipv6_only{true}; @@ -400,12 +447,13 @@ class Server { bool m_running{false}; std::int32_t m_timeout_ms{-1}; std::atomic_bool m_exit{false}; - std::atomic m_state{state_t::need_init}; + std::atomic m_state{state_t::init_needed}; std::mutex m_mutex; std::mutex m_cv_mutex; std::condition_variable m_cv; OcspCache m_cache; CertificateStatusRequestV2 m_status_request_v2; + std::function m_init_callback{nullptr}; /** * \brief initialise the server socket @@ -432,27 +480,43 @@ class Server { /** * \brief initialise the server socket and TLS configuration * \param[in] cfg server configuration - * \return true on success - * \note when the server certificate and key change then the server needs - * to be stopped, initialised and start serving. + * \param[in] init_ssl function to collect certificates and keys, can be nullptr + * \return need_init - initialisation failed + * socket_init - server socket created and ready for serve() + * init_complete - SSL certificates and keys loaded + * + * It is possible to initialise the server and start listening for + * connections before certificates and keys are available. + * when init() returns socket_init the server will call init_ssl() with a + * reference to the object so that update() can be called with updated + * OCSP and SSL configuration. + * + * init_ssl() should return true when SSL has been configured so that the + * incoming connection is accepted. */ - bool init(const config_t& cfg); + state_t init(const config_t& cfg, const std::function& init_ssl); /** - * \brief update the OCSP cache + * \brief update the OCSP cache and SSL certificates and keys * \param[in] cfg server configuration * \return true on success - * \note used to update OCSP caches + * \note used to update OCSP caches and SSL config */ - bool update_ocsp(const config_t& cfg); + bool update(const config_t& cfg); /** * \brief wait for incomming connections * \param[in] handler called when there is a new connection - * \return false when there was an error listening for connections - * \note this is a blocking call that will not return until stop() has been called. - */ - bool serve(const std::function& ctx)>& handler); + * \return stopped after it has been running, or init_ values when listening + * can not start + * \note this is a blocking call that will not return until stop() has been + * called (unless it couldn't start listening) + * \note changing socket configuration requires stopping the server and + * calling init() + * \note after server() returns stopped init() will need to be called + * before further connections can be managed + */ + state_t serve(const std::function& ctx)>& handler); /** * \brief stop listening for new connections diff --git a/modules/EvseV2G/connection/tls_connection.cpp b/modules/EvseV2G/connection/tls_connection.cpp index bbe3ba4a5..ac6aeced6 100644 --- a/modules/EvseV2G/connection/tls_connection.cpp +++ b/modules/EvseV2G/connection/tls_connection.cpp @@ -86,18 +86,13 @@ void server_loop_thread(struct v2g_context* ctx) { assert(ctx != nullptr); assert(ctx->tls_server != nullptr); const auto res = ctx->tls_server->serve([ctx](auto con) { handle_new_connection_cb(con, ctx); }); - if (!res) { + if (res != tls::Server::state_t::stopped) { dlog(DLOG_LEVEL_ERROR, "tls::Server failed to serve"); } } -} // namespace - -namespace tls { - -int connection_init(struct v2g_context* ctx) { +bool build_config(tls::Server::config_t& config, struct v2g_context* ctx) { assert(ctx != nullptr); - assert(ctx->tls_server != nullptr); assert(ctx->r_security != nullptr); using types::evse_security::CaCertificateType; @@ -111,13 +106,18 @@ int connection_init(struct v2g_context* ctx) { * hence private keys are always encrypted. */ - tls::Server::config_t config; - bool bResult = false; + bool bResult{false}; config.cipher_list = "ECDHE-ECDSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256"; config.ciphersuites = ""; // disable TLS 1.3 config.verify_client = false; // contract certificate managed in-band in 15118-2 + // use the existing configured socket + // TODO(james-ctc): switch to server socket init code otherwise there + // may be issues with reinitialisation + config.socket = ctx->tls_socket.fd; + config.io_timeout_ms = static_cast(ctx->network_read_timeout_tls); + // information from libevse-security const auto cert_info = ctx->r_security->call_get_leaf_certificate_info(LeafCertificateType::V2G, EncodingFormat::PEM, false); @@ -146,19 +146,57 @@ int connection_init(struct v2g_context* ctx) { } } - // use the existing configured socket - config.socket = ctx->tls_socket.fd; - config.io_timeout_ms = static_cast(ctx->network_read_timeout_tls); - - ctx->tls_server->stop(); - ctx->tls_server->wait_stopped(); - bResult = ctx->tls_server->init(config); + bResult = true; } else { dlog(DLOG_LEVEL_ERROR, "Failed to read cert_info! Empty response"); } } - return (bResult) ? 0 : -1; + return bResult; +} + +bool configure_ssl(tls::Server& server, struct v2g_context* ctx) { + tls::Server::config_t config; + bool bResult{false}; + + dlog(DLOG_LEVEL_WARNING, "configure_ssl"); + + // The config of interest is from Evse Security, no point in updating + // config when there is a problem + if (build_config(config, ctx)) { + bResult = server.update(config); + } + + return bResult; +} + +} // namespace + +namespace tls { + +int connection_init(struct v2g_context* ctx) { + using state_t = tls::Server::state_t; + + assert(ctx != nullptr); + assert(ctx->tls_server != nullptr); + assert(ctx->r_security != nullptr); + + int res{-1}; + tls::Server::config_t config; + + // build_config can fail due to issues with Evse Security, + // this can be retried later. Not treated as an error. + (void)build_config(config, ctx); + + // apply config + ctx->tls_server->stop(); + ctx->tls_server->wait_stopped(); + const auto result = ctx->tls_server->init(config, [ctx](auto& server) { return configure_ssl(server, ctx); }); + if ((result == state_t::init_complete) || (result == state_t::init_socket)) { + res = 0; + } + + return res; } int connection_start_server(struct v2g_context* ctx) { @@ -171,6 +209,10 @@ int connection_start_server(struct v2g_context* ctx) { try { ctx->tls_server->stop(); ctx->tls_server->wait_stopped(); + if (ctx->tls_server->state() == tls::Server::state_t::stopped) { + // need to re-initialise + tls::connection_init(ctx); + } std::thread serve_loop(server_loop_thread, ctx); serve_loop.detach(); ctx->tls_server->wait_running(); diff --git a/modules/EvseV2G/tests/README.md b/modules/EvseV2G/tests/README.md index 3168e4040..21b4cd129 100644 --- a/modules/EvseV2G/tests/README.md +++ b/modules/EvseV2G/tests/README.md @@ -17,7 +17,7 @@ $ ninja install ## Run EVerest in SIL 1. start MQTT broker -2. from `build/run-scripts` run `./run-sil-dc.sh` +2. from `build/run-scripts` run `./run-sil-dc-tls.sh` 3. from `build/run-scripts` run `./nodered-sil-dc.sh` 4. open web browser [EVerest Node-RED dashboard](http://localhost:1880/ui/)