From 5fbb02d929ae40c4ccfdbdd83113130964125334 Mon Sep 17 00:00:00 2001 From: James Chapman Date: Fri, 21 Jun 2024 14:58:49 +0100 Subject: [PATCH] feat: add optional support for TLS trusted_ca_keys extension feat: add documentation fix: consistent type naming convention applied fix: failed tests feat: added signal to stop stop server feat: SA_RESTART flag + notes on client certificates fix: memory leak feat: testing non-blocking read feat: patched test only includes the test that would fail on an unpatched OpenSSL fix: logging to omit filename and line information fix: partial config support improved where trust anchors are missing feat: added non-blocking read/write/connect/accept to TLS implementation non-blocking is selected by specifying timout_ms == 0 fix: improve string copy safety for private key password feat: simplified ConfigItem class ConfigItem is now based on std::optional and std::string reverted condition variable wait since an occasional failure was occuring during test runs (not seen previously). fix: revert back to using EVLOG_ rather than calling boost directly fix: codacy suggestions addressed fix: more codacy recommendations and review from AW fix: error when building MBedTLS version fix: changed to use auto& ref as per review comment fix: re-enabled unit tests fix: attempt to fix getting required files to the right place for unit tests fix: attempt to fix non-patched OpenSSL unit tests Signed-off-by: James Chapman --- lib/staging/tls/CMakeLists.txt | 2 + lib/staging/tls/extensions/helpers.cpp | 51 + lib/staging/tls/extensions/helpers.hpp | 174 ++ lib/staging/tls/extensions/status_request.cpp | 587 ++++++ lib/staging/tls/extensions/status_request.hpp | 276 +++ lib/staging/tls/extensions/tls_types.hpp | 65 + .../tls/extensions/trusted_ca_keys.cpp | 444 +++++ .../tls/extensions/trusted_ca_keys.hpp | 262 +++ lib/staging/tls/openssl-patch.md | 1 - lib/staging/tls/openssl_conv.cpp | 2 +- lib/staging/tls/openssl_conv.hpp | 57 +- lib/staging/tls/openssl_util.cpp | 337 +++- lib/staging/tls/openssl_util.hpp | 252 ++- lib/staging/tls/tests/CMakeLists.txt | 54 +- lib/staging/tls/tests/README.md | 1 - lib/staging/tls/tests/crypto_test.cpp | 3 +- lib/staging/tls/tests/gtest_main.cpp | 26 +- lib/staging/tls/tests/openssl_util_test.cpp | 416 +++- lib/staging/tls/tests/patched_test.cpp | 348 +--- .../tls/tests/pki/alt_openssl-pki.conf | 143 ++ lib/staging/tls/tests/pki/openssl-pki.conf | 4 +- lib/staging/tls/tests/pki/pki.sh | 89 +- lib/staging/tls/tests/tls_client_main.cpp | 43 +- lib/staging/tls/tests/tls_connection_test.cpp | 829 ++++++++ lib/staging/tls/tests/tls_connection_test.hpp | 287 +++ lib/staging/tls/tests/tls_main.cpp | 95 +- lib/staging/tls/tests/tls_test.cpp | 369 +++- lib/staging/tls/tls.cpp | 1736 ++++++++--------- lib/staging/tls/tls.hpp | 605 +++--- lib/staging/util/EnumFlags.hpp | 11 +- modules/EvseV2G/CMakeLists.txt | 2 +- modules/EvseV2G/EvseV2G.cpp | 6 +- .../EvseV2G/charger/ISO15118_chargerImpl.cpp | 2 +- modules/EvseV2G/connection/tls_connection.cpp | 103 +- modules/EvseV2G/crypto/crypto_mbedtls.cpp | 2 +- modules/EvseV2G/crypto/crypto_openssl.cpp | 16 +- modules/EvseV2G/crypto/crypto_openssl.hpp | 12 +- modules/EvseV2G/iso_server.cpp | 22 +- modules/EvseV2G/tests/CMakeLists.txt | 36 +- modules/EvseV2G/tests/openssl_test.cpp | 13 +- modules/EvseV2G/tests/v2g_main.cpp | 2 +- modules/EvseV2G/v2g.hpp | 2 +- modules/EvseV2G/v2g_server.cpp | 4 +- 43 files changed, 5838 insertions(+), 1953 deletions(-) create mode 100644 lib/staging/tls/extensions/helpers.cpp create mode 100644 lib/staging/tls/extensions/helpers.hpp create mode 100644 lib/staging/tls/extensions/status_request.cpp create mode 100644 lib/staging/tls/extensions/status_request.hpp create mode 100644 lib/staging/tls/extensions/tls_types.hpp create mode 100644 lib/staging/tls/extensions/trusted_ca_keys.cpp create mode 100644 lib/staging/tls/extensions/trusted_ca_keys.hpp create mode 100644 lib/staging/tls/tests/pki/alt_openssl-pki.conf create mode 100644 lib/staging/tls/tests/tls_connection_test.cpp create mode 100644 lib/staging/tls/tests/tls_connection_test.hpp diff --git a/lib/staging/tls/CMakeLists.txt b/lib/staging/tls/CMakeLists.txt index 8d789f2c7..50318a13c 100644 --- a/lib/staging/tls/CMakeLists.txt +++ b/lib/staging/tls/CMakeLists.txt @@ -5,6 +5,8 @@ find_package(OpenSSL 3) target_sources(tls PRIVATE + extensions/status_request.cpp + extensions/trusted_ca_keys.cpp openssl_conv.cpp openssl_util.cpp tls.cpp diff --git a/lib/staging/tls/extensions/helpers.cpp b/lib/staging/tls/extensions/helpers.cpp new file mode 100644 index 000000000..f08739719 --- /dev/null +++ b/lib/staging/tls/extensions/helpers.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Pionix GmbH and Contributors to EVerest + +#include "helpers.hpp" +#include "trusted_ca_keys.hpp" +#include + +std::ostream& operator<<(std::ostream& out, const openssl::certificate_ptr& obj) { + const auto subject = openssl::certificate_subject(obj.get()); + if (!subject.empty()) { + out << "subject:"; + for (const auto& itt : subject) { + out << " " << itt.first << ":" << itt.second; + } + } + return out; +} + +std::ostream& operator<<(std::ostream& out, const openssl::sha_1_digest_t& obj) { + const auto sav = out.flags(); + for (const auto& c : obj) { + out << std::setw(2) << std::setfill('0') << std::hex << static_cast(c); + } + out.flags(sav); + return out; +} + +std::ostream& operator<<(std::ostream& out, const tls::trusted_ca_keys::trusted_ca_keys_t& obj) { + out << "trusted ca keys: pre-agreed: " << obj.pre_agreed << std::endl; + if (!obj.cert_sha1_hash.empty()) { + for (const auto& hash : obj.cert_sha1_hash) { + out << " certificate hash: " << hash << std::endl; + } + } + if (!obj.key_sha1_hash.empty()) { + for (const auto& hash : obj.key_sha1_hash) { + out << " subject key hash: " << hash << std::endl; + } + } + return out; +} + +std::ostream& operator<<(std::ostream& out, const openssl::DER& obj) { + const auto sav = out.flags(); + const auto* ptr = obj.get(); + for (std::size_t i = 0; i < obj.size(); i++) { + out << std::setw(2) << std::setfill('0') << std::hex << static_cast(*ptr++); + } + out.flags(sav); + return out; +} diff --git a/lib/staging/tls/extensions/helpers.hpp b/lib/staging/tls/extensions/helpers.hpp new file mode 100644 index 000000000..8b1a9bcc4 --- /dev/null +++ b/lib/staging/tls/extensions/helpers.hpp @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Pionix GmbH and Contributors to EVerest + +#ifndef EXTENSIONS_HELPERS_ +#define EXTENSIONS_HELPERS_ + +#include +#include +#include +#include +#include + +#ifdef UNIT_TEST +#include +#endif + +#include + +namespace tls { + +using openssl::log_warning; + +/** + * \brief update position and remaining by amount + * \param[inout] ptr the pointer to increment + * \param[inout] remaining the value to decrement + */ +constexpr void update_position(const std::uint8_t*& ptr, std::int32_t& remaining, std::size_t amount) { + ptr += amount; + remaining -= amount; +} + +/** + * \brief update position and remaining by amount + * \param[inout] ptr the pointer to increment + * \param[inout] remaining the value to decrement + */ +constexpr void update_position(std::uint8_t*& ptr, std::int32_t& remaining, std::size_t amount) { + ptr += amount; + remaining -= amount; +} + +/** + * \brief copy structure from data pointer + * \param[out] dest the destination structure + * \param[inout] ptr the pointer the start of the data, updated to point to the + * next byte (ptr += sizeof(dest)) + * \param[inout] remaining updated with the remaining number of bytes + * (remaining -= sizeof(dest)) + * \param[in] err_message to log on error + * \return true when structure populated from data + */ +template +constexpr bool struct_copy(T& dest, const std::uint8_t*& ptr, std::int32_t& remaining, const std::string& err_message) { + bool bResult{false}; + if (remaining < sizeof(T)) { + log_warning(err_message); + } else { + std::memcpy(&dest, ptr, sizeof(T)); + update_position(ptr, remaining, sizeof(T)); + bResult = true; + } + return bResult; +} + +/** + * \brief copy structure to data pointer + * \param[out] ptr the destination pointer, updated to point to the + * next byte (ptr += sizeof(src)) + * \param[inout] src the source structure + * \param[inout] remaining updated with the remaining number of bytes + * (remaining -= sizeof(src)) + * \param[in] err_message to log on error + * \return true when ptr populated from structure + */ +template +constexpr bool struct_copy(std::uint8_t*& ptr, const T& src, std::int32_t& remaining, const std::string& err_message) { + bool bResult{false}; + if (remaining < sizeof(T)) { + log_warning(err_message); + } else { + std::memcpy(ptr, &src, sizeof(T)); + update_position(ptr, remaining, sizeof(T)); + bResult = true; + } + return bResult; +} + +/** + * \brief copy DER to data pointer + * \param[out] ptr the destination pointer, updated to point to the + * next byte (ptr += src.size()) + * \param[inout] src the DER source object + * \param[inout] remaining updated with the remaining number of bytes + * (remaining -= src.size()) + * \param[in] err_message to log on error + * \return true when ptr populated from structure + */ +inline bool der_copy(std::uint8_t*& ptr, const openssl::DER& src, std::int32_t& remaining, + const std::string& err_message) { + bool bResult{false}; + if (remaining < src.size()) { + log_warning(err_message + ' ' + std::to_string(remaining) + '/' + std::to_string(src.size())); + } else { + std::memcpy(ptr, src.get(), src.size()); + update_position(ptr, remaining, src.size()); + bResult = true; + } + return bResult; +} + +/** + * \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; +} + +/** + * \brief convert a big endian 2 byte (16 bit) unsigned value to uint16 + * \param[in] ptr the pointer to the most significant byte + * \return the interpreted value + */ +constexpr std::uint16_t uint16(const std::uint8_t* ptr) { + return (static_cast(ptr[0]) << 8U) | static_cast(ptr[1]); +} + +/** + * \brief convert a uint16 to big endian 2 byte (16 bit) value + * \param[in] ptr the pointer to the most significant byte + * \param[in] value the 16 bit value + */ +constexpr void uint16(std::uint8_t* ptr, std::uint32_t value) { + ptr[0] = (value >> 8U) & 0xffU; + ptr[1] = value & 0xffU; +} + +template std::string to_string(const 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(); +} + +} // namespace tls + +#ifdef UNIT_TEST +namespace tls::trusted_ca_keys { +struct trusted_ca_keys_t; +} + +std::ostream& operator<<(std::ostream& out, const openssl::certificate_ptr& obj); +std::ostream& operator<<(std::ostream& out, const openssl::sha_1_digest_t& obj); +std::ostream& operator<<(std::ostream& out, const tls::trusted_ca_keys::trusted_ca_keys_t& obj); +std::ostream& operator<<(std::ostream& out, const openssl::DER& obj); +#endif + +#endif // EXTENSIONS_HELPERS_ diff --git a/lib/staging/tls/extensions/status_request.cpp b/lib/staging/tls/extensions/status_request.cpp new file mode 100644 index 000000000..ff8f12bc4 --- /dev/null +++ b/lib/staging/tls/extensions/status_request.cpp @@ -0,0 +1,587 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Pionix GmbH and Contributors to EVerest + +#include "helpers.hpp" +#include "openssl_util.hpp" +#include +#include + +#include + +#include +#include +#include +#include +#include + +#ifdef TLSEXT_STATUSTYPE_ocsp_multi +#define OPENSSL_PATCHED +#endif + +using openssl::log_debug; +using openssl::log_error; + +namespace { + +/** + * \brief load OCSP response from file + * \param[in] filename is the file to read + * \return a pointer to the OCSP reponse which will need to be freed + * using OCSP_RESPONSE_free() + */ +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 OCSP_RESPONSE_ptr = std::shared_ptr; + +struct ocsp_cache_ctx { + std::map cache; +}; + +// ---------------------------------------------------------------------------- +// OcspCache +OcspCache::OcspCache() : m_context(std::make_unique()) { +} + +OcspCache::~OcspCache() = default; + +bool OcspCache::load(const ocsp_entry_list_t& 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; +} + +OcspCache::OcspResponse_t OcspCache::lookup(const digest_t& digest) { + assert(m_context != nullptr); + + OcspResponse_t 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(digest_t& digest, const x509_st* cert) { + assert(cert != nullptr); + return openssl::certificate_sha_1(digest, cert); +} + +namespace status_request { + +// ---------------------------------------------------------------------------- +// ServerStatusRequestV2 + +int ServerStatusRequestV2::s_index{-1}; + +ServerStatusRequestV2::ServerStatusRequestV2(OcspCache& cache) : m_cache(cache) { + if (s_index == -1) { + s_index = CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_SSL, 0, nullptr, nullptr, nullptr, nullptr); + } +} + +bool ServerStatusRequestV2::init_ssl(SSL_CTX* ctx) { + bool bRes{true}; + SSL_CTX_set_client_hello_cb(ctx, &client_hello_cb, nullptr); + + if (SSL_CTX_set_tlsext_status_cb(ctx, &status_request_cb) != 1) { + log_error("SSL_CTX_set_tlsext_status_cb"); + bRes = false; + } + + if (SSL_CTX_set_tlsext_status_arg(ctx, this) != 1) { + log_error("SSL_CTX_set_tlsext_status_arg"); + bRes = false; + } + + // TLS 1.2 and below only - managed differently in TLS 1.3 + constexpr int context_srv2 = 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_srv2, &status_request_v2_add, + &status_request_v2_free, this, &status_request_v2_cb, nullptr) != 1) { + log_error("SSL_CTX_add_custom_ext status_request_v2"); + bRes = false; + } + return bRes; +} + +bool ServerStatusRequestV2::set_ocsp_response(const 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("SSL_set_tlsext_status_ocsp_resp"); + OPENSSL_free(der); + } + } + } + return bResult; +} + +int ServerStatusRequestV2::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}; + int result = SSL_TLSEXT_ERR_NOACK; + digest_t digest{}; + + if (ctx != nullptr) { + const auto* cert = SSL_get_certificate(ctx); + bSet = OcspCache::digest(digest, cert); + } + + bool tls_1_3{false}; + 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* flags_p = get_data(ctx); + if (flags_p != 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 (flags_p->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 ServerStatusRequestV2::set_ocsp_v2_response(const digest_list_t& 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 ServerStatusRequestV2::status_request_v2_add(SSL* ctx, unsigned int ext_type, unsigned int context, + const unsigned char** out, std::size_t* outlen, X509* 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 + digest_t digest{}; + digest_list_t digest_chain; + + if (ctx != nullptr) { + const auto* cert = SSL_get_certificate(ctx); + 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); + 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 ServerStatusRequestV2::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 ServerStatusRequestV2::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 ServerStatusRequestV2::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* flags_p = get_data(ctx); + if (flags_p != nullptr) { + int* extensions{nullptr}; + std::size_t length{0}; + if (SSL_client_hello_get1_extensions_present(ctx, &extensions, &length) == 1) { + std::string logstr("Extensions:"); + for (std::size_t i = 0; i < length; i++) { + logstr += " " + std::to_string(extensions[i]); + if (extensions[i] == TLSEXT_TYPE_status_request) { + flags_p->status_request_received(); + } else if (extensions[i] == TLSEXT_TYPE_status_request_v2) { + flags_p->status_request_v2_received(); + } + } + log_debug(logstr); + OPENSSL_free(extensions); + } + } + return 1; +} + +void ServerStatusRequestV2::set_data(SSL* ctx, StatusFlags* ptr) { + assert(ctx != nullptr); + SSL_set_ex_data(ctx, s_index, ptr); +} + +StatusFlags* ServerStatusRequestV2::get_data(SSL* ctx) { + assert(ctx != nullptr); + return reinterpret_cast(SSL_get_ex_data(ctx, s_index)); +} + +bool ClientStatusRequestV2::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) { + log_error("d2i_OCSP_RESPONSE: decode error"); + } 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; +} + +int ClientStatusRequestV2::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) && (total_length <= std::numeric_limits::max())) { + // there is a response + + const auto* tmp_p = &response[0]; + + if (response[0] == 0x30) { + // not a multi response + auto* resp = d2i_OCSP_RESPONSE(nullptr, &tmp_p, total_length); + if (resp == nullptr) { + log_error("d2i_OCSP_RESPONSE (single)"); + result = 0; + } + OCSP_RESPONSE_free(resp); + } else { + // multiple responses + auto remaining = static_cast(total_length); + const unsigned char* ptr{response}; + + while (remaining >= 3) { + const auto len = uint24(ptr); + update_position(ptr, remaining, 3); + // d2i_OCSP_RESPONSE updates tmp_p + tmp_p = ptr; + auto* resp = d2i_OCSP_RESPONSE(nullptr, &tmp_p, len); + update_position(ptr, remaining, len); + if ((resp == nullptr) || (ptr != tmp_p)) { + log_error("d2i_OCSP_RESPONSE (multi)"); + result = 0; + remaining = -1; + } + OCSP_RESPONSE_free(resp); + } + + if (remaining != 0) { + log_error("OCSP_RESPONSE decode error (multi)"); + result = 0; + } + } + } + + return result; +} + +int ClientStatusRequestV2::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("ClientStatusRequestV2::status_request_v2_multi_cb missing Client *"); + } + return result; +} + +int ClientStatusRequestV2::status_request_v2_add(SSL* ctx, unsigned int ext_type, unsigned int context, + const unsigned char** out, std::size_t* outlen, X509* cert, + std::size_t chainidx, int* alert, void* object) { + int result{0}; + if (context == SSL_EXT_CLIENT_HELLO) { + /* + * struct { + * CertificateStatusType status_type; + * uint16 request_length; // Length of request field in bytes + * select (status_type) { + * case ocsp: OCSPStatusRequest; + * case ocsp_multi: OCSPStatusRequest; + * } request; + * } CertificateStatusRequestItemV2; + * + * enum { ocsp(1), ocsp_multi(2), (255) } CertificateStatusType; + * + * struct { + * ResponderID responder_id_list<0..2^16-1>; + * Extensions request_extensions; + * } OCSPStatusRequest; + * + * opaque ResponderID<1..2^16-1>; + * opaque Extensions<0..2^16-1>; + * + * struct { + * CertificateStatusRequestItemV2 + * certificate_status_req_list<1..2^16-1>; + * } CertificateStatusRequestListV2; + * + * Minimal request: + * 0x0007 certificate_status_req_list length + * 0x02 CertificateStatusType - OCSP multi + * 0x0004 request_length (uint 16) + * 0x0000 responder_id_list length + * 0x0000 request_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); + /* + * 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) { +#ifdef OPENSSL_PATCHED + SSL_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp_multi); +#else + SSL_set_tlsext_status_type(ctx, 2); +#endif // OPENSSL_PATCHED + } + result = 1; + } + return result; +} + +int ClientStatusRequestV2::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 status_request +} // namespace tls diff --git a/lib/staging/tls/extensions/status_request.hpp b/lib/staging/tls/extensions/status_request.hpp new file mode 100644 index 000000000..a9ab2eba1 --- /dev/null +++ b/lib/staging/tls/extensions/status_request.hpp @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Pionix GmbH and Contributors to EVerest + +#ifndef EXTENSIONS_STATUS_REQUEST_ +#define EXTENSIONS_STATUS_REQUEST_ + +#include +#include + +#include +#include +#include + +namespace tls { + +struct ocsp_cache_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 digest_t = openssl::sha_1_digest_t; + using ocsp_entry_t = std::tuple; + using ocsp_entry_list_t = std::vector; + using OcspResponse_t = std::shared_ptr; + +private: + std::unique_ptr m_context; //!< opaque cache data + 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(); + + /** + * \brief populate the cache from a list of (digest, filename) pairs + * \param[in] filenames is a list of (digest, filename) pairs + * \return true when successfully loaded + * \note the OCSP response from the file is cached rather than the filename + */ + bool load(const ocsp_entry_list_t& filenames); + + /** + * \brief return a pointer to the OCSP response for the given digest + * \param[in] digest is the lookup key + * \return the OCSP response or nullptr + */ + OcspResponse_t lookup(const digest_t& digest); + + /** + * \brief calculate the digest for the specified certificate + * \param[out] digest is the calculated hash + * \param[in] cert is the certificate to hash + * \return true on success + */ + static bool digest(digest_t& digest, const x509_st* cert); +}; + +namespace status_request { +// ---------------------------------------------------------------------------- +// TLS handshake extension status_request and status_request_v2 support + +using digest_t = OcspCache::digest_t; +using digest_list_t = std::vector; + +/// \brief status_request and status_request_v2 handler for TLS server +class ServerStatusRequestV2 { +private: + static int s_index; //!< index used for storing per connection data + OcspCache& m_cache; //!< reference to the OCSP cache + +public: + using StatusFlags = tls::StatusFlags; + + explicit ServerStatusRequestV2(OcspCache& cache); + ServerStatusRequestV2() = delete; + ServerStatusRequestV2(const ServerStatusRequestV2&) = delete; + ServerStatusRequestV2(ServerStatusRequestV2&&) = delete; + ServerStatusRequestV2& operator=(const ServerStatusRequestV2&) = delete; + ServerStatusRequestV2& operator=(ServerStatusRequestV2&&) = delete; + ~ServerStatusRequestV2() = default; + + /** + * \brief add the extension to the SSL context + * \param[inout] ctx the context to configure + * \return true on success + */ + bool init_ssl(SslContext* ctx); + + /** + * \brief set the OCSP response for the SSL context + * \param[in] digest the certificate requested + * \param[in] ctx the connection context + * \return true on success + * \note for status_request extension + */ + bool set_ocsp_response(const 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 ServerStatusRequestV2 + * \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 response for the SSL context + * \param[in] digest the certificate requested + * \param[in] ctx the connection context + * \return true on success + * \note for status_request_v2 extension + */ + bool set_ocsp_v2_response(const digest_list_t& 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 ServerStatusRequestV2 + * \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 ServerStatusRequestV2 - not used + */ + 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 not used + * \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 not used + * \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. + * + * Calls StatusFlags::has_status_request() when a status_request extension + * is present. + * + * Calls StatusFlags::has_status_request_v2() when a status_request_v2 + * extension is present. + */ + static int client_hello_cb(Ssl* ctx, int* alert, void* object); + + /** + * \brief store pointer to connection data + * \param[in] ctx the connection context + * \param[in] ptr pointer to the data + */ + static void set_data(Ssl* ctx, StatusFlags* ptr); + + /** + * \brief retrieve pointer to connection data + * \param[in] ctx the connection context + * \return pointer to the data or nullptr + */ + static StatusFlags* get_data(Ssl* ctx); +}; + +/// \brief status_request and status_request_v2 handler for TLS client +class ClientStatusRequestV2 { +public: + ClientStatusRequestV2() = default; + ClientStatusRequestV2(const ClientStatusRequestV2&) = delete; + ClientStatusRequestV2(ClientStatusRequestV2&&) = delete; + ClientStatusRequestV2& operator=(const ClientStatusRequestV2&) = delete; + ClientStatusRequestV2& operator=(ClientStatusRequestV2&&) = delete; + virtual ~ClientStatusRequestV2() = default; + + /** + * \brief print an OCSP response to the specified stream + * \param[in] stream the output stream to use + * \param[in] response the OCSP response + * \param[in] length of the OCSP response + * \return true on success + */ + static bool print_ocsp_response(FILE* stream, const unsigned char*& response, std::size_t length); + + /** + * \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 ClientStatusRequestV2 + * \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 not used + * \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 not used + * \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 status_request +} // namespace tls + +#endif // EXTENSIONS_STATUS_REQUEST_ diff --git a/lib/staging/tls/extensions/tls_types.hpp b/lib/staging/tls/extensions/tls_types.hpp new file mode 100644 index 000000000..f07f73790 --- /dev/null +++ b/lib/staging/tls/extensions/tls_types.hpp @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Pionix GmbH and Contributors to EVerest + +#ifndef EXTENSIONS_TLS_TYPES_ +#define EXTENSIONS_TLS_TYPES_ + +#include + +#include + +struct ocsp_response_st; +struct ssl_ctx_st; +struct ssl_st; +struct x509_st; +struct evp_pkey_st; + +namespace tls { + +/// \brief flags used to keep track of TLS extensions in the client hello +class StatusFlags { +private: + enum class flags_t : std::uint8_t { + status_request, + status_request_v2, + trusted_ca_keys, + last = trusted_ca_keys, + }; + + util::AtomicEnumFlags flags; + +public: + void status_request_received() { + flags.set(flags_t::status_request); + } + void status_request_v2_received() { + flags.set(flags_t::status_request_v2); + } + void trusted_ca_keys_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); + } + [[nodiscard]] bool has_trusted_ca_keys() const { + return flags.is_set(flags_t::status_request_v2); + } +}; + +// opaque types + +using Certificate = struct ::x509_st; +using OcspResponse = struct ::ocsp_response_st; +using PKey = struct ::evp_pkey_st; +using Ssl = struct ::ssl_st; +using SslContext = struct ::ssl_ctx_st; + +// see https://datatracker.ietf.org/doc/html/rfc6961 +constexpr int TLSEXT_TYPE_status_request_v2 = 17; + +} // namespace tls + +#endif // EXTENSIONS_TLS_TYPES_ diff --git a/lib/staging/tls/extensions/trusted_ca_keys.cpp b/lib/staging/tls/extensions/trusted_ca_keys.cpp new file mode 100644 index 000000000..6efdbd66b --- /dev/null +++ b/lib/staging/tls/extensions/trusted_ca_keys.cpp @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Pionix GmbH and Contributors to EVerest + +#include "helpers.hpp" +#include "openssl_util.hpp" +#include +#include + +#include +#include + +#include +#include + +namespace { + +using namespace tls::trusted_ca_keys; +using namespace openssl; + +using hash_fn = bool (*)(sha_1_digest_t& digest, const x509_st* cert); + +/** + * \brief check a list of certificates against trusted hashes + * \param[in] hashes is the list of trusted hashes + * \param[in] certs is the list of trust anchors + * \param[in] gen_hash is the function to generate the hash + * \return true when there is a match + * \note this is a generic function that can be used to check certificate hashes + * or certificate public key hashes + */ +bool match_hash(const digest_list& hashes, const certificate_list& certs, hash_fn gen_hash) { + bool result{false}; + if (!hashes.empty()) { + // check for a match against all known trust anchors + // select the first one to match + for (const auto& ta : certs) { + sha_1_digest_t digest; + if (gen_hash(digest, ta.get())) { + // with the digest from the trust anchor check against + // the trusted CA keys hashes + for (const auto& hash : hashes) { + result = digest == hash; + if (result) { + break; + } + } + } + if (result) { + break; + } + } + } + return result; +} + +/** + * \brief check trust anchors against certificate hashes + * \param[in] extension contains the list of trusted certificate hashes + * \param[in] chain contains the list of trust anchors + * \return true when there is a match + */ +inline bool match_cert_hash(const trusted_ca_keys_t& extension, const chain_t& chain) { + return match_hash(extension.cert_sha1_hash, chain.chain.trust_anchors, &certificate_sha_1); +} + +/** + * \brief check trust anchors against certificate public key hashes + * \param[in] extension contains the list of trusted public key hashes + * \param[in] chain contains the list of trust anchors + * \return true when there is a match + */ +inline bool match_key_hash(const trusted_ca_keys_t& extension, const chain_t& chain) { + return match_hash(extension.key_sha1_hash, chain.chain.trust_anchors, &certificate_subject_public_key_sha_1); +} + +/** + * \brief check trust anchors against certificate subject names + * \param[in] extension contains the list of trusted certificate subject names + * \param[in] chain contains the list of trust anchors + * \return true when there is a match + * \note compares the DER encoded certificate subject name + */ +bool match_name(const trusted_ca_keys_t& extension, const chain_t& chain) { + bool result{false}; + if (!extension.x509_name.empty()) { + for (const auto& ta : chain.chain.trust_anchors) { + auto subject = certificate_subject_der(ta.get()); + for (const auto& name : extension.x509_name) { + result = subject == name; + if (result) { + break; + } + } + if (result) { + break; + } + } + } + return result; +} + +} // namespace + +namespace tls::trusted_ca_keys { + +// ---------------------------------------------------------------------------- +// TrustedCaKeys + +bool extract_TrustedAuthority(trusted_ca_keys_t& result, const std::uint8_t*& ptr, std::int32_t& remaining) { + bool bResult{false}; + if ((remaining > 0) && (ptr != nullptr)) { + const auto identifier = static_cast(*ptr); + update_position(ptr, remaining, 1); + + switch (identifier) { + case IdentifierType::pre_agreed: + result.pre_agreed = true; + bResult = true; + break; + case IdentifierType::key_sha1_hash: { + digest_t digest; + bResult = struct_copy(digest, ptr, remaining, "trusted_ca_keys extension: key_sha1_hash decode error"); + if (bResult) { + result.key_sha1_hash.emplace_back(digest); + } + break; + } + case IdentifierType::x509_name: { + if (remaining >= 2) { + const auto name_len = uint16(ptr); + update_position(ptr, remaining, 2); + if (remaining >= name_len) { + DER name(ptr, name_len); + update_position(ptr, remaining, name_len); + result.x509_name.emplace_back(std::move(name)); + bResult = true; + } + } + break; + } + case IdentifierType::cert_sha1_hash: { + digest_t digest; + bResult = struct_copy(digest, ptr, remaining, "trusted_ca_keys extension: cert_sha1_hash decode error"); + if (bResult) { + result.cert_sha1_hash.emplace_back(digest); + } + break; + } + default: + log_warning("trusted_ca_keys extension: IdentifierType decode error: " + + std::to_string(static_cast(identifier))); + break; + } + } + return bResult; +} + +std::size_t TrustedAuthority_size(const trusted_ca_keys_t& data) { + // list length in bytes (2 bytes) + 1 if data.pre_agreed is true + std::size_t size = (data.pre_agreed) ? 3 : 2; + + // IdentifierType (1 byte) + SHA1 digest size + constexpr std::size_t hash_size = sizeof(digest_t) + 1; + size += (data.key_sha1_hash.size() + data.cert_sha1_hash.size()) * hash_size; + + if (!data.x509_name.empty()) { + for (const auto& i : data.x509_name) { + // IdentifierType (1 byte) + 2 bytes length + DER X509 name + size += i.size() + 3; + } + } + return size; +} + +bool certificate_digest(digest_t& digest, const x509_st* cert) { + assert(cert != nullptr); + return openssl::certificate_sha_1(digest, cert); +} + +bool public_key_digest(digest_t& digest, const x509_st* cert) { + assert(cert != nullptr); + return openssl::certificate_subject_public_key_sha_1(digest, cert); +} + +/* + * Presentation Language + * https://datatracker.ietf.org/doc/html/rfc5246 + * + * extension_data (see https://datatracker.ietf.org/doc/html/rfc6066) + * struct { + * TrustedAuthority trusted_authorities_list<0..2^16-1>; + * } TrustedAuthorities; + * + * struct { + * IdentifierType identifier_type; + * select (identifier_type) { + * case pre_agreed: struct {}; + * case key_sha1_hash: SHA1Hash; + * case x509_name: DistinguishedName; + * case cert_sha1_hash: SHA1Hash; + * } identifier; + * } TrustedAuthority; + * + * enum { + * pre_agreed(0), key_sha1_hash(1), x509_name(2), + * cert_sha1_hash(3), (255) + * } IdentifierType; + * + * opaque DistinguishedName<1..2^16-1>; + * + * (note the extension is not DER encoded, only DistinguishedName is) + * + * === captured traces === + * + * PEV1 + * 0069 trusted_authorities_list length + * 03 identifier_type cert_sha1_hash d8367e861f5807f8141fea572d676dbf58bb5f7c SHA1Hash + * 03 identifier_type cert_sha1_hash b491ddd08fafe72d9f6f9bafc68eb04da84cc09a SHA1Hash + * 03 identifier_type cert_sha1_hash 30aaaab25b1cc8a09a7b32652c33cc5a973c13f3 SHA1Hash + * 03 identifier_type cert_sha1_hash 700bf78ad58e0819dac6fcaead5ed20f7bb0554f SHA1Hash + * 03 identifier_type cert_sha1_hash 8c821f41604ed4c3431cf6d19f2ae107cf1f50e1 SHA1Hash + * + * PEV2 + * 002a trusted_authorities_list length + * 03 identifier_type cert_sha1_hash d8367e861f5807f8141fea572d676dbf58bb5f7c SHA1Hash + * 03 identifier_type cert_sha1_hash 8c821f41604ed4c3431cf6d19f2ae107cf1f50e1 SHA1Hash + * + * PEV3 (invalid missing the size of trusted_authorities_list) + * 01 identifier_type key_sha1_hash 4cd7290bf592d2c1ba90f56e08946d4c8e99dc38 SHA1Hash + * 01 identifier_type key_sha1_hash 00fae3900795c888a4d4d7bd9fdffa60418ac19f SHA1Hash + */ + +trusted_authority convert(const trusted_ca_keys_t& keys) { + const auto size = TrustedAuthority_size(keys); + trusted_authority result{}; + + // empty trusted_ca_keys_t has a size of 2 + if ((size > 2) && (size <= std::numeric_limits::max())) { + result = trusted_authority(size); + auto* ptr = result.get(); + auto remaining = static_cast(size); + uint16(ptr, size - 2); + update_position(ptr, remaining, 2); + if (keys.pre_agreed) { + *ptr = static_cast(IdentifierType::pre_agreed); + update_position(ptr, remaining, 1); + } + for (const auto& i : keys.cert_sha1_hash) { + *ptr = static_cast(IdentifierType::cert_sha1_hash); + update_position(ptr, remaining, 1); + if (!struct_copy(ptr, i, remaining, "trusted_ca_keys extension: cert_sha1_hash encode error")) { + break; + } + } + for (const auto& i : keys.key_sha1_hash) { + *ptr = static_cast(IdentifierType::key_sha1_hash); + update_position(ptr, remaining, 1); + if (!struct_copy(ptr, i, remaining, "trusted_ca_keys extension: key_sha1_hash encode error")) { + break; + } + } + for (const auto& i : keys.x509_name) { + *ptr = static_cast(IdentifierType::x509_name); + update_position(ptr, remaining, 1); + uint16(ptr, i.size()); + update_position(ptr, remaining, 2); + if (!der_copy(ptr, i, remaining, "trusted_ca_keys extension: x509_name encode error")) { + break; + } + } + } + + return result; +} + +trusted_ca_keys_t convert(const std::uint8_t* extension_data, const std::size_t len) { + trusted_ca_keys_t result{}; + bool bResult{false}; + + if ((extension_data != nullptr) && (len <= std::numeric_limits::max()) && (len >= 2)) { + std::int32_t remaining = uint16(extension_data); + extension_data += 2; + if (remaining != (len - 2)) { + log_warning("trusted_ca_keys extension: TrustedAuthorities decode error"); + } else { + bResult = true; + while (bResult && remaining > 0) { + bResult = extract_TrustedAuthority(result, extension_data, remaining); + } + } + } + + // do not return partially parsed extension + return (bResult) ? std::move(result) : std::move(trusted_ca_keys_t()); +} + +bool match(const trusted_ca_keys_t& extension, const chain_t& chain) { + bool result = match_cert_hash(extension, chain); + result = result || match_key_hash(extension, chain); + result = result || match_name(extension, chain); + return result; +} + +const chain_t* select(const trusted_ca_keys_t& extension, const chain_list& chains) { + const chain_t* result{nullptr}; + for (const auto& chain : chains) { + if (match(extension, chain)) { + result = &chain; + break; + } + } + return result; +} + +int ServerTrustedCaKeys::s_index{-1}; + +ServerTrustedCaKeys::ServerTrustedCaKeys() { + if (s_index == -1) { + s_index = CRYPTO_get_ex_new_index(CRYPTO_EX_INDEX_SSL, 0, nullptr, nullptr, nullptr, nullptr); + } +} + +bool ServerTrustedCaKeys::init_ssl(SslContext* ctx) { + bool bRes{true}; + // TLS 1.2 and below only - use certificate_authorities in TLS 1.3 + constexpr int context_tck = + SSL_EXT_TLS_ONLY | SSL_EXT_TLS1_2_AND_BELOW_ONLY | SSL_EXT_IGNORE_ON_RESUMPTION | SSL_EXT_CLIENT_HELLO; + if (SSL_CTX_add_custom_ext(ctx, TLSEXT_TYPE_trusted_ca_keys, context_tck, nullptr, nullptr, nullptr, + &ServerTrustedCaKeys::trusted_ca_keys_cb, nullptr) != 1) { + log_error("SSL_CTX_add_custom_ext"); + bRes = false; + } + + // used to change the server certificate depending on trusted_ca_keys + SSL_CTX_set_cert_cb(ctx, &ServerTrustedCaKeys::handle_certificate_cb, this); + return bRes; +} + +void ServerTrustedCaKeys::update(chain_list&& new_chains) { + std::lock_guard lock(m_mux); + m_chains = std::move(new_chains); +} + +const chain_t* ServerTrustedCaKeys::select(const trusted_ca_keys_t& extension) { + return trusted_ca_keys::select(extension, m_chains); +} + +const chain_t* ServerTrustedCaKeys::select_default() { + return (m_chains.empty()) ? nullptr : m_chains.data(); +} + +int ServerTrustedCaKeys::trusted_ca_keys_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) { + /* + * return values: + * - fatal, abort handshake and sent TLS Alert: result = 0 or negative and *alert = alert value + * - success: result = 1 + */ + auto* keys_p = get_data(ctx); + if (keys_p != nullptr) { + keys_p->flags.trusted_ca_keys_received(); + keys_p->tck = convert(data, datalen); + } + return 1; +} + +int ServerTrustedCaKeys::handle_certificate_cb(SSL* ssl, void* arg) { + /* + * return values: + * - fatal, abort handshake and sent TLS Alert: result = 0 or negative + * - success: result = 1 + */ + + int result{1}; + + auto* tck_p = reinterpret_cast(arg); + auto* keys_p = get_data(ssl); + + /* + * From OpenSSL man page + * An application will typically call SSL_use_certificate() and SSL_use_PrivateKey() + * to set the end entity certificate and private key. It can add intermediate and + * optionally the root CA certificates using SSL_add1_chain_cert(). + * It might also call SSL_certs_clear(). + */ + + if ((tck_p != nullptr) && (keys_p != nullptr) && (keys_p->flags.has_trusted_ca_keys())) { + // prevent update() from changing pointers + std::lock_guard lock(tck_p->m_mux); + + const auto* selected = tck_p->select(keys_p->tck); + if (selected != nullptr) { + if (!use_certificate_and_key(ssl, *selected)) { + // setting failed - try and use the default + selected = tck_p->select_default(); + if (selected != nullptr) { + if (!use_certificate_and_key(ssl, *selected)) { + // there has been a problem setting the server + // certificate, key and chain + result = 0; + log_warning("terminating TLS handshake: trusted_ca_keys"); + } + } + } + } + } + return result; +} + +void ServerTrustedCaKeys::set_data(SSL* ctx, server_trusted_ca_keys_t* ptr) { + assert(ctx != nullptr); + SSL_set_ex_data(ctx, s_index, ptr); +} + +server_trusted_ca_keys_t* ServerTrustedCaKeys::get_data(SSL* ctx) { + assert(ctx != nullptr); + return reinterpret_cast(SSL_get_ex_data(ctx, s_index)); +} + +int ClientTrustedCaKeys::trusted_ca_keys_add(SSL* ctx, unsigned int ext_type, unsigned int context, + const unsigned char** out, std::size_t* outlen, X509* cert, + std::size_t chainidx, int* alert, void* object) { + int result{0}; + if ((context == SSL_EXT_CLIENT_HELLO) && (object != nullptr)) { + auto* config = reinterpret_cast(object); + auto der = convert(*config); + const auto len = der.size(); + auto* ptr = der.release(); + if (ptr != nullptr) { + *out = ptr; + *outlen = len; + result = 1; + } + } + return result; +} + +void ClientTrustedCaKeys::trusted_ca_keys_free(SSL* ctx, unsigned int ext_type, unsigned int context, + const unsigned char* out, void* object) { + openssl::DER::free(const_cast(out)); +} + +} // namespace tls::trusted_ca_keys diff --git a/lib/staging/tls/extensions/trusted_ca_keys.hpp b/lib/staging/tls/extensions/trusted_ca_keys.hpp new file mode 100644 index 000000000..3ddc18fe3 --- /dev/null +++ b/lib/staging/tls/extensions/trusted_ca_keys.hpp @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Pionix GmbH and Contributors to EVerest + +#ifndef EXTENSIONS_TRUSTED_CA_KEYS_ +#define EXTENSIONS_TRUSTED_CA_KEYS_ + +#include +#include + +#include + +namespace tls::trusted_ca_keys { + +using der_ptr = openssl::der_ptr; +using DER = openssl::DER; +using der_list = std::vector; + +using digest_t = openssl::sha_1_digest_t; +using digest_list = std::vector; + +// The trusted_ca_keys extension isn't DER encoded +// openssl::DER class can be reused though +using trusted_authority = openssl::DER; + +using openssl::chain_list; +using openssl::chain_t; + +/** + * \brief items that can be requested in the trusted_ca_keys extension + */ +struct trusted_ca_keys_t { + der_list x509_name; //!< a list of DER encoded certificate subject names + digest_list key_sha1_hash; //!< a list of certificate key hashes + digest_list cert_sha1_hash; //!< a list of certificate hashes + bool pre_agreed{false}; //!< use the pre-agreed certificate +}; + +enum class IdentifierType : std::uint8_t { + pre_agreed = 0, + key_sha1_hash = 1, + x509_name = 2, + cert_sha1_hash = 3, +}; + +/** + * \brief extract item from trusted_ca_keys extension + * \param[out] result updated structure of parsed options + * \param[inout] ptr a pointer to the first byte of the next item, updated to + * be the first byte of the next item + * \param[inout] remaining number of bytes left (updated with new value) + * \return true on success + */ +bool extract_trusted_authority(trusted_ca_keys_t& result, const std::uint8_t*& ptr, std::int32_t& remaining); + +/** + * \brief calculate the size of array needed to hold the extension + * \param[in] data the trusted items to include + * \return the number of bytes needed + */ +std::size_t trusted_authority_size(const trusted_ca_keys_t& data); + +/** + * \brief calculate the digest over the certificate + * \param[in] cert is the certificate + * \param[out] digest the calculated digest + * \return true on success + */ +bool certificate_digest(digest_t& digest, const x509_st* cert); + +/** + * \brief calculate the digest over the certificate's public key + * \param[in] cert is the certificate + * \param[out] digest the calculated digest + * \return true on success + */ +bool public_key_digest(digest_t& digest, const x509_st* cert); + +/** + * \brief convert a trusted_ca_keys_t to the encoded extension + * \param[in] keys the items to include in the extension + * \return the encoded extension + */ +trusted_authority convert(const trusted_ca_keys_t& keys); + +/** + * \brief convert a encoded extension into trusted_ca_keys_t + * \param[in] extension_data a pointer to the extension data + * \param[in] len is the length of the extension data + * \return the populated trusted_ca_keys_t + * \note trusted_ca_keys_t will be empty if there is any problem decoding the + * extension. It will not contain partial results. + */ +trusted_ca_keys_t convert(const std::uint8_t* extension_data, std::size_t len); + +/** + * \brief convert a encoded extension into trusted_ca_keys_t + * \param[in] extension_data the items to include in the extension + * \return the populated trusted_ca_keys_t + * \note trusted_ca_keys_t will be empty if there is any problem decoding the + * extension. It will not contain partial results. + */ +inline trusted_ca_keys_t convert(const trusted_authority& extension_data) { + return convert(extension_data.get(), extension_data.size()); +} + +/** + * \brief check if one of the trust anchors matches the trusted_ca_keys + * \param[in] extension the information to check against + * \param[in] chain the certificate chain to check against + * \return true when the chain matches the extension + */ +bool match(const trusted_ca_keys_t& extension, const chain_t& chain); + +/** + * \brief select the certificate chain to use + * \param[in] extension the information to check against + * \param[in] chains the list of certificate chains to check against + * \return a pointer to the chain (in chains) or nullptr when none match + */ +const chain_t* select(const trusted_ca_keys_t& extension, const chain_list& chains); + +/// \brief per connection data +struct server_trusted_ca_keys_t { + trusted_ca_keys_t& tck; //!< parsed values + tls::StatusFlags& flags; //!< flags to indicate which extensions were present +}; + +/// \brief trusted_ca_keys extension handler for a TLS server +class ServerTrustedCaKeys { +private: + static int s_index; //!< index used for storing per connection data + chain_list m_chains; //!< known certificate chains + std::mutex m_mux; //!< protects m_chains + +public: + ServerTrustedCaKeys(); + ServerTrustedCaKeys(const ServerTrustedCaKeys&) = delete; + ServerTrustedCaKeys(ServerTrustedCaKeys&&) = delete; + ServerTrustedCaKeys& operator=(const ServerTrustedCaKeys&) = delete; + ServerTrustedCaKeys& operator=(ServerTrustedCaKeys&&) = delete; + ~ServerTrustedCaKeys() = default; + + /** + * \brief add the extension to the SSL context + * \param[inout] ctx the context to configure + * \return true on success + */ + bool init_ssl(SslContext* ctx); + + /** + * \brief update m_chains with the new certificate chains to support + * \param[in] new_chains the chains to support + * \note new_chains is moved to m_chains and hence will be empty after + * calling update() + */ + void update(chain_list&& new_chains); + + /** + * \brief select chain to use based on parsed extension + * \param[in] extension is the parsed extension + * \return a pointer to the chain (in m_chains) or nullptr when none match + * \note pointer will be invalid if update() called before it is used + */ + const chain_t* select(const trusted_ca_keys_t& extension); + + /** + * \brief select the default chain to use (first entry in m_chains) + * \return a pointer to the first chain (in m_chains) or nullptr on error + * \note pointer will be invalid if update() called before it is used + */ + const chain_t* select_default(); + + /** + * \brief the OpenSSL callback for the trusted_ca_keys 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 + * + * Parses the extension data to extract and populate trusted_ca_keys_t. + * Calls StatusFlags::has_trusted_ca_keys() when a trusted_ca_keys extension + * is present. Note that trusted_ca_keys_t may contain no information if + * there was a parsing error. + */ + static int trusted_ca_keys_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 trusted_ca_keys extension + * \param[in] ctx the connection context + * \param[in] arg A ServerTrustedCaKeys object + * \return success = 1, error = zero or negative + * + * Calls select() and select_default() after acquiring the mutex to ensure + * that calls to update() don't invalidate the pointers. + */ + static int handle_certificate_cb(Ssl* ssl, void* arg); + + /** + * \brief store pointer to connection data + * \param[in] ctx the connection context + * \param[in] ptr pointer to the data + */ + static void set_data(Ssl* ctx, server_trusted_ca_keys_t* ptr); + + /** + * \brief retrieve pointer to connection data + * \param[in] ctx the connection context + * \return pointer to the data or nullptr + */ + static server_trusted_ca_keys_t* get_data(Ssl* ctx); +}; + +/// \brief trusted_ca_keys extension handler for a TLS client +class ClientTrustedCaKeys { +public: + ClientTrustedCaKeys() = default; + ClientTrustedCaKeys(const ClientTrustedCaKeys&) = delete; + ClientTrustedCaKeys(ClientTrustedCaKeys&&) = delete; + ClientTrustedCaKeys& operator=(const ClientTrustedCaKeys&) = delete; + ClientTrustedCaKeys& operator=(ClientTrustedCaKeys&&) = delete; + ~ClientTrustedCaKeys() = default; + + /** + * \brief add trusted_ca_keys 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 pointer to trusted_ca_keys_t to populate the extension + * \return success = 1, do not include = 0, error == -1 + */ + static int trusted_ca_keys_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 trusted_ca_keys 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 not used + */ + static void trusted_ca_keys_free(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char* out, + void* object); +}; + +} // namespace tls::trusted_ca_keys + +#endif // EXTENSIONS_TRUSTED_CA_KEYS_ diff --git a/lib/staging/tls/openssl-patch.md b/lib/staging/tls/openssl-patch.md index c5477b6e4..a9c62d00b 100644 --- a/lib/staging/tls/openssl-patch.md +++ b/lib/staging/tls/openssl-patch.md @@ -1,4 +1,3 @@ - # OpenSSL 3.0.8 patch The file `openssl-3.0.8-feat-updates-to-support-status_request_v2.patch` is a diff --git a/lib/staging/tls/openssl_conv.cpp b/lib/staging/tls/openssl_conv.cpp index 63b129e04..1ebd83111 100644 --- a/lib/staging/tls/openssl_conv.cpp +++ b/lib/staging/tls/openssl_conv.cpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest #include diff --git a/lib/staging/tls/openssl_conv.hpp b/lib/staging/tls/openssl_conv.hpp index 82322535a..efc3815b0 100644 --- a/lib/staging/tls/openssl_conv.hpp +++ b/lib/staging/tls/openssl_conv.hpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest #ifndef OPENSSL_CONV_HPP_ #define OPENSSL_CONV_HPP_ @@ -8,9 +8,64 @@ #include #include +/** + * \file Convert OpenSSL X509 certificates for use with EVSE Security + * + * The approach is to increase the reference count on the X509 certificate + * rather than passing ownership. This is done so that certificates that are + * already part of a structure (e.g. client certificate and chain from a SSL + * connection) can be used easily. Otherwise the caller would need to remember + * to make a copy before using these functions. + * + * example using an existing certificate: + * \code + * auto cert = to_X509Wrapper(SSL_get0_peer_certificate(ssl)); + * std::cout << cert.get_common_name() << std::endl; + * \endcode + * + * example using a new certificate: + * \code + * auto* fp = fopen("certificate.pem", "r"); + * auto* x509 = PEM_read_X509(fp, nullptr, nullptr, nullptr); + * fclose(fp); + * auto cert = to_X509Wrapper(x509); + * X509_free(x509); + * std::cout << cert.get_common_name() << std::endl; + * \endcode + * + * The following would lead to a memory leak because 'get1' returns a reference + * to the certificate that needs to be freed. + * \code + * auto cert = to_X509Wrapper(SSL_get1_peer_certificate(ssl)); + * std::cout << cert.get_common_name() << std::endl; + * \endcode + * + * The solution is to keep the result so it can be freed. + * \code + * auto* x509 = SSL_get1_peer_certificate(ssl); + * auto cert = to_X509Wrapper(x509); + * X509_free(x509); + * std::cout << cert.get_common_name() << std::endl; + * \endcode + */ + namespace openssl::conversions { +/** + * \brief create an EVSE Security certificate handle from a OpenSSL X509 pointer + * \param[in] cert a pointer to an OpenSSL X509 structure + * \return a EVSE Security managed pointer + * \note the reference count on cert is incremented. It may still need to be + * freed. + */ evse_security::X509Handle_ptr to_X509Handle_ptr(x509_st* cert); +/** + * \brief create an EVSE Security certificate wrapper from a OpenSSL X509 pointer + * \param[in] cert a pointer to an OpenSSL X509 structure + * \return a EVSE Security managed wrapper + * \note the reference count on cert is incremented. It may still need to be + * freed. + */ evse_security::X509Wrapper to_X509Wrapper(x509_st* cert); } // namespace openssl::conversions diff --git a/lib/staging/tls/openssl_util.cpp b/lib/staging/tls/openssl_util.cpp index a582813bd..ef3742383 100644 --- a/lib/staging/tls/openssl_util.cpp +++ b/lib/staging/tls/openssl_util.cpp @@ -1,38 +1,70 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest #include #include #include #include #include +#include #include -#include -#include -#include #include -#include #include "openssl_util.hpp" #include +#include #include #include #include +#include +#include #include +#include #include +#include #include namespace { -openssl::log_handler_t s_log_handler{nullptr}; +openssl::log_handler_t s_log_handler{nullptr}; //!< logs are passed to this function +/** + * \brief add OpenSSL error information to the string + * \param[in] str is the OpenSSL \n\0 terminated error string + * \param[in] len is the length of the string including the \n + * \param[in] u is user data - a std::string to append to + * \return 0 on success + */ 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); + *list += '\n' + std::string(str, len - 1); return 0; } + +/** + * \brief present a password for private key files + * \param[in] buf is a pointer for where to place the password + * \param[in] size is the size of buf i.e. max password size + * \param[in] rwflag to indicate whether a file os being read or written + * \param[in] u is user data which must be set to the required password + * \return the length of the password written to buf or -1 on error + * \note this callback is used to prevent attempts to prompt for a password + * from a terminal + */ +int password_cb(char* buf, int size, int rwflag, void* u) { + int result{-1}; + + if ((u != nullptr) && (buf != nullptr) && (size > 0)) { + std::strncpy(buf, static_cast(u), size); + buf[size - 1] = '\0'; + result = static_cast(std::strlen(buf)); + } + + return result; +} + } // namespace namespace openssl { @@ -57,10 +89,14 @@ log_handler_t set_log_handler(log_handler_t handler) { namespace { -void DER_Signature_free(std::uint8_t* ptr) { - OPENSSL_free(ptr); -} - +/** + * \brief calculate a SHA digest + * \param[in] data is the data to hash + * \param[in] len is the length of the data to hash + * \param[out] digest is the calculated digest + * \param[in] HASH is the type of digest e.g. SHA1, SHA256 ... + * \return true on success + */ template bool sha_impl(const void* data, std::size_t len, DIGEST& digest, const EVP_MD* HASH) { std::array buffer{}; unsigned int digestlen{0}; @@ -79,6 +115,9 @@ template bool sha_impl(const void* data, std::size_t len, DIGE template bool sha(const void* data, std::size_t len, DIGEST& digest); +template <> bool sha(const void* data, std::size_t len, openssl::sha_1_digest_t& digest) { + return sha_impl(data, len, digest, EVP_sha1()); +} template <> bool sha(const void* data, std::size_t len, openssl::sha_256_digest_t& digest) { return sha_impl(data, len, digest, EVP_sha256()); } @@ -93,7 +132,82 @@ template <> bool sha(const void* data, std::size_t len, openssl::sha_512_digest_ namespace openssl { -bool sign(evp_pkey_st* pkey, bn_t& r, bn_t& s, const sha_256_digest_t& digest) { +DER::DER(std::size_t size) : DER(nullptr, size) { +} + +DER::DER(const der_underlying_t* src, std::size_t size) { + auto* tmp = static_cast(OPENSSL_malloc(size)); + if (tmp != nullptr) { + ptr = der_ptr{tmp, &DER::free}; + len = size; + if (src != nullptr) { + std::memcpy(ptr.get(), src, size); + } else { + std::memset(ptr.get(), 0, size); + } + } +} + +DER::DER(const DER& obj) : DER(obj.ptr.get(), obj.len) { +} + +DER& DER::operator=(const DER& obj) { + if (&obj != this) { + *this = DER(obj); + } + return *this; +} + +DER::DER(DER&& obj) noexcept : ptr(std::move(obj.ptr)), len(obj.len) { + obj.len = 0; +} + +DER& DER::operator=(DER&& obj) noexcept { + if (this != &obj) { + ptr = std::move(obj.ptr); + len = obj.len; + obj.len = 0; + } + return *this; +} + +bool DER::operator==(const DER& rhs) const { + if (&rhs == this) { + return true; + } + + bool result{false}; + const auto* lhs_p = ptr.get(); + const auto* rhs_p = rhs.ptr.get(); + + if ((lhs_p != nullptr) && (rhs_p != nullptr)) { + result = len == rhs.len; + result = result && (std::memcmp(lhs_p, rhs_p, len) == 0); + } + return result; +} + +bool DER::operator==(const der_underlying_t* rhs) const { + return ptr.get() == rhs; +} + +DER::operator bool() const { + return (ptr.get() != nullptr) && (len > 0); +} + +der_underlying_t* DER::dup(const DER& obj) { + auto* ptr = static_cast(OPENSSL_malloc(obj.len)); + if ((ptr != nullptr) && (obj.ptr != nullptr)) { + std::memcpy(ptr, obj.ptr.get(), obj.len); + } + return ptr; +} + +void DER::free(der_underlying_t* ptr) { + OPENSSL_free(ptr); +} + +bool sign(EVP_PKEY* pkey, bn_t& r, bn_t& s, const sha_256_digest_t& digest) { bool bRes{false}; std::array signature{}; auto len = signature.size(); @@ -142,15 +256,15 @@ bool sign(EVP_PKEY* pkey, unsigned char* sig, std::size_t& siglen, const unsigne return bRes; } -bool verify(evp_pkey_st* pkey, const bn_t& r, const bn_t& s, const sha_256_digest_t& digest) { +bool verify(EVP_PKEY* 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 verify(EVP_PKEY* 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); + auto signature = bn_to_signature(r, s); + if (signature) { + bRes = verify(pkey, signature.get(), signature.size(), digest.data(), sha_256_digest_size); } return bRes; } @@ -183,6 +297,10 @@ bool verify(EVP_PKEY* pkey, const unsigned char* sig, std::size_t siglen, const return bRes; } +bool sha_1(const void* data, std::size_t len, sha_1_digest_t& digest) { + return sha(data, len, digest); +} + bool sha_256(const void* data, std::size_t len, sha_256_digest_t& digest) { return sha(data, len, digest); } @@ -270,11 +388,26 @@ std::string base64_encode(const std::uint8_t* data, std::size_t len, bool newLin return result; } -std::tuple bn_to_signature(const bn_t& r, const bn_t& s) { +pkey_ptr load_private_key(const char* filename, const char* password) { + pkey_ptr private_key{nullptr, nullptr}; + auto* bio = BIO_new_file(filename, "r"); + if (bio != nullptr) { + // password is passed to password_cb() as parameter u which is never + // written to, hence const_cast is okay + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, &password_cb, const_cast(password)); + if (pkey != nullptr) { + private_key = pkey_ptr{pkey, &EVP_PKEY_free}; + } + BIO_free(bio); + } + return private_key; +} + +DER 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) { +DER 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}; @@ -304,7 +437,8 @@ std::tuple bn_to_signature(const std::uint8_t* r BN_free(rbn); BN_free(sbn); ECDSA_SIG_free(signature); - return {DER_Signature_ptr(sig_p, &DER_Signature_free), signature_len}; + // move sig_p to DER + return {der_ptr{sig_p, &DER::free}, signature_len}; }; bool signature_to_bn(bn_t& r, bn_t& s, const std::uint8_t* sig_p, std::size_t len) { @@ -328,9 +462,8 @@ bool signature_to_bn(bn_t& r, bn_t& s, const std::uint8_t* sig_p, std::size_t le return bRes; }; -std::vector load_certificates(const char* filename) { - std::vector result{}; - +certificate_list load_certificates(const char* filename) { + certificate_list result{}; if (filename != nullptr) { auto* store = OSSL_STORE_open(filename, UI_null(), nullptr, nullptr, nullptr); if (store != nullptr) { @@ -360,7 +493,87 @@ std::vector load_certificates(const char* filename) { return result; } -std::string certificate_to_pem(const x509_st* cert) { +certificate_list load_certificates(const std::vector& filenames) { + certificate_list result{}; + for (const auto* i : filenames) { + auto tmp = load_certificates(i); + std::move(tmp.begin(), tmp.end(), std::back_inserter(result)); + } + return result; +} + +chain_info_t load_certificates(const char* leaf_file, const char* chain_file, const char* root_file) { + certificate_ptr leaf_cert{nullptr, nullptr}; + auto leaf = load_certificates(leaf_file); + auto chain = load_certificates(chain_file); + auto root = load_certificates(root_file); + + if (leaf.empty()) { + if (!chain.empty()) { + leaf_cert.swap(chain[0]); + chain.erase(chain.begin()); + } + } else { + leaf_cert.swap(leaf[0]); + } + + if (leaf_cert && !root.empty()) { + if (verify_certificate(leaf_cert.get(), root, chain) == verify_result_t::Verified) { + return {std::move(leaf_cert), std::move(chain), std::move(root)}; + } + } + + return {{nullptr, nullptr}, {}, {}}; +} + +chain_info_list_t load_certificates(const chain_filenames_list_t& chains) { + chain_info_list_t result; + result.reserve(chains.size()); + for (const auto& chain : chains) { + result.emplace_back(load_certificates(chain)); + } + return result; +} + +bool verify_certificate_key(const X509* cert, const EVP_PKEY* pkey) { + return X509_check_private_key(cert, pkey) == 1; +} + +bool verify_chain(const chain_info_t& chain) { + return verify_certificate(chain.leaf.get(), chain.trust_anchors, chain.chain) == verify_result_t::Verified; +} + +bool verify_chain(const chain_t& chain) { + bool result = verify_chain(chain.chain); + result = result && verify_certificate_key(chain.chain.leaf.get(), chain.private_key.get()); + return result; +} + +bool use_certificate_and_key(SSL* ssl, const chain_t& chain) { + assert(ssl != nullptr); + + bool result{false}; + SSL_certs_clear(ssl); + auto* untrusted = sk_X509_new_null(); + if (untrusted == nullptr) { + log_error("sk_X509_new_null"); + } else { + for (const auto& cert : chain.chain.chain) { + if (X509_add_cert(untrusted, cert.get(), + X509_ADD_FLAG_UP_REF | X509_ADD_FLAG_NO_DUP | X509_ADD_FLAG_NO_SS) != 1) { + log_error("X509_add_cert"); + } + } + result = SSL_use_cert_and_key(ssl, chain.chain.leaf.get(), chain.private_key.get(), untrusted, 1) == 1; + if (!result) { + log_error("SSL_use_cert_and_key"); + } + sk_X509_pop_free(untrusted, X509_free); + } + return result; +} + +std::string certificate_to_pem(const X509* cert) { assert(cert != nullptr); auto* mem = BIO_new(BIO_s_mem()); @@ -379,21 +592,21 @@ std::string certificate_to_pem(const x509_st* cert) { return result; } -Certificate_ptr der_to_certificate(const std::uint8_t* der, std::size_t len) { - Certificate_ptr result{nullptr, nullptr}; +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}; + 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; +verify_result_t verify_certificate(const X509* cert, const certificate_list& trust_anchors, + const certificate_list& 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(); @@ -422,7 +635,7 @@ verify_result_t verify_certificate(const x509_st* cert, const CertificateList& t } } - if (result == verify_result_t::verified) { + if (result == verify_result_t::Verified) { result = verify_result_t::OtherError; for (const auto& i : trust_anchors) { @@ -472,7 +685,7 @@ verify_result_t verify_certificate(const x509_st* cert, const CertificateList& t break; } } else { - result = verify_result_t::verified; + result = verify_result_t::Verified; } } } @@ -484,7 +697,7 @@ verify_result_t verify_certificate(const x509_st* cert, const CertificateList& t return result; } -std::map certificate_subject(const x509_st* cert) { +std::map certificate_subject(const X509* cert) { assert(cert != nullptr); std::map result; @@ -509,15 +722,67 @@ std::map certificate_subject(const x509_st* cert) { return result; } -PKey_ptr certificate_public_key(x509_st* cert) { - PKey_ptr result{nullptr, nullptr}; +DER certificate_subject_der(const X509* cert) { + assert(cert != nullptr); + + int len{0}; + unsigned char* data{nullptr}; + + // DO NOT FREE - internal pointers to certificate + const auto* subject = X509_get_subject_name(cert); + if (subject != nullptr) { + len = i2d_X509_NAME(subject, &data); + } + + // move data to DER + return {der_ptr{data, &DER::free}, static_cast(len)}; +} + +pkey_ptr certificate_public_key(X509* 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); + result = pkey_ptr(pkey, &EVP_PKEY_free); } return result; } +bool certificate_sha_1(openssl::sha_1_digest_t& digest, const X509* 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_1(data, len, digest); + } + OPENSSL_free(data); + } + + return bResult; +} + +bool certificate_subject_public_key_sha_1(openssl::sha_1_digest_t& digest, const X509* cert) { + assert(cert != nullptr); + + bool bResult{false}; + const auto* pubkey = X509_get_X509_PUBKEY(cert); + if (pubkey != nullptr) { + unsigned char* data{nullptr}; + const auto len = i2d_X509_PUBKEY(pubkey, &data); + if (len > 0) { + bResult = openssl::sha_1(data, len, digest); + } + OPENSSL_free(data); + } + + return bResult; +} + } // namespace openssl diff --git a/lib/staging/tls/openssl_util.hpp b/lib/staging/tls/openssl_util.hpp index 661a36bef..5df6c5f2e 100644 --- a/lib/staging/tls/openssl_util.hpp +++ b/lib/staging/tls/openssl_util.hpp @@ -1,9 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest #ifndef OPENSSL_UTIL_HPP_ #define OPENSSL_UTIL_HPP_ +#include "extensions/tls_types.hpp" + #include #include #include @@ -11,16 +13,17 @@ #include #include #include -#include #include struct evp_pkey_st; +struct ssl_st; struct x509_st; namespace openssl { +/// X509 certificate verification result enum class verify_result_t : std::uint8_t { - verified, + Verified, CertChainError, CertificateExpired, CertificateRevoked, @@ -31,26 +34,139 @@ enum class verify_result_t : std::uint8_t { 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_1_digest_size = 20; 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 { + sha1, sha256, sha384, sha512, }; +using sha_1_digest_t = std::array; 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; +using certificate_ptr = std::unique_ptr; +using certificate_list = std::vector; +using der_underlying_t = std::uint8_t; +using der_ptr = std::unique_ptr; +using pkey_ptr = std::unique_ptr; + +/** + * \brief represents DER encoded ASN1 data + * + * This class simplifies managing DER data ensuring that it is managed and + * supporting equality tests. + */ +class DER { +private: + der_ptr ptr{nullptr, nullptr}; //!< pointer to the data + std::size_t len{0}; //!< length of the data + +public: + DER() = default; + /** + * \brief create space for DER data of specified size + * \param[in] size to allocate (note data is zeroed) + */ + explicit DER(std::size_t size); + /** + * \brief create a copy of the supplied DER data + * \param[in] crc pointer to the DER data + * \param[in] size of the DER data + */ + DER(const der_underlying_t* src, std::size_t size); + /** + * \brief move a unique pointer and size to this object + * \param[in] der a unique pointer to DER data + * \param[in] size the size of the DER data + */ + DER(der_ptr&& der, std::size_t size) : ptr(std::move(der)), len(size) { + } + + DER(const DER& obj); + DER& operator=(const DER& obj); + + DER(DER&& obj) noexcept; + DER& operator=(DER&& obj) noexcept; + + bool operator==(const DER& rhs) const; + inline bool operator!=(const DER& rhs) const { + return !(*this == rhs); + } + bool operator==(const der_underlying_t* rhs) const; + inline bool operator!=(const der_underlying_t* rhs) const { + return !(*this == rhs); + } + explicit operator bool() const; + + [[nodiscard]] inline der_underlying_t* get() { + return ptr.get(); + } + + /** + * \brief release the pointer, must be freed using DER::free() + * \return the pointer to DER data (or nullptr) + */ + [[nodiscard]] inline der_underlying_t* release() { + len = 0; + return ptr.release(); + } + + [[nodiscard]] inline const der_underlying_t* get() const { + return ptr.get(); + } + + [[nodiscard]] inline std::size_t size() const { + return len; + } + + /** + * \brief create unmanaged copy which must be freed using DER::free() + * \param[in] obj copy the memory contents from this object + * \return a pointer to newly allocated heap memory + */ + static der_underlying_t* dup(const DER& obj); + + /** + * \brief free memory allocated by DER::dup() + * \param[in] ptr pointer to the allocated memory (can be nullptr) + */ + static void free(der_underlying_t* ptr); +}; + +/// contains filenames for the leaf, intermediate CAs and roots +struct chain_filenames_t { + const char* leaf; + const char* chain; + const char* root; +}; + +using chain_filenames_list_t = std::vector; + +/// contains the X509 certificates for leaf, intermediate CAs and roots +struct chain_info_t { + certificate_ptr leaf; + certificate_list chain; + certificate_list trust_anchors; +}; + +using chain_info_list_t = std::vector; + +/// X509 certificate chain and the private key for the leaf certificate +struct chain_t { + chain_info_t chain{{nullptr, nullptr}, {}, {}}; + pkey_ptr private_key{nullptr, nullptr}; +}; + +using chain_list = std::vector; /** * \brief sign using ECDSA on curve secp256r1/prime256v1/P-256 of a SHA 256 digest @@ -105,6 +221,15 @@ bool verify(evp_pkey_st* pkey, const std::uint8_t* r, const std::uint8_t* s, con 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 SHA1 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 SHA1 digest + * \return true on success + */ +bool sha_1(const void* data, std::size_t len, sha_1_digest_t& digest); + /** * \brief calculate the SHA256 digest over an array of bytes * \param[in] data the start of the data @@ -178,21 +303,27 @@ template constexpr void zero(T& mem) { std::memset(mem.data(), 0, mem.size()); } +/** + * \brief load a private key from file + * \param mem the structure to zero + */ +pkey_ptr load_private_key(const char* filename, const char* password); + /** * \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 + * \return The DER signature and its length */ -std::tuple bn_to_signature(const bn_t& r, const bn_t& s); +DER 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 + * \return The DER signature and its length */ -std::tuple bn_to_signature(const std::uint8_t* r, const std::uint8_t* s); +DER bn_to_signature(const std::uint8_t* r, const std::uint8_t* s); /** * \brief convert DER signature into BIGNUM R and S components @@ -209,7 +340,73 @@ bool signature_to_bn(openssl::bn_t& r, openssl::bn_t& s, const std::uint8_t* sig * \param[in] filename * \return a list of 0 or more certificates */ -CertificateList load_certificates(const char* filename); +certificate_list load_certificates(const char* filename); + +/** + * \brief load any PEM encoded certificates from list of files + * \param[in] filenames + * \return a list of 0 or more certificates + */ +certificate_list load_certificates(const std::vector& filenames); + +/** + * \brief load a PKI chain from leaf to root + * \param[in] leaf_file is the server certificate + * \param[in] chain_file is the file of intermediate certificates + * \param[in] root_file is the self signed trust anchor + * \return the certificate chain. chain_info_t::leaf will be nullptr when a chain + * cannot be built + * \note when leaf_file is null pointer the server certificate is the first certificate in the chain_file + */ +chain_info_t load_certificates(const char* leaf_file, const char* chain_file, const char* root_file); + +/** + * \brief load a PKI chain from leaf to root + * \param[in] chain is the structure containing the three filenames + * \return the certificate chain. chain_info_t::leaf will be nullptr when a chain + * cannot be built + * \note when leaf_file is null pointer the server certificate is the first certificate in the chain_file + */ +static inline chain_info_t load_certificates(const chain_filenames_t& chain) { + return load_certificates(chain.leaf, chain.chain, chain.root); +} + +/** + * \brief load a PKI chains from leaf to root from a list of chain filenames + * \param[in] chains is a list of structures containing the three filenames + * \return a list of valid certificate chains (can be an empty list) + */ +chain_info_list_t load_certificates(const chain_filenames_list_t& chains); + +/** + * \brief check that a private key is associated with a certificate + * \param[in] cert is the certificate + * \param[in] pkey is the private key + * \return true when the key matches the certificate + */ +bool verify_certificate_key(const x509_st* cert, const evp_pkey_st* pkey); + +/** + * \brief verify a certificate chain from leaf to trust anchor(s) + * \param[in] chain the structure containing the certificates + * \return true when there is a valid chain + */ +bool verify_chain(const chain_info_t& chain); + +/** + * \brief verify a certificate chain from leaf to trust anchor(s) and that + * the private key is associated with the leaf certificate + * \param[in] chain the structure containing the certificates and private key + * \return true when there is a valid chain and the key matches + */ +bool verify_chain(const chain_t& chain); + +/** + * \brief apply the certificates and private key to the SSL session + * \param[in] chain the structure containing the certificates and private key + * \return true when successfully applied + */ +bool use_certificate_and_key(ssl_st* ssl, const chain_t& chain); /** * \brief convert a certificate to a PEM string @@ -224,7 +421,7 @@ std::string certificate_to_pem(const x509_st* cert); * \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); +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 @@ -235,8 +432,8 @@ Certificate_ptr der_to_certificate(const std::uint8_t* der, std::size_t len); * \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); +verify_result_t verify_certificate(const x509_st* cert, const certificate_list& trust_anchors, + const certificate_list& untrusted); /** * \brief extract the certificate subject as a dictionary of name/value pairs @@ -247,12 +444,35 @@ verify_result_t verify_certificate(const x509_st* cert, const CertificateList& t */ std::map certificate_subject(const x509_st* cert); +/** + * \brief extract the certificate subject as DER encoded data + * \param cert the certificate + * \return the DER encoded subject or nullptr on error + */ +DER certificate_subject_der(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); +pkey_ptr certificate_public_key(x509_st* cert); + +/** + * \brief calculate SHA1 hash over the DER certificate + * \param[out] digest the SHA1 digest of the certificate + * \param[in] cert the certificate + * \return true on success + */ +bool certificate_sha_1(openssl::sha_1_digest_t& digest, const x509_st* cert); + +/** + * \brief calculate SHA1 hash over the DER certificate's subject public key + * \param[out] digest the SHA1 digest of the public key + * \param[in] cert the certificate + * \return true on success + */ +bool certificate_subject_public_key_sha_1(openssl::sha_1_digest_t& digest, const x509_st* cert); enum class log_level_t : std::uint8_t { debug, diff --git a/lib/staging/tls/tests/CMakeLists.txt b/lib/staging/tls/tests/CMakeLists.txt index 7ab5443b8..36c2ca2a0 100644 --- a/lib/staging/tls/tests/CMakeLists.txt +++ b/lib/staging/tls/tests/CMakeLists.txt @@ -1,7 +1,27 @@ find_package(OpenSSL 3) +set(TLS_TEST_FILES + alt_openssl-pki.conf + iso_pkey.asn1 + openssl-pki.conf + ocsp_response.der + pki.sh +) + +add_custom_command( + OUTPUT ${TLS_TEST_FILES} + COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/pki + COMMAND cd pki && cp ${TLS_TEST_FILES} ${CMAKE_CURRENT_BINARY_DIR}/ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_custom_target(tls_test_files_target + DEPENDS ${TLS_TEST_FILES} +) + set(TLS_GTEST_NAME tls_test) add_executable(${TLS_GTEST_NAME}) +add_dependencies(${TLS_GTEST_NAME} tls_test_files_target) target_include_directories(${TLS_GTEST_NAME} PRIVATE . .. ../../util @@ -17,6 +37,10 @@ target_sources(${TLS_GTEST_NAME} PRIVATE crypto_test.cpp openssl_util_test.cpp tls_test.cpp + tls_connection_test.cpp + ../extensions/helpers.cpp + ../extensions/status_request.cpp + ../extensions/trusted_ca_keys.cpp ../openssl_conv.cpp ../openssl_util.cpp ../tls.cpp @@ -31,6 +55,7 @@ target_link_libraries(${TLS_GTEST_NAME} PRIVATE set(TLS_MAIN_NAME tls_server) add_executable(${TLS_MAIN_NAME}) +add_dependencies(${TLS_MAIN_NAME} tls_test_files_target) target_include_directories(${TLS_MAIN_NAME} PRIVATE . .. ../../util @@ -42,6 +67,9 @@ target_compile_definitions(${TLS_MAIN_NAME} PRIVATE target_sources(${TLS_MAIN_NAME} PRIVATE tls_main.cpp + ../extensions/helpers.cpp + ../extensions/status_request.cpp + ../extensions/trusted_ca_keys.cpp ../openssl_util.cpp ../tls.cpp ) @@ -53,6 +81,7 @@ target_link_libraries(${TLS_MAIN_NAME} PRIVATE set(TLS_CLIENT_NAME tls_client) add_executable(${TLS_CLIENT_NAME}) +add_dependencies(${TLS_CLIENT_NAME} tls_test_files_target) target_include_directories(${TLS_CLIENT_NAME} PRIVATE . .. ../../util @@ -64,6 +93,9 @@ target_compile_definitions(${TLS_CLIENT_NAME} PRIVATE target_sources(${TLS_CLIENT_NAME} PRIVATE tls_client_main.cpp + ../extensions/helpers.cpp + ../extensions/status_request.cpp + ../extensions/trusted_ca_keys.cpp ../openssl_util.cpp ../tls.cpp ) @@ -75,6 +107,7 @@ target_link_libraries(${TLS_CLIENT_NAME} PRIVATE set(TLS_PATCH_NAME patched_test) add_executable(${TLS_PATCH_NAME}) +add_dependencies(${TLS_PATCH_NAME} tls_test_files_target) target_include_directories(${TLS_PATCH_NAME} PRIVATE . .. ../../util @@ -86,6 +119,9 @@ target_compile_definitions(${TLS_PATCH_NAME} PRIVATE target_sources(${TLS_PATCH_NAME} PRIVATE patched_test.cpp + ../extensions/helpers.cpp + ../extensions/status_request.cpp + ../extensions/trusted_ca_keys.cpp ../openssl_util.cpp ../tls.cpp ) @@ -96,20 +132,4 @@ target_link_libraries(${TLS_PATCH_NAME} PRIVATE 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}) +add_test(${TLS_GTEST_NAME} ${TLS_GTEST_NAME}) diff --git a/lib/staging/tls/tests/README.md b/lib/staging/tls/tests/README.md index e9b25ad0c..7e8e72732 100644 --- a/lib/staging/tls/tests/README.md +++ b/lib/staging/tls/tests/README.md @@ -1,4 +1,3 @@ - # Tests Building tests: diff --git a/lib/staging/tls/tests/crypto_test.cpp b/lib/staging/tls/tests/crypto_test.cpp index b3b024576..559c48113 100644 --- a/lib/staging/tls/tests/crypto_test.cpp +++ b/lib/staging/tls/tests/crypto_test.cpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest #include "gtest/gtest.h" @@ -15,7 +15,6 @@ namespace { using evse_security::HashAlgorithm; -using evse_security::X509CertificateHierarchy; using evse_security::X509Wrapper; using openssl::load_certificates; using openssl::conversions::to_X509Wrapper; diff --git a/lib/staging/tls/tests/gtest_main.cpp b/lib/staging/tls/tests/gtest_main.cpp index 477e18bba..b73259c2a 100644 --- a/lib/staging/tls/tests/gtest_main.cpp +++ b/lib/staging/tls/tests/gtest_main.cpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest #include #include @@ -8,8 +8,32 @@ #include +#include + +namespace { + +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; + } +} + +} // namespace + int main(int argc, char** argv) { // create test certificates and keys + openssl::set_log_handler(log_handler); if (std::system("./pki.sh") != 0) { std::cerr << "Problem creating test certificates and keys" << std::endl; char buf[PATH_MAX]; diff --git a/lib/staging/tls/tests/openssl_util_test.cpp b/lib/staging/tls/tests/openssl_util_test.cpp index 970f863d5..8ced07c31 100644 --- a/lib/staging/tls/tests/openssl_util_test.cpp +++ b/lib/staging/tls/tests/openssl_util_test.cpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest #include "gtest/gtest.h" #include @@ -7,7 +7,20 @@ #include #include #include +#include #include +#include + +bool operator==(const ::openssl::certificate_ptr& lhs, const ::openssl::certificate_ptr& rhs) { + using ::openssl::certificate_to_pem; + if (lhs && rhs) { + const auto res_lhs = certificate_to_pem(lhs.get()); + const auto res_rhs = certificate_to_pem(rhs.get()); + return res_lhs == res_rhs; + } + + return false; +} namespace { @@ -35,18 +48,6 @@ constexpr test_vectors_t sha_256_test[] = { {"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}; @@ -103,6 +104,93 @@ TEST(util, removeHyphen) { EXPECT_EQ(cert_emaid, expected); } +TEST(DER, equal) { + const std::uint8_t data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + openssl::DER a; + openssl::DER b(sizeof(data)); + openssl::DER c1(&data[0], sizeof(data)); + openssl::DER c2(&data[0], sizeof(data)); + openssl::DER d(&data[0], sizeof(data) - 1); + + EXPECT_FALSE(a); + EXPECT_TRUE(b); + EXPECT_TRUE(c1); + EXPECT_TRUE(c2); + EXPECT_TRUE(d); + + EXPECT_EQ(a, nullptr); + EXPECT_NE(b, nullptr); + EXPECT_NE(c1, nullptr); + EXPECT_NE(c2, nullptr); + EXPECT_NE(d, nullptr); + + EXPECT_NE(c1.get(), c2.get()); + EXPECT_EQ(c1.size(), c2.size()); + EXPECT_EQ(c1, c2); + EXPECT_EQ(c1, c1); + + EXPECT_NE(c1, a); + EXPECT_NE(c1, b); + EXPECT_NE(c1, d); + EXPECT_NE(a, c1); + EXPECT_NE(b, c1); + EXPECT_NE(d, c1); +} + +TEST(DER, construct) { + const std::uint8_t data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + const openssl::DER a(&data[0], sizeof(data)); + + auto b = a; + EXPECT_NE(a.get(), b.get()); + EXPECT_EQ(a, b); + + auto c{a}; + EXPECT_NE(a.get(), c.get()); + EXPECT_EQ(a, c); + EXPECT_EQ(b, c); + + auto d = std::move(b); + EXPECT_EQ(a, d); + EXPECT_EQ(b.size(), 0); + EXPECT_EQ(b, nullptr); + EXPECT_FALSE(b); + + auto e(std::move(c)); + EXPECT_EQ(a, e); + EXPECT_EQ(c.size(), 0); + EXPECT_EQ(c, nullptr); + EXPECT_FALSE(c); + + const std::uint8_t alt[] = {9, 8, 7, 6, 5, 4}; + openssl::DER x(&alt[0], sizeof(alt)); + x = e; + EXPECT_EQ(x, a); + EXPECT_NE(x, a.get()); + EXPECT_NE(x, e.get()); +} + +TEST(DER, release) { + const std::uint8_t data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + openssl::DER a(&data[0], sizeof(data)); + EXPECT_EQ(a.size(), sizeof(data)); + EXPECT_NE(a.get(), &data[0]); + const auto* tmp_p = a.get(); + + auto* ptr = a.release(); + EXPECT_EQ(a.size(), 0); + EXPECT_EQ(a.get(), nullptr); + EXPECT_EQ(ptr, tmp_p); + openssl::DER::free(ptr); +} + +TEST(openssl, sizes) { + EXPECT_EQ(sizeof(openssl::sha_1_digest_t), openssl::sha_1_digest_size); + EXPECT_EQ(sizeof(openssl::sha_256_digest_t), openssl::sha_256_digest_size); + EXPECT_EQ(sizeof(openssl::sha_384_digest_t), openssl::sha_384_digest_size); + EXPECT_EQ(sizeof(openssl::sha_512_digest_t), openssl::sha_512_digest_size); +} + 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); @@ -168,28 +256,22 @@ TEST(openssl, sha256Exi) { } 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); + auto pkey = openssl::load_private_key("server_priv.pem", nullptr); + ASSERT_TRUE(pkey); 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); + EXPECT_TRUE(openssl::sign(pkey.get(), sig_der.data(), sig_der_len, digest.data(), digest.size())); + // std::cout << "signature size: " << sig_der_len << std::endl; + EXPECT_TRUE(openssl::verify(pkey.get(), sig_der.data(), sig_der_len, digest.data(), digest.size())); } 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); + auto pkey = openssl::load_private_key("server_priv.pem", nullptr); + ASSERT_TRUE(pkey); openssl::bn_t r; openssl::bn_t s; @@ -197,70 +279,50 @@ TEST(openssl, signVerifyBn) { 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); + EXPECT_TRUE(openssl::sign(pkey.get(), r, s, digest)); + // std::cout << "signature size: " << sig_der_len << std::endl; + EXPECT_TRUE(openssl::verify(pkey.get(), r, s, digest)); } 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); + auto pkey = openssl::load_private_key("server_priv.pem", nullptr); + ASSERT_TRUE(pkey); 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::sign(pkey.get(), 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); + EXPECT_TRUE(openssl::verify(pkey.get(), r, s, digest)); } 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); + auto pkey = openssl::load_private_key("server_priv.pem", nullptr); + ASSERT_TRUE(pkey); + auto pkey_inv = openssl::load_private_key("client_priv.pem", nullptr); + ASSERT_TRUE(pkey); 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); + EXPECT_TRUE(openssl::sign(pkey.get(), sig_der.data(), sig_der_len, digest.data(), digest.size())); + // std::cout << "signature size: " << sig_der_len << std::endl; + EXPECT_FALSE(openssl::verify(pkey_inv.get(), sig_der.data(), sig_der_len, digest.data(), digest.size())); } 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 pkey = openssl::load_private_key("iso_priv.pem", nullptr); + ASSERT_TRUE(pkey); - 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); + auto sig = openssl::bn_to_signature(&iso_exi_sig[0], &iso_exi_sig[32]); + EXPECT_TRUE(openssl::verify(pkey.get(), sig.get(), sig.size(), &iso_exi_b_hash[0], sizeof(iso_exi_b_hash))); } TEST(certificateLoad, single) { @@ -278,6 +340,153 @@ TEST(certificateLoad, key) { EXPECT_EQ(certs.size(), 0); } +TEST(certificateLoad, missing) { + auto certs = ::openssl::load_certificates("server_priv.pem-not-found"); + EXPECT_EQ(certs.size(), 0); +} + +TEST(certificateLoad, nullptr) { + auto certs = ::openssl::load_certificates(nullptr); + EXPECT_EQ(certs.size(), 0); +} + +TEST(certificateLoad, empty) { + auto certs = ::openssl::load_certificates(""); + EXPECT_EQ(certs.size(), 0); +} + +TEST(certificateLoad, multiFile) { + std::vector files{"server_chain.pem", "alt_server_chain.pem"}; + + auto certs = ::openssl::load_certificates(files); + ASSERT_EQ(certs.size(), 4); + + auto server = ::openssl::load_certificates("server_cert.pem"); + auto ca = ::openssl::load_certificates("server_ca_cert.pem"); + auto alt_server = ::openssl::load_certificates("alt_server_cert.pem"); + auto alt_ca = ::openssl::load_certificates("alt_server_ca_cert.pem"); + + EXPECT_EQ(certs[0], server[0]); + EXPECT_EQ(certs[1], ca[0]); + EXPECT_EQ(certs[2], alt_server[0]); + EXPECT_EQ(certs[3], alt_ca[0]); +} + +TEST(certificateLoad, multiFileEmpty) { + std::vector files{}; + + auto certs = ::openssl::load_certificates(files); + ASSERT_EQ(certs.size(), 0); +} + +TEST(certificateLoad, multiFileMissing) { + std::vector files{"server_chain.pem", "alt_server_chain.pem-not-found"}; + + auto certs = ::openssl::load_certificates(files); + ASSERT_EQ(certs.size(), 2); + + auto server = ::openssl::load_certificates("server_cert.pem"); + auto ca = ::openssl::load_certificates("server_ca_cert.pem"); + + EXPECT_EQ(certs[0], server[0]); + EXPECT_EQ(certs[1], ca[0]); +} + +TEST(certificateLoad, multiFileNullptr) { + std::vector files{nullptr, "alt_server_chain.pem"}; + + auto certs = ::openssl::load_certificates(files); + ASSERT_EQ(certs.size(), 2); + + auto alt_server = ::openssl::load_certificates("alt_server_cert.pem"); + auto alt_ca = ::openssl::load_certificates("alt_server_ca_cert.pem"); + + EXPECT_EQ(certs[0], alt_server[0]); + EXPECT_EQ(certs[1], alt_ca[0]); +} + +TEST(certificateLoadPki, none) { + auto certs = ::openssl::load_certificates(nullptr, nullptr, nullptr); + EXPECT_EQ(certs.leaf, nullptr); + EXPECT_EQ(certs.chain.size(), 0); + EXPECT_EQ(certs.trust_anchors.size(), 0); + + certs = ::openssl::load_certificates("server_cert.pem", nullptr, nullptr); + EXPECT_EQ(certs.leaf, nullptr); + EXPECT_EQ(certs.chain.size(), 0); + EXPECT_EQ(certs.trust_anchors.size(), 0); + + certs = ::openssl::load_certificates(nullptr, "server_chain.pem", nullptr); + EXPECT_EQ(certs.leaf, nullptr); + EXPECT_EQ(certs.chain.size(), 0); + EXPECT_EQ(certs.trust_anchors.size(), 0); + + certs = ::openssl::load_certificates(nullptr, nullptr, "server_root_cert.pem"); + EXPECT_EQ(certs.leaf, nullptr); + EXPECT_EQ(certs.chain.size(), 0); + EXPECT_EQ(certs.trust_anchors.size(), 0); +} + +TEST(certificateLoadPki, full) { + auto certs = ::openssl::load_certificates("server_cert.pem", "server_ca_cert.pem", "server_root_cert.pem"); + + auto server = ::openssl::load_certificates("server_cert.pem"); + ASSERT_EQ(server.size(), 1); + auto chain = ::openssl::load_certificates("server_ca_cert.pem"); + ASSERT_EQ(chain.size(), 1); + auto trust_anchors = ::openssl::load_certificates("server_root_cert.pem"); + ASSERT_EQ(trust_anchors.size(), 1); + + ASSERT_NE(certs.leaf, nullptr); + EXPECT_EQ(certs.leaf, server[0]); + ASSERT_EQ(certs.chain.size(), 1); + ASSERT_EQ(certs.trust_anchors.size(), 1); + + EXPECT_EQ(certs.chain, chain); + EXPECT_EQ(certs.trust_anchors, trust_anchors); +} + +TEST(certificateLoadPki, noLeaf) { + // should work since leaf is 1st certificate in server_chain.pem + auto certs = ::openssl::load_certificates(nullptr, "server_chain.pem", "server_root_cert.pem"); + + auto server = ::openssl::load_certificates("server_cert.pem"); + ASSERT_EQ(server.size(), 1); + auto chain = ::openssl::load_certificates("server_ca_cert.pem"); + ASSERT_EQ(chain.size(), 1); + auto trust_anchors = ::openssl::load_certificates("server_root_cert.pem"); + ASSERT_EQ(trust_anchors.size(), 1); + + EXPECT_EQ(certs.leaf, server[0]); + ASSERT_EQ(certs.chain.size(), 1); + ASSERT_EQ(certs.trust_anchors.size(), 1); + + EXPECT_EQ(certs.chain, chain); + EXPECT_EQ(certs.trust_anchors, trust_anchors); +} + +TEST(certificateLoadPki, invalid) { + auto certs = ::openssl::load_certificates("client_cert.pem", "server_ca_cert.pem", "server_root_cert.pem"); + EXPECT_EQ(certs.leaf, nullptr); + EXPECT_EQ(certs.chain.size(), 0); + EXPECT_EQ(certs.trust_anchors.size(), 0); + + certs = ::openssl::load_certificates("server_cert.pem", "client_ca_cert.pem", "server_root_cert.pem"); + EXPECT_EQ(certs.leaf, nullptr); + EXPECT_EQ(certs.chain.size(), 0); + EXPECT_EQ(certs.trust_anchors.size(), 0); + + certs = ::openssl::load_certificates("server_cert.pem", "server_ca_cert.pem", "client_root_cert.pem"); + EXPECT_EQ(certs.leaf, nullptr); + EXPECT_EQ(certs.chain.size(), 0); + EXPECT_EQ(certs.trust_anchors.size(), 0); + + certs = ::openssl::load_certificates(nullptr, "server_chain.pem", "client_root_cert.pem"); + EXPECT_EQ(certs.leaf, nullptr); + EXPECT_EQ(certs.chain.size(), 0); + EXPECT_EQ(certs.trust_anchors.size(), 0); +} + TEST(certificate, toPem) { auto certs = ::openssl::load_certificates("client_ca_cert.pem"); ASSERT_EQ(certs.size(), 1); @@ -295,7 +504,19 @@ TEST(certificate, verify) { 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); + EXPECT_EQ(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::Verified); +} + +TEST(certificate, verifyCross) { + auto client = ::openssl::load_certificates("server_cert.pem"); + auto chain = ::openssl::load_certificates("cross_ca_cert.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) { @@ -308,12 +529,12 @@ TEST(certificate, verifyRemoveClientFromChain) { EXPECT_EQ(root.size(), 1); // client certificate is 1st in the list - openssl::CertificateList new_chain; + openssl::certificate_list 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); + EXPECT_EQ(::openssl::verify_certificate(client[0].get(), root, new_chain), openssl::verify_result_t::Verified); } TEST(certificate, verifyNoClient) { @@ -324,7 +545,7 @@ TEST(certificate, verifyNoClient) { EXPECT_GT(chain.size(), 0); EXPECT_EQ(root.size(), 1); - EXPECT_EQ(::openssl::verify_certificate(nullptr, root, chain), openssl::verify_result_t::verified); + EXPECT_EQ(::openssl::verify_certificate(nullptr, root, chain), openssl::verify_result_t::Verified); } TEST(certificate, verifyFailWrongClient) { @@ -336,7 +557,7 @@ TEST(certificate, verifyFailWrongClient) { 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); + EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::Verified); } TEST(certificate, verifyFailWrongRoot) { @@ -348,7 +569,7 @@ TEST(certificate, verifyFailWrongRoot) { 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); + EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::Verified); } TEST(certificate, verifyFailWrongChain) { @@ -360,7 +581,7 @@ TEST(certificate, verifyFailWrongChain) { 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); + EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::Verified); } TEST(certificate, subjectName) { @@ -370,12 +591,55 @@ TEST(certificate, subjectName) { 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 } } +TEST(certificateChainInfo, valid) { + auto chain = openssl::load_certificates("client_cert.pem", "client_ca_cert.pem", "client_root_cert.pem"); + EXPECT_TRUE(openssl::verify_chain(chain)); +} + +TEST(certificateChainInfo, invalid) { + auto chain = openssl::load_certificates("server_cert.pem", "client_ca_cert.pem", "client_root_cert.pem"); + EXPECT_FALSE(openssl::verify_chain(chain)); + chain = openssl::load_certificates("client_cert.pem", "server_ca_cert.pem", "client_root_cert.pem"); + EXPECT_FALSE(openssl::verify_chain(chain)); + chain = openssl::load_certificates("client_cert.pem", "client_ca_cert.pem", "server_root_cert.pem"); + EXPECT_FALSE(openssl::verify_chain(chain)); +} + +TEST(certificateChain, valid) { + auto pkey = openssl::load_private_key("client_priv.pem", nullptr); + auto chain_info = openssl::load_certificates("client_cert.pem", "client_ca_cert.pem", "client_root_cert.pem"); + openssl::chain_t chain = {std::move(chain_info), std::move(pkey)}; + EXPECT_TRUE(openssl::verify_chain(chain)); +} + +TEST(certificateChain, invalid) { + auto pkey = openssl::load_private_key("server_priv.pem", nullptr); + auto chain_info = openssl::load_certificates("client_cert.pem", "client_ca_cert.pem", "client_root_cert.pem"); + openssl::chain_t chain = {std::move(chain_info), std::move(pkey)}; + EXPECT_FALSE(openssl::verify_chain(chain)); + chain_info = openssl::load_certificates("server_cert.pem", "client_ca_cert.pem", "client_root_cert.pem"); + chain.chain = std::move(chain_info); + EXPECT_FALSE(openssl::verify_chain(chain)); + chain_info = openssl::load_certificates("client_cert.pem", "server_ca_cert.pem", "client_root_cert.pem"); + chain.chain = std::move(chain_info); + EXPECT_FALSE(openssl::verify_chain(chain)); + chain_info = openssl::load_certificates("client_cert.pem", "client_ca_cert.pem", "server_root_cert.pem"); + chain.chain = std::move(chain_info); + EXPECT_FALSE(openssl::verify_chain(chain)); +} + +TEST(certificate, apply) { + auto pkey = openssl::load_private_key("client_priv.pem", nullptr); + auto chain_info = openssl::load_certificates("client_cert.pem", "client_ca_cert.pem", "client_root_cert.pem"); + openssl::chain_t chain = {std::move(chain_info), std::move(pkey)}; + auto* ctx = SSL_CTX_new(TLS_server_method()); + auto* ssl = SSL_new(ctx); + EXPECT_TRUE(openssl::use_certificate_and_key(ssl, chain)); + SSL_free(ssl); + SSL_CTX_free(ctx); +} + } // namespace diff --git a/lib/staging/tls/tests/patched_test.cpp b/lib/staging/tls/tests/patched_test.cpp index 525396647..20f785fb1 100644 --- a/lib/staging/tls/tests/patched_test.cpp +++ b/lib/staging/tls/tests/patched_test.cpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest /** * \file testing patched version of OpenSSL @@ -8,279 +8,21 @@ * (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` + * e.g. `./patched_test --gtest_filter=TlsTest.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 +#include "tls_connection_test.hpp" namespace { // ---------------------------------------------------------------------------- -// set up code +// The tests - only pass on a patched OpenSSL -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 = 500; - - 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(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(); - } - } - - 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); - } -}; - -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 - -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, 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_F(TlsTest, TLS12) { // test using TLS 1.2 start(); connect(); @@ -317,84 +59,4 @@ TEST_F(OcspTest, TLS12) { 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/alt_openssl-pki.conf b/lib/staging/tls/tests/pki/alt_openssl-pki.conf new file mode 100644 index 000000000..a94d1857a --- /dev/null +++ b/lib/staging/tls/tests/pki/alt_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 = Alternate Root Trust Anchor + +[req_dn_server_ca] +C = GB +O = Pionix +L = London +CN = Alternate Intermediate CA + +[req_dn_server] +C = GB +O = Pionix +L = London +CN = 11111111 + +[req_dn_client] +C = GB +O = Pionix +L = London +CN = 98765432 + +[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 +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 = Alternate Root Trust Anchor + +[req_dn_client_ca] +C = DE +O = Pionix +L = Frankfurt +CN = Alternate Intermediate CA + +[req_dn_client] +C = DE +O = Pionix +L = Frankfurt +CN = 66666666 + +[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 +keyUsage = digitalSignature, keyEncipherment, keyAgreement +extendedKeyUsage = clientAuth diff --git a/lib/staging/tls/tests/pki/openssl-pki.conf b/lib/staging/tls/tests/pki/openssl-pki.conf index 90147e4d8..5e77ccb11 100644 --- a/lib/staging/tls/tests/pki/openssl-pki.conf +++ b/lib/staging/tls/tests/pki/openssl-pki.conf @@ -75,7 +75,7 @@ keyUsage = keyCertSign, cRLSign [v3_server] subjectKeyIdentifier=hash -authorityKeyIdentifier=keyid:always,issuer:always +authorityKeyIdentifier=keyid:always keyUsage = digitalSignature, keyEncipherment, keyAgreement extendedKeyUsage = serverAuth, clientAuth subjectAltName = IP:192.168.245.1, DNS:evse.pionix.de @@ -138,6 +138,6 @@ keyUsage = keyCertSign, cRLSign [v3_client] subjectKeyIdentifier=hash -authorityKeyIdentifier=keyid:always,issuer:always +authorityKeyIdentifier=keyid: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 index feec62e92..5ea7224c0 100755 --- a/lib/staging/tls/tests/pki/pki.sh +++ b/lib/staging/tls/tests/pki/pki.sh @@ -1,5 +1,51 @@ #!/bin/sh +generate() { + local base="$1" + # generate keys + for i in "${base}${server_root_priv}" "${base}${server_ca_priv}" "${base}${server_priv}" \ + "${base}${client_root_priv}" "${base}${client_ca_priv}" "${base}${client_priv}" + do + openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out "$i" + chmod 644 "$i" + done + + export OPENSSL_CONF="${base}${cfg}" + + echo "Generate ${base}server_root" + openssl req \ + -config "${base}${cfg}" -x509 -section req_server_root -extensions v3_server_root \ + -key "${base}${server_root_priv}" -out "${base}${server_root_cert}" + echo "Generate ${base}server_ca" + openssl req \ + -config "${base}${cfg}" -x509 -section req_server_ca -extensions v3_server_ca \ + -key "${base}${server_ca_priv}" -CA "${base}${server_root_cert}" \ + -CAkey "${base}${server_root_priv}" -out "${base}${server_ca_cert}" + echo "Generate ${base}server" + openssl req \ + -config "${base}${cfg}" -x509 -section req_server -extensions v3_server \ + -key "${base}${server_priv}" -CA "${base}${server_ca_cert}" \ + -CAkey "${base}${server_ca_priv}" -out "${base}${server_cert}" + cat "${base}${server_cert}" "${base}${server_ca_cert}" > "${base}${server_chain}" + + echo "Generate ${base}client_root" + openssl req \ + -config "${base}${cfg}" -x509 -section req_client_root -extensions v3_client_root \ + -key "${base}${client_root_priv}" -out "${base}${client_root_cert}" + echo "Generate ${base}client_ca" + openssl req \ + -config "${base}${cfg}" -x509 -section req_client_ca -extensions v3_client_ca \ + -key "${base}${client_ca_priv}" -CA "${base}${client_root_cert}" \ + -CAkey "${base}${client_root_priv}" -out "${base}${client_ca_cert}" + echo "Generate ${base}client" + openssl req \ + -config "${base}${cfg}" -x509 -section req_client -extensions v3_client \ + -key "${base}${client_priv}" -CA "${base}${client_ca_cert}" \ + -CAkey "${base}${client_ca_priv}" -out "${base}${client_cert}" + + cat "${base}${client_cert}" "${base}${client_ca_cert}" > "${base}${client_chain}" +} + cfg=openssl-pki.conf server_root_priv=server_root_priv.pem @@ -20,44 +66,15 @@ 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} +generate +generate alt_ -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" +# cross signed intermediate certificate +echo "Generate cross_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} + -config "${cfg}" -x509 -section req_server_ca -extensions v3_server_ca \ + -key "${base}${server_ca_priv}" -CA "${base}${client_root_cert}" \ + -CAkey "${base}${client_root_priv}" -out cross_ca_cert.pem # 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 index 0baa90713..729093e13 100644 --- a/lib/staging/tls/tests/tls_client_main.cpp +++ b/lib/staging/tls/tests/tls_client_main.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest +#include #include #include @@ -11,10 +12,11 @@ using namespace std::chrono_literals; namespace { -const char* short_opts = "h123"; +const char* short_opts = "h123r:"; bool use_tls1_3{false}; bool use_status_request{false}; bool use_status_request_v2{false}; +const char* trust_anchor{nullptr}; void parse_options(int argc, char** argv) { int c; @@ -31,12 +33,16 @@ void parse_options(int argc, char** argv) { case '3': use_tls1_3 = true; break; + case 'r': + trust_anchor = optarg; + break; case 'h': case '?': - std::cout << "Usage: " << argv[0] << " [-1|-2|-3]" << std::endl; + std::cout << "Usage: " << argv[0] << " [-1|-2|-3] [-r server_root_cert.pem]" << 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; + std::cout << " -r root certificate / trust anchor" << std::endl; exit(1); break; default: @@ -49,6 +55,7 @@ void parse_options(int argc, char** argv) { int main(int argc, char** argv) { parse_options(argc, argv); + // used test client for extra output tls::Client client; tls::Client::config_t config; @@ -68,6 +75,19 @@ int main(int argc, char** argv) { config.io_timeout_ms = 500; config.verify_server = false; + if (trust_anchor != nullptr) { + openssl::sha_1_digest_t digest; + auto certs = openssl::load_certificates(trust_anchor); + for (const auto& ta : certs) { + if (openssl::certificate_sha_1(digest, ta.get())) { + config.trusted_ca_keys_data.cert_sha1_hash.push_back(digest); + } + } + config.verify_locations_file = trust_anchor; + config.trusted_ca_keys = true; + config.trusted_ca_keys_data.pre_agreed = true; + } + if (use_status_request) { config.status_request = true; std::cout << "use_status_request true" << std::endl; @@ -86,10 +106,21 @@ int main(int argc, char** argv) { client.init(config); - // localhost works in some cases but not in the CI pipeline - auto connection = client.connect("ip6-localhost", "8444", true); + // localhost works in some cases but not in the CI pipeline ip6-localhost is an option + auto connection = client.connect("localhost", "8444", false, 1000); if (connection) { - if (connection->connect()) { + if (connection->connect() == tls::Connection::result_t::success) { + const auto* cert = connection->peer_certificate(); + if (cert != nullptr) { + const auto subject = openssl::certificate_subject(cert); + if (!subject.empty()) { + std::cout << "subject:"; + for (const auto& itt : subject) { + std::cout << " " << itt.first << ":" << itt.second; + } + std::cout << std::endl; + } + } std::array buffer{}; std::size_t readbytes = 0; std::cout << "about to read" << std::endl; diff --git a/lib/staging/tls/tests/tls_connection_test.cpp b/lib/staging/tls/tests/tls_connection_test.cpp new file mode 100644 index 000000000..9bcef8993 --- /dev/null +++ b/lib/staging/tls/tests/tls_connection_test.cpp @@ -0,0 +1,829 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 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=TlsTest.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 "tls_connection_test.hpp" + +#include +#include +#include + +using namespace std::chrono_literals; + +namespace { +using result_t = tls::Connection::result_t; +using tls::status_request::ClientStatusRequestV2; + +constexpr auto server_root_CN = "00000000"; +constexpr auto alt_server_root_CN = "11111111"; + +void do_poll(std::array& fds, int server_soc, int client_soc) { + const std::int16_t events = POLLOUT | POLLIN; + fds[0].fd = server_soc; + fds[0].events = events; + fds[0].revents = 0; + fds[1].fd = client_soc; + fds[1].events = events; + fds[1].revents = 0; + auto poll_res = poll(fds.data(), fds.size(), -1); + ASSERT_NE(poll_res, -1); +} + +tls::Server::OptionalConfig ssl_init() { + std::cout << "ssl_init" << std::endl; + auto server_config = std::make_unique(); + server_config->cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + server_config->ciphersuites = ""; + auto& ref = server_config->chains.emplace_back(); + ref.certificate_chain_file = "server_chain.pem"; + ref.private_key_file = "server_priv.pem"; + ref.trust_anchor_file = "server_root_cert.pem"; + ref.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 = 500; + return {{std::move(server_config)}}; +} + +// ---------------------------------------------------------------------------- +// The tests + +TEST_F(TlsTest, StartStop) { + // test shouldn't hang + start(); + + // check TearDown on stopped server is okay + server.stop(); + server.wait_stopped(); + if (server_thread.joinable()) { + server_thread.join(); + } +} + +TEST_F(TlsTest, StartConnectDisconnect) { + // 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(TlsTest, NonBlocking) { + client_config.io_timeout_ms = 0; + server_config.io_timeout_ms = 0; + std::mutex mux; + mux.lock(); + + tls::Server::ConnectionPtr server_connection; + tls::Client::ConnectionPtr client_connection; + + auto server_handler_fn = [&server_connection, &mux](tls::Server::ConnectionPtr&& connection) { + server_connection = std::move(connection); + mux.unlock(); + }; + start(server_handler_fn); + + auto client_handler_fn = [&client_connection](tls::Client::ConnectionPtr& connection) { + if (connection) { + client_connection = std::move(connection); + } + }; + connect(client_handler_fn); + + mux.lock(); + // check there is a TCP connection + ASSERT_TRUE(server_connection); + ASSERT_TRUE(client_connection); + + int server_soc = server_connection->socket(); + int client_soc = client_connection->socket(); + std::array fds; + + EXPECT_EQ(server_connection->accept(0), result_t::want_read); + EXPECT_EQ(client_connection->connect(0), result_t::want_read); + + bool s_complete{false}; + bool c_complete{false}; + std::uint32_t s_count{0}; + std::uint32_t c_count{0}; + + while (!s_complete && !c_complete) { + do_poll(fds, server_soc, client_soc); + if (((fds[0].revents & POLLIN) != 0) || ((fds[0].revents & POLLOUT) != 0)) { + s_complete = server_connection->accept(0) == result_t::success; + s_count++; + } + if (((fds[1].revents & POLLIN) != 0) || ((fds[1].revents & POLLOUT) != 0)) { + c_complete = client_connection->connect(0) == result_t::success; + c_count++; + } + + ASSERT_EQ(fds[0].revents & POLLHUP, 0); + ASSERT_EQ(fds[1].revents & POLLHUP, 0); + ASSERT_EQ(fds[0].revents & POLLERR, 0); + ASSERT_EQ(fds[1].revents & POLLERR, 0); + } + + // std::cout << "counts: " << s_count << " " << c_count << std::endl; + EXPECT_GT(s_count, 0); + EXPECT_GT(c_count, 0); + + const std::byte data{0xf3}; + + std::byte s_buf{0}; + std::size_t s_readbytes{0}; + std::size_t s_writebytes{0}; + std::byte c_buf{0}; + std::size_t c_readbytes{0}; + std::size_t c_writebytes{0}; + + EXPECT_EQ(server_connection->read(&s_buf, sizeof(s_buf), s_readbytes, 0), result_t::want_read); + EXPECT_EQ(client_connection->read(&c_buf, sizeof(c_buf), c_readbytes, 0), result_t::want_read); + + EXPECT_EQ(server_connection->write(&data, sizeof(data), s_writebytes, 0), result_t::success); + EXPECT_EQ(client_connection->write(&data, sizeof(data), c_writebytes, 0), result_t::success); + + s_complete = false; + c_complete = false; + s_count = 0; + c_count = 0; + + while (!s_complete && !c_complete) { + do_poll(fds, server_soc, client_soc); + if ((fds[0].revents & POLLIN) != 0) { + s_complete = server_connection->read(&s_buf, sizeof(s_buf), s_readbytes, 0) == result_t::success; + s_count++; + } + if ((fds[1].revents & POLLIN) != 0) { + c_complete = client_connection->read(&c_buf, sizeof(c_buf), c_readbytes, 0) == result_t::success; + c_count++; + } + + ASSERT_EQ(fds[0].revents & POLLHUP, 0); + ASSERT_EQ(fds[1].revents & POLLHUP, 0); + ASSERT_EQ(fds[0].revents & POLLERR, 0); + ASSERT_EQ(fds[1].revents & POLLERR, 0); + } + + EXPECT_EQ(s_readbytes, 1); + EXPECT_EQ(s_buf, data); + EXPECT_EQ(c_readbytes, 1); + EXPECT_EQ(c_buf, data); + + // std::cout << "counts: " << s_count << " " << c_count << std::endl; + EXPECT_GT(s_count, 0); + EXPECT_GT(c_count, 0); + + s_complete = false; + c_complete = false; + s_count = 0; + c_count = 0; + + EXPECT_EQ(server_connection->read(&s_buf, sizeof(s_buf), s_readbytes, 0), result_t::want_read); + EXPECT_EQ(client_connection->shutdown(0), result_t::closed); // closed + while (!s_complete && !c_complete) { + do_poll(fds, server_soc, client_soc); + if (((fds[0].revents & POLLIN) != 0) || ((fds[0].revents & POLLOUT) != 0)) { + s_complete = server_connection->read(&s_buf, sizeof(s_buf), s_readbytes, 0) == result_t::closed; + s_count++; + } + if (((fds[1].revents & POLLIN) != 0) || ((fds[1].revents & POLLOUT) != 0)) { + c_complete = client_connection->shutdown(0) == result_t::success; + c_count++; + } + + ASSERT_EQ(fds[0].revents & POLLERR, 0); + ASSERT_EQ(fds[1].revents & POLLERR, 0); + } + + // std::cout << "counts: " << s_count << " " << c_count << std::endl; + EXPECT_GT(s_count, 0); + EXPECT_GT(c_count, 0); +} + +TEST_F(TlsTest, NonBlockingClientClose) { + std::mutex mux; + mux.lock(); + + tls::Server::ConnectionPtr server_connection; + tls::Client::ConnectionPtr client_connection; + + auto server_handler_fn = [&server_connection, &mux](tls::Server::ConnectionPtr&& connection) { + if (connection->accept() == result_t::success) { + server_connection = std::move(connection); + mux.unlock(); + } + }; + start(server_handler_fn); + + auto client_handler_fn = [&client_connection](tls::Client::ConnectionPtr& connection) { + if (connection) { + if (connection->connect() == result_t::success) { + client_connection = std::move(connection); + } + } + }; + connect(client_handler_fn); + + mux.lock(); + // check there is a TCP connection + ASSERT_TRUE(server_connection); + ASSERT_TRUE(client_connection); + + int server_soc = server_connection->socket(); + int client_soc = client_connection->socket(); + std::array fds; + + bool s_complete{false}; + bool c_complete{false}; + std::uint32_t s_count{0}; + std::uint32_t c_count{0}; + + std::byte buf{0}; + std::size_t readbytes{0}; + + EXPECT_EQ(server_connection->read(&buf, sizeof(buf), readbytes, 0), result_t::want_read); + EXPECT_EQ(client_connection->shutdown(0), result_t::closed); // closed + while (!s_complete && !c_complete) { + do_poll(fds, server_soc, client_soc); + if (((fds[0].revents & POLLIN) != 0) || ((fds[0].revents & POLLOUT) != 0)) { + s_complete = server_connection->read(&buf, sizeof(buf), readbytes, 0) == result_t::closed; + s_count++; + } + if (((fds[1].revents & POLLIN) != 0) || ((fds[1].revents & POLLOUT) != 0)) { + c_complete = client_connection->shutdown(0) == result_t::closed; + c_count++; + } + + ASSERT_EQ(fds[0].revents & POLLERR, 0); + ASSERT_EQ(fds[1].revents & POLLERR, 0); + } + + // std::cout << "counts: " << s_count << " " << c_count << std::endl; + EXPECT_GT(s_count, 0); + EXPECT_GT(c_count, 0); +} + +TEST_F(TlsTest, NonBlockingServerClose) { + std::mutex mux; + mux.lock(); + + tls::Server::ConnectionPtr server_connection; + tls::Client::ConnectionPtr client_connection; + + auto server_handler_fn = [&server_connection, &mux](tls::Server::ConnectionPtr&& connection) { + if (connection->accept() == result_t::success) { + server_connection = std::move(connection); + mux.unlock(); + } + }; + start(server_handler_fn); + + auto client_handler_fn = [&client_connection](tls::Client::ConnectionPtr& connection) { + if (connection) { + if (connection->connect() == result_t::success) { + client_connection = std::move(connection); + } + } + }; + connect(client_handler_fn); + + mux.lock(); + // check there is a TCP connection + ASSERT_TRUE(server_connection); + ASSERT_TRUE(client_connection); + + int server_soc = server_connection->socket(); + int client_soc = client_connection->socket(); + std::array fds; + + bool s_complete{false}; + bool c_complete{false}; + std::uint32_t s_count{0}; + std::uint32_t c_count{0}; + + std::byte buf{0}; + std::size_t readbytes{0}; + + EXPECT_EQ(server_connection->shutdown(0), result_t::closed); // closed + EXPECT_EQ(client_connection->read(&buf, sizeof(buf), readbytes, 0), result_t::want_read); + while (!s_complete && !c_complete) { + do_poll(fds, server_soc, client_soc); + if (((fds[0].revents & POLLIN) != 0) || ((fds[0].revents & POLLOUT) != 0)) { + s_complete = server_connection->shutdown(0) == result_t::closed; + s_count++; + } + if (((fds[1].revents & POLLIN) != 0) || ((fds[1].revents & POLLOUT) != 0)) { + c_complete = client_connection->read(&buf, sizeof(buf), readbytes, 0) == result_t::closed; + c_count++; + } + + ASSERT_EQ(fds[0].revents & POLLERR, 0); + ASSERT_EQ(fds[1].revents & POLLERR, 0); + } + + // std::cout << "counts: " << s_count << " " << c_count << std::endl; + EXPECT_GT(s_count, 0); + EXPECT_GT(c_count, 0); +} + +TEST_F(TlsTest, ClientReadTimeout) { + // test shouldn't hang + client_config.io_timeout_ms = 50; + + auto client_handler_fn = [this](tls::Client::ConnectionPtr& connection) { + if (connection) { + if (connection->connect() == result_t::success) { + this->set(ClientTest::flags_t::connected); + std::byte buffer{}; + std::size_t readbytes{0}; + auto res = connection->read(&buffer, sizeof(buffer), readbytes); + EXPECT_EQ(readbytes, 0); + EXPECT_EQ(res, result_t::timeout); + if (res != result_t::closed) { + connection->shutdown(); + } + connection->shutdown(); + } + } + }; + + start(); + connect(client_handler_fn); + 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(TlsTest, ClientWriteTimeout) { + // test shouldn't hang + client_config.io_timeout_ms = 50; + + bool did_timeout{false}; + std::size_t count{0}; + std::mutex mux; + mux.lock(); + + constexpr std::size_t max_bytes = 1024 * 1024 * 1024; + + auto server_handler_fn = [&mux](tls::Server::ConnectionPtr&& con) { + if (con->accept() == result_t::success) { + mux.lock(); + con->shutdown(); + } + }; + + auto client_handler_fn = [this, &mux, &did_timeout, &count](tls::Client::ConnectionPtr& connection) { + if (connection) { + if (connection->connect() == result_t::success) { + this->set(ClientTest::flags_t::connected); + std::array buffer{}; + std::size_t writebytes{0}; + + bool exit{false}; + + while (!exit) { + switch (connection->write(buffer.data(), buffer.size(), writebytes)) { + case result_t::success: + count += writebytes; + exit = count > max_bytes; + break; + case result_t::timeout: + // std::cout << "timeout: " << count << " bytes" << std::endl; + did_timeout = true; + exit = true; + break; + case result_t::closed: + default: + exit = true; + break; + } + } + mux.unlock(); + std::size_t readbytes = 0; + auto res = connection->read(buffer.data(), buffer.size(), readbytes); + if (res != result_t::closed) { + connection->shutdown(); + } + } + } + }; + + start(server_handler_fn); + connect(client_handler_fn); + EXPECT_TRUE(did_timeout); + EXPECT_LE(count, max_bytes); + 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(TlsTest, ServerReadTimeout) { + // test shouldn't hang + bool did_timeout{false}; + std::mutex mux; + mux.lock(); + + auto server_handler_fn = [&mux, &did_timeout](tls::Server::ConnectionPtr&& con) { + if (con->accept() == result_t::success) { + std::array buffer{}; + std::size_t readbytes = 0; + auto res = con->read(buffer.data(), buffer.size(), readbytes); + did_timeout = res == result_t::timeout; + mux.unlock(); + con->shutdown(); + } + }; + + auto client_handler_fn = [this, &mux](tls::Client::ConnectionPtr& connection) { + if (connection) { + if (connection->connect() == result_t::success) { + this->set(ClientTest::flags_t::connected); + mux.lock(); + connection->shutdown(); + } + } + }; + + start(server_handler_fn); + connect(client_handler_fn); + EXPECT_TRUE(did_timeout); + 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(TlsTest, ServerWriteTimeout) { + // test shouldn't hang + bool did_timeout{false}; + std::size_t count{0}; + std::mutex mux; + mux.lock(); + + constexpr std::size_t max_bytes = 1024 * 1024 * 1024; + + auto server_handler_fn = [&mux, &did_timeout, &count](tls::Server::ConnectionPtr&& con) { + if (con->accept() == result_t::success) { + std::array buffer{}; + std::size_t writebytes{0}; + + bool exit{false}; + + while (!exit) { + switch (con->write(buffer.data(), buffer.size(), writebytes)) { + case result_t::success: + count += writebytes; + exit = count > max_bytes; + break; + case result_t::timeout: + // std::cout << "timeout: " << count << " bytes" << std::endl; + did_timeout = true; + exit = true; + break; + case result_t::closed: + default: + exit = true; + break; + } + } + + mux.unlock(); + std::size_t readbytes = 0; + auto res = con->read(buffer.data(), buffer.size(), readbytes); + if (res != result_t::closed) { + con->shutdown(); + } + } + }; + + auto client_handler_fn = [this, &mux](tls::Client::ConnectionPtr& connection) { + if (connection) { + if (connection->connect() == result_t::success) { + this->set(ClientTest::flags_t::connected); + } + mux.lock(); + connection->shutdown(); + } + }; + + start(server_handler_fn); + connect(client_handler_fn); + EXPECT_TRUE(did_timeout); + EXPECT_LE(count, max_bytes); + 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(TlsTest, delayedConfig) { + // partial config + server_config.chains.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(TlsTest, partialConfig) { + // partial config - no support for trusted_ca_keys + for (auto& i : server_config.chains) { + i.trust_anchor_file = nullptr; + } + + 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(TlsTest, 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(TlsTest, NoOcspFiles) { + // test using TLS 1.2 + for (auto& chain : server_config.chains) { + chain.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)); +} + +TEST_F(TlsTest, CertVerify) { + client_config.verify_locations_file = "alt_server_root_cert.pem"; + start(); + connect(); + EXPECT_FALSE(is_set(flags_t::connected)); +} + +TEST_F(TlsTest, TCKeysNone) { + // trusted_ca_keys - none match - default certificate should be used + std::map subject; + + client_config.trusted_ca_keys = true; + client_config.trusted_ca_keys_data.pre_agreed = true; + add_ta_cert_hash("client_root_cert.pem"); + add_ta_key_hash("client_root_cert.pem"); + add_ta_name("client_root_cert.pem"); + + auto client_handler_fn = [this, &subject](tls::Client::ConnectionPtr& connection) { + if (connection) { + if (connection->connect() == result_t::success) { + this->set(ClientTest::flags_t::connected); + subject = openssl::certificate_subject(connection->peer_certificate()); + connection->shutdown(); + } + } + }; + + start(); + connect(client_handler_fn); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_EQ(subject["CN"], server_root_CN); +} + +TEST_F(TlsTest, TCKeysCert) { + // trusted_ca_keys - cert hash matches + std::map subject; + + client_config.trusted_ca_keys = true; + client_config.verify_locations_file = "alt_server_root_cert.pem"; + add_ta_cert_hash("alt_server_root_cert.pem"); + + auto client_handler_fn = [this, &subject](tls::Client::ConnectionPtr& connection) { + if (connection) { + if (connection->connect() == result_t::success) { + this->set(ClientTest::flags_t::connected); + subject = openssl::certificate_subject(connection->peer_certificate()); + connection->shutdown(); + } + } + }; + + start(); + connect(client_handler_fn); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_EQ(subject["CN"], alt_server_root_CN); + + client_config.trusted_ca_keys_data.x509_name.clear(); + add_ta_cert_hash("client_root_cert.pem"); + add_ta_cert_hash("alt_server_root_cert.pem"); + + connect(client_handler_fn); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_EQ(subject["CN"], alt_server_root_CN); +} + +TEST_F(TlsTest, TCKeysKey) { + // trusted_ca_keys - key hash matches + std::map subject; + + client_config.trusted_ca_keys = true; + client_config.verify_locations_file = "alt_server_root_cert.pem"; + add_ta_key_hash("alt_server_root_cert.pem"); + + auto client_handler_fn = [this, &subject](tls::Client::ConnectionPtr& connection) { + if (connection) { + if (connection->connect() == result_t::success) { + this->set(ClientTest::flags_t::connected); + subject = openssl::certificate_subject(connection->peer_certificate()); + connection->shutdown(); + } + } + }; + + start(); + connect(client_handler_fn); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_EQ(subject["CN"], alt_server_root_CN); + + client_config.trusted_ca_keys_data.x509_name.clear(); + add_ta_key_hash("client_root_cert.pem"); + add_ta_key_hash("alt_server_root_cert.pem"); + + connect(client_handler_fn); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_EQ(subject["CN"], alt_server_root_CN); +} + +TEST_F(TlsTest, TCKeysName) { + // trusted_ca_keys - subject name matches + std::map subject; + + client_config.trusted_ca_keys = true; + client_config.verify_locations_file = "alt_server_root_cert.pem"; + add_ta_name("alt_server_root_cert.pem"); + + auto client_handler_fn = [this, &subject](tls::Client::ConnectionPtr& connection) { + if (connection) { + if (connection->connect() == result_t::success) { + this->set(ClientTest::flags_t::connected); + subject = openssl::certificate_subject(connection->peer_certificate()); + connection->shutdown(); + } + } + }; + + start(); + connect(client_handler_fn); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_EQ(subject["CN"], alt_server_root_CN); + + client_config.trusted_ca_keys_data.x509_name.clear(); + add_ta_name("client_root_cert.pem"); + add_ta_name("alt_server_root_cert.pem"); + + connect(client_handler_fn); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_EQ(subject["CN"], alt_server_root_CN); +} + +// based on an example seen in a WireShark log +// (invalid missing the size of trusted_authorities_list) +// 01 identifier_type key_sha1_hash 4cd7290bf592d2c1ba90f56e08946d4c8e99dc38 SHA1Hash +// 01 identifier_type key_sha1_hash 00fae3900795c888a4d4d7bd9fdffa60418ac19f SHA1Hash +int trusted_ca_keys_add_bad(SSL* ctx, unsigned int ext_type, unsigned int context, const unsigned char** out, + std::size_t* outlen, X509* cert, std::size_t chainidx, int* alert, void* object) { + // std::cout << "trusted_ca_keys_add_bad" << std::endl; + int result{0}; + if ((context == SSL_EXT_CLIENT_HELLO) && (object != nullptr)) { + constexpr std::uint8_t value[] = { + 0x01, 0x4c, 0xd7, 0x29, 0x0b, 0xf5, 0x92, 0xd2, 0xc1, 0xba, 0x90, 0xf5, 0x6e, 0x08, + 0x94, 0x6d, 0x4c, 0x8e, 0x99, 0xdc, 0x38, 0x01, 0x00, 0xfa, 0xe3, 0x90, 0x07, 0x95, + 0xc8, 0x88, 0xa4, 0xd4, 0xd7, 0xbd, 0x9f, 0xdf, 0xfa, 0x60, 0x41, 0x8a, 0xc1, 0x9f, + }; + openssl::DER der(&value[0], sizeof(value)); + const auto len = der.size(); + auto* ptr = openssl::DER::dup(der); + if (ptr != nullptr) { + *out = ptr; + *outlen = len; + result = 1; + } + } + return result; +} + +TEST_F(TlsTest, TCKeysInvalid) { + // trusted_ca_keys - incorrectly formatted extension, connect using defaults + std::map subject; + + client_config.trusted_ca_keys = true; + client_config.verify_locations_file = "server_root_cert.pem"; + + auto override = tls::Client::default_overrides(); + override.trusted_ca_keys_add = &trusted_ca_keys_add_bad; + + start(); + client.init(client_config, override); + 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, 1000); + if (connection) { + if (connection->connect() == result_t::success) { + set(ClientTest::flags_t::connected); + subject = openssl::certificate_subject(connection->peer_certificate()); + connection->shutdown(); + } + } + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_EQ(subject["CN"], server_root_CN); +} + +} // namespace diff --git a/lib/staging/tls/tests/tls_connection_test.hpp b/lib/staging/tls/tests/tls_connection_test.hpp new file mode 100644 index 000000000..83416fecd --- /dev/null +++ b/lib/staging/tls/tests/tls_connection_test.hpp @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Pionix GmbH and Contributors to EVerest + +#ifndef TLS_CONNECTION_TEST_HPP_ +#define TLS_CONNECTION_TEST_HPP_ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +namespace { +using tls::status_request::ClientStatusRequestV2; + +// ---------------------------------------------------------------------------- +// set up code + +struct ClientStatusRequestV2Test : public ClientStatusRequestV2 { + enum class flags_t : std::uint8_t { + status_request_cb, + status_request, + status_request_v2, + connected, + last = connected, + }; + + util::AtomicEnumFlags& flags; + + ClientStatusRequestV2Test() = delete; + explicit ClientStatusRequestV2Test(util::AtomicEnumFlags& flag_ref) : flags(flag_ref) { + } + + int status_request_cb(tls::Ssl* ctx) override { + /* + * 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 + */ + + int result{1}; + const unsigned char* response{nullptr}; + const auto total_length = SSL_get_tlsext_status_ocsp_resp(ctx, &response); + flags.set(flags_t::status_request_cb); + if ((response != nullptr) && (total_length > 0) && (total_length <= std::numeric_limits::max())) { + switch (response[0]) { + case 0x30: // a status_request response + flags.set(flags_t::status_request); + if (!print_ocsp_response(stdout, response, total_length)) { + result = 0; + } + break; + case 0x00: // a status_request_v2 response + { + flags.set(flags_t::status_request_v2); + + // multiple responses + auto remaining = static_cast(total_length); + const unsigned char* ptr{response}; + + while (remaining >= 3) { + const auto len = tls::uint24(ptr); + tls::update_position(ptr, remaining, 3); + // print_ocsp_response updates tmp_p + auto* tmp_p = ptr; + const auto res = print_ocsp_response(stdout, tmp_p, len); + tls::update_position(ptr, remaining, len); + if (!res || (ptr != tmp_p)) { + result = 0; + remaining = -1; + } + } + + if (remaining != 0) { + result = 0; + } + break; + } + default: + break; + } + } + return result; + } +}; + +struct ClientTest : public tls::Client { + using flags_t = ClientStatusRequestV2Test::flags_t; + util::AtomicEnumFlags flags; + + ClientTest() : tls::Client(std::unique_ptr(new ClientStatusRequestV2Test(flags))) { + } + + void reset() { + flags.reset(); + } +}; + +void handler(tls::Server::ConnectionPtr&& con) { + if (con->accept() == tls::Connection::result_t::success) { + 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::closed: + default: + bExit = true; + break; + } + break; + case tls::Connection::result_t::timeout: + count++; + if (count > 10) { + bExit = true; + } + break; + case tls::Connection::result_t::closed: + default: + bExit = true; + break; + } + } + con->shutdown(); + } +} + +void run_server(tls::Server& server) { + server.serve(&handler); +} + +class TlsTest : 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() { + struct sigaction action; + std::memset(&action, 0, sizeof(action)); + action.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &action, nullptr); + tls::Server::configure_signal_handler(SIGUSR1); + } + + 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 = ""; + auto& ref0 = server_config.chains.emplace_back(); + ref0.certificate_chain_file = "server_chain.pem"; + ref0.private_key_file = "server_priv.pem"; + ref0.trust_anchor_file = "server_root_cert.pem"; + ref0.ocsp_response_files = {"ocsp_response.der", "ocsp_response.der"}; + auto& ref1 = server_config.chains.emplace_back(); + ref1.certificate_chain_file = "alt_server_chain.pem"; + ref1.private_key_file = "alt_server_priv.pem"; + ref1.trust_anchor_file = "alt_server_root_cert.pem"; + ref1.ocsp_response_files = {"ocsp_response.der", "ocsp_response.der"}; + // server_config.verify_locations_file = "client_root_cert.pem"; + server_config.host = "localhost"; + server_config.service = "8444"; + server_config.ipv6_only = false; + server_config.verify_client = false; + server_config.io_timeout_ms = 1000; // no lower than 200ms, valgrind need much higher + + 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 = 1000; + client_config.verify_server = true; + 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(const tls::Server::ConfigurationCallback& 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(); + } + } + + void start(const std::function& handler) { + using state_t = tls::Server::state_t; + const auto res = server.init(server_config, nullptr); + if ((res == state_t::init_complete) || (res == state_t::init_socket)) { + server_thread = std::thread([this, handler]() { this->server.serve(handler); }); + server.wait_running(); + } + } + + void connect(const std::function& handler = nullptr) { + 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, 1000); + if (handler == nullptr) { + if (connection) { + if (connection->connect() == tls::Connection::result_t::success) { + set(ClientTest::flags_t::connected); + connection->shutdown(); + } + } + } else { + handler(connection); + } + } + + 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); + } + + void add_ta_cert_hash(const char* filename) { + openssl::sha_1_digest_t digest; + auto certs = openssl::load_certificates(filename); + for (const auto& ta : certs) { + if (openssl::certificate_sha_1(digest, ta.get())) { + client_config.trusted_ca_keys_data.cert_sha1_hash.push_back(digest); + } + } + } + void add_ta_key_hash(const char* filename) { + openssl::sha_1_digest_t digest; + auto certs = openssl::load_certificates(filename); + for (const auto& ta : certs) { + if (openssl::certificate_subject_public_key_sha_1(digest, ta.get())) { + client_config.trusted_ca_keys_data.key_sha1_hash.push_back(digest); + } + } + } + void add_ta_name(const char* filename) { + auto certs = openssl::load_certificates(filename); + for (const auto& ta : certs) { + auto res = openssl::certificate_subject_der(ta.get()); + if (res) { + client_config.trusted_ca_keys_data.x509_name.emplace_back(std::move(res)); + } + } + } +}; + +} // namespace +#endif // TLS_CONNECTION_TEST_HPP_ diff --git a/lib/staging/tls/tests/tls_main.cpp b/lib/staging/tls/tests/tls_main.cpp index c521fe010..c3b226302 100644 --- a/lib/staging/tls/tests/tls_main.cpp +++ b/lib/staging/tls/tests/tls_main.cpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest /* * testing options @@ -11,15 +11,53 @@ #include #include +#include #include #include #include +#include using namespace std::chrono_literals; -void handle_connection(std::shared_ptr& con) { +namespace { + +const char* short_opts = "ch36"; +bool disable_tls1_3{false}; +bool ipv6_only{false}; +bool verify_client{false}; + +void parse_options(int argc, char** argv) { + int c; + + while ((c = getopt(argc, argv, short_opts)) != -1) { + switch (c) { + break; + case 'c': + verify_client = true; + break; + case '3': + disable_tls1_3 = true; + break; + case '6': + ipv6_only = true; + break; + case 'h': + case '?': + std::cout << "Usage: " << argv[0] << " [-c|-3|-6]" << std::endl; + std::cout << " -c verify client" << std::endl; + std::cout << " -3 disable TLS 1.3" << std::endl; + std::cout << " -6 IPv6 only" << std::endl; + exit(1); + break; + default: + exit(2); + } + } +} + +void handle_connection(tls::Server::ConnectionPtr&& con) { std::cout << "Connection" << std::endl; - if (con->accept()) { + if (con->accept() == tls::Connection::result_t::success) { std::uint32_t count{0}; std::array buffer{}; bool bExit = false; @@ -33,7 +71,7 @@ void handle_connection(std::shared_ptr& con) { case tls::Connection::result_t::success: break; case tls::Connection::result_t::timeout: - case tls::Connection::result_t::error: + case tls::Connection::result_t::closed: default: bExit = true; break; @@ -45,7 +83,7 @@ void handle_connection(std::shared_ptr& con) { bExit = true; } break; - case tls::Connection::result_t::error: + case tls::Connection::result_t::closed: default: bExit = true; break; @@ -57,40 +95,51 @@ void handle_connection(std::shared_ptr& con) { std::cout << "Connection closed" << std::endl; } -int main() { +} // namespace + +int main(int argc, char** argv) { + parse_options(argc, argv); + + tls::Server::configure_signal_handler(SIGUSR1); 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 + // 15118 required suites, ECDH-ECDSA-AES128-SHA256 not supported by OpenSSL // 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"; + if (disable_tls1_3) { + config.ciphersuites = ""; + } else { + config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; + } + + auto& ref0 = config.chains.emplace_back(); + ref0.certificate_chain_file = "server_chain.pem"; + ref0.private_key_file = "server_priv.pem"; + ref0.trust_anchor_file = "server_root_cert.pem"; + ref0.ocsp_response_files = {"ocsp_response.der", "ocsp_response.der"}; + auto& ref1 = config.chains.emplace_back(); + ref1.certificate_chain_file = "alt_server_chain.pem"; + ref1.private_key_file = "alt_server_priv.pem"; + ref1.trust_anchor_file = "alt_server_root_cert.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; + config.ipv6_only = ipv6_only; + config.verify_client = verify_client; + config.io_timeout_ms = 10000; std::thread stop([&server]() { std::this_thread::sleep_for(30s); + std::cout << "stopping ..." << std::endl; server.stop(); }); server.init(config, nullptr); server.wait_stopped(); - // server.serve(&handle_connection); - server.serve([](auto con) { handle_connection(con); }); + server.serve(&handle_connection); server.wait_stopped(); stop.join(); diff --git a/lib/staging/tls/tests/tls_test.cpp b/lib/staging/tls/tests/tls_test.cpp index 5d6acb500..d47ad602c 100644 --- a/lib/staging/tls/tests/tls_test.cpp +++ b/lib/staging/tls/tests/tls_test.cpp @@ -1,21 +1,29 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest +#include "extensions/trusted_ca_keys.hpp" +#include "openssl_util.hpp" #include +#include #include #include +#include -std::string to_string(const openssl::sha_256_digest_t& digest) { +std::string to_string(const std::uint8_t* const ptr, const std::size_t len) { 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]; + for (int idx = 0; idx < len; ++idx) + string_stream << std::setw(2) << std::setfill('0') << (int)ptr[idx]; return string_stream.str(); } +std::string to_string(const tls::OcspCache::digest_t& digest) { + return to_string(reinterpret_cast(&digest), sizeof(digest)); +} + namespace { TEST(strdup, usage) { @@ -44,6 +52,7 @@ TEST(string, use) { } TEST(ConfigItem, test) { + // tests reduced with new ConfigStore implementation tls::ConfigItem i1; tls::ConfigItem i2{nullptr}; tls::ConfigItem i3{"Hello"}; @@ -56,39 +65,61 @@ TEST(ConfigItem, test) { EXPECT_EQ(i5, nullptr); EXPECT_EQ(i2, i5); - EXPECT_EQ(i3, i6); + EXPECT_STREQ(i3, i6); EXPECT_EQ(i1, i2); EXPECT_NE(i1, i3); EXPECT_EQ(i1, i5); EXPECT_NE(i1, i6); + tls::ConfigItem j2{""}; + auto j1(std::move(i3)); - auto j2 = std::move(i6); - EXPECT_EQ(i6, i3); - EXPECT_EQ(j1, j2); - EXPECT_EQ(j1, "Hello"); + j2 = std::move(i6); + EXPECT_STREQ(i6, i3); + EXPECT_STREQ(j1, j2); + EXPECT_STREQ(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); + // 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_STREQ(j3[0], "one"); + EXPECT_STREQ(j3[1], "two"); EXPECT_EQ(j3[2], nullptr); const char* p = j1; EXPECT_STREQ(p, "Hello"); + j1 = "Goodbye"; + EXPECT_STRNE(j1, "Hello"); + j1 = j2; + EXPECT_STREQ(j1, j2); } +TEST(ConfigItem, testB) { + tls::ConfigItem i1; + tls::ConfigItem i2{nullptr}; + tls::ConfigItem i3{""}; + tls::ConfigItem i4{"Hello"}; + + EXPECT_EQ(i1, nullptr); + EXPECT_EQ(i2, nullptr); + EXPECT_STREQ(i3, ""); + EXPECT_STREQ(i4, "Hello"); + EXPECT_STREQ("Hello", i4); +} + +using namespace tls::trusted_ca_keys; +using namespace openssl; + TEST(OcspCache, initEmpty) { tls::OcspCache cache; - openssl::sha_256_digest_t digest{}; + tls::OcspCache::digest_t digest{}; auto res = cache.lookup(digest); EXPECT_EQ(res.get(), nullptr); } @@ -99,7 +130,7 @@ TEST(OcspCache, init) { auto chain = openssl::load_certificates("client_chain.pem"); std::vector entries; - openssl::sha_256_digest_t digest{}; + tls::OcspCache::digest_t digest{}; for (const auto& cert : chain) { ASSERT_TRUE(tls::OcspCache::digest(digest, cert.get())); // std::cout << "digest: " << to_string(digest) << std::endl; @@ -112,4 +143,310 @@ TEST(OcspCache, init) { EXPECT_NE(res.get(), nullptr); } +TEST(TrustedCaKeys, parseAudi) { + + /* + * Audi + * 0069 trusted_authorities_list length + * 03 identifier_type cert_sha1_hash d8367e861f5807f8141fea572d676dbf58bb5f7c SHA1Hash + * 03 identifier_type cert_sha1_hash b491ddd08fafe72d9f6f9bafc68eb04da84cc09a SHA1Hash + * 03 identifier_type cert_sha1_hash 30aaaab25b1cc8a09a7b32652c33cc5a973c13f3 SHA1Hash + * 03 identifier_type cert_sha1_hash 700bf78ad58e0819dac6fcaead5ed20f7bb0554f SHA1Hash + * 03 identifier_type cert_sha1_hash 8c821f41604ed4c3431cf6d19f2ae107cf1f50e1 SHA1Hash + */ + + using trusted_authority = tls::trusted_ca_keys::trusted_authority; + + std::uint8_t extension[] = { + 0x00, 0x69, 0x03, 0xd8, 0x36, 0x7e, 0x86, 0x1f, 0x58, 0x07, 0xf8, 0x14, 0x1f, 0xea, 0x57, 0x2d, 0x67, 0x6d, + 0xbf, 0x58, 0xbb, 0x5f, 0x7c, 0x03, 0xb4, 0x91, 0xdd, 0xd0, 0x8f, 0xaf, 0xe7, 0x2d, 0x9f, 0x6f, 0x9b, 0xaf, + 0xc6, 0x8e, 0xb0, 0x4d, 0xa8, 0x4c, 0xc0, 0x9a, 0x03, 0x30, 0xaa, 0xaa, 0xb2, 0x5b, 0x1c, 0xc8, 0xa0, 0x9a, + 0x7b, 0x32, 0x65, 0x2c, 0x33, 0xcc, 0x5a, 0x97, 0x3c, 0x13, 0xf3, 0x03, 0x70, 0x0b, 0xf7, 0x8a, 0xd5, 0x8e, + 0x08, 0x19, 0xda, 0xc6, 0xfc, 0xae, 0xad, 0x5e, 0xd2, 0x0f, 0x7b, 0xb0, 0x55, 0x4f, 0x03, 0x8c, 0x82, 0x1f, + 0x41, 0x60, 0x4e, 0xd4, 0xc3, 0x43, 0x1c, 0xf6, 0xd1, 0x9f, 0x2a, 0xe1, 0x07, 0xcf, 0x1f, 0x50, 0xe1, + }; + + trusted_authority ext{&extension[0], sizeof(extension)}; + auto res = tls::trusted_ca_keys::convert(ext); + EXPECT_EQ(res.cert_sha1_hash.size(), 5); + EXPECT_EQ(res.key_sha1_hash.size(), 0); + EXPECT_EQ(res.x509_name.size(), 0); + EXPECT_FALSE(res.pre_agreed); +} + +TEST(TrustedCaKeys, parseBuzz) { + + /* + * Buzz + * 002a trusted_authorities_list length + * 03 identifier_type cert_sha1_hash d8367e861f5807f8141fea572d676dbf58bb5f7c SHA1Hash + * 03 identifier_type cert_sha1_hash 8c821f41604ed4c3431cf6d19f2ae107cf1f50e1 SHA1Hash + */ + + using trusted_authority = tls::trusted_ca_keys::trusted_authority; + + std::uint8_t extension[] = { + 0x00, 0x2a, 0x03, 0xd8, 0x36, 0x7e, 0x86, 0x1f, 0x58, 0x07, 0xf8, 0x14, 0x1f, 0xea, 0x57, + 0x2d, 0x67, 0x6d, 0xbf, 0x58, 0xbb, 0x5f, 0x7c, 0x03, 0x8c, 0x82, 0x1f, 0x41, 0x60, 0x4e, + 0xd4, 0xc3, 0x43, 0x1c, 0xf6, 0xd1, 0x9f, 0x2a, 0xe1, 0x07, 0xcf, 0x1f, 0x50, 0xe1, + }; + + trusted_authority ext{&extension[0], sizeof(extension)}; + auto res = tls::trusted_ca_keys::convert(ext); + EXPECT_EQ(res.cert_sha1_hash.size(), 2); + EXPECT_EQ(res.key_sha1_hash.size(), 0); + EXPECT_EQ(res.x509_name.size(), 0); + EXPECT_FALSE(res.pre_agreed); +} + +TEST(TrustedCaKeys, parseIoniq6) { + + /* + * Ioniq 6 (invalid missing the size of trusted_authorities_list) + * 01 identifier_type key_sha1_hash 4cd7290bf592d2c1ba90f56e08946d4c8e99dc38 SHA1Hash + * 01 identifier_type key_sha1_hash 00fae3900795c888a4d4d7bd9fdffa60418ac19f SHA1Hash + */ + + using trusted_authority = tls::trusted_ca_keys::trusted_authority; + + std::uint8_t extension[] = { + 0x00, 0x2a, 0x01, 0x4c, 0xd7, 0x29, 0x0b, 0xf5, 0x92, 0xd2, 0xc1, 0xba, 0x90, 0xf5, 0x6e, + 0x08, 0x94, 0x6d, 0x4c, 0x8e, 0x99, 0xdc, 0x38, 0x01, 0x00, 0xfa, 0xe3, 0x90, 0x07, 0x95, + 0xc8, 0x88, 0xa4, 0xd4, 0xd7, 0xbd, 0x9f, 0xdf, 0xfa, 0x60, 0x41, 0x8a, 0xc1, 0x9f, + }; + + trusted_authority ext{&extension[2], sizeof(extension) - 2}; + auto res = tls::trusted_ca_keys::convert(ext); + EXPECT_EQ(res.cert_sha1_hash.size(), 0); + EXPECT_EQ(res.key_sha1_hash.size(), 0); + EXPECT_EQ(res.x509_name.size(), 0); + EXPECT_FALSE(res.pre_agreed); + + ext = trusted_authority{&extension[0], sizeof(extension)}; + res = tls::trusted_ca_keys::convert(ext); + EXPECT_EQ(res.cert_sha1_hash.size(), 0); + EXPECT_EQ(res.key_sha1_hash.size(), 2); + EXPECT_EQ(res.x509_name.size(), 0); + EXPECT_FALSE(res.pre_agreed); +} + +TEST(TrustedCaKeys, generateBuzz) { + + /* + * Buzz + * 002a trusted_authorities_list length + * 03 identifier_type cert_sha1_hash d8367e861f5807f8141fea572d676dbf58bb5f7c SHA1Hash + * 03 identifier_type cert_sha1_hash 8c821f41604ed4c3431cf6d19f2ae107cf1f50e1 SHA1Hash + */ + + using trusted_ca_keys_t = tls::trusted_ca_keys::trusted_ca_keys_t; + + openssl::sha_1_digest_t hash1 = { + 0xd8, 0x36, 0x7e, 0x86, 0x1f, 0x58, 0x07, 0xf8, 0x14, 0x1f, + 0xea, 0x57, 0x2d, 0x67, 0x6d, 0xbf, 0x58, 0xbb, 0x5f, 0x7c, + }; + openssl::sha_1_digest_t hash2 = { + 0x8c, 0x82, 0x1f, 0x41, 0x60, 0x4e, 0xd4, 0xc3, 0x43, 0x1c, + 0xf6, 0xd1, 0x9f, 0x2a, 0xe1, 0x07, 0xcf, 0x1f, 0x50, 0xe1, + }; + + std::uint8_t extension[] = { + 0x00, 0x2a, 0x03, 0xd8, 0x36, 0x7e, 0x86, 0x1f, 0x58, 0x07, 0xf8, 0x14, 0x1f, 0xea, 0x57, + 0x2d, 0x67, 0x6d, 0xbf, 0x58, 0xbb, 0x5f, 0x7c, 0x03, 0x8c, 0x82, 0x1f, 0x41, 0x60, 0x4e, + 0xd4, 0xc3, 0x43, 0x1c, 0xf6, 0xd1, 0x9f, 0x2a, 0xe1, 0x07, 0xcf, 0x1f, 0x50, 0xe1, + }; + + trusted_ca_keys_t tck; + tck.cert_sha1_hash.push_back(hash1); + tck.cert_sha1_hash.push_back(hash2); + + auto res = tls::trusted_ca_keys::convert(tck); + + EXPECT_EQ(res.size(), sizeof(extension)); + EXPECT_EQ(std::memcmp(res.get(), &extension, sizeof(extension)), 0); + // std::cout << "A: " << to_string(std::get(res).get(), sizeof(extension)) << std::endl; + // std::cout << "B: " << to_string(&extension[0], sizeof(extension)) << std::endl; +} + +TEST(TrustedCaKeys, CertChain) { + // match server certificate to trust anchors + auto server_root = openssl::load_certificates("server_root_cert.pem"); + auto server_ca = openssl::load_certificates("server_ca_cert.pem"); + auto server = openssl::load_certificates("server_cert.pem"); + auto alt_server_root = openssl::load_certificates("alt_server_root_cert.pem"); + auto alt_server_ca = openssl::load_certificates("alt_server_ca_cert.pem"); + auto alt_server = openssl::load_certificates("alt_server_cert.pem"); + + openssl::certificate_list chain; + std::move(server_ca.begin(), server_ca.end(), std::back_inserter(chain)); + std::move(alt_server_ca.begin(), alt_server_ca.end(), std::back_inserter(chain)); + + ASSERT_EQ(server_root.size(), 1); + ASSERT_EQ(alt_server_root.size(), 1); + + EXPECT_EQ(openssl::verify_certificate(server[0].get(), server_root, chain), openssl::verify_result_t::Verified); + EXPECT_EQ(openssl::verify_certificate(alt_server[0].get(), alt_server_root, chain), + openssl::verify_result_t::Verified); + + EXPECT_EQ(openssl::verify_certificate(server[0].get(), alt_server_root, chain), + openssl::verify_result_t::NoCertificateAvailable); + EXPECT_EQ(openssl::verify_certificate(alt_server[0].get(), server_root, chain), + openssl::verify_result_t::NoCertificateAvailable); +} + +TEST(TrustedCaKeys, matchNone) { + + trusted_ca_keys_t keys; + chain_t chain; + + EXPECT_FALSE(match(keys, chain)); + + auto root = load_certificates("server_root_cert.pem"); + const auto* root_cert = root[0].get(); + + sha_1_digest_t digest; + + keys.x509_name.emplace_back(certificate_subject_der(root_cert)); + EXPECT_TRUE(certificate_sha_1(digest, root_cert)); + keys.cert_sha1_hash.push_back(digest); + EXPECT_TRUE(certificate_subject_public_key_sha_1(digest, root_cert)); + keys.key_sha1_hash.push_back(digest); + + EXPECT_FALSE(match(keys, chain)); + + auto alt_root = load_certificates("client_root_cert.pem"); + chain.chain.trust_anchors = std::move(alt_root); + EXPECT_FALSE(match(keys, chain)); +} + +TEST(TrustedCaKeys, matchName) { + + trusted_ca_keys_t keys; + chain_t chain; + + auto root = load_certificates("server_root_cert.pem"); + chain.chain.trust_anchors = std::move(root); + + const auto* root_cert = chain.chain.trust_anchors[0].get(); + + keys.x509_name.emplace_back(certificate_subject_der(root_cert)); + EXPECT_TRUE(match(keys, chain)); + + sha_1_digest_t digest; + + EXPECT_TRUE(certificate_sha_1(digest, root_cert)); + keys.cert_sha1_hash.push_back(digest); + EXPECT_TRUE(match(keys, chain)); + EXPECT_TRUE(certificate_subject_public_key_sha_1(digest, root_cert)); + keys.key_sha1_hash.push_back(digest); + EXPECT_TRUE(match(keys, chain)); +} + +TEST(TrustedCaKeys, matchCertHash) { + + trusted_ca_keys_t keys; + chain_t chain; + + auto root = load_certificates("server_root_cert.pem"); + chain.chain.trust_anchors.emplace_back(std::move(root[0])); + root = load_certificates("client_root_cert.pem"); + chain.chain.trust_anchors.emplace_back(std::move(root[0])); + ASSERT_EQ(chain.chain.trust_anchors.size(), 2); + + const auto* server_root_cert = chain.chain.trust_anchors[0].get(); + const auto* client_root_cert = chain.chain.trust_anchors[1].get(); + + sha_1_digest_t digest; + EXPECT_TRUE(certificate_sha_1(digest, client_root_cert)); + keys.cert_sha1_hash.push_back(digest); + EXPECT_TRUE(match(keys, chain)); + + EXPECT_TRUE(certificate_sha_1(digest, server_root_cert)); + keys.cert_sha1_hash.clear(); + keys.cert_sha1_hash.push_back(digest); + EXPECT_TRUE(match(keys, chain)); +} + +TEST(TrustedCaKeys, matchKeyHash) { + + trusted_ca_keys_t keys; + chain_t chain; + + auto root = load_certificates("server_root_cert.pem"); + chain.chain.trust_anchors.emplace_back(std::move(root[0])); + root = load_certificates("client_root_cert.pem"); + chain.chain.trust_anchors.emplace_back(std::move(root[0])); + ASSERT_EQ(chain.chain.trust_anchors.size(), 2); + + const auto* server_root_cert = chain.chain.trust_anchors[0].get(); + const auto* client_root_cert = chain.chain.trust_anchors[1].get(); + + sha_1_digest_t digest; + EXPECT_TRUE(certificate_subject_public_key_sha_1(digest, client_root_cert)); + keys.key_sha1_hash.push_back(digest); + EXPECT_TRUE(match(keys, chain)); + + EXPECT_TRUE(certificate_subject_public_key_sha_1(digest, server_root_cert)); + keys.key_sha1_hash.clear(); + keys.key_sha1_hash.push_back(digest); + EXPECT_TRUE(match(keys, chain)); +} + +TEST(TrustedCaKeys, selectNone) { + + trusted_ca_keys_t keys; + chain_list chains; + EXPECT_EQ(select(keys, chains), nullptr); + + chains.emplace_back(); + auto root = load_certificates("server_root_cert.pem"); + chains[0].chain.trust_anchors.emplace_back(std::move(root[0])); + EXPECT_EQ(select(keys, chains), nullptr); + + chains.emplace_back(); + root = load_certificates("alt_server_root_cert.pem"); + chains[1].chain.trust_anchors.emplace_back(std::move(root[0])); + EXPECT_EQ(select(keys, chains), nullptr); + + sha_1_digest_t digest; + root = load_certificates("client_root_cert.pem"); + auto* client_root_cert = root[0].get(); + EXPECT_TRUE(certificate_sha_1(digest, client_root_cert)); + keys.cert_sha1_hash.push_back(digest); + EXPECT_TRUE(certificate_subject_public_key_sha_1(digest, client_root_cert)); + keys.key_sha1_hash.push_back(digest); +} + +TEST(TrustedCaKeys, select) { + + trusted_ca_keys_t keys; + chain_list chains; + EXPECT_EQ(select(keys, chains), nullptr); + + chains.emplace_back(); + auto root = load_certificates("server_root_cert.pem"); + chains[0].chain.trust_anchors.emplace_back(std::move(root[0])); + EXPECT_EQ(select(keys, chains), nullptr); + + chains.emplace_back(); + root = load_certificates("alt_server_root_cert.pem"); + chains[1].chain.trust_anchors.emplace_back(std::move(root[0])); + EXPECT_EQ(select(keys, chains), nullptr); + + sha_1_digest_t digest; + root = load_certificates("client_root_cert.pem"); + auto* client_root_cert = root[0].get(); + EXPECT_TRUE(certificate_sha_1(digest, client_root_cert)); + keys.cert_sha1_hash.push_back(digest); + EXPECT_TRUE(certificate_subject_public_key_sha_1(digest, client_root_cert)); + keys.key_sha1_hash.push_back(digest); + + chains.emplace_back(); + root = load_certificates("client_root_cert.pem"); + chains[2].chain.trust_anchors.emplace_back(std::move(root[0])); + auto result = select(keys, chains); + EXPECT_NE(result, nullptr); + EXPECT_EQ(result, &chains[2]); +} + } // namespace diff --git a/lib/staging/tls/tls.cpp b/lib/staging/tls/tls.cpp index 2220fb721..d2b0a821b 100644 --- a/lib/staging/tls/tls.cpp +++ b/lib/staging/tls/tls.cpp @@ -1,23 +1,23 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest #include "tls.hpp" +#include "extensions/status_request.hpp" +#include "extensions/trusted_ca_keys.hpp" #include "openssl_util.hpp" #include #include +#include #include #include +#include #include -#include -#include #include #include #include #include -#include #include -#include #include #include @@ -28,9 +28,11 @@ #include #include #include +#include +#include -#ifdef TLSEXT_STATUSTYPE_ocsp_multi -#define OPENSSL_PATCHED +#ifdef UNIT_TEST +#include "extensions/helpers.hpp" #endif namespace std { @@ -46,6 +48,18 @@ template <> class default_delete { ::SSL_CTX_free(ptr); } }; +template <> class default_delete { +public: + void operator()(BIO_ADDR* ptr) const { + ::BIO_ADDR_free(ptr); + } +}; +template <> class default_delete { +public: + void operator()(BIO_ADDRINFO* ptr) const { + ::BIO_ADDRINFO_free(ptr); + } +}; } // namespace std using ::openssl::log_error; @@ -54,40 +68,14 @@ using ::openssl::log_warning; 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 + * \brief signal handler that does nothing + * \param[in] sig is the signal that was received (not used) + * \note exists so that poll() can be interrupted */ -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(); +void sig_int_handler(int sig) { } -constexpr std::uint32_t c_shutdown_timeout_ms = 5000; // 5 seconds - +/// OpenSSL results mapped to an enum enum class ssl_error_t : std::uint8_t { error, error_ssl, @@ -105,6 +93,11 @@ enum class ssl_error_t : std::uint8_t { timeout, // not an OpenSSL result }; +/** + * \brief convert OpenSSL result into an enum + * \param[in] err is the OpenSSL result + * \return enum equivalent of err + */ constexpr ssl_error_t convert(const int err) { ssl_error_t res{ssl_error_t::error}; switch (err) { @@ -151,14 +144,22 @@ constexpr ssl_error_t convert(const int err) { return res; } +/// subset of OpenSSL results to simplify error recovery enum class ssl_result_t : std::uint8_t { - error, - error_syscall, + error, //!< error - connection still active + error_syscall, //!< error - connection closed, socket no longer valid success, closed, timeout, + want_read, //!< non-blocking - operation waiting for read available on socket + want_write, //!< non-blocking - operation waiting for write available on socket }; +/** + * \brief map OpenSSL result enum to a simplified subset + * \param[in] err is the OpenSSL result enum value + * \return the simplified version of err + */ constexpr ssl_result_t convert(ssl_error_t err) { switch (err) { case ssl_error_t::none: @@ -170,302 +171,322 @@ constexpr ssl_result_t convert(ssl_error_t err) { return ssl_result_t::error_syscall; case ssl_error_t::zero_return: return ssl_result_t::closed; + case ssl_error_t::want_read: + return ssl_result_t::want_read; + case ssl_error_t::want_write: + return ssl_result_t::want_write; case ssl_error_t::error: - case ssl_error_t::want_accept: + case ssl_error_t::want_accept: // only from BIO_s_accept() + case ssl_error_t::want_connect: // only from BIO_s_connect() 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; } } +/** + * \brief convert the simplified result into the result used in the API + * \param[in] err is the simplified result + * \return the mapped API result value + */ 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::want_read: + return tls::Connection::result_t::want_read; + case ssl_result_t::want_write: + return tls::Connection::result_t::want_write; case ssl_result_t::closed: case ssl_result_t::error: case ssl_result_t::error_syscall: default: - return tls::Connection::result_t::error; + return tls::Connection::result_t::closed; } } +/** + * \brief wait for an event on a socket + * \param[in] soc is the file descriptor/socket + * \param[in] forWrite wait for a write event when true, read event otherwise + * \param[in] timeout_ms -1 is wait forever, 0 checks for events and returns + * immediately, >0 maximum time to wait in milliseconds + * \return >0 when there are events on the socket + * 0 for timeout + * -1 for error (check errno) + * -2 for error where errno is EINTR (interrupted call) + */ int wait_for(int soc, bool forWrite, std::int32_t timeout_ms) { - std::int16_t event = POLLIN; - if (forWrite) { - event = POLLOUT; - } + const std::int16_t event = (forWrite) ? POLLOUT : POLLIN; 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; - } + auto 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)); + } else { + poll_res = -2; } - // 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; +/** + * \brief wait for an event on a socket retrying when interrupted + * \param[in] soc is the file descriptor/socket + * \param[in] forWrite wait for a write event when true, read event otherwise + * \param[in] timeout_ms -1 is wait forever, 0 checks for events and returns + * immediately, >0 maximum time to wait in milliseconds + * \return >0 when there are events on the socket + * 0 for timeout + * -1 for error (check errno) + */ +int wait_for_loop(int soc, bool forWrite, std::int32_t timeout_ms) { + int res{-2}; + while (res == -2) { + res = wait_for(soc, forWrite, timeout_ms); } - - return bLoop; + return res; } -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); +/** + * \brief read user data from a SSL connection + * \param[in] ctx is SSL connection data + * \param[in] buf is where to place received data + * \param[in] num is the size of buff (the maximum number of bytes to receive) + * \param[out] readbytes number of bytes received + * \returns the result of the operation + */ +[[nodiscard]] ssl_result_t ssl_read(SSL* ctx, std::byte* buf, std::size_t num, std::size_t& readbytes); +/** + * \brief write user data to a SSL connection + * \param[in] ctx is SSL connection data + * \param[in] buf is the data to send + * \param[in] num is the size of buff + * \param[out] writebytes number of bytes sent + * \returns the result of the operation + */ +[[nodiscard]] ssl_result_t ssl_write(SSL* ctx, const std::byte* buf, std::size_t num, std::size_t& writebytes); +/** + * \brief accept an incoming SSL connection, runs the TLS handshake (sever) + * \param[in] ctx is SSL connection data + * \returns the result of the operation + */ +[[nodiscard]] ssl_result_t ssl_accept(SSL* ctx); +/** + * \brief start a SSL connection, runs the TLS handshake (client) + * \param[in] ctx is SSL connection data + * \returns the result of the operation + */ +[[nodiscard]] ssl_result_t ssl_connect(SSL* ctx); +/** + * \brief close a SSL connection + * \param[in] ctx is SSL connection data + * \returns the result of the operation + */ +ssl_result_t ssl_shutdown(SSL* ctx); + +/// operation being performed +enum class operation_t : std::uint8_t { + ssl_read, + ssl_write, + ssl_accept, + ssl_connect, + ssl_shutdown, }; -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); +/** + * \brief text representation of the operation being performed + * \param[in] the operation + * \return a string representing the operation + */ +const char* operation_str(operation_t operation) { + switch (operation) { + case operation_t::ssl_read: + return "SSL_read: "; + case operation_t::ssl_write: + return "SSL_write: "; + case operation_t::ssl_accept: + return "SSL_accept: "; + case operation_t::ssl_connect: + return "SSL_connect: "; + case operation_t::ssl_shutdown: + return "SSL_shutdown: "; + default: + return ": "; } - 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); +/** + * \brief manage the result from a SSL operation + * \param[in] ctx is SSL connection data + * \param[in] res is the result of the SSL operation + * \return result is the simplified result mapped from res + */ +ssl_result_t process_result(SSL* ctx, operation_t operation, const int res) { + ssl_error_t result{ssl_error_t::error}; + + if (ctx != nullptr) { + result = ssl_error_t::none; // success + 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: + case ssl_error_t::want_read: + case ssl_error_t::want_write: + break; + case ssl_error_t::error_syscall: + // no further operation permitted on the connection + if (errno != 0) { + log_error(operation_str(operation) + std::string("SSL_ERROR_SYSCALL ") + std::to_string(errno)); + } + break; + case ssl_error_t::error_ssl: + if (operation != operation_t::ssl_shutdown) { + log_error(operation_str(operation) + std::to_string(res) + " " + std::to_string(sslerr_raw)); + } + break; + 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_x509_lookup: + default: + log_error(operation_str(operation) + std::to_string(res) + " " + std::to_string(sslerr_raw)); + break; + } + } } + 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; +ssl_result_t ssl_read(SSL* ctx, std::byte* buf, const std::size_t num, std::size_t& readbytes) { + const auto res = SSL_read_ex(ctx, buf, num, &readbytes); + return process_result(ctx, operation_t::ssl_read, res); +}; - 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); +ssl_result_t ssl_write(SSL* ctx, const std::byte* buf, const std::size_t num, std::size_t& writebytes) { + const auto res = SSL_write_ex(ctx, buf, num, &writebytes); + return process_result(ctx, operation_t::ssl_read, res); } -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); - } +ssl_result_t ssl_accept(SSL* ctx) { + const auto res = SSL_accept(ctx); + // 0 is handshake not successful (ssl_error_t::zero_return -> ssl_result_t::closed) + // < 0 is other error + return process_result(ctx, operation_t::ssl_read, res); +} + +ssl_result_t ssl_connect(SSL* ctx) { + const auto res = SSL_connect(ctx); + // 0 is handshake not successful (ssl_error_t::zero_return -> ssl_result_t::closed) + // < 0 is other error + return process_result(ctx, operation_t::ssl_read, res); } +ssl_result_t ssl_shutdown(SSL* ctx) { + const auto res = SSL_shutdown(ctx); + return process_result(ctx, operation_t::ssl_read, res); +} + +/** + * \brief configure SSL context with certificates and keys + * \param[in] ctx is SSL context data + * \param[in] ciphersuites are the TLS 1.3 cipher suites, + * nullptr means use default, "" disables TSL 1.3 + * \param[in] cipher_list are the TLS 1.2 ciphers, nullptr means use default + * \param[in] cert_config are one of more sets of key and certificates + * \param[in] required when true, fail when cert_config is missing + * \return true when successful + * \note required will be true for a TLS server and can be false for a TLS client + */ 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 required) { - bool bRes{true}; + const tls::Server::certificate_config_t& cert_config, bool required) { + bool result{true}; + + // TODO(james-ctc) TPM2 support if (ctx == nullptr) { log_error("server_init::SSL_CTX_new"); - bRes = false; + result = false; } else { if (SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 0) { log_error("SSL_CTX_set_min_proto_version"); - bRes = false; + result = 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; + result = false; } } if (cipher_list != nullptr) { if (SSL_CTX_set_cipher_list(ctx, cipher_list) == 0) { log_error("SSL_CTX_set_cipher_list"); - bRes = false; + result = false; } } if (ciphersuites != nullptr) { if (SSL_CTX_set_ciphersuites(ctx, ciphersuites) == 0) { log_error("SSL_CTX_set_ciphersuites"); - bRes = false; + result = false; } } - if (certificate_chain_file != nullptr) { - if (SSL_CTX_use_certificate_chain_file(ctx, certificate_chain_file) != 1) { + if (cert_config.certificate_chain_file != nullptr) { + if (SSL_CTX_use_certificate_chain_file(ctx, cert_config.certificate_chain_file) != 1) { log_error("SSL_CTX_use_certificate_chain_file"); - bRes = false; + result = false; } } else { - bRes = !required; + if (required) { + result = false; + } } - if (private_key_file != nullptr) { + if (cert_config.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; + if (cert_config.private_key_password != nullptr) { + pass_str = cert_config.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) { + if (SSL_CTX_use_PrivateKey_file(ctx, cert_config.private_key_file, SSL_FILETYPE_PEM) != 1) { log_error("SSL_CTX_use_PrivateKey_file"); - bRes = false; + result = false; } if (SSL_CTX_check_private_key(ctx) != 1) { log_error("SSL_CTX_check_private_key"); - bRes = false; + result = false; } } else { - bRes = !required; - } - } - - 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"); + if (required) { + result = false; + } } } - return resp; -} - -constexpr char* dup(const char* value) { - char* res = nullptr; - if (value != nullptr) { - res = strdup(value); - } - return res; + return result; } } // 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; @@ -477,7 +498,7 @@ struct connection_ctx { }; struct ocsp_cache_ctx { - std::map cache; + std::map cache; }; struct server_ctx { @@ -489,320 +510,7 @@ struct client_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 represents a TLS connection (client and server) 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) { @@ -812,7 +520,9 @@ Connection::Connection(SslContext* ctx, int soc, const char* ip_in, const char* if (m_context->ctx == nullptr) { log_error("Connection::SSL_new"); } else { + // BIO_free is handled when SSL_free is done (SSL_ptr) m_context->soc_bio = BIO_new_socket(soc, BIO_CLOSE); + SSL_set_bio(m_context->ctx.get(), m_context->soc_bio, m_context->soc_bio); } if (m_context->soc_bio == nullptr) { @@ -822,68 +532,155 @@ Connection::Connection(SslContext* ctx, int soc, const char* ip_in, const char* Connection::~Connection() = default; -Connection::result_t Connection::read(std::byte* buf, std::size_t num, std::size_t& readbytes) { +Connection::result_t Connection::read(std::byte* buf, std::size_t num, std::size_t& readbytes, int timeout_ms) { 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; + auto ctx = m_context->ctx.get(); + bool loop{true}; + while (loop) { + loop = false; + result = ssl_read(ctx, buf, num, readbytes); + switch (result) { + case ssl_result_t::success: + case ssl_result_t::timeout: + break; + case ssl_result_t::want_read: + case ssl_result_t::want_write: + if (timeout_ms != 0) { + const auto res = ::wait_for_loop(SSL_get_fd(ctx), result == ssl_result_t::want_write, timeout_ms); + loop = res > 0; // event received + result = 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) { +Connection::result_t Connection::write(const std::byte* buf, std::size_t num, std::size_t& writebytes, int timeout_ms) { 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; + auto ctx = m_context->ctx.get(); + bool loop{true}; + while (loop) { + loop = false; + result = ssl_write(ctx, buf, num, writebytes); + switch (result) { + case ssl_result_t::success: + case ssl_result_t::timeout: + break; + case ssl_result_t::want_read: + case ssl_result_t::want_write: + if (timeout_ms != 0) { + const auto res = ::wait_for_loop(SSL_get_fd(ctx), result == ssl_result_t::want_write, timeout_ms); + loop = res > 0; // event received + result = 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() { +Connection::result_t Connection::shutdown(int timeout_ms) { assert(m_context != nullptr); + ssl_result_t result{ssl_result_t::error}; + if (m_state == state_t::connected) { - ssl_shutdown(m_context->ctx.get(), c_shutdown_timeout_ms); - m_state = state_t::closed; + auto ctx = m_context->ctx.get(); + bool loop{true}; + while (loop) { + loop = false; + result = ssl_shutdown(ctx); + switch (result) { + case ssl_result_t::closed: + case ssl_result_t::success: + case ssl_result_t::timeout: + m_state = state_t::closed; + break; + case ssl_result_t::want_read: + case ssl_result_t::want_write: + if (timeout_ms != 0) { + const auto res = ::wait_for_loop(SSL_get_fd(ctx), result == ssl_result_t::want_write, timeout_ms); + loop = res > 0; // event received + result = ssl_result_t::timeout; + } + break; + case ssl_result_t::error: + case ssl_result_t::error_syscall: + default: + m_state = state_t::fault; + break; + } + } } + return convert(result); +} + +Connection::result_t Connection::wait_for(result_t action, int timeout_ms) { + result_t result{action}; + auto ctx = m_context->ctx.get(); + + switch (action) { + case result_t::want_read: + case result_t::want_write: { + const auto res = wait_for_loop(SSL_get_fd(ctx), action == result_t::want_write, timeout_ms); + if (res == 0) { + result = result_t::timeout; + } else if (res == -1) { + result = result_t::closed; + } else { + result = result_t::success; + } + break; + } + case result_t::success: + case result_t::closed: + case result_t::timeout: + default: + break; + } + + return result; } int Connection::socket() const { return m_context->soc; } +const Certificate* Connection::peer_certificate() const { + assert(m_context != nullptr); + return SSL_get0_peer_certificate(m_context->ctx.get()); +} + // ---------------------------------------------------------------------------- // ServerConnection represents a TLS server connection @@ -893,16 +690,15 @@ 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) { + Connection(ctx, soc, ip_in, service_in, timeout_ms), m_tck_data{m_trusted_ca_keys, m_flags} { { 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); + ServerStatusRequestV2::set_data(m_context->ctx.get(), &m_flags); + ServerTrustedCaKeys::set_data(m_context->ctx.get(), &m_tck_data); } } @@ -912,31 +708,45 @@ ServerConnection::~ServerConnection() { m_count--; } m_cv.notify_all(); -}; +} -bool ServerConnection::accept() { +Connection::result_t ServerConnection::accept(int timeout_ms) { 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; + auto ctx = m_context->ctx.get(); + bool loop{true}; + while (loop) { + loop = false; + result = ssl_accept(ctx); + switch (result) { + case ssl_result_t::success: + m_state = state_t::connected; + break; + case ssl_result_t::want_read: + case ssl_result_t::want_write: + if (timeout_ms != 0) { + const auto res = ::wait_for_loop(SSL_get_fd(ctx), result == ssl_result_t::want_write, timeout_ms); + loop = res > 0; // event received + result = 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 result == ssl_result_t::success; + return convert(result); } void ServerConnection::wait_all_closed() { @@ -946,49 +756,62 @@ void ServerConnection::wait_all_closed() { } // ---------------------------------------------------------------------------- -// ClientConnection represents a TLS server connection +// ClientConnection represents a TLS client 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() { +Connection::result_t ClientConnection::connect(int timeout_ms) { 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; + auto ctx = m_context->ctx.get(); + bool loop{true}; + while (loop) { + loop = false; + result = ssl_connect(ctx); + switch (result) { + case ssl_result_t::success: + m_state = state_t::connected; + break; + case ssl_result_t::want_read: + case ssl_result_t::want_write: + if (timeout_ms != 0) { + const auto res = ::wait_for_loop(SSL_get_fd(ctx), result == ssl_result_t::want_write, timeout_ms); + loop = res > 0; // event received + result = 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 result == ssl_result_t::success; + return convert(result); } // ---------------------------------------------------------------------------- // TLS Server +int Server::s_sig_int{-1}; + Server::Server() : m_context(std::make_unique()), m_status_request_v2(m_cache) { } @@ -998,111 +821,263 @@ Server::~Server() { } bool Server::init_socket(const config_t& cfg) { - bool bRes = false; + bool result = false; if (cfg.socket == INVALID_SOCKET) { - BIO_ADDRINFO* addrinfo{nullptr}; + BIO_ADDRINFO* addrinfo_tmp{nullptr}; + // AF_UNSPEC is another option but seems to prefer IPv4 + const int family{(cfg.ipv6_only) ? AF_INET6 : AF_INET}; - bRes = BIO_lookup_ex(cfg.host, cfg.service, BIO_LOOKUP_SERVER, AF_UNSPEC, SOCK_STREAM, IPPROTO_TCP, - &addrinfo) != 0; + result = BIO_lookup_ex(cfg.host, cfg.service, BIO_LOOKUP_SERVER, family, SOCK_STREAM, IPPROTO_TCP, + &addrinfo_tmp) != 0; - if (!bRes) { + if (!result) { 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; - } - + std::unique_ptr addrinfo(addrinfo_tmp); + const auto sock_family = BIO_ADDRINFO_family(addrinfo.get()); + const auto sock_type = BIO_ADDRINFO_socktype(addrinfo.get()); + const auto sock_protocol = BIO_ADDRINFO_protocol(addrinfo.get()); + const auto* sock_address = BIO_ADDRINFO_address(addrinfo.get()); m_socket = BIO_socket(sock_family, sock_type, sock_protocol, 0); if (m_socket == INVALID_SOCKET) { log_error("init_socket::BIO_socket"); + if (cfg.ipv6_only) { + log_warning("Verify that the configured interface has a valid IPv6 link local address configured."); + } } else { - bRes = BIO_listen(m_socket, sock_address, sock_options) != 0; - if (!bRes) { + 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; + } + + result = BIO_listen(m_socket, sock_address, sock_options) != 0; + if (!result) { 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; + result = true; } - return bRes; + return result; } 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, true); - if (bRes) { - int mode = SSL_VERIFY_NONE; + bool result = ctx != nullptr; + result = result && (cfg.chains.size() > 0); + + if (result) { + // use the first server chain + result = configure_ssl_ctx(ctx, cfg.ciphersuites, cfg.cipher_list, cfg.chains[0], true); + if (result) { + int mode = SSL_VERIFY_NONE; + + // TODO(james-ctc): verify may need to change based on TLS version + // 15118-2 mandates TLS 1.2 and no client certificate + // 15118-20 mandates TLS 1.3 and requires a client certificate + // There might be a requirement to support mutual authentication on + // TLS 1.2 + // + // Potential solution is to provide optional mutual authentication + // for TLS 1.2 and mandatory mutual authentication for TLS 1.3 + // SSL_set_verify() could be used in + // ServerStatusRequestV2::client_hello_cb + // TLS 1.3 has a post handshake flag SSL_VERIFY_POST_HANDSHAKE + // which might be a better approach + + 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"); + result = false; + } + } + SSL_CTX_set_verify(ctx, mode, nullptr); - 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"); + result = result && m_status_request_v2.init_ssl(ctx); + result = result && m_server_trusted_ca_keys.init_ssl(ctx); + } + } + + if (!result) { + SSL_CTX_free(ctx); + ctx = nullptr; + } + + m_context->ctx = SSL_CTX_ptr(ctx); + return ctx != nullptr; +} + +bool Server::init_certificates(const std::vector& chain_files) { + std::vector entries; + openssl::chain_list chains; + + for (const auto& i : chain_files) { + auto certs = openssl::load_certificates(i.certificate_chain_file); + auto tas = openssl::load_certificates(i.trust_anchor_file); + auto pkey = openssl::load_private_key(i.private_key_file, i.private_key_password); + + if (certs.size() > 0) { + openssl::chain_t chain; + + // update OCSP cache + if (certs.size() == i.ocsp_response_files.size()) { + for (std::size_t c = 0; c < certs.size(); c++) { + const auto& file = i.ocsp_response_files[c]; + const auto& cert = certs[c]; + + if (file != nullptr) { + OcspCache::digest_t digest{}; + if (OcspCache::digest(digest, cert.get())) { + entries.emplace_back(digest, file); + } + } + } + } else { + log_warning(" certificates != OCSP responses"); } - } else { - if (SSL_CTX_set_default_verify_paths(ctx) != 1) { - log_error("SSL_CTX_set_default_verify_paths"); - bRes = false; + + /* + * If there are no trust anchors then the chain can't be verified + * it also means that trusted_ca_keys can't be supported for the + * chain. + */ + + if (!tas.empty()) { + // update trusted CA keys information + chain.chain.leaf = std::move(certs[0]); + // remove server cert from intermediate list + certs.erase(certs.begin()); + chain.chain.chain = std::move(certs); + chain.chain.trust_anchors = std::move(tas); + chain.private_key = std::move(pkey); + + if (openssl::verify_chain(chain)) { + chains.emplace_back(std::move(chain)); + } + } else { + const auto subject = openssl::certificate_subject(certs[0].get()); + std::string msg("No trust anchors for certificate:"); + for (const auto& item : subject) { + msg += ' '; + msg += item.first; + msg += ':'; + msg += item.second; + } + log_warning(msg); } } + } - SSL_CTX_set_verify(ctx, mode, nullptr); - SSL_CTX_set_client_hello_cb(ctx, &CertificateStatusRequestV2::client_hello_cb, nullptr); + bool result{true}; + + if (chains.empty()) { + // continue without trusted_ca_keys support + log_warning("trusted_ca_keys support disabled"); + } else { + m_server_trusted_ca_keys.update(std::move(chains)); + } - if (SSL_CTX_set_tlsext_status_cb(ctx, &CertificateStatusRequestV2::status_request_cb) != 1) { - log_error("SSL_CTX_set_tlsext_status_cb"); - bRes = false; + // don't error when there are no OCSP cached responses + if (!entries.empty()) { + if (!m_cache.load(entries)) { + result = false; } + } - if (SSL_CTX_set_tlsext_status_arg(ctx, &m_status_request_v2) != 1) { - log_error("SSL_CTX_set_tlsext_status_arg"); - bRes = false; + return result; +} + +void Server::wait_for_connection(const ConnectionHandler& handler) { + std::unique_ptr 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, c_serve_timeout_ms); + if (poll_res == -1) { + // poll() has failed + m_exit = true; + } else if ((poll_res == -2) || (poll_res == 0)) { + // EINTR is -2 - triggered by stop() + // timeout is 0 + // nothing to accept, check for m_exit + } else { + soc = BIO_accept_ex(m_socket, peer.get(), BIO_SOCK_NONBLOCK); + if (BIO_sock_should_retry(soc) == 0) { + break; + } + } + }; + + // attempt to get SSL configuration when not set yet + if ((soc >= 0) && (m_state == state_t::init_socket)) { + auto new_config = m_init_callback(); + bool success{false}; + if (new_config && new_config.value()) { + success = update(*new_config.value()); + } + if (success) { + m_state = state_t::running; + } else { + // updated configuration failed + BIO_closesocket(soc); + soc = INVALID_SOCKET; + } } - 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 (m_exit) { + if (soc >= 0) { + BIO_closesocket(soc); + } + } else { + if (soc < 0) { + log_error("serve::BIO_accept_ex"); + } else { + // new connection, pass to handler + auto* ip = BIO_ADDR_hostname_string(peer.get(), 1); + auto* service = BIO_ADDR_service_string(peer.get(), 1); + auto connection = + std::make_unique(m_context->ctx.get(), soc, ip, service, m_timeout_ms); + handler(std::move(connection)); + OPENSSL_free(ip); + OPENSSL_free(service); + } } } +} - if (!bRes) { - SSL_CTX_free(ctx); - ctx = nullptr; +void Server::configure_signal_handler(int interrupt_signal) { + s_sig_int = interrupt_signal; + struct sigaction action {}; + action.sa_handler = &sig_int_handler; + action.sa_flags = SA_RESTART; + if (sigaction(s_sig_int, &action, nullptr) == -1) { + log_error("Server::configure_signal_handler: " + std::to_string(errno)); } - - m_context->ctx = SSL_CTX_ptr(ctx); - return ctx != nullptr; } -Server::state_t Server::init(const config_t& cfg, const std::function& init_ssl) { +Server::state_t Server::init(const config_t& cfg, const ConfigurationCallback& init_ssl_cb) { + // prevent multiple calls to init, or calling init whilst serve() is running std::lock_guard lock(m_mutex); - m_timeout_ms = cfg.io_timeout_ms; - m_init_callback = init_ssl; + m_init_callback = init_ssl_cb; // save handler for later m_state = state_t::init_needed; if (init_socket(cfg)) { m_state = state_t::init_socket; @@ -1114,49 +1089,34 @@ Server::state_t Server::init(const config_t& cfg, const std::function 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); - } - } - } + // does not change server socket settings, use init() if needed + std::vector entries; - bRes = m_cache.load(entries); - } else { - log_warning(std::string("update_ocsp: ocsp files != ") + std::to_string(chain.size())); - } + m_timeout_ms = cfg.io_timeout_ms; + // always try init_certificates() and init_ssl() + bool result = init_certificates(cfg.chains); + if (!init_ssl(cfg)) { + result = false; } - - return bRes; + return result; } -Server::state_t Server::serve(const std::function& ctx)>& handler) { +Server::state_t Server::serve(const ConnectionHandler& handler) { assert(m_context != nullptr); // prevent init() or server() being called while serve is running std::lock_guard lock(m_mutex); - bool bRes = false; + // check server socket configuration has been successful + bool result{false}; state_t tmp = m_state; - switch (tmp) { case state_t::init_socket: if (m_init_callback != nullptr) { - bRes = m_socket != INVALID_SOCKET; + result = m_socket != INVALID_SOCKET; } break; case state_t::init_complete: - bRes = m_socket != INVALID_SOCKET; + result = m_socket != INVALID_SOCKET; break; case state_t::init_needed: case state_t::running: @@ -1165,74 +1125,27 @@ Server::state_t Server::serve(const std::function= 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); - } - } 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); + wait_for_connection(handler); } BIO_closesocket(m_socket); m_socket = INVALID_SOCKET; - bRes = true; m_state = state_t::stopped; } + // wakeup wait_stopped() { std::lock_guard lock(m_cv_mutex); m_running = false; @@ -1243,56 +1156,59 @@ Server::state_t Server::serve(const std::functionm_running; }); } void Server::wait_stopped() { std::unique_lock lock(m_cv_mutex); - m_cv.wait(lock, [this] { return !m_running; }); - lock.unlock(); + m_cv.wait(lock, [this]() { return !this->m_running; }); } // ---------------------------------------------------------------------------- // Client -Client::Client() : m_context(std::make_unique()) { +Client::Client() : + m_context(std::make_unique()), m_status_request_v2(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}; +Client::Client(std::unique_ptr&& handler) : + m_context(std::make_unique()), m_status_request_v2(std::move(handler)) { +} - 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); - } - } +Client::~Client() = default; - return ocsp != nullptr; +bool Client::init(const config_t& cfg) { + return init(cfg, default_overrides()); } -bool Client::init(const config_t& cfg) { +bool Client::init(const config_t& cfg, const override_t& override) { assert(m_context != nullptr); + assert(override.tlsext_status_cb != nullptr); + assert(override.status_request_v2_cb != nullptr); + assert(override.status_request_v2_add != nullptr); + assert(override.trusted_ca_keys_add != nullptr); + assert(override.trusted_ca_keys_free != nullptr); - // TODO(james-ctc) TPM2 support - + m_timeout_ms = cfg.io_timeout_ms; + m_trusted_ca_keys = cfg.trusted_ca_keys_data; 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, false); - if (bRes) { + const Server::certificate_config_t cert_config = { + cfg.certificate_chain_file, + nullptr, + cfg.private_key_file, + cfg.private_key_password, + }; + auto result = configure_ssl_ctx(ctx, cfg.ciphersuites, cfg.cipher_list, cert_config, false); + if (result) { int mode = SSL_VERIFY_NONE; if (cfg.verify_server) { @@ -1303,48 +1219,52 @@ bool Client::init(const config_t& cfg) { } else { if (SSL_CTX_set_default_verify_paths(ctx) != 1) { log_error("SSL_CTX_set_default_verify_paths"); - bRes = false; + result = 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; + result = 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 (SSL_CTX_add_custom_ext(ctx, TLSEXT_TYPE_status_request_v2, context, override.status_request_v2_add, + nullptr, nullptr, override.status_request_v2_cb, + m_status_request_v2.get()) != 1) { + log_error("SSL_CTX_add_custom_ext status_request_v2"); + result = false; } } if (cfg.status_request || cfg.status_request_v2) { - if (SSL_CTX_set_tlsext_status_arg(ctx, this) != 1) { + if (SSL_CTX_set_tlsext_status_cb(ctx, override.tlsext_status_cb) != 1) { + log_error("SSL_CTX_set_tlsext_status_cb"); + result = false; + } + if (SSL_CTX_set_tlsext_status_arg(ctx, m_status_request_v2.get()) != 1) { log_error("SSL_CTX_set_tlsext_status_arg"); - bRes = false; + result = false; + } + } + + if (cfg.trusted_ca_keys) { + constexpr int context = SSL_EXT_TLS_ONLY | SSL_EXT_IGNORE_ON_RESUMPTION | SSL_EXT_CLIENT_HELLO; + if (SSL_CTX_add_custom_ext(ctx, TLSEXT_TYPE_trusted_ca_keys, context, override.trusted_ca_keys_add, + override.trusted_ca_keys_free, &m_trusted_ca_keys, nullptr, nullptr) != 1) { + log_error("SSL_CTX_add_custom_ext trusted_ca_keys"); + result = false; } } } - if (bRes) { + if (result) { m_context->ctx = SSL_CTX_ptr(ctx); - m_state = state_t::init; } else { SSL_CTX_free(ctx); ctx = nullptr; @@ -1353,94 +1273,55 @@ bool Client::init(const config_t& cfg) { return ctx != nullptr; } -std::unique_ptr Client::connect(const char* host, const char* service, bool ipv6_only) { - BIO_ADDRINFO* addrinfo{nullptr}; +std::unique_ptr Client::connect(const char* host, const char* service, bool ipv6_only, + int timeout_ms) { + BIO_ADDRINFO* addrinfo_tmp{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; + const bool lookup_result = + BIO_lookup_ex(host, service, BIO_LOOKUP_CLIENT, family, SOCK_STREAM, IPPROTO_TCP, &addrinfo_tmp) != 0; - if (!bRes) { + if (!lookup_result) { 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}; + std::unique_ptr addrinfo(addrinfo_tmp); + const auto sock_family = BIO_ADDRINFO_family(addrinfo.get()); + const auto sock_type = BIO_ADDRINFO_socktype(addrinfo.get()); + const auto sock_protocol = BIO_ADDRINFO_protocol(addrinfo.get()); + const auto* sock_address = BIO_ADDRINFO_address(addrinfo.get()); 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); + bool connected{true}; + constexpr int soc_opt = BIO_SOCK_NONBLOCK; + + // calls connect() - hence checking errno + if (BIO_connect(socket, sock_address, soc_opt) != 1) { + connected = false; + if (errno == EINPROGRESS) { + // wait for write on the socket + auto res = wait_for_loop(socket, true, timeout_ms); + if (res == 0) { + log_error("connect::wait_for: timeout"); + } else if (res > 0) { + res = BIO_sock_error(socket); + if (res == 0) { + connected = true; + } else { + log_error("connect::BIO_sock_error: " + std::to_string(res)); + } + } + } else { + log_error("connect::BIO_connect: " + std::to_string(errno)); } + } - if (!b_okay) { - result = 0; - remaining = -1; - } + if (connected) { + result = std::make_unique(m_context->ctx.get(), socket, host, service, m_timeout_ms); } } } @@ -1448,71 +1329,12 @@ int Client::status_request_cb(Ssl* ctx) { 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; +Client::override_t Client::default_overrides() { + return { + &ClientStatusRequestV2::status_request_v2_multi_cb, &ClientStatusRequestV2::status_request_v2_add, + &ClientStatusRequestV2::status_request_v2_cb, &ClientTrustedCaKeys::trusted_ca_keys_add, + &ClientTrustedCaKeys::trusted_ca_keys_free, + }; } } // namespace tls diff --git a/lib/staging/tls/tls.hpp b/lib/staging/tls/tls.hpp index 60f27cb31..aa838010e 100644 --- a/lib/staging/tls/tls.hpp +++ b/lib/staging/tls/tls.hpp @@ -1,10 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright 2024 Pionix GmbH and Contributors to EVerest #ifndef TLS_HPP_ #define TLS_HPP_ -#include "openssl_util.hpp" +#include "extensions/status_request.hpp" +#include "extensions/tls_types.hpp" +#include "extensions/trusted_ca_keys.hpp" #include #include @@ -13,28 +15,18 @@ #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; +constexpr std::uint32_t c_shutdown_timeout_ms = 5000; // 5 seconds +constexpr std::uint32_t c_serve_timeout_ms = 60000; // 60 seconds struct connection_ctx; -struct ocsp_cache_ctx; struct server_ctx; struct client_ctx; @@ -45,183 +37,48 @@ struct client_ctx; * \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. + * unlike std::string this class allows nullptr as a valid setting and it + * doesn't have scope issues since it holds a copy. */ class ConfigItem { private: - char* m_ptr{nullptr}; + std::optional value{}; 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; + ConfigItem(const char* ptr) { + if (ptr != nullptr) { + value = ptr; + } } - - inline bool operator!=(const ConfigItem& obj) const { - return !(*this == obj); + inline operator const char*() const { + return (value) ? value.value().c_str() : nullptr; } }; -// ---------------------------------------------------------------------------- -// 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 + * \note small timeout values (under 200ms) can cause connections to fail + * even on the loopback interface. + * + * Non-blocking I/O is configured on the underlying socket. Its use is controlled + * by the 'timeout_ms' parameter: + * + * timeout_ms is -1 means wait forever. The method will block until there is an + * event on the socket. + + * timeout_ms is 0 means don't wait. The method will return want_read or + * want_write when it is unable to complete the operation. The method should be + * called with the same arguments once read/write is available on the socket - + * via select() or poll(). The underlying socket is available via socket(). + * This supports waiting for events on many sockets. + * A helper method wait_for() is provided where waiting on only one socket is + * needed. + * + * timeout_ms > 0 means wait no more than timeout_ms. */ class Connection { public: @@ -236,20 +93,24 @@ class Connection { }; enum class result_t : std::uint8_t { - success, - error, - timeout, + success, //!< operation completed successfully + closed, //!< connection closed (possibly due to error) + timeout, //!< operation timed out + want_read, //!< non-blocking - operation waiting for read available on socket + want_write, //!< non-blocking - operation waiting for write available on socket }; 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; + std::unique_ptr m_context; //!< opaque connection data + state_t m_state{state_t::idle}; //!< connection state + std::string m_ip; //!< peer IP address + std::string m_service; //!< peer port + std::int32_t m_timeout_ms; //!< default operation timeout -public: + // prevent standalone construction Connection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms); + +public: Connection() = delete; Connection(const Connection&) = delete; Connection(Connection&&) = delete; @@ -257,30 +118,74 @@ class Connection { Connection& operator=(Connection&&) = delete; ~Connection(); + /** + * \brief change the default operation timeout + * \param[in] timeout_ms new timeout value + */ + void timeout(std::int32_t timeout_ms) { + m_timeout_ms = timeout_ms; + } + + /** + * \brief get the default operation timeout + * \return timeout value + */ + [[nodiscard]] std::int32_t timeout() const { + return m_timeout_ms; + } + /** * \brief read bytes from the TLS connection * \param[out] buf pointer to output buffer * \param[in] num size of output buffer + * \param[in] timeout_ms time to wait in milliseconds, -1 is wait forever, 0 is don't wait * \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 + * \return see result_t. On error the connection will have + * been closed */ - [[nodiscard]] result_t read(std::byte* buf, std::size_t num, std::size_t& readbytes); + [[nodiscard]] result_t read(std::byte* buf, std::size_t num, std::size_t& readbytes, int timeout_ms); + [[nodiscard]] inline result_t read(std::byte* buf, std::size_t num, std::size_t& readbytes) { + return read(buf, num, readbytes, m_timeout_ms); + } /** * \brief write bytes to the TLS connection * \param[in] buf pointer to input buffer * \param[in] num size of input buffer + * \param[in] timeout_ms time to wait in milliseconds, -1 is wait forever, 0 is don't wait * \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 + * \return see result_t. On error the connection will have + * been closed */ - [[nodiscard]] result_t write(const std::byte* buf, std::size_t num, std::size_t& writebytes); + [[nodiscard]] result_t write(const std::byte* buf, std::size_t num, std::size_t& writebytes, int timeout_ms); + [[nodiscard]] inline result_t write(const std::byte* buf, std::size_t num, std::size_t& writeBytes) { + return write(buf, num, writeBytes, m_timeout_ms); + } /** * \brief close the TLS connection + * \param[in] timeout_ms time to wait in milliseconds + * \return see result_t + * \note result_t::closed is more likely than result_t::success since the + * connection is closed. + */ + result_t shutdown(int timeout_ms); + inline result_t shutdown() { + return shutdown(c_shutdown_timeout_ms); + } + + /** + * \brief wait for activity on the socket + * \param[in] action is result_t::want_read or result_t::want_write + * \param[in] timeout_ms time to wait in milliseconds, -1 is wait forever, 0 is don't wait + * \return see result_t */ - void shutdown(); + result_t wait_for(result_t action, int timeout_ms); + inline result_t wait_for(result_t action) { + return wait_for(action, m_timeout_ms); + } /** * IP address of the connection's peer @@ -305,28 +210,35 @@ class Connection { } /** - * \brief obtain the underlying socket for use with poll or select + * \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 obtain the peer certificate + * \returns the certificate or nullptr + * \note the certificate must not be freed + */ + [[nodiscard]] const Certificate* peer_certificate() const; }; /** - * \brief class representing a TLS connection + * \brief class representing a TLS connection (server side) */ 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; + using ServerStatusRequestV2 = status_request::ServerStatusRequestV2; + using ServerTrustedCaKeys = trusted_ca_keys::ServerTrustedCaKeys; + using trusted_ca_keys_t = trusted_ca_keys::trusted_ca_keys_t; + using server_trusted_ca_keys_t = trusted_ca_keys::server_trusted_ca_keys_t; + + static std::uint32_t m_count; //!< number of active connections + static std::mutex m_cv_mutex; //!< used for wait_all_closed() + static std::condition_variable m_cv; //!< used for wait_all_closed() + trusted_ca_keys_t m_trusted_ca_keys; //!< trusted CA keys configuration + StatusFlags m_flags; //!< extension flags + server_trusted_ca_keys_t m_tck_data; //!< extension per connection data public: ServerConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms); @@ -337,24 +249,16 @@ class ServerConnection : public Connection { 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 + * \param[in] timeout_ms time to wait in milliseconds, -1 is wait forever, 0 is don't wait + * \return see result_t. On error the connection will have + * been closed */ - [[nodiscard]] bool accept(); + [[nodiscard]] result_t accept(int timeout_ms); + [[nodiscard]] inline result_t accept() { + return accept(m_timeout_ms); + } /** * \brief wait for all connections to be closed @@ -371,7 +275,7 @@ class ServerConnection : public Connection { }; /** - * \brief class representing a TLS connection + * \brief class representing a TLS connection (client side) */ class ClientConnection : public Connection { public: @@ -385,9 +289,14 @@ class ClientConnection : public Connection { /** * \brief run the TLS handshake - * \return true when the TLS connection has been established + * \param[in] timeout_ms time to wait in milliseconds, -1 is wait forever, 0 is don't wait + * \return see result_t. On error the connection will have + * been closed */ - [[nodiscard]] bool connect(); + [[nodiscard]] result_t connect(int timeout_ms); + [[nodiscard]] inline result_t connect() { + return connect(m_timeout_ms); + } }; // ---------------------------------------------------------------------------- @@ -402,14 +311,18 @@ class ClientConnection : public Connection { * * 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. + * 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. + * 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. + * + * Listens on either IPv4 or IPv6. OpenSSL recommends that two listen sockets + * are used to support both IPv4 and IPv6. (not implemented) */ class Server { public: @@ -424,36 +337,58 @@ class Server { stopped, //!< stopped - reinitialisation will be needed }; - struct config_t { - ConfigItem cipher_list{nullptr}; // nullptr means use default - ConfigItem ciphersuites{nullptr}; // nullptr means use default, "" disables TSL 1.3 + struct certificate_config_t { + //!< server certificate is the first certificate in the file followed by any intermediate CAs 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}; - bool verify_client{true}; + ConfigItem trust_anchor_file{nullptr}; //!< one or more trust anchor PEM certificates + ConfigItem private_key_file{nullptr}; //!< key associated with the server certificate + ConfigItem private_key_password{nullptr}; //!< optional password to read private key + std::vector ocsp_response_files; //!< list of OCSP files in certificate chain order + }; + + struct config_t { + ConfigItem cipher_list{nullptr}; //!< nullptr means use default + ConfigItem ciphersuites{nullptr}; //!< nullptr means use default, "" disables TSL 1.3 + + std::vector chains; //!< server certificate chains - must be at least one + //!< one or more trust anchor PEM certificates for client certificate verification + ConfigItem verify_locations_file{nullptr}; + ConfigItem verify_locations_path{nullptr}; //!< for client certificate + std::int32_t io_timeout_ms{-1}; //!< socket timeout in milliseconds (recommend > 1 sec) + bool verify_client{true}; //!< client certificate required + + // config not used on update() + ConfigItem host{nullptr}; //!< see BIO_lookup_ex() + ConfigItem service{nullptr}; //!< TLS port number as a string + int socket{INVALID_SOCKET}; //!< use this specific socket - bypasses socket setup in init_socket() when set + bool ipv6_only{true}; //!< listen on IPv6 only, when false listen on IPv4 only }; + using ConnectionPtr = std::unique_ptr; + using ConnectionHandler = std::function; + using OptionalConfig = std::optional>; + using ConfigurationCallback = std::function; + 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::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}; + using ServerStatusRequestV2 = status_request::ServerStatusRequestV2; + using trusted_ca_keys_t = trusted_ca_keys::trusted_ca_keys_t; + using ServerTrustedCaKeys = trusted_ca_keys::ServerTrustedCaKeys; + + std::unique_ptr m_context; //!< opaque object data + int m_socket{INVALID_SOCKET}; //!< server socket value + volatile bool m_running{false}; //!< server is listening for connections + std::int32_t m_timeout_ms{-1}; //!< default operation timeout passed to new connections + std::atomic_bool m_exit{false}; //!< stop listening for connections + std::atomic m_state{state_t::init_needed}; //!< server state + std::mutex m_mutex; //!< prevent multiple initialisation or serve requests + std::mutex m_cv_mutex; //!< used by wait_running() and wait_stopped() + std::condition_variable m_cv; //!< used by wait_running() and wait_stopped() + OcspCache m_cache; //!< cached OCSP responses + ServerStatusRequestV2 m_status_request_v2; //!< status request extension handler + ServerTrustedCaKeys m_server_trusted_ca_keys; //!< trusted ca keys extension handler + pthread_t m_server_thread{}; //!< serve() POSIX threads ID + static int s_sig_int; //!< signal to use to wakeup serve() + ConfigurationCallback m_init_callback{nullptr}; //!< callback to retrieve SSL configuration /** * \brief initialise the server socket @@ -469,6 +404,19 @@ class Server { */ bool init_ssl(const config_t& cfg); + /** + * \brief initialise server certificate chains + * \param[in] chain_files server certificate chains + * \return true on success + */ + bool init_certificates(const std::vector& chain_files); + + /** + * \brief waits for incoming connections + * \param[in] handler - called with the new connection socket + */ + void wait_for_connection(const ConnectionHandler& handler); + public: Server(); Server(const Server&) = delete; @@ -477,6 +425,12 @@ class Server { Server& operator=(Server&&) = delete; ~Server(); + /** + * \brief configure a signal handler to wakeup serve() + * \note stop() sends interrupt_signal to m_server_thread to interrupt poll() + */ + static void configure_signal_handler(int interrupt_signal); + /** * \brief initialise the server socket and TLS configuration * \param[in] cfg server configuration @@ -494,20 +448,21 @@ class Server { * init_ssl() should return true when SSL has been configured so that the * incoming connection is accepted. */ - state_t init(const config_t& cfg, const std::function& init_ssl); + state_t init(const config_t& cfg, const ConfigurationCallback& init_ssl); /** - * \brief update the OCSP cache and SSL certificates and keys + * \brief update configuration * \param[in] cfg server configuration * \return true on success - * \note used to update OCSP caches and SSL config + * \note used to update OCSP caches and SSL certificates and keys. + * Does not change the listen socket settings */ bool update(const config_t& cfg); /** * \brief wait for incomming connections * \param[in] handler called when there is a new connection - * \return stopped after it has been running, or init_ values when listening + * \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) @@ -516,11 +471,14 @@ class Server { * \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); + state_t serve(const ConnectionHandler& handler); /** * \brief stop listening for new connections * \note returns immediately + * + * may raise a signal on the m_server_thread to terminate poll() so that + * serve() stops quickly. (see configure_signal_handler()) */ void stop(); @@ -550,9 +508,34 @@ class Server { /** * \brief represents a TLS client + * \note mainly intended for unit and system tests */ class Client { public: + using trusted_ca_keys_t = trusted_ca_keys::trusted_ca_keys_t; + using ClientStatusRequestV2 = status_request::ClientStatusRequestV2; + using ClientTrustedCaKeys = trusted_ca_keys::ClientTrustedCaKeys; + + /** + * \brief configure different extension handler callbacks. + * \note all callbacks must be specified, nullptr is not allowed. + * \note mainly used for unit tests to send invalid extensions to the server. + */ + struct override_t { + int (*tlsext_status_cb)(Ssl* ctx, void* object){nullptr}; + 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){nullptr}; + 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){nullptr}; + int (*trusted_ca_keys_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){nullptr}; + void (*trusted_ca_keys_free)(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char* out, + void* object){nullptr}; + }; + /** * \brief client state */ @@ -564,108 +547,70 @@ class Client { }; 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}; + 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}; //!< client certificate and intermediate CAs + const char* private_key_file{nullptr}; //!< private key for client certificate + const char* private_key_password{nullptr}; //!< optional password for private key + const char* verify_locations_file{nullptr}; //!< PEM trust anchors for server certificate + const char* verify_locations_path{nullptr}; //!< for server certificate + trusted_ca_keys_t trusted_ca_keys_data; //!< trusted CA keys configuration data + std::int32_t io_timeout_ms{-1}; //!< default socket timeout in milliseconds (recommend > 1 sec) + bool verify_server{true}; //!< verify the server certificate + bool status_request{false}; //!< include a status request extension in the client hello + bool status_request_v2{false}; //!< include a status request v2 extension in the client hello + bool trusted_ca_keys{false}; //!< include a trusted ca keys extension in the client hello }; + using ConnectionPtr = std::unique_ptr; + private: - std::unique_ptr m_context; - std::int32_t m_timeout_ms{-1}; - std::atomic m_state{state_t::need_init}; + std::unique_ptr m_context; //!< opaque object data + std::int32_t m_timeout_ms{-1}; //!< default operation timeout + trusted_ca_keys_t m_trusted_ca_keys; //!< trusted CA keys configuration data + std::unique_ptr m_status_request_v2; //!< status request extension handler public: Client(); + explicit Client(std::unique_ptr&& handler); 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 initialise the client socket and TLS configuration + * \param[in] cfg server configuration + * \param[in] override override SSL callbacks + * \return true on success + */ + bool init(const config_t& cfg, const override_t& override); + /** * \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 + * \param[in] timeout_ms how long to wait for a connection in milliseconds * \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; + [[nodiscard]] ConnectionPtr connect(const char* host, const char* service, bool ipv6_only, int timeout_ms); + [[nodiscard]] inline ConnectionPtr connect(const char* host, const char* service, bool ipv6_only) { + return connect(host, service, ipv6_only, m_timeout_ms); } /** - * \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 + * \brief the default SSL callbacks */ - 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); + static override_t default_overrides(); }; } // namespace tls diff --git a/lib/staging/util/EnumFlags.hpp b/lib/staging/util/EnumFlags.hpp index af892001e..e4f80788b 100644 --- a/lib/staging/util/EnumFlags.hpp +++ b/lib/staging/util/EnumFlags.hpp @@ -1,20 +1,21 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest +// Copyright 2024 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"); +template class AtomicEnumFlags { + static_assert(std::is_enum(), "Not enum"); + static_assert(std::is_integral(), "Not integer"); static_assert((sizeof(B) * 8) >= static_cast(T::last) + 1, "Underlying flag type too small"); std::atomic _value{0ULL}; +public: constexpr std::size_t bit(const T& flag) const { return 1ULL << static_cast>(flag); } diff --git a/modules/EvseV2G/CMakeLists.txt b/modules/EvseV2G/CMakeLists.txt index da6f883fc..37702aa7a 100644 --- a/modules/EvseV2G/CMakeLists.txt +++ b/modules/EvseV2G/CMakeLists.txt @@ -59,7 +59,7 @@ target_sources(${MODULE_NAME} if(USING_MBED_TLS) # needed for header file enum definition target_include_directories(${MODULE_NAME} PRIVATE - ../../lib/staging/tls + ../../lib/staging/tls ../../lib/staging/util ) target_link_libraries(${MODULE_NAME} PRIVATE diff --git a/modules/EvseV2G/EvseV2G.cpp b/modules/EvseV2G/EvseV2G.cpp index 67e4b77b1..90dee57eb 100644 --- a/modules/EvseV2G/EvseV2G.cpp +++ b/modules/EvseV2G/EvseV2G.cpp @@ -2,12 +2,13 @@ // Copyright (C) 2022-2023 chargebyte GmbH // Copyright (C) 2022-2023 Contributors to EVerest #include "EvseV2G.hpp" -#include "connection.hpp" -#include "everest/logging.hpp" +#include "connection/connection.hpp" #include "log.hpp" #include "sdp.hpp" +#include #ifndef EVEREST_MBED_TLS +#include #include namespace { void log_handler(openssl::log_level_t level, const std::string& str) { @@ -40,6 +41,7 @@ void EvseV2G::init() { #ifndef EVEREST_MBED_TLS (void)openssl::set_log_handler(log_handler); + tls::Server::configure_signal_handler(SIGUSR1); v2g_ctx->tls_server = &tls_server; #endif // EVEREST_MBED_TLS diff --git a/modules/EvseV2G/charger/ISO15118_chargerImpl.cpp b/modules/EvseV2G/charger/ISO15118_chargerImpl.cpp index 2dc8cfe15..c7b8376ee 100644 --- a/modules/EvseV2G/charger/ISO15118_chargerImpl.cpp +++ b/modules/EvseV2G/charger/ISO15118_chargerImpl.cpp @@ -270,7 +270,7 @@ void ISO15118_chargerImpl::handle_stop_charging(bool& stop) { if (stop) { // spawn new thread to not block command handler - std::thread([this, stop] { + std::thread([stop] { // try to gracefully shutdown charging session v2g_ctx->evse_v2g_data.evse_notification = iso2_EVSENotificationType_StopCharging; memset(v2g_ctx->evse_v2g_data.evse_status_code, iso2_DC_EVSEStatusCodeType_EVSE_Shutdown, diff --git a/modules/EvseV2G/connection/tls_connection.cpp b/modules/EvseV2G/connection/tls_connection.cpp index ac6aeced6..586613ad2 100644 --- a/modules/EvseV2G/connection/tls_connection.cpp +++ b/modules/EvseV2G/connection/tls_connection.cpp @@ -3,16 +3,17 @@ #include "tls_connection.hpp" #include "connection.hpp" +#include "everest/logging.hpp" #include "log.hpp" #include "v2g.hpp" #include "v2g_server.hpp" +#include #include #include #include #include #include -#include #include #include #include @@ -37,11 +38,14 @@ ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::s namespace { +// used when ctx->network_read_timeout_tls is 0 +constexpr int default_timeout_ms = 1000; + 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}; + openssl::pkey_ptr contract_public_key{nullptr, nullptr}; auto connection = std::make_unique(); connection->ctx = ctx; connection->is_tls_connection = true; @@ -52,28 +56,46 @@ void process_connection_thread(std::shared_ptr con, struc dlog(DLOG_LEVEL_INFO, "Incoming TLS connection"); - if (con->accept()) { - // TODO(james-ctc) v2g_ctx->tls_key_logging + bool loop{true}; + while (loop) { + loop = false; + const auto result = con->accept(); + switch (result) { + case tls::Connection::result_t::success: - 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"); - } + // TODO(james-ctc) v2g_ctx->tls_key_logging - con->shutdown(); + 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(); + break; + case tls::Connection::result_t::want_read: + case tls::Connection::result_t::want_write: + loop = con->wait_for(result, default_timeout_ms) == tls::Connection::result_t::success; + break; + case tls::Connection::result_t::closed: + case tls::Connection::result_t::timeout: + default: + break; + } } ::connection_teardown(connection.get()); } -void handle_new_connection_cb(std::shared_ptr con, struct v2g_context* ctx) { +void handle_new_connection_cb(tls::Server::ConnectionPtr&& 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); + // passing unique pointers through thread parameters is problematic + std::shared_ptr connection(con.release()); + std::thread connection_loop(process_connection_thread, connection, ctx); connection_loop.detach(); } catch (const std::system_error&) { // unable to start thread @@ -85,7 +107,7 @@ void handle_new_connection_cb(std::shared_ptr con, struct 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); }); + const auto res = ctx->tls_server->serve([ctx](auto con) { handle_new_connection_cb(std::move(con), ctx); }); if (res != tls::Server::state_t::stopped) { dlog(DLOG_LEVEL_ERROR, "tls::Server failed to serve"); } @@ -132,9 +154,10 @@ bool build_config(tls::Server::config_t& config, struct v2g_context* ctx) { // 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(); + auto& ref = config.chains.emplace_back(); + ref.certificate_chain_file = cert_path.c_str(); + ref.private_key_file = key_path.c_str(); + ref.private_key_password = key_password.c_str(); if (info.ocsp) { for (const auto& ocsp : info.ocsp.value()) { @@ -142,7 +165,7 @@ bool build_config(tls::Server::config_t& config, struct v2g_context* ctx) { if (ocsp.ocsp_path) { file = ocsp.ocsp_path.value().c_str(); } - config.ocsp_response_files.push_back(file); + ref.ocsp_response_files.push_back(file); } } @@ -155,19 +178,21 @@ bool build_config(tls::Server::config_t& config, struct v2g_context* ctx) { return bResult; } -bool configure_ssl(tls::Server& server, struct v2g_context* ctx) { - tls::Server::config_t config; - bool bResult{false}; +tls::Server::OptionalConfig configure_ssl(struct v2g_context* ctx) { + try { + dlog(DLOG_LEVEL_WARNING, "configure_ssl"); + auto config = std::make_unique(); - dlog(DLOG_LEVEL_WARNING, "configure_ssl"); + // The config of interest is from Evse Security, no point in updating + // config when there is a problem - // 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); + if (build_config(*config, ctx)) { + return {{std::move(config)}}; + } + } catch (const std::bad_alloc&) { + dlog(DLOG_LEVEL_ERROR, "unable to create TLS config"); } - - return bResult; + return std::nullopt; } } // namespace @@ -191,7 +216,7 @@ int connection_init(struct v2g_context* 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); }); + const auto result = ctx->tls_server->init(config, [ctx]() { return configure_ssl(ctx); }); if ((result == state_t::init_complete) || (result == state_t::init_socket)) { res = 0; } @@ -241,13 +266,19 @@ ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, const s std::size_t bytes_in{0}; auto* ptr = reinterpret_cast(&buf[bytes_read]); - switch (conn->tls_connection->read(ptr, remaining, bytes_in)) { + const auto read_res = conn->tls_connection->read(ptr, remaining, bytes_in); + switch (read_res) { case tls::Connection::result_t::success: bytes_read += bytes_in; break; + case tls::Connection::result_t::want_read: + case tls::Connection::result_t::want_write: + conn->tls_connection->wait_for(read_res, default_timeout_ms); + break; case tls::Connection::result_t::timeout: + // the MBedTLS code loops on timeout, is_sequence_timeout() is used instead break; - case tls::Connection::result_t::error: + case tls::Connection::result_t::closed: default: result = -1; break; @@ -279,13 +310,19 @@ ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::s std::size_t bytes_out{0}; const auto* ptr = reinterpret_cast(&buf[bytes_written]); - switch (conn->tls_connection->write(ptr, remaining, bytes_out)) { + const auto write_res = conn->tls_connection->write(ptr, remaining, bytes_out); + switch (write_res) { case tls::Connection::result_t::success: bytes_written += bytes_out; break; + case tls::Connection::result_t::want_read: + case tls::Connection::result_t::want_write: + conn->tls_connection->wait_for(write_res, default_timeout_ms); + break; case tls::Connection::result_t::timeout: + // the MBedTLS code loops on timeout break; - case tls::Connection::result_t::error: + case tls::Connection::result_t::closed: default: result = -1; break; diff --git a/modules/EvseV2G/crypto/crypto_mbedtls.cpp b/modules/EvseV2G/crypto/crypto_mbedtls.cpp index 5f1b96872..bd149e666 100644 --- a/modules/EvseV2G/crypto/crypto_mbedtls.cpp +++ b/modules/EvseV2G/crypto/crypto_mbedtls.cpp @@ -373,7 +373,7 @@ verify_result_t verify_certificate(Certificate_ptr& contract_crt, const void* ch const char* mo_root_cert_path, bool debugMode) { Certificate_ptr contract_root_crt; uint32_t flags; - verify_result_t result{verify_result_t::verified}; + 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)) { diff --git a/modules/EvseV2G/crypto/crypto_openssl.cpp b/modules/EvseV2G/crypto/crypto_openssl.cpp index a142ba0c9..96c482aea 100644 --- a/modules/EvseV2G/crypto/crypto_openssl.cpp +++ b/modules/EvseV2G/crypto/crypto_openssl.cpp @@ -127,7 +127,7 @@ bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, EVP_P return bRes; } -bool load_contract_root_cert(::openssl::CertificateList& trust_anchors, const char* V2G_file_path, +bool load_contract_root_cert(::openssl::certificate_list& 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 @@ -145,7 +145,7 @@ bool load_contract_root_cert(::openssl::CertificateList& trust_anchors, const ch return !trust_anchors.empty(); } -int load_certificate(::openssl::CertificateList* chain, const std::uint8_t* bytes, std::uint16_t bytesLen) { +int load_certificate(::openssl::certificate_list* chain, const std::uint8_t* bytes, std::uint16_t bytesLen) { assert(chain != nullptr); int result{-1}; @@ -158,12 +158,12 @@ int load_certificate(::openssl::CertificateList* chain, const std::uint8_t* byte return result; } -int parse_contract_certificate(::openssl::Certificate_ptr& crt, const std::uint8_t* buf, std::size_t buflen) { +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 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()) { @@ -173,7 +173,7 @@ std::string getEmaidFromContractCert(const ::openssl::Certificate_ptr& crt) { return cert_emaid; } -std::string chain_to_pem(const ::openssl::Certificate_ptr& cert, const ::openssl::CertificateList* chain) { +std::string chain_to_pem(const ::openssl::certificate_ptr& cert, const ::openssl::certificate_list* chain) { assert(chain != nullptr); std::string contract_cert_chain_pem(::openssl::certificate_to_pem(cert.get())); @@ -189,13 +189,13 @@ std::string chain_to_pem(const ::openssl::Certificate_ptr& cert, const ::openssl return contract_cert_chain_pem; } -verify_result_t verify_certificate(const ::openssl::Certificate_ptr& cert, const ::openssl::CertificateList* chain, +verify_result_t verify_certificate(const ::openssl::certificate_ptr& cert, const ::openssl::certificate_list* 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; + verify_result_t result{verify_result_t::Verified}; + ::openssl::certificate_list trust_anchors; if (!load_contract_root_cert(trust_anchors, v2g_root_cert_path, mo_root_cert_path)) { result = verify_result_t::NoCertificateAvailable; diff --git a/modules/EvseV2G/crypto/crypto_openssl.hpp b/modules/EvseV2G/crypto/crypto_openssl.hpp index a65b56401..97646febb 100644 --- a/modules/EvseV2G/crypto/crypto_openssl.hpp +++ b/modules/EvseV2G/crypto/crypto_openssl.hpp @@ -41,7 +41,7 @@ bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, evp_p * \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, +bool load_contract_root_cert(::openssl::certificate_list& trust_anchors, const char* V2G_file_path, const char* MO_file_path); /** @@ -59,7 +59,7 @@ constexpr void free_connection_crypto_data(v2g_connection* conn) { * \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); +int load_certificate(::openssl::certificate_list* chain, const std::uint8_t* bytes, std::uint16_t bytesLen); /** * \brief load the contract certificate from the V2G message as DER bytes @@ -68,14 +68,14 @@ int load_certificate(::openssl::CertificateList* chain, const std::uint8_t* byte * \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); +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); +std::string getEmaidFromContractCert(const ::openssl::certificate_ptr& crt); /** * \brief convert a list of certificates into a PEM string starting with the contract certificate @@ -83,7 +83,7 @@ std::string getEmaidFromContractCert(const ::openssl::Certificate_ptr& crt); * \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); +std::string chain_to_pem(const ::openssl::certificate_ptr& cert, const ::openssl::certificate_list* chain); /** * \brief verify certification path of the contract certificate through to a trust anchor @@ -94,7 +94,7 @@ std::string chain_to_pem(const ::openssl::Certificate_ptr& cert, const ::openssl * \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, +verify_result_t verify_certificate(const ::openssl::certificate_ptr& cert, const ::openssl::certificate_list* chain, const char* v2g_root_cert_path, const char* mo_root_cert_path, bool debugMode); } // namespace crypto::openssl diff --git a/modules/EvseV2G/iso_server.cpp b/modules/EvseV2G/iso_server.cpp index 9998fc102..5cdf04279 100644 --- a/modules/EvseV2G/iso_server.cpp +++ b/modules/EvseV2G/iso_server.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2023 chargebyte GmbH // Copyright (C) 2023 Contributors to EVerest +#include "openssl_util.hpp" #include #include //for V2GTP_HEADER_LENGTHs #include @@ -455,12 +456,12 @@ static bool publish_iso_certificate_installation_exi_req(struct v2g_context* ctx // PnC only bool rv = true; - unsigned char* base64Buffer = NULL; - size_t olen; types::iso15118_charger::RequestExiStreamSchema certificate_request; #ifdef EVEREST_MBED_TLS /* Parse contract leaf certificate */ + unsigned char* base64Buffer = NULL; + size_t olen; mbedtls_base64_encode(NULL, 0, &olen, static_cast(AExiBuffer), AExiBufferSize); if (MQTT_MAX_PAYLOAD_SIZE < olen) { @@ -494,9 +495,11 @@ static bool publish_iso_certificate_installation_exi_req(struct v2g_context* ctx certificate_request.certificate_action = types::iso15118_charger::CertificateActionEnum::Install; ctx->p_charger->publish_certificate_request(certificate_request); +#ifdef EVEREST_MBED_TLS exit: if (base64Buffer != NULL) free(base64Buffer); +#endif // EVEREST_MBED_TLS return rv; } @@ -744,7 +747,7 @@ static enum v2g_event handle_iso_payment_service_selection(struct v2g_connection * in the message; if we are not happy -> bail out */ for (idx = 0; idx < conn->ctx->evse_v2g_data.payment_option_list_len; idx++) { - if ((conn->ctx->evse_v2g_data.payment_option_list[idx] == req->SelectedPaymentOption)) { + if (conn->ctx->evse_v2g_data.payment_option_list[idx] == req->SelectedPaymentOption) { list_element_found = true; conn->ctx->p_charger->publish_selected_payment_option( static_cast(req->SelectedPaymentOption)); @@ -852,8 +855,8 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { Certificate_ptr contract_crt; const void* chain{nullptr}; #else - Certificate_ptr contract_crt{nullptr, nullptr}; - CertificateList chain{}; + certificate_ptr contract_crt{nullptr, nullptr}; + certificate_list chain{}; #endif // EVEREST_MBED_TLS if (req->ContractSignatureCertChain.Certificate.bytesLen != 0) { @@ -942,7 +945,7 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { err = -1; switch (vRes) { - case crypto::verify_result_t::verified: + case crypto::verify_result_t::Verified: err = 0; break; case crypto::verify_result_t::CertificateExpired: @@ -1024,7 +1027,6 @@ static enum v2g_event handle_iso_authorization(struct v2g_connection* conn) { struct iso2_AuthorizationReqType* req = &conn->exi_in.iso2EXIDocument->V2G_Message.Body.AuthorizationReq; struct iso2_AuthorizationResType* res = &conn->exi_out.iso2EXIDocument->V2G_Message.Body.AuthorizationRes; enum v2g_event next_event = V2G_EVENT_NO_EVENT; - struct timespec ts_abs_timeout; bool is_payment_option_contract = conn->ctx->session.iso_selected_payment_option == iso2_paymentOptionType_Contract; /* At first, publish the received ev request message to the customer mqtt interface */ @@ -1109,7 +1111,6 @@ static enum v2g_event handle_iso_charge_parameter_discovery(struct v2g_connectio struct iso2_ChargeParameterDiscoveryResType* res = &conn->exi_out.iso2EXIDocument->V2G_Message.Body.ChargeParameterDiscoveryRes; enum v2g_event next_event = V2G_EVENT_NO_EVENT; - struct timespec ts_abs_timeout; /* At first, publish the received ev request message to the MQTT interface */ publish_iso_charge_parameter_discovery_req(conn->ctx, req); @@ -1425,8 +1426,8 @@ static enum v2g_event handle_iso_power_delivery(struct v2g_connection* conn) { for (sa_schedule_tuple_idx = 0; sa_schedule_tuple_idx < conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.arrayLen; sa_schedule_tuple_idx++) { - if ((conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[sa_schedule_tuple_idx] - .SAScheduleTupleID == req->SAScheduleTupleID)) { + if (conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[sa_schedule_tuple_idx] + .SAScheduleTupleID == req->SAScheduleTupleID) { entry_found = true; conn->ctx->session.sa_schedule_tuple_id = req->SAScheduleTupleID; break; @@ -1996,7 +1997,6 @@ static enum v2g_event handle_iso_session_stop(struct v2g_connection* conn) { enum v2g_event iso_handle_request(v2g_connection* conn) { struct iso2_exiDocument* exi_in = conn->exi_in.iso2EXIDocument; struct iso2_exiDocument* exi_out = conn->exi_out.iso2EXIDocument; - bool resume_trial; enum v2g_event next_v2g_event = V2G_EVENT_TERMINATE_CONNECTION; /* extract session id */ diff --git a/modules/EvseV2G/tests/CMakeLists.txt b/modules/EvseV2G/tests/CMakeLists.txt index 2cc4fc01c..5b5adf32b 100644 --- a/modules/EvseV2G/tests/CMakeLists.txt +++ b/modules/EvseV2G/tests/CMakeLists.txt @@ -1,8 +1,28 @@ get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR) find_package(libevent) +set(TLS_TEST_FILES + alt_openssl-pki.conf + iso_pkey.asn1 + openssl-pki.conf + ocsp_response.der + pki.sh +) + +add_custom_command( + OUTPUT ${TLS_TEST_FILES} + COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/pki + COMMAND cd pki && cp ${TLS_TEST_FILES} ${CMAKE_CURRENT_BINARY_DIR}/ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}../../../../lib/staging/tls/tests/ +) + +add_custom_target(v2g_test_files_target + DEPENDS ${TLS_TEST_FILES} +) + set(TLS_GTEST_NAME v2g_openssl_test) add_executable(${TLS_GTEST_NAME}) +add_dependencies(${TLS_GTEST_NAME} v2g_test_files_target) add_dependencies(${TLS_GTEST_NAME} generate_cpp_files) @@ -70,19 +90,5 @@ target_link_libraries(${V2G_MAIN_NAME} PRIVATE -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}) +add_test(${TLS_GTEST_NAME} ${TLS_GTEST_NAME}) diff --git a/modules/EvseV2G/tests/openssl_test.cpp b/modules/EvseV2G/tests/openssl_test.cpp index c9e47f934..ecc982c71 100644 --- a/modules/EvseV2G/tests/openssl_test.cpp +++ b/modules/EvseV2G/tests/openssl_test.cpp @@ -34,6 +34,8 @@ struct test_vectors_t { const std::uint8_t digest[32]; }; +#if 0 +// not used, useful to keep all the exi message test values together constexpr std::uint8_t sign_test[] = {0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}; @@ -54,6 +56,7 @@ constexpr std::uint8_t iso_public_key[] = {0x43, 0xe4, 0xfc, 0x4c, 0xcb, 0x64, 0 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}; +#endif // 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, @@ -64,6 +67,8 @@ constexpr std::uint8_t iso_exi_a_hash[] = {0xd1, 0xb5, 0xe0, 0x3d, 0x00, 0x65, 0 0x84, 0x45, 0x30, 0x51, 0xeb, 0x54, 0xca, 0x18, 0xfc, 0x0e, 0x09, 0x16, 0x17, 0x4f, 0x8b, 0x3c, 0x77, 0xa9, 0x8f, 0x4a, 0xa9}; +#if 0 +// not used, useful to keep all the exi message test values together // 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, @@ -77,6 +82,7 @@ constexpr std::uint8_t iso_exi_b[] = { 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}; +#endif // checked okay constexpr std::uint8_t iso_exi_b_hash[] = {0xa4, 0xe9, 0x03, 0xe1, 0x82, 0x43, 0x04, 0x1b, 0x55, 0x4e, 0x11, @@ -90,6 +96,8 @@ constexpr std::uint8_t iso_exi_sig[] = {0x4c, 0x8f, 0x20, 0xc1, 0x40, 0x0b, 0xa6 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}; +#if 0 +// not used, useful to keep all the exi message test values together const char iso_exi_a_hash_b64[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk="; const char iso_exi_a_hash_b64_nl[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=\n"; @@ -97,6 +105,7 @@ const char iso_exi_sig_b64[] = "TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBvqNVDCLpY8FZrlrr2ks5ZUA=="; const char iso_exi_sig_b64_nl[] = "TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBv\nqNVDCLpY8FZrlrr2ks5ZUA==\n"; +#endif TEST(openssl, verifyIso) { auto* bio = BIO_new_file("iso_priv.pem", "r"); @@ -105,8 +114,8 @@ TEST(openssl, verifyIso) { 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))); + auto sig = openssl::bn_to_signature(&iso_exi_sig[0], &iso_exi_sig[32]); + EXPECT_TRUE(openssl::verify(pkey, sig.get(), sig.size(), &iso_exi_b_hash[0], sizeof(iso_exi_b_hash))); EVP_PKEY_free(pkey); } diff --git a/modules/EvseV2G/tests/v2g_main.cpp b/modules/EvseV2G/tests/v2g_main.cpp index a773dcad2..c1c3e97c3 100644 --- a/modules/EvseV2G/tests/v2g_main.cpp +++ b/modules/EvseV2G/tests/v2g_main.cpp @@ -83,7 +83,7 @@ struct EvseSecurity : public module::stub::evse_securityIntfStub { return "client_root_cert.pem"; } - virtual Result get_leaf_certificate_info(const Requirement& req, const Parameters& args) { + virtual Result get_leaf_certificate_info(const Requirement& req, const Parameters& args) override { // using types::evse_security::CertificateHashDataType; using types::evse_security::CertificateInfo; using types::evse_security::CertificateOCSP; diff --git a/modules/EvseV2G/v2g.hpp b/modules/EvseV2G/v2g.hpp index e0de0b474..9b69a0973 100644 --- a/modules/EvseV2G/v2g.hpp +++ b/modules/EvseV2G/v2g.hpp @@ -403,7 +403,7 @@ struct v2g_connection { } conn; tls::Connection* tls_connection; - openssl::PKey_ptr* pubkey; + openssl::pkey_ptr* pubkey; #endif // EVEREST_MBED_TLS ssize_t (*read)(struct v2g_connection* conn, unsigned char* buf, std::size_t count); diff --git a/modules/EvseV2G/v2g_server.cpp b/modules/EvseV2G/v2g_server.cpp index ba2eed150..4cb886327 100644 --- a/modules/EvseV2G/v2g_server.cpp +++ b/modules/EvseV2G/v2g_server.cpp @@ -95,8 +95,10 @@ static types::iso15118_charger::V2gMessageId get_v2g_message_id(enum V2gMsgTypeI case V2G_SESSION_STOP_MSG: return is_req == true ? types::iso15118_charger::V2gMessageId::SessionStopReq : types::iso15118_charger::V2gMessageId::SessionStopRes; + case V2G_UNKNOWN_MSG: + default: + return types::iso15118_charger::V2gMessageId::UnknownMessage; } - return types::iso15118_charger::V2gMessageId::UnknownMessage; } /*!