Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1 @@
!*.patch

View File

@@ -0,0 +1,31 @@
load("@rules_cc//cc:defs.bzl", "cc_library")
cc_library(
name = "tls",
srcs = [
"extensions/helpers.cpp",
"extensions/status_request.cpp",
"extensions/trusted_ca_keys.cpp",
"src/openssl_conv.cpp",
"src/openssl_util.cpp",
"src/tls.cpp",
],
hdrs = [
"extensions/helpers.hpp",
"extensions/status_request.hpp",
"extensions/trusted_ca_keys.hpp",
"include/everest/tls/openssl_conv.hpp",
"include/everest/tls/openssl_util.hpp",
"include/everest/tls/tls.hpp",
"include/everest/tls/tls_types.hpp"
],
copts = ["-std=c++17"],
includes = [".", "include"],
visibility = ["//visibility:public"],
deps = [
"//lib/everest/util",
"//lib/everest/evse_security:libevse-security",
"//third-party/bazel/openssl:crypto",
"//third-party/bazel/openssl:ssl",
],
)

View File

@@ -0,0 +1,61 @@
add_library(tls STATIC)
add_library(everest::tls ALIAS tls)
ev_register_library_target(tls)
find_package(OpenSSL 3)
target_sources(tls
PRIVATE
extensions/status_request.cpp
extensions/trusted_ca_keys.cpp
src/openssl_conv.cpp
src/openssl_util.cpp
src/tls.cpp
)
target_compile_definitions(tls PRIVATE
-DLIBEVSE_CRYPTO_SUPPLIER_OPENSSL
)
target_include_directories(tls
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
# FIXME (aw): check whether all of this needs to be publicly exposed
target_link_libraries(tls
PUBLIC
OpenSSL::SSL
OpenSSL::Crypto
everest::evse_security
everest::util
)
set_target_properties(tls
PROPERTIES
POSITION_INDEPENDENT_CODE ON
)
if(EVEREST_CORE_BUILD_TESTING)
add_subdirectory(tests)
endif()
if(DISABLE_EDM)
install(
TARGETS
tls
EXPORT everest-tls-targets
LIBRARY
)
evc_setup_package(
NAME everest-tls
EXPORT everest-tls-targets
NAMESPACE everest
ADDITIONAL_CONTENT
"find_dependency(everest-util)"
"find_dependency(everest-evse_security)"
"find_dependency(OpenSSL)"
)
endif()

View File

@@ -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 <cstddef>
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<std::uint32_t>(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<std::uint32_t>(*ptr++);
}
out.flags(sav);
return out;
}

View File

@@ -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 <cstdint>
#include <cstring>
#include <iomanip>
#include <sstream>
#include <string>
#ifdef UNIT_TEST
#include <iostream>
#endif
#include <everest/tls/openssl_util.hpp>
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 <typename T>
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 <typename T>
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<std::uint32_t>(ptr[0]) << 16U) | (static_cast<std::uint32_t>(ptr[1]) << 8U) |
static_cast<std::uint32_t>(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<std::uint32_t>(ptr[0]) << 8U) | static_cast<std::uint32_t>(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 <typename T> 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<int>(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_

View File

@@ -0,0 +1,587 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include "extensions/status_request.hpp"
#include "helpers.hpp"
#include <cstdint>
#include <everest/tls/openssl_util.hpp>
#include <cassert>
#include <limits>
#include <openssl/ocsp.h>
#include <openssl/ssl.h>
#include <openssl/tls1.h>
#include <string>
#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<OCSP_RESPONSE>;
struct ocsp_cache_ctx {
std::map<OcspCache::digest_t, OCSP_RESPONSE_ptr> cache;
};
// ----------------------------------------------------------------------------
// OcspCache
OcspCache::OcspCache() : m_context(std::make_unique<ocsp_cache_ctx>()) {
}
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<digest_t, OCSP_RESPONSE_ptr> updates;
for (const auto& entry : filenames) {
const auto& digest = std::get<digest_t>(entry);
const auto* filename = std::get<const char*>(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<OCSP_RESPONSE>(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<ServerStatusRequestV2*>(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<std::pair<std::size_t, std::uint8_t*>> 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<std::uint8_t*>(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<std::uint8_t*>(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<ServerStatusRequestV2*>(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<unsigned char*>(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<StatusFlags*>(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<std::int64_t>(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<std::int32_t>::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<std::int32_t>(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<ClientStatusRequestV2*>(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

View File

@@ -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 <everest/tls/openssl_util.hpp>
#include <everest/tls/tls_types.hpp>
#include <cstddef>
#include <memory>
#include <mutex>
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<digest_t, const char*>;
using ocsp_entry_list_t = std::vector<ocsp_entry_t>;
using OcspResponse_t = std::shared_ptr<OcspResponse>;
private:
std::unique_ptr<ocsp_cache_ctx> 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<digest_t>;
/// \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_

View File

@@ -0,0 +1,444 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include "extensions/trusted_ca_keys.hpp"
#include "helpers.hpp"
#include <boost/smart_ptr/shared_ptr.hpp>
#include <everest/tls/openssl_util.hpp>
#include <cassert>
#include <limits>
#include <openssl/ssl.h>
#include <string>
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<IdentifierType>(*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<int>(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<std::uint16_t>::max())) {
result = trusted_authority(size);
auto* ptr = result.get();
auto remaining = static_cast<std::int32_t>(size);
uint16(ptr, size - 2);
update_position(ptr, remaining, 2);
if (keys.pre_agreed) {
*ptr = static_cast<std::uint8_t>(IdentifierType::pre_agreed);
update_position(ptr, remaining, 1);
}
for (const auto& i : keys.cert_sha1_hash) {
*ptr = static_cast<std::uint8_t>(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<std::uint8_t>(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<std::uint8_t>(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<std::uint16_t>::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<ServerTrustedCaKeys*>(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<server_trusted_ca_keys_t*>(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<trusted_ca_keys_t*>(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<unsigned char*>(out));
}
} // namespace tls::trusted_ca_keys

View File

@@ -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 <everest/tls/openssl_util.hpp>
#include <everest/tls/tls_types.hpp>
#include <mutex>
namespace tls::trusted_ca_keys {
using der_ptr = openssl::der_ptr;
using DER = openssl::DER;
using der_list = std::vector<DER>;
using digest_t = openssl::sha_1_digest_t;
using digest_list = std::vector<digest_t>;
// 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_

View File

@@ -0,0 +1,73 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#ifndef OPENSSL_CONV_HPP_
#define OPENSSL_CONV_HPP_
#include <everest/tls/openssl_util.hpp>
#include <evse_security/certificate/x509_wrapper.hpp>
#include <evse_security/crypto/interface/crypto_types.hpp>
/**
* \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
#endif // OPENSSL_CONV_HPP_

View File

@@ -0,0 +1,549 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#ifndef OPENSSL_UTIL_HPP_
#define OPENSSL_UTIL_HPP_
#include <everest/tls/tls_types.hpp>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <map>
#include <memory>
#include <string>
#include <vector>
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,
CertChainError,
CertificateExpired,
CertificateRevoked,
NoCertificateAvailable, // no root certificate available at all
CertificateNotAllowed, // correct root certificate not available
OtherError,
};
constexpr std::size_t signature_size = 64;
constexpr std::size_t signature_n_size = 32;
constexpr std::size_t signature_der_size = 128;
constexpr std::size_t sha_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<std::uint8_t, sha_1_digest_size>;
using sha_256_digest_t = std::array<std::uint8_t, sha_256_digest_size>;
using sha_384_digest_t = std::array<std::uint8_t, sha_384_digest_size>;
using sha_512_digest_t = std::array<std::uint8_t, sha_512_digest_size>;
using bn_t = std::array<std::uint8_t, signature_n_size>;
using bn_const_t = std::array<const std::uint8_t, signature_n_size>;
using certificate_ptr = std::unique_ptr<x509_st, void (*)(x509_st*)>;
using certificate_list = std::vector<certificate_ptr>;
using der_underlying_t = std::uint8_t;
using der_ptr = std::unique_ptr<der_underlying_t, void (*)(der_underlying_t*)>;
using pkey_ptr = std::unique_ptr<evp_pkey_st, void (*)(evp_pkey_st*)>;
/**
* \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<chain_filenames_t>;
/// 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<chain_info_t>;
/// 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<chain_t>;
/**
* \brief sign using ECDSA on curve secp256r1/prime256v1/P-256 of a SHA 256 digest
* \param[in] pkey the private key
* \param[out] r the R component of the signature as a BIGNUM
* \param[out] s the S component of the signature as a BIGNUM
* \param[out] digest the SHA256 digest to sign
* \return true when successful
*/
bool sign(evp_pkey_st* pkey, bn_t& r, bn_t& s, const sha_256_digest_t& digest);
/**
* \brief sign using ECDSA on curve secp256r1/prime256v1/P-256 of a SHA 256 digest
* \param[in] pkey the private key
* \param[out] sig the buffer where the DER encoded signature will be placed
* \param[inout] siglen the size of the signature buffer, updated to be the size of the signature
* \param[in] tbs a pointer to the SHA256 digest
* \param[in] tbslen the size of the SHA256 digest
* \return true when successful
*/
bool sign(evp_pkey_st* pkey, unsigned char* sig, std::size_t& siglen, const unsigned char* tbs, std::size_t tbslen);
/**
* \brief verify a signature against a SHA 256 digest using ECDSA on curve secp256r1/prime256v1/P-256
* \param[in] pkey the public key
* \param[in] r the R component of the signature as a BIGNUM
* \param[in] s the S component of the signature as a BIGNUM
* \param[in] digest the SHA256 digest to sign
* \return true when successful
*/
bool verify(evp_pkey_st* pkey, const bn_t& r, const bn_t& s, const sha_256_digest_t& digest);
/**
* \brief verify a signature against a SHA 256 digest using ECDSA on curve secp256r1/prime256v1/P-256
* \param[in] pkey the public key
* \param[in] r the R component of the signature as a BIGNUM (0-padded 32 bytes)
* \param[in] s the S component of the signature as a BIGNUM (0-padded 32 bytes)
* \param[in] digest the SHA256 digest to sign
* \return true when successful
*/
bool verify(evp_pkey_st* pkey, const std::uint8_t* r, const std::uint8_t* s, const sha_256_digest_t& digest);
/**
* \brief verify a signature against a SHA 256 digest using ECDSA on curve secp256r1/prime256v1/P-256
* \param[in] pkey the public key
* \param[in] sig the DER encoded signature
* \param[in] siglen the size of the DER encoded signature
* \param[in] tbs a pointer to the SHA256 digest
* \param[in] tbslen the size of the SHA256 digest
* \return true when successful
*/
bool verify(evp_pkey_st* pkey, const unsigned char* sig, std::size_t siglen, const unsigned char* tbs,
std::size_t tbslen);
/**
* \brief calculate the 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
* \param[in] len the length of the data
* \param[out] the SHA256 digest
* \return true on success
*/
bool sha_256(const void* data, std::size_t len, sha_256_digest_t& digest);
/**
* \brief calculate the SHA384 digest over an array of bytes
* \param[in] data the start of the data
* \param[in] len the length of the data
* \param[out] the SHA384 digest
* \return true on success
*/
bool sha_384(const void* data, std::size_t len, sha_384_digest_t& digest);
/**
* \brief calculate the SHA512 digest over an array of bytes
* \param[in] data the start of the data
* \param[in] len the length of the data
* \param[out] the SHA512 digest
* \return true on success
*/
bool sha_512(const void* data, std::size_t len, sha_512_digest_t& digest);
/**
* \brief decode a base64 string into it's binary form
* \param[in] text the base64 string (does not need to be \0 terminated)
* \param[in] len the length of the string (excluding any terminating \0)
* \return binary array or empty on error
* \note tolerates stray '\t', '\n', '\v', '\f', '\r', ' ' (matching
* EVP_Decode*'s B64_WS set); other non-alphabet bytes cause the
* underlying BIO_f_base64 to fail and an empty vector is returned.
* An embedded '\0' terminates the scan: anything past the first
* '\0' is ignored.
*/
std::vector<std::uint8_t> base64_decode(const char* text, std::size_t len);
/**
* \brief decode a base64 string into it's binary form
* \param[in] text the base64 string (does not need to be \0 terminated)
* \param[in] len the length of the string (excluding any terminating \0)
* \param[out] out_data where to place the decoded data
* \param[inout] out_len the size of out_data, updated to be the length of the decoded data
* \return true on success
*/
bool base64_decode(const char* text, std::size_t len, std::uint8_t* out_data, std::size_t& out_len);
/**
* \brief encode data into a base64 text string
* \param[in] data the data to encode
* \param[in] len the length of the data
* \param[in] newLine when true add a \n to break the result into multiple lines
* \return base64 string or empty on error
*/
std::string base64_encode(const std::uint8_t* data, std::size_t len, bool newLine);
/**
* \brief encode data into a base64 text string
* \param[in] data the data to encode
* \param[in] len the length of the data
* \return base64 string or empty on error
* \note the return string doesn't include line breaks
*/
inline std::string base64_encode(const std::uint8_t* data, std::size_t len) {
return base64_encode(data, len, false);
}
/**
* \brief zero a structure
* \param mem the structure to zero
*/
template <typename T> 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 its length
*/
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 its length
*/
DER bn_to_signature(const std::uint8_t* r, const std::uint8_t* s);
/**
* \brief convert DER signature into BIGNUM R and S components
* \param[out] r the BIGNUM R component of the signature
* \param[out] s the BIGNUM S component of the signature
* \param[in] sig_p a pointer to the DER encoded signature
* \param[in] len the length of the DER encoded signature
* \return true when successful
*/
bool signature_to_bn(openssl::bn_t& r, openssl::bn_t& s, const std::uint8_t* sig_p, std::size_t len);
/**
* \brief load any PEM encoded certificates from a string
* \param[in] pem_string
* \return a list of 0 or more certificates
* \note PEM string only supports certificates and not other PEM types
*/
certificate_list load_certificates_pem(const char* pem_string);
/**
* \brief load any PEM encoded certificates from a file
* \param[in] filename
* \return a list of 0 or more certificates
*/
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<const char*>& 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
* \param[in] cert the certificate
* \return the PEM string or empty on error
*/
std::string certificate_to_pem(const x509_st* cert);
/**
* \brief convert a PEM string to a certificate
* \param[in] pem the PEM string
* \return the certificate or empty unique_ptr on error
*/
certificate_ptr pem_to_certificate(const std::string& pem);
/**
* \brief parse a DER (ASN.1) encoded certificate
* \param[in] der a pointer to the DER encoded certificate
* \param[in] len the length of the DER encoded certificate
* \return the certificate or empty unique_ptr on error
*/
certificate_ptr der_to_certificate(const std::uint8_t* der, std::size_t len);
/**
* \brief encode a certificate to DER (ASN.1)
* \param[in] cert the certificate
* \return the DER encoded certificate or nullptr on error
*/
DER certificate_to_der(const x509_st* cert);
/**
* \brief verify a certificate against a certificate chain and trust anchors
* \param[in] cert the certificate to verify - when nullptr the certificate must
* be the first certificate in the untrusted list
* \param[in] trust_anchors a list of trust anchors. Must not contain any
* intermediate CAs
* \param[in] untrusted intermediate CAs needed to form a chain from the leaf
* certificate to one of the supplied trust anchors
*/
verify_result_t verify_certificate(const x509_st* cert, const certificate_list& trust_anchors,
const certificate_list& untrusted);
/**
* \brief extract the certificate subject as a dictionary of name/value pairs
* \param cert the certificate
* \return dictionary of the (short name, value) pairs
* \note short name examples "CN" for CommonName "OU" for OrganizationalUnit
* "C" for Country ...
*/
std::map<std::string, std::string> 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);
/**
* \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
* \note this is the hash of the whole certificate including signature
*/
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,
info,
warning,
error,
};
/**
* \brief log an OpenSSL event
* \param[in] level the event level
* \param[in] str string to display
* \note any OpenSSL error is displayed after the string
*/
void log(log_level_t level, const std::string& str);
static inline void log_error(const std::string& str) {
log(log_level_t::error, str);
}
static inline void log_warning(const std::string& str) {
log(log_level_t::warning, str);
}
static inline void log_debug(const std::string& str) {
log(log_level_t::debug, str);
}
static inline void log_info(const std::string& str) {
log(log_level_t::info, str);
}
using log_handler_t = void (*)(log_level_t level, const std::string& err);
/**
* \brief set log handler function
* \param[in] handler a pointer to the function
* \return the pointer to the previous handler or nullptr
* where there is no previous handler
*/
log_handler_t set_log_handler(log_handler_t handler);
} // namespace openssl
#endif // OPENSSL_UTIL_HPP_

View File

@@ -0,0 +1,691 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#ifndef TLS_HPP_
#define TLS_HPP_
#include "extensions/status_request.hpp"
#include "extensions/trusted_ca_keys.hpp"
#include <everest/tls/tls_types.hpp>
#include <atomic>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <memory>
#include <mutex>
#include <netinet/in.h>
#include <openssl/types.h>
#include <optional>
#include <pthread.h>
#include <string>
#include <unistd.h>
namespace tls {
constexpr int INVALID_SOCKET{-1};
constexpr std::uint32_t c_shutdown_timeout_ms = 5000; // 5 seconds
#ifdef UNIT_TEST
constexpr std::uint32_t c_serve_timeout_ms = 1000; // 1 second
#else
constexpr std::uint32_t c_serve_timeout_ms = 60000; // 60 seconds
#endif
struct connection_ctx;
struct server_ctx;
struct client_ctx;
// ----------------------------------------------------------------------------
// ConfigItem - store configuration item allowing nullptr
/**
* \brief class to hold configuration strings, behaves like const char *
* but keeps a copy
*
* unlike std::string this class allows nullptr as a valid setting and it
* doesn't have scope issues since it holds a copy.
*/
class ConfigItem {
private:
std::optional<std::string> value{};
public:
ConfigItem() = default;
ConfigItem(const char* ptr) {
if (ptr != nullptr) {
value = ptr;
}
}
inline operator const char*() const {
return (value) ? value.value().c_str() : nullptr;
}
};
class TlsKeyLoggingServer {
public:
TlsKeyLoggingServer(const std::string& interface_name, uint16_t port_);
~TlsKeyLoggingServer();
ssize_t send(const char* line);
auto get_fd() const {
return fd;
}
auto get_port() const {
return port;
}
private:
int fd{-1};
uint16_t port{0};
sockaddr_in6 destination_address{};
};
// ----------------------------------------------------------------------------
// 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:
/**
* \brief connection state
*/
enum class state_t : std::uint8_t {
idle, //!< no connection in progress
connected, //!< active connection
closed, //!< connection has closed
fault, //!< connection has faulted
};
enum class result_t : std::uint8_t {
success, //!< 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<connection_ctx> 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
// 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;
Connection& operator=(const Connection&) = delete;
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 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, 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 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, 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
*/
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
*/
[[nodiscard]] const std::string& ip_address() const {
return m_ip;
}
/**
* Service (port number) of the connection's peer
*/
[[nodiscard]] const std::string& service() const {
return m_service;
}
/**
* \brief return the current state
* \return the current state
*/
[[nodiscard]] state_t state() const {
return m_state;
}
/**
* \brief obtain the underlying socket for use with poll() or select()
* \returns the underlying socket or INVALID_SOCKET on error
*/
[[nodiscard]] int socket() const;
/**
* \brief obtain the peer certificate
* \returns the certificate or nullptr
* \note the certificate must not be freed
*/
[[nodiscard]] const Certificate* peer_certificate() const;
/**
* \brief obtain the underlying SSL context
* \returns the underlying SSL context pointer
*/
[[nodiscard]] [[deprecated(
"Temporarily used with IsoMux module. Will be removed together with IsoMux module in the future.")]] SSL*
ssl_context() const;
/**
* \brief set the read timeout in ms
*/
[[deprecated(
"Temporarily used with IsoMux module. Will be removed together with IsoMux module in the future.")]] void
set_read_timeout(int ms) {
m_timeout_ms = ms;
}
};
/**
* \brief class representing a TLS connection (server side)
*/
class ServerConnection : public Connection {
private:
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
std::unique_ptr<TlsKeyLoggingServer> m_keylog_server{nullptr};
public:
ServerConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms,
const ConfigItem& tls_key_interface);
ServerConnection() = delete;
ServerConnection(const ServerConnection&) = delete;
ServerConnection(ServerConnection&&) = delete;
ServerConnection& operator=(const ServerConnection&) = delete;
ServerConnection& operator=(ServerConnection&&) = delete;
~ServerConnection();
/**
* \brief accept the incoming connection and run the TLS handshake
* \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]] result_t accept(int timeout_ms);
[[nodiscard]] inline result_t accept() {
return accept(m_timeout_ms);
}
/**
* \brief wait for all connections to be closed
*/
static void wait_all_closed();
/**
* \brief return number of active connections (indicative only)
* \return number of active connections
*/
static std::uint32_t active_connections() {
return m_count;
}
};
/**
* \brief class representing a TLS connection (client side)
*/
class ClientConnection : public Connection {
public:
ClientConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms);
ClientConnection() = delete;
ClientConnection(const ClientConnection&) = delete;
ClientConnection(ClientConnection&&) = delete;
ClientConnection& operator=(const ClientConnection&) = delete;
ClientConnection& operator=(ClientConnection&&) = delete;
~ClientConnection();
/**
* \brief run the TLS handshake
* \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]] result_t connect(int timeout_ms);
[[nodiscard]] inline result_t connect() {
return connect(m_timeout_ms);
}
};
// ----------------------------------------------------------------------------
// TLS server
/**
* \brief represents a TLS server
*
* The TLS server does not make use of any threads. This is to support multiple
* use cases giving developers the option on how best to support multiple
* incoming connections.
*
* Example code in tests has some examples on how this can be done.
*
* One option is to have a server thread calling Server.serve() with the
* supplied handler creating a new connection thread when a connection arrives.
*
* For unit tests the connection handler can perform the test directly which
* has the advantage of preventing new connections from being accepted.
*
* Another option is for the connection handler to add the new connection to a
* list which is serviced by an event handler - i.e. one thread could manage
* all connections.
*
* 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:
/**
* \brief server state
*/
enum class state_t : std::uint8_t {
init_needed, //!< not initialised yet - call init()
init_socket, //!< TCP listen socket initialised (but not SSL) - call update()
init_complete, //!< initialised but not running - call serve()
running, //!< waiting for connections - fully initialised
stopped, //!< stopped - reinitialisation will be needed
};
struct certificate_config_t {
//!< server certificate is the first certificate in the file followed by any intermediate CAs
ConfigItem certificate_chain_file{nullptr};
ConfigItem trust_anchor_file{nullptr}; //!< one or more trust anchor PEM certificates
ConfigItem trust_anchor_pem{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<ConfigItem> 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<certificate_config_t> 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
bool tls_key_logging{false}; //!< tls key logging is active when true
std::string tls_key_logging_path; //!< tls key logging file path
};
using ConnectionPtr = std::unique_ptr<ServerConnection>;
using ConnectionHandler = std::function<void(ConnectionPtr&& ctx)>;
using OptionalConfig = std::optional<std::unique_ptr<config_t>>;
using ConfigurationCallback = std::function<OptionalConfig()>;
private:
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<server_ctx> 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<state_t> m_state{state_t::init_needed}; //!< server state
std::mutex m_mutex; //!< prevent multiple initialisation or serve requests
std::mutex m_update_mutex; //!< ensure safe updating of SSL configuration
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
ConfigItem m_tls_key_interface{nullptr};
std::filesystem::path tls_key_log_file_path{};
/**
* \brief initialise the server socket
* \param[in] cfg server configuration
* \return true on success
*/
bool init_socket(const config_t& cfg);
/**
* \brief initialise TLS configuration
* \param[in] cfg server configuration
* \return true on success
*/
bool init_ssl(const config_t& cfg);
/**
* \brief unconfigure SSL
*/
void deinit_ssl();
/**
* \brief initialise server certificate chains
* \param[in] chain_files server certificate chains
* \return true on success
*/
bool init_certificates(const std::vector<certificate_config_t>& chain_files);
/**
* \brief unconfigure SSL certificates
*/
void deinit_certificates();
/**
* \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;
Server(Server&&) = delete;
Server& operator=(const Server&) = delete;
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
* \param[in] init_ssl function to collect certificates and keys, can be nullptr
* \return need_init - initialisation failed
* socket_init - server socket created and ready for serve()
* init_complete - SSL certificates and keys loaded
*
* It is possible to initialise the server and start listening for
* connections before certificates and keys are available.
* when init() returns socket_init the server will call init_ssl() with a
* reference to the object so that update() can be called with updated
* OCSP and SSL configuration.
*
* init_ssl() should return true when SSL has been configured so that the
* incoming connection is accepted.
*/
state_t init(const config_t& cfg, const ConfigurationCallback& init_ssl);
/**
* \brief update configuration
* \param[in] cfg server configuration
* \return true on success
* \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
* can not start
* \note this is a blocking call that will not return until stop() has been
* called (unless it couldn't start listening)
* \note changing socket configuration requires stopping the server and
* calling init()
* \note after server() returns stopped init() will need to be called
* before further connections can be managed
*/
state_t serve(const ConnectionHandler& handler);
/**
* \brief stop accepting new connections and wait until SSL configuration
* is loaded via update()
* \returns true when the suspend was successful
*/
bool suspend();
/**
* \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();
/**
* \brief wait for server to start listening for connections
* \note blocks until server is running
*/
void wait_running();
/**
* \brief wait for server to stop
* \note blocks until server has stopped
*/
void wait_stopped();
/**
* \brief return the current server state (indicative only)
* \return current server state
*/
[[nodiscard]] state_t state() const {
return m_state;
}
};
// ----------------------------------------------------------------------------
// TLS client
/**
* \brief represents a TLS client
* \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
*/
enum class state_t : std::uint8_t {
need_init, //!< not initialised yet
init, //!< initialised but not connected
connected, //!< connected to server
stopped, //!< stopped
};
struct config_t {
const char* cipher_list{nullptr}; //!< nullptr means use default
const char* ciphersuites{nullptr}; //!< nullptr means use default, "" disables TSL 1.3
const char* certificate_chain_file{nullptr}; //!< 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<ClientConnection>;
private:
std::unique_ptr<client_ctx> 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<ClientStatusRequestV2> m_status_request_v2; //!< status request extension handler
public:
Client();
explicit Client(std::unique_ptr<ClientStatusRequestV2>&& handler);
Client(const Client&) = delete;
Client(Client&&) = delete;
Client& operator=(const Client&) = delete;
Client& operator=(Client&&) = delete;
virtual ~Client();
/**
* \brief initialise the client socket and TLS configuration
* \param[in] cfg server configuration
* \return true on success
*/
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)
*/
[[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 default SSL callbacks
*/
static override_t default_overrides();
};
} // namespace tls
#endif // TLS_HPP_

View File

@@ -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 <cstdint>
#include <everest/util/enum/EnumFlags.hpp>
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,
};
everest::lib::util::AtomicEnumFlags<flags_t> 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::trusted_ca_keys);
}
[[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::trusted_ca_keys);
}
};
// 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_

View File

@@ -0,0 +1,139 @@
From 92125584f2fe87023cbfe96bba06358111ed8c13 Mon Sep 17 00:00:00 2001
From: James Chapman <james.chapman@pionix.de>
Date: Fri, 21 Jun 2024 10:29:44 +0100
Subject: [PATCH 1/1] feat: updates to support status_request_v2
Signed-off-by: James Chapman <james.chapman@pionix.de>
---
include/openssl/ssl.h.in | 2 ++
include/openssl/tls1.h | 7 +++++++
ssl/s3_lib.c | 8 ++++++++
ssl/statem/extensions_clnt.c | 3 ++-
ssl/statem/extensions_srvr.c | 4 ++++
ssl/statem/statem_clnt.c | 3 ++-
6 files changed, 25 insertions(+), 2 deletions(-)
diff --git a/include/openssl/ssl.h.in b/include/openssl/ssl.h.in
index 105b4a4a3c..b29f65fbfa 100644
--- a/include/openssl/ssl.h.in
+++ b/include/openssl/ssl.h.in
@@ -1251,6 +1251,8 @@ DECLARE_PEM_rw(SSL_SESSION, SSL_SESSION)
# define SSL_CTRL_SET_TLSEXT_STATUS_REQ_IDS 69
# define SSL_CTRL_GET_TLSEXT_STATUS_REQ_OCSP_RESP 70
# define SSL_CTRL_SET_TLSEXT_STATUS_REQ_OCSP_RESP 71
+# define SSL_CTRL_GET_TLSEXT_STATUS_EXPECTED 270
+# define SSL_CTRL_SET_TLSEXT_STATUS_EXPECTED 271
# ifndef OPENSSL_NO_DEPRECATED_3_0
# define SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB 72
# endif
diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h
index d6e9331fa1..f0a8413703 100644
--- a/include/openssl/tls1.h
+++ b/include/openssl/tls1.h
@@ -160,6 +160,7 @@ extern "C" {
# define TLSEXT_NAMETYPE_host_name 0
/* status request value from RFC3546 */
# define TLSEXT_STATUSTYPE_ocsp 1
+# define TLSEXT_STATUSTYPE_ocsp_multi 2
/* ECPointFormat values from RFC4492 */
# define TLSEXT_ECPOINTFORMAT_first 0
@@ -291,6 +292,12 @@ __owur int SSL_check_chain(SSL *s, X509 *x, EVP_PKEY *pk, STACK_OF(X509) *chain)
# define SSL_set_tlsext_status_ocsp_resp(ssl, arg, arglen) \
SSL_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_REQ_OCSP_RESP,arglen,arg)
+# define SSL_get_tlsext_status_expected(ssl) \
+ SSL_ctrl(ssl,SSL_CTRL_GET_TLSEXT_STATUS_EXPECTED,0,NULL)
+
+# define SSL_set_tlsext_status_expected(ssl, arg) \
+ SSL_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_EXPECTED,arg,NULL)
+
# define SSL_CTX_set_tlsext_servername_callback(ctx, cb) \
SSL_CTX_callback_ctrl(ctx,SSL_CTRL_SET_TLSEXT_SERVERNAME_CB,\
(void (*)(void))cb)
diff --git a/ssl/s3_lib.c b/ssl/s3_lib.c
index 78d4f04056..ede3a56f2f 100644
--- a/ssl/s3_lib.c
+++ b/ssl/s3_lib.c
@@ -3556,6 +3556,14 @@ long ssl3_ctrl(SSL *s, int cmd, long larg, void *parg)
ret = 1;
break;
+ case SSL_CTRL_GET_TLSEXT_STATUS_EXPECTED:
+ return (long)s->ext.status_expected;
+
+ case SSL_CTRL_SET_TLSEXT_STATUS_EXPECTED:
+ s->ext.status_expected = larg;
+ ret = 1;
+ break;
+
case SSL_CTRL_CHAIN:
if (larg)
return ssl_cert_set1_chain(s, NULL, (STACK_OF(X509) *)parg);
diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c
index 842be0722b..b9d5493e72 100644
--- a/ssl/statem/extensions_clnt.c
+++ b/ssl/statem/extensions_clnt.c
@@ -8,6 +8,7 @@
*/
#include <openssl/ocsp.h>
+#include <openssl/tls1.h>
#include "../ssl_local.h"
#include "internal/cryptlib.h"
#include "statem_local.h"
@@ -1397,7 +1398,7 @@ int tls_parse_stoc_status_request(SSL *s, PACKET *pkt, unsigned int context,
* MUST only be sent if we've requested a status
* request message. In TLS <= 1.2 it must also be empty.
*/
- if (s->ext.status_type != TLSEXT_STATUSTYPE_ocsp) {
+ if ((s->ext.status_type != TLSEXT_STATUSTYPE_ocsp) && (s->ext.status_type != TLSEXT_STATUSTYPE_ocsp_multi)) {
SSLfatal(s, SSL_AD_UNSUPPORTED_EXTENSION, SSL_R_BAD_EXTENSION);
return 0;
}
diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c
index 16765a5a5b..7fb67937bf 100644
--- a/ssl/statem/extensions_srvr.c
+++ b/ssl/statem/extensions_srvr.c
@@ -8,6 +8,7 @@
*/
#include <openssl/ocsp.h>
+#include <openssl/tls1.h>
#include "../ssl_local.h"
#include "statem_local.h"
#include "internal/cryptlib.h"
@@ -1421,6 +1422,9 @@ EXT_RETURN tls_construct_stoc_status_request(SSL *s, WPACKET *pkt,
if (!s->ext.status_expected)
return EXT_RETURN_NOT_SENT;
+ if (s->ext.status_type == TLSEXT_STATUSTYPE_ocsp_multi)
+ return EXT_RETURN_NOT_SENT;
+
if (SSL_IS_TLS13(s) && chainidx != 0)
return EXT_RETURN_NOT_SENT;
diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c
index 3cd1ee2d3d..29a07bd413 100644
--- a/ssl/statem/statem_clnt.c
+++ b/ssl/statem/statem_clnt.c
@@ -9,6 +9,7 @@
* https://www.openssl.org/source/license.html
*/
+#include <openssl/tls1.h>
#include <stdio.h>
#include <time.h>
#include <assert.h>
@@ -2636,7 +2637,7 @@ int tls_process_cert_status_body(SSL *s, PACKET *pkt)
unsigned int type;
if (!PACKET_get_1(pkt, &type)
- || type != TLSEXT_STATUSTYPE_ocsp) {
+ || (type != TLSEXT_STATUSTYPE_ocsp) && (type != TLSEXT_STATUSTYPE_ocsp_multi)) {
SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_UNSUPPORTED_STATUS_TYPE);
return 0;
}
--
2.34.1

View File

@@ -0,0 +1,97 @@
# OpenSSL 3.0.8 patch
The file `openssl-3.0.8-feat-updates-to-support-status_request_v2.patch` is a
patch to OpenSSL 3.0.8 to support the `status_request_v2` TLS extension defined
in [RFC 6961](https://datatracker.ietf.org/doc/html/rfc6961).
## Apply the patch
Assuming `openssl-3.0.8-feat-updates-to-support-status_request_v2.patch` is in
the current directory:
```sh
$ git clone --branch openssl-3.0.8 https://github.com/openssl/openssl.git
$ cd openssl
$ patch -p1 < ../openssl-3.0.8-feat-updates-to-support-status_request_v2.patch
$ ./Configure
$ make
$ sudo make install
```
The patch can also be added to `SRC_URI` in a yocto bbappend file
`openssl_3.0.8.bbappend`:
```bitbake
SRC_URI:append = " file://openssl-3.0.8-feat-updates-to-support-status_request_v2.patch"
```
## Notes
The patch is designed to be a minimal change so that `status_request_v2` can be
supported with the emphasis on TLS server support. TLS client support exists to
facilitate testing.
`status_request_v2` is deprecated for TLS 1.3 and must not be used. The code
ignores `status_request_v2` extensions when TLS 1.3 has been negotiated.
When a client requests `status_request` and `status_request_v2` then
`status_request_v2` is used and `status_request` ignored.
## Implementation
`status_request_v2` is implemented in `tls.cpp` and relies on OCSP responses
being available in separate files that are associated with the server
certificate and chain.
The patch defines `TLSEXT_STATUSTYPE_ocsp_multi` which is used in `tls.cpp` to
detect a patched version of OpenSSL.
### OpenSSL
OpenSSL contains a framework for adding handlers for TLS extensions that are not
natively handled. `status_request` is supported and the same mechanism is used
to to build the `status_request_v2` response.
Unfortunately both `status_request` and `status_request_v2` add an additional
TLS handshake record `Certificate Status` containing the OCSP responses rather
than including them as part of the extension. The OpenSSL extension framework
doesn't provide a mechanism to add a `Certificate Status` record.
The solution is to reuse the support for `status_request` and provide the
`status_request_v2` data for the `Certificate Status` record in application
code.
The patch adds the additional status type `TLSEXT_STATUSTYPE_ocsp_multi` for use
with `SSL_set_tlsext_status_type()` and updates checks on `ext.status_type` so
that it isn't rejected.
Additional functions `SSL_get_tlsext_status_expected()` and
`SSL_set_tlsext_status_expected()` are added so that application code can
indicate to OpenSSL that the `Certificate Status` record needs to be added.
`SSL_set_tlsext_status_ocsp_resp()` is used by both `status_request` and
`status_request_v2` to populate the response.
An early `Client Hello` handler is used to detect `status_request` and
`status_request_v2` extensions so that the `status_request` handler can ignore
the request (unless TLS 1.3 had been negotiated).
### OcspCache
Contains a digest method that produces a digest of a certificate. This digest
is paired with the OCSP response filename which provides the association used
in the OCSP cache.
When responding to a `status_request_v2` the server iterates through the server
certificates and builds the response including the cached OCSP response for each
certificate where available.
## Testing
The primary testing has been performed using `Wireshark` to ensure that the
`Server Hello` and `Certificate Status` records are correctly formed.
There is a googletest test suite `patched_test` that checks operation via the
OpenSSL APIs but it isn't able to check the handshake records directly.
There are a test TLS server and client that can be used to check operation.

View File

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include <everest/tls/openssl_conv.hpp>
#include <evse_security/crypto/openssl/openssl_types.hpp>
#include <memory>
#include <openssl/x509.h>
using evse_security::X509Handle_ptr;
using evse_security::X509HandleOpenSSL;
using evse_security::X509Wrapper;
namespace openssl::conversions {
X509Handle_ptr to_X509Handle_ptr(x509_st* cert) {
X509Handle_ptr ptr;
if (X509_up_ref(cert) == 1) {
ptr = std::make_unique<X509HandleOpenSSL>(cert);
}
return ptr;
}
X509Wrapper to_X509Wrapper(x509_st* cert) {
return X509Wrapper(to_X509Handle_ptr(cert));
}
} // namespace openssl::conversions

View File

@@ -0,0 +1,855 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <memory>
#include <string>
#include <everest/tls/openssl_util.hpp>
#include <evse_security/crypto/openssl/openssl_provider.hpp>
#include <openssl/bio.h>
#include <openssl/crypto.h>
#include <openssl/ecdsa.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/store.h>
#include <openssl/types.h>
#include <openssl/ui.h>
#include <openssl/x509.h>
#include <openssl/x509_vfy.h>
namespace {
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<std::string*>(u);
*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<const char*>(u), size);
buf[size - 1] = '\0';
result = static_cast<int>(std::strlen(buf));
}
return result;
}
} // namespace
namespace openssl {
void log(log_level_t level, const std::string& str) {
std::string messages = {str};
ERR_print_errors_cb(&add_error_str, &messages);
if (s_log_handler == nullptr) {
std::cerr << messages << std::endl;
} else {
s_log_handler(level, messages);
}
}
log_handler_t set_log_handler(log_handler_t handler) {
const auto tmp = s_log_handler;
s_log_handler = handler;
return tmp;
}
} // namespace openssl
namespace {
/**
* \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 <typename DIGEST> bool sha_impl(const void* data, std::size_t len, DIGEST& digest, const EVP_MD* HASH) {
std::array<std::uint8_t, EVP_MAX_MD_SIZE> buffer{};
unsigned int digestlen{0};
const auto res = EVP_Digest(data, len, buffer.data(), &digestlen, HASH, nullptr);
if (res == 1) {
if (digestlen == digest.size()) {
std::memcpy(digest.data(), buffer.data(), digest.size());
} else {
openssl::log_error("EVP_Digest - size");
}
} else {
openssl::log_error("EVP_Digest");
}
return res == 1;
}
template <typename DIGEST> 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());
}
template <> bool sha(const void* data, std::size_t len, openssl::sha_384_digest_t& digest) {
return sha_impl<openssl::sha_384_digest_t>(data, len, digest, EVP_sha384());
}
template <> bool sha(const void* data, std::size_t len, openssl::sha_512_digest_t& digest) {
return sha_impl(data, len, digest, EVP_sha512());
}
} // namespace
namespace openssl {
using evse_security::OpenSSLProvider;
DER::DER(std::size_t size) : DER(nullptr, size) {
}
DER::DER(const der_underlying_t* src, std::size_t size) {
auto* tmp = static_cast<std::uint8_t*>(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<std::uint8_t*>(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<std::uint8_t, signature_der_size> signature{};
auto len = signature.size();
bRes = sign(pkey, signature.data(), len, digest.data(), sha_256_digest_size);
if (bRes) {
bRes = signature_to_bn(r, s, signature.data(), len);
}
return bRes;
}
bool sign(EVP_PKEY* pkey, unsigned char* sig, std::size_t& siglen, const unsigned char* tbs, std::size_t tbslen) {
bool bRes{true};
auto* ctx = EVP_PKEY_CTX_new(pkey, nullptr);
if (ctx == nullptr) {
log_error("EVP_PKEY_CTX_new");
bRes = false;
}
if (bRes && (EVP_PKEY_sign_init(ctx) != 1)) {
log_error("EVP_PKEY_sign_init");
bRes = false;
}
if (bRes && (EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha256()) != 1)) {
log_error("EVP_PKEY_CTX_set_signature_md");
bRes = false;
}
if (bRes) {
// calculate signature size
std::size_t length{0};
if (EVP_PKEY_sign(ctx, nullptr, &length, tbs, tbslen) != 1) {
log_error("EVP_PKEY_sign - length");
bRes = false;
} else if (siglen < length) {
log_error("EVP_PKEY_sign - length too small: " + std::to_string(length));
bRes = false;
}
if (bRes) {
const auto res = EVP_PKEY_sign(ctx, sig, &siglen, tbs, tbslen);
if (res != 1) {
log_error("EVP_PKEY_sign" + std::to_string(res));
bRes = false;
}
}
}
EVP_PKEY_CTX_free(ctx);
return bRes;
}
bool verify(EVP_PKEY* 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* pkey, const std::uint8_t* r, const std::uint8_t* s, const sha_256_digest_t& digest) {
bool bRes{false};
auto signature = bn_to_signature(r, s);
if (signature) {
bRes = verify(pkey, signature.get(), signature.size(), digest.data(), sha_256_digest_size);
}
return bRes;
}
bool verify(EVP_PKEY* pkey, const unsigned char* sig, std::size_t siglen, const unsigned char* tbs,
std::size_t tbslen) {
bool bRes{true};
auto* ctx = EVP_PKEY_CTX_new(pkey, nullptr);
if (ctx == nullptr) {
log_error("EVP_PKEY_CTX_new");
bRes = false;
}
if (bRes && (EVP_PKEY_verify_init(ctx) != 1)) {
log_error("EVP_PKEY_verify_init");
bRes = false;
}
if (bRes && (EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha256()) != 1)) {
log_error("EVP_PKEY_CTX_set_signature_md");
bRes = false;
}
if (bRes) {
const auto res = EVP_PKEY_verify(ctx, sig, siglen, tbs, tbslen);
if (res != 1) {
log_error("EVP_PKEY_verify: " + std::to_string(res));
bRes = false;
}
}
EVP_PKEY_CTX_free(ctx);
return bRes;
}
bool sha_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);
}
bool sha_384(const void* data, std::size_t len, sha_384_digest_t& digest) {
return sha(data, len, digest);
}
bool sha_512(const void* data, std::size_t len, sha_512_digest_t& digest) {
return sha(data, len, digest);
}
std::vector<std::uint8_t> base64_decode(const char* text, std::size_t len) {
// Strip whitespace; pass everything else to BIO_f_base64. Byte set matches
// EVP_Decode*'s B64_WS table so this stays drop-in equivalent to the
// evse_security path (consolidation target). NUL terminates the scan: an
// embedded NUL almost certainly indicates a length-parameter error and
// continuing would feed garbage to the decoder.
auto input = std::make_unique<std::uint8_t[]>(len);
std::size_t input_len{0};
for (std::size_t i = 0; i < len; i++) {
const auto item = text[i];
if (item == '\0') {
break;
}
switch (item) {
case '\t':
case '\n':
case '\v':
case '\f':
case '\r':
case ' ':
continue;
default:
input.get()[input_len++] = static_cast<std::uint8_t>(item);
}
}
auto* b64 = BIO_new(BIO_f_base64());
auto* mem = BIO_new_mem_buf(input.get(), static_cast<int>(input_len));
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
BIO_push(b64, mem);
std::size_t output_len{0};
int read_len{0};
std::array<char, 256> buffer{};
std::vector<std::uint8_t> result(len);
while ((read_len = BIO_read(b64, buffer.data(), buffer.size())) > 0) {
if ((output_len + read_len) <= result.size()) {
std::memcpy(&result[output_len], buffer.data(), read_len);
output_len += static_cast<std::size_t>(read_len);
} else {
// decoded data is larger than the input - can't happen!
output_len = 0;
break;
}
}
result.resize(output_len);
BIO_free_all(b64);
return result;
}
bool base64_decode(const char* text, std::size_t len, std::uint8_t* out_data, std::size_t& out_len) {
assert(out_data != nullptr);
bool bResult = false;
auto res = base64_decode(text, len);
if ((res.size() > 0) && (res.size() <= out_len)) {
std::memcpy(out_data, res.data(), res.size());
out_len = res.size();
bResult = true;
}
return bResult;
}
std::string base64_encode(const std::uint8_t* data, std::size_t len, bool newLine) {
auto* b64 = BIO_new(BIO_f_base64());
auto* mem = BIO_new(BIO_s_mem());
BIO_push(b64, mem);
if (!newLine) {
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
}
BIO_write(b64, data, static_cast<int>(len));
BIO_flush(b64);
char* ptr{nullptr};
const auto size = BIO_get_mem_data(mem, &ptr);
std::string result(ptr, size);
BIO_free_all(b64);
return result;
}
pkey_ptr load_private_key(const char* filename, const char* password) {
/*
* should read the file into memory to check the key type so the correct
* provider can be selected. For simplicity reuse existing function
* that causes key file to be opened an additional time
*/
{
OpenSSLProvider provider; // ensure providers are loaded
// minimise holding the mutex
}
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<char*>(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());
};
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};
BIGNUM* sbn{nullptr};
auto* signature = ECDSA_SIG_new();
if (signature == nullptr) {
log_error("ECDSA_SIG_new");
} else {
rbn = BN_bin2bn(r, signature_n_size, nullptr);
sbn = BN_bin2bn(s, signature_n_size, nullptr);
}
if (rbn != nullptr && sbn != nullptr) {
if (ECDSA_SIG_set0(signature, rbn, sbn) == 1) {
/* Set these to NULL since they are now owned by obj */
rbn = sbn = nullptr;
signature_len = i2d_ECDSA_SIG(signature, &sig_p);
if (signature_len == 0) {
log_error("i2d_ECDSA_SIG");
}
} else {
log_error("ECDSA_SIG_set0");
}
}
BN_free(rbn);
BN_free(sbn);
ECDSA_SIG_free(signature);
// 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) {
bool bRes{false};
auto* signature = d2i_ECDSA_SIG(nullptr, &sig_p, static_cast<long>(len));
if (signature == nullptr) {
log_error("d2i_ECDSA_SIG");
} else {
const auto* rbn = ECDSA_SIG_get0_r(signature);
const auto* sbn = ECDSA_SIG_get0_s(signature);
bRes = BN_bn2binpad(rbn, r.data(), static_cast<int>(r.size())) != -1;
bRes = bRes && BN_bn2binpad(sbn, s.data(), static_cast<int>(s.size())) != -1;
if (!bRes) {
log_error("BN_bn2binpad");
}
}
ECDSA_SIG_free(signature);
return bRes;
};
certificate_list load_certificates_pem(const char* pem_string) {
certificate_list result{};
if (pem_string != nullptr) {
const auto len = std::strlen(pem_string);
auto* mem = BIO_new_mem_buf(pem_string, static_cast<int>(len));
X509* cert = nullptr;
while (!BIO_eof(mem)) {
if (PEM_read_bio_X509(mem, &cert, nullptr, nullptr) == nullptr) {
log_error("PEM_read_bio_X509");
break;
} else {
result.emplace_back(certificate_ptr{cert, &X509_free});
cert = nullptr;
}
}
BIO_free(mem);
}
return 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) {
while (OSSL_STORE_eof(store) != 1) {
auto* info = OSSL_STORE_load(store);
if (info != nullptr) {
if (OSSL_STORE_error(store) == 1) {
log_error("OSSL_STORE_load");
} else {
const auto type = OSSL_STORE_INFO_get_type(info);
if (type == OSSL_STORE_INFO_CERT) {
// get a copy of the certificate
auto cert = OSSL_STORE_INFO_get1_CERT(info);
result.push_back({cert, &X509_free});
}
}
}
OSSL_STORE_INFO_free(info);
}
}
OSSL_STORE_close(store);
}
return result;
}
certificate_list load_certificates(const std::vector<const char*>& 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());
std::string result;
if (PEM_write_bio_X509(mem, cert) != 1) {
log_error("PEM_write_bio_X509");
} else {
BIO_flush(mem);
char* ptr{nullptr};
const auto size = BIO_get_mem_data(mem, &ptr);
result = std::string(ptr, size);
}
BIO_free(mem);
return result;
}
certificate_ptr pem_to_certificate(const std::string& pem) {
certificate_ptr result{nullptr, nullptr};
auto* mem = BIO_new_mem_buf(pem.c_str(), static_cast<int>(pem.size()));
X509* cert = nullptr;
if (PEM_read_bio_X509(mem, &cert, nullptr, nullptr) == nullptr) {
log_error("PEM_read_bio_X509");
} else {
result = certificate_ptr{cert, &X509_free};
}
BIO_free(mem);
return result;
}
certificate_ptr der_to_certificate(const std::uint8_t* der, std::size_t len) {
certificate_ptr result{nullptr, nullptr};
const auto* ptr = der;
auto* cert = d2i_X509(nullptr, &ptr, static_cast<std::int64_t>(len));
if (cert == nullptr) {
log_error("d2i_X509");
} else {
result = certificate_ptr{cert, &X509_free};
}
return result;
}
DER certificate_to_der(const x509_st* cert) {
assert(cert != nullptr);
unsigned char* data{nullptr};
// DO NOT FREE - internal pointers to certificate
int len = i2d_X509(cert, &data);
// move data to DER
return {der_ptr{data, &DER::free}, static_cast<std::size_t>(len)};
}
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();
X509* target{nullptr};
if (trust_anchors.empty()) {
log_error("No trust anchors provided");
return verify_result_t::NoCertificateAvailable;
}
if (store_ctx == nullptr) {
log_error("X509_STORE_CTX_new");
result = verify_result_t::OtherError;
}
if (ta_store == nullptr) {
log_error("X509_STORE_new");
result = verify_result_t::OtherError;
}
if (chain == nullptr) {
log_error("sk_X509_new_null");
result = verify_result_t::OtherError;
}
if (cert != nullptr) {
target = X509_dup(cert);
if (target == nullptr) {
log_error("X509_dup");
result = verify_result_t::OtherError;
}
}
if (result == verify_result_t::Verified) {
result = verify_result_t::OtherError;
for (const auto& i : trust_anchors) {
if (X509_STORE_add_cert(ta_store, i.get()) != 1) {
log_error("X509_STORE_add_cert");
}
}
for (const auto& j : untrusted) {
if (X509_add_cert(chain, j.get(), X509_ADD_FLAG_UP_REF | X509_ADD_FLAG_NO_DUP | X509_ADD_FLAG_NO_SS) != 1) {
log_error("X509_add_cert");
}
}
if (X509_STORE_CTX_init(store_ctx, ta_store, target, chain) != 1) {
log_error("X509_STORE_CTX_init");
} else {
if (X509_STORE_CTX_verify(store_ctx) != 1) {
const auto err = X509_STORE_CTX_get_error(store_ctx);
if (err != X509_V_OK) {
log_error("X509_STORE_CTX_verify (" + std::to_string(X509_STORE_CTX_get_error_depth(store_ctx)) +
") " + X509_verify_cert_error_string(err));
}
switch (err) {
case X509_V_ERR_CERT_CHAIN_TOO_LONG:
case X509_V_ERR_CERT_SIGNATURE_FAILURE:
case X509_V_ERR_CERT_UNTRUSTED:
case X509_V_ERR_PATH_LENGTH_EXCEEDED:
case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN:
case X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE:
case X509_V_ERR_UNSPECIFIED:
result = verify_result_t::CertChainError;
break;
case X509_V_ERR_CERT_HAS_EXPIRED:
case X509_V_ERR_CERT_NOT_YET_VALID:
result = verify_result_t::CertificateExpired;
break;
case X509_V_ERR_CERT_REVOKED:
result = verify_result_t::CertificateRevoked;
break;
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
result = verify_result_t::CertificateNotAllowed;
break;
default:
break;
}
} else {
result = verify_result_t::Verified;
}
}
}
X509_STORE_CTX_free(store_ctx);
X509_STORE_free(ta_store);
sk_X509_pop_free(chain, X509_free);
X509_free(target);
return result;
}
std::map<std::string, std::string> certificate_subject(const X509* cert) {
assert(cert != nullptr);
std::map<std::string, std::string> result;
// DO NOT FREE - internal pointers to certificate
const auto* subject = X509_get_subject_name(cert);
if (subject != nullptr) {
for (int i = 0; i < X509_NAME_entry_count(subject); i++) {
const auto* name_entry = X509_NAME_get_entry(subject, i);
if (name_entry != nullptr) {
const auto* object = X509_NAME_ENTRY_get_object(name_entry);
const auto* data = X509_NAME_ENTRY_get_data(name_entry);
if ((object != nullptr) && (data != nullptr)) {
std::string name(OBJ_nid2sn(OBJ_obj2nid(object)));
std::string value(reinterpret_cast<const char*>(ASN1_STRING_get0_data(data)),
ASN1_STRING_length(data));
result[name] = value;
}
}
}
}
return result;
}
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<std::size_t>(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);
}
return result;
}
bool certificate_sha_1(openssl::sha_1_digest_t& digest, const X509* cert) {
assert(cert != nullptr);
bool bResult{false};
auto der = certificate_to_der(cert);
if (der) {
bResult = openssl::sha_1(der.get(), der.size(), digest);
}
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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,161 @@
find_package(OpenSSL 3)
set(TLS_TEST_FILES
alt_openssl-pki.conf
iso_pkey.asn1
openssl-pki.conf
ocsp_response.der
pki.sh
pki-tpm.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
..
../include
)
target_compile_definitions(${TLS_GTEST_NAME} PRIVATE
-DUNIT_TEST
-DLIBEVSE_CRYPTO_SUPPLIER_OPENSSL
)
target_sources(${TLS_GTEST_NAME} PRIVATE
gtest_main.cpp
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
../src/openssl_conv.cpp
../src/openssl_util.cpp
../src/tls.cpp
)
if(USING_TPM2)
target_sources(${TLS_GTEST_NAME} PRIVATE
tls_connection_test_tpm.cpp
)
target_compile_definitions(${TLS_GTEST_NAME} PRIVATE
USING_TPM2
)
endif()
target_link_libraries(${TLS_GTEST_NAME}
PRIVATE
GTest::gtest
OpenSSL::SSL
OpenSSL::Crypto
everest::evse_security
everest::util
)
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
..
../include
)
target_compile_definitions(${TLS_MAIN_NAME} PRIVATE
-DUNIT_TEST
)
target_sources(${TLS_MAIN_NAME} PRIVATE
tls_main.cpp
../extensions/helpers.cpp
../extensions/status_request.cpp
../extensions/trusted_ca_keys.cpp
../src/openssl_util.cpp
../src/tls.cpp
)
target_link_libraries(${TLS_MAIN_NAME}
PRIVATE
OpenSSL::SSL
OpenSSL::Crypto
everest::evse_security
everest::util
)
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
..
../include
)
target_compile_definitions(${TLS_CLIENT_NAME} PRIVATE
-DUNIT_TEST
)
target_sources(${TLS_CLIENT_NAME} PRIVATE
tls_client_main.cpp
../extensions/helpers.cpp
../extensions/status_request.cpp
../extensions/trusted_ca_keys.cpp
../src/openssl_util.cpp
../src/tls.cpp
)
target_link_libraries(${TLS_CLIENT_NAME}
PRIVATE
OpenSSL::SSL
OpenSSL::Crypto
everest::evse_security
everest::util
)
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
..
../include
)
target_compile_definitions(${TLS_PATCH_NAME} PRIVATE
-DUNIT_TEST
)
target_sources(${TLS_PATCH_NAME} PRIVATE
patched_test.cpp
../extensions/helpers.cpp
../extensions/status_request.cpp
../extensions/trusted_ca_keys.cpp
../src/openssl_util.cpp
../src/tls.cpp
)
target_link_libraries(${TLS_PATCH_NAME}
PRIVATE
GTest::gtest_main
OpenSSL::SSL
OpenSSL::Crypto
everest::evse_security
everest::util
)
add_test(${TLS_GTEST_NAME} ${TLS_GTEST_NAME})
ev_register_test_target(${TLS_GTEST_NAME})

View File

@@ -0,0 +1,42 @@
# Tests
Building tests:
```sh
$ cd EVerest
$ mkdir build
$ cd build
$ cmake -GNinja -DEVEREST_CORE_BUILD_TESTING=ON ..
$ ninja install
```
`touch release.json` may be needed if it hasn't been created
(then re-run `ninja install`).
## Unit tests
- `./tls_test` and `./patched_test`
- automatically runs `pki.sh`
- run from the directory containing the executable
## Standalone server
- Run `pki.sh` to build the test certificates and keys
- use openssl_s_client to make test connections
- run from the directory containing the executable
### Standalone TLS server
Tests the Server class in isolation.
- `./tls_server`
- connects to IPv4 and IPv6
- only one connection at a time
- gracefully terminates after 30 seconds
- `valgrind` can be used to check memory allocations (should be none)
- requires client certificate and supports `status_request` extension
- s_client echos back what is typed
```sh
openssl s_client -connect localhost:8444 -verify 2 -CAfile server_root_cert.pem -cert client_cert.pem -cert_chain client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname evse.pionix.de -status
```

View File

@@ -0,0 +1,41 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include "gtest/gtest.h"
#include <string>
#include <vector>
#include <everest/tls/openssl_conv.hpp>
#include <everest/tls/openssl_util.hpp>
#include <evse_security/certificate/x509_hierarchy.hpp>
#include <evse_security/certificate/x509_wrapper.hpp>
namespace {
using evse_security::HashAlgorithm;
using evse_security::X509Wrapper;
using openssl::load_certificates;
using openssl::conversions::to_X509Wrapper;
TEST(evseSecurity, certificateHash) {
auto chain = load_certificates("client_chain.pem");
ASSERT_GT(chain.size(), 0);
std::vector<X509Wrapper> certs;
for (const auto& cert : chain) {
certs.push_back(to_X509Wrapper(cert.get()));
}
for (std::uint8_t i = 0; i < certs.size() - 1; i++) {
SCOPED_TRACE("i=" + std::to_string(i));
const auto& cert = certs[i];
const auto& issuer = certs[i + 1];
const auto resA = cert.get_certificate_hash_data(issuer);
EXPECT_EQ(resA.hash_algorithm, HashAlgorithm::SHA256);
}
}
} // namespace

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include <cstdlib>
#include <iostream>
#include <linux/limits.h>
#include <unistd.h>
#include <gtest/gtest.h>
#include <everest/tls/openssl_util.hpp>
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::info:
std::cout << "INFO: " << 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];
if (getcwd(&buf[0], sizeof(buf)) != nullptr) {
std::cerr << "./pki.sh not found in " << buf << std::endl;
}
return 1;
}
#ifdef USING_TPM2
if (std::system("./pki-tpm.sh") != 0) {
std::cerr << "Problem creating TPM test certificates and keys" << std::endl;
char buf[PATH_MAX];
if (getcwd(&buf[0], sizeof(buf)) != nullptr) {
std::cerr << "./pki-tpm.sh not found in " << buf << std::endl;
}
return 1;
}
#endif
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@@ -0,0 +1,817 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include "gtest/gtest.h"
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <everest/tls/openssl_util.hpp>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <vector>
#include <evse_security/crypto/openssl/openssl_provider.hpp>
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 {
template <typename T> constexpr void setCharacters(T& dest, const std::string& s) {
dest.charactersLen = s.size();
std::memcpy(&dest.characters[0], s.c_str(), s.size());
}
template <typename T> constexpr void setBytes(T& dest, const std::uint8_t* b, std::size_t len) {
dest.bytesLen = len;
std::memcpy(&dest.bytes[0], b, len);
}
struct test_vectors_t {
const char* input;
const std::uint8_t digest[32];
};
constexpr std::uint8_t sign_test[] = {0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55};
constexpr test_vectors_t sha_256_test[] = {
{"", {0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}},
{"abc", {0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}}};
// EXI AuthorizationReq: checked okay (hash computes correctly)
constexpr std::uint8_t iso_exi_a[] = {0x80, 0x04, 0x01, 0x52, 0x51, 0x0c, 0x40, 0x82, 0x9b, 0x7b, 0x6b, 0x29, 0x02,
0x93, 0x0b, 0x73, 0x23, 0x7b, 0x69, 0x02, 0x23, 0x0b, 0xa3, 0x09, 0xe8};
// checked okay
constexpr std::uint8_t iso_exi_a_hash[] = {0xd1, 0xb5, 0xe0, 0x3d, 0x00, 0x65, 0xbe, 0xe5, 0x6b, 0x31, 0x79,
0x84, 0x45, 0x30, 0x51, 0xeb, 0x54, 0xca, 0x18, 0xfc, 0x0e, 0x09,
0x16, 0x17, 0x4f, 0x8b, 0x3c, 0x77, 0xa9, 0x8f, 0x4a, 0xa9};
// EXI AuthorizationReq signature block: checked okay (hash computes correctly)
constexpr std::uint8_t iso_exi_b[] = {
0x80, 0x81, 0x12, 0xb4, 0x3a, 0x3a, 0x38, 0x1d, 0x17, 0x97, 0xbb, 0xbb, 0xbb, 0x97, 0x3b, 0x99, 0x97, 0x37, 0xb9,
0x33, 0x97, 0xaa, 0x29, 0x17, 0xb1, 0xb0, 0xb7, 0x37, 0xb7, 0x34, 0xb1, 0xb0, 0xb6, 0x16, 0xb2, 0xbc, 0x34, 0x97,
0xa1, 0xab, 0x43, 0xa3, 0xa3, 0x81, 0xd1, 0x79, 0x7b, 0xbb, 0xbb, 0xb9, 0x73, 0xb9, 0x99, 0x73, 0x7b, 0x93, 0x39,
0x79, 0x91, 0x81, 0x81, 0x89, 0x79, 0x81, 0xa1, 0x7b, 0xc3, 0x6b, 0x63, 0x23, 0x9b, 0x4b, 0x39, 0x6b, 0x6b, 0x7b,
0x93, 0x29, 0x1b, 0x2b, 0x1b, 0x23, 0x9b, 0x09, 0x6b, 0x9b, 0x43, 0x09, 0x91, 0xa9, 0xb2, 0x20, 0x62, 0x34, 0x94,
0x43, 0x10, 0x25, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72,
0x67, 0x2f, 0x54, 0x52, 0x2f, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x2d, 0x65, 0x78, 0x69, 0x2f,
0x48, 0x52, 0xd0, 0xe8, 0xe8, 0xe0, 0x74, 0x5e, 0x5e, 0xee, 0xee, 0xee, 0x5c, 0xee, 0x66, 0x5c, 0xde, 0xe4, 0xce,
0x5e, 0x64, 0x60, 0x60, 0x62, 0x5e, 0x60, 0x68, 0x5e, 0xf0, 0xda, 0xd8, 0xca, 0xdc, 0xc6, 0x46, 0xe6, 0xd0, 0xc2,
0x64, 0x6a, 0x6c, 0x84, 0x1a, 0x36, 0xbc, 0x07, 0xa0, 0x0c, 0xb7, 0xdc, 0xad, 0x66, 0x2f, 0x30, 0x88, 0xa6, 0x0a,
0x3d, 0x6a, 0x99, 0x43, 0x1f, 0x81, 0xc1, 0x22, 0xc2, 0xe9, 0xf1, 0x67, 0x8e, 0xf5, 0x31, 0xe9, 0x55, 0x23, 0x70};
// checked okay
constexpr std::uint8_t iso_exi_b_hash[] = {0xa4, 0xe9, 0x03, 0xe1, 0x82, 0x43, 0x04, 0x1b, 0x55, 0x4e, 0x11,
0x64, 0x7e, 0x10, 0x1e, 0xd2, 0x5f, 0xc9, 0xf2, 0x15, 0x2a, 0xf4,
0x67, 0x40, 0x14, 0xfe, 0x2a, 0xde, 0xac, 0x1e, 0x1c, 0xf7};
// checked okay (verifies iso_exi_b_hash with iso_priv.pem)
constexpr std::uint8_t iso_exi_sig[] = {0x4c, 0x8f, 0x20, 0xc1, 0x40, 0x0b, 0xa6, 0x76, 0x06, 0xaa, 0x48, 0x11, 0x57,
0x2a, 0x2f, 0x1a, 0xd3, 0xc1, 0x50, 0x89, 0xd9, 0x54, 0x20, 0x36, 0x34, 0x30,
0xbb, 0x26, 0xb4, 0x9d, 0xb1, 0x04, 0xf0, 0x8d, 0xfa, 0x8b, 0xf8, 0x05, 0x5e,
0x63, 0xa4, 0xb7, 0x5a, 0x8d, 0x31, 0x69, 0x20, 0x6f, 0xa8, 0xd5, 0x43, 0x08,
0xba, 0x58, 0xf0, 0x56, 0x6b, 0x96, 0xba, 0xf6, 0x92, 0xce, 0x59, 0x50};
const char iso_exi_a_hash_b64[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=";
const char iso_exi_a_hash_b64_nl[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=\n";
const char iso_exi_a_hash_b64_crlf[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=\r\n";
const char iso_exi_a_hash_b64_cr[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=\r";
const char iso_exi_a_hash_b64_spaces[] = "0bXgPQBlvuVr MXmERTBR61TK GPwOCRYXT4s8d6mPSqk=";
const char iso_exi_a_hash_b64_tabs[] = "0bXgPQBlvuVr\tMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=";
const char iso_exi_sig_b64[] =
"TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBvqNVDCLpY8FZrlrr2ks5ZUA==";
const char iso_exi_sig_b64_nl[] =
"TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBv\nqNVDCLpY8FZrlrr2ks5ZUA==\n";
const char test_cert_pem[] = "-----BEGIN CERTIFICATE-----\n"
"MIICBDCCAaqgAwIBAgIUQnMkyWtvc/a5OG8dZr9ziA5uQqYwCgYIKoZIzj0EAwIw\n"
"TjELMAkGA1UEBhMCR0IxDzANBgNVBAcMBkxvbmRvbjEPMA0GA1UECgwGUGlvbml4\n"
"MR0wGwYDVQQDDBRDUyBSb290IFRydXN0IEFuY2hvcjAeFw0yNDA5MTkxMzQwMDBa\n"
"Fw0yNDEwMjExMzQwMDBaME4xCzAJBgNVBAYTAkdCMQ8wDQYDVQQHDAZMb25kb24x\n"
"DzANBgNVBAoMBlBpb25peDEdMBsGA1UEAwwUQ1MgUm9vdCBUcnVzdCBBbmNob3Iw\n"
"WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARLkawitst5NtPoYGpDCp8/GBTDrNRJ\n"
"pCzS3KHT2lZJDOwzegRn+Zhs0csqXIQgbkCqdSozg+d83QNKcpmJk4FYo2YwZDAO\n"
"BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFB6Ytfi9uSF7NSYGXmyZEcKsWHwJMB8G\n"
"A1UdIwQYMBaAFB6Ytfi9uSF7NSYGXmyZEcKsWHwJMBIGA1UdEwEB/wQIMAYBAf8C\n"
"AQIwCgYIKoZIzj0EAwIDSAAwRQIge4+uxc2EFYD7AkHR+9d/NbULUnKFIBRLqYE+\n"
"Ib4h2CMCIQCtFWyvxwOUNidUTZGqyZXFmDyutJiNM0mi1iuFk8/8Mw==\n"
"-----END CERTIFICATE-----\n";
const char test_cert_hash[] = "082f891b26de97c8bdedb159f8d59113cfb55dc0";
const char test_cert_key_hash[] = "3b094e5f2594a3ae4511a9ff4285acd91fcd11c0";
inline const auto to_hex_string(const openssl::sha_1_digest_t& b) {
std::stringstream string_stream;
string_stream << std::hex;
for (int idx = 0; idx < sizeof(b); ++idx)
string_stream << std::setw(2) << std::setfill('0') << (int)b[idx];
return string_stream.str();
}
TEST(util, removeHyphen) {
const std::string expected{"UKSWI123456791A"};
std::string cert_emaid{"UKSWI123456791A"};
EXPECT_EQ(cert_emaid, expected);
cert_emaid.erase(std::remove(cert_emaid.begin(), cert_emaid.end(), '-'), cert_emaid.end());
EXPECT_EQ(cert_emaid, expected);
cert_emaid = std::string{"-UKSWI-123456791-A-"};
cert_emaid.erase(std::remove(cert_emaid.begin(), cert_emaid.end(), '-'), cert_emaid.end());
EXPECT_EQ(cert_emaid, expected);
}
TEST(certificate_sha_1, hash) {
auto cert = openssl::pem_to_certificate(test_cert_pem);
EXPECT_TRUE(cert);
openssl::sha_1_digest_t digest;
auto res = openssl::certificate_sha_1(digest, cert.get());
EXPECT_TRUE(res);
EXPECT_EQ(to_hex_string(digest), test_cert_hash);
}
TEST(certificate_subject_public_key_sha_1, hash) {
auto cert = openssl::pem_to_certificate(test_cert_pem);
EXPECT_TRUE(cert);
openssl::sha_1_digest_t digest;
auto res = openssl::certificate_subject_public_key_sha_1(digest, cert.get());
EXPECT_TRUE(res);
EXPECT_EQ(to_hex_string(digest), test_cert_key_hash);
}
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);
res = openssl::base64_encode(&iso_exi_sig[0], sizeof(iso_exi_sig));
EXPECT_EQ(res, iso_exi_sig_b64);
}
TEST(openssl, base64EncodeNl) {
auto res = openssl::base64_encode(&iso_exi_a_hash[0], sizeof(iso_exi_a_hash), true);
EXPECT_EQ(res, iso_exi_a_hash_b64_nl);
res = openssl::base64_encode(&iso_exi_sig[0], sizeof(iso_exi_sig), true);
EXPECT_EQ(res, iso_exi_sig_b64_nl);
}
TEST(openssl, base64Decode) {
auto res = openssl::base64_decode(&iso_exi_a_hash_b64[0], sizeof(iso_exi_a_hash_b64) - 1);
ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash));
EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0);
res = openssl::base64_decode(&iso_exi_sig_b64[0], sizeof(iso_exi_sig_b64) - 1);
ASSERT_EQ(res.size(), sizeof(iso_exi_sig));
EXPECT_EQ(std::memcmp(res.data(), &iso_exi_sig[0], res.size()), 0);
std::array<std::uint8_t, 512> buffer{};
std::size_t buffer_len = buffer.size();
EXPECT_TRUE(
openssl::base64_decode(&iso_exi_a_hash_b64[0], sizeof(iso_exi_a_hash_b64) - 1, buffer.data(), buffer_len));
ASSERT_EQ(buffer_len, sizeof(iso_exi_a_hash));
EXPECT_EQ(std::memcmp(buffer.data(), &iso_exi_a_hash[0], buffer_len), 0);
}
TEST(openssl, base64DecodeNl) {
auto res = openssl::base64_decode(&iso_exi_a_hash_b64_nl[0], sizeof(iso_exi_a_hash_b64_nl) - 1);
ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash));
EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0);
res = openssl::base64_decode(&iso_exi_sig_b64_nl[0], sizeof(iso_exi_sig_b64_nl) - 1);
ASSERT_EQ(res.size(), sizeof(iso_exi_sig));
EXPECT_EQ(std::memcmp(res.data(), &iso_exi_sig[0], res.size()), 0);
std::array<std::uint8_t, 512> buffer{};
std::size_t buffer_len = buffer.size();
EXPECT_TRUE(openssl::base64_decode(&iso_exi_a_hash_b64_nl[0], sizeof(iso_exi_a_hash_b64_nl) - 1, buffer.data(),
buffer_len));
ASSERT_EQ(buffer_len, sizeof(iso_exi_a_hash));
EXPECT_EQ(std::memcmp(buffer.data(), &iso_exi_a_hash[0], buffer_len), 0);
}
TEST(openssl, base64DecodeCrlf) {
auto res = openssl::base64_decode(&iso_exi_a_hash_b64_crlf[0], sizeof(iso_exi_a_hash_b64_crlf) - 1);
ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash));
EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0);
}
TEST(openssl, base64DecodeCrOnly) {
auto res = openssl::base64_decode(&iso_exi_a_hash_b64_cr[0], sizeof(iso_exi_a_hash_b64_cr) - 1);
ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash));
EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0);
}
TEST(openssl, base64DecodeInternalSpaces) {
auto res = openssl::base64_decode(&iso_exi_a_hash_b64_spaces[0], sizeof(iso_exi_a_hash_b64_spaces) - 1);
ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash));
EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0);
}
TEST(openssl, base64DecodeTabs) {
auto res = openssl::base64_decode(&iso_exi_a_hash_b64_tabs[0], sizeof(iso_exi_a_hash_b64_tabs) - 1);
ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash));
EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0);
}
TEST(openssl, base64DecodeTrailingNulIsTolerated) {
// sizeof(literal) includes the trailing NUL; impl must skip it.
auto res = openssl::base64_decode(&iso_exi_a_hash_b64[0], sizeof(iso_exi_a_hash_b64));
ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash));
EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0);
}
TEST(openssl, base64DecodeVerticalTabAndFormFeed) {
const char vt_ff[] = "0bXgPQBlvuVr\vMXmERTBR61TKGPwOC\fRYXT4s8d6mPSqk=";
auto res = openssl::base64_decode(&vt_ff[0], sizeof(vt_ff) - 1);
ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash));
EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0);
}
TEST(openssl, base64DecodeInvalidByteRejected) {
// Non-whitespace non-alphabet bytes still produce empty output.
auto res = openssl::base64_decode("@@@@", 4);
EXPECT_TRUE(res.empty());
}
TEST(openssl, base64DecodeEmptyInputReturnsEmpty) {
auto res = openssl::base64_decode("", 0);
EXPECT_TRUE(res.empty());
std::array<std::uint8_t, 16> buffer{};
std::size_t buffer_len = buffer.size();
EXPECT_FALSE(openssl::base64_decode("", 0, buffer.data(), buffer_len));
}
TEST(openssl, base64EncodeEmptyInputReturnsEmpty) {
const std::uint8_t empty_buf[1] = {0};
auto res = openssl::base64_encode(empty_buf, 0);
EXPECT_TRUE(res.empty());
}
TEST(openssl, sha256) {
openssl::sha_256_digest_t digest;
EXPECT_TRUE(openssl::sha_256(sha_256_test[0].input, 0, digest));
EXPECT_EQ(std::memcmp(digest.data(), &sha_256_test[0].digest[0], 32), 0);
EXPECT_TRUE(openssl::sha_256(sha_256_test[1].input, 3, digest));
EXPECT_EQ(std::memcmp(digest.data(), &sha_256_test[1].digest[0], 32), 0);
}
TEST(openssl, sha256Exi) {
openssl::sha_256_digest_t digest;
EXPECT_TRUE(openssl::sha_256(&iso_exi_a[0], sizeof(iso_exi_a), digest));
EXPECT_EQ(std::memcmp(digest.data(), &iso_exi_a_hash[0], 32), 0);
EXPECT_TRUE(openssl::sha_256(&iso_exi_b[0], sizeof(iso_exi_b), digest));
EXPECT_EQ(std::memcmp(digest.data(), &iso_exi_b_hash[0], 32), 0);
}
TEST(openssl, signVerify) {
auto pkey = openssl::load_private_key("server_priv.pem", nullptr);
ASSERT_TRUE(pkey);
std::array<std::uint8_t, 256> 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.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()));
}
#ifdef USING_TPM2
TEST(opensslTpm, signVerify) {
auto pkey = openssl::load_private_key("tpm_pki/server_priv.pem", nullptr);
ASSERT_TRUE(pkey);
// looks like sign/verify may not be supported
// TODO(james-ctc) investigation needed
// perhaps the public key needs to be extracted
#if 0
if (EVP_PKEY_can_sign(pkey.get()) == 1) {
std::array<std::uint8_t, 256> sig_der{};
std::size_t sig_der_len{sig_der.size()};
openssl::sha_256_digest_t digest;
evse_security::OpenSSLProvider provider;
provider.set_global_mode(evse_security::OpenSSLProvider::mode_t::custom_provider);
EXPECT_TRUE(openssl::sha_256(&sign_test[0], openssl::sha_256_digest_size, digest));
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()));
} else {
std::cout << "sign/verify not supported" << std::endl;
}
#endif
}
#endif
TEST(openssl, signVerifyBn) {
auto pkey = openssl::load_private_key("server_priv.pem", nullptr);
ASSERT_TRUE(pkey);
openssl::bn_t r;
openssl::bn_t s;
openssl::sha_256_digest_t digest;
EXPECT_TRUE(openssl::sha_256(&sign_test[0], openssl::sha_256_digest_size, digest));
EXPECT_TRUE(openssl::sign(pkey.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 pkey = openssl::load_private_key("server_priv.pem", nullptr);
ASSERT_TRUE(pkey);
std::array<std::uint8_t, 80> 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.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.get(), r, s, digest));
}
TEST(openssl, signVerifyFail) {
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<std::uint8_t, 256> 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.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 pkey = openssl::load_private_key("iso_priv.pem", nullptr);
ASSERT_TRUE(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) {
auto certs = ::openssl::load_certificates("server_cert.pem");
EXPECT_EQ(certs.size(), 1);
}
TEST(certificateLoad, chain) {
auto certs = ::openssl::load_certificates("server_chain.pem");
EXPECT_EQ(certs.size(), 2);
}
TEST(certificateLoad, key) {
auto certs = ::openssl::load_certificates("server_priv.pem");
EXPECT_EQ(certs.size(), 0);
}
TEST(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<const char*> 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<const char*> files{};
auto certs = ::openssl::load_certificates(files);
ASSERT_EQ(certs.size(), 0);
}
TEST(certificateLoad, multiFileMissing) {
std::vector<const char*> 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<const char*> 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);
auto pem = ::openssl::certificate_to_pem(certs[0].get());
EXPECT_FALSE(pem.empty());
// std::cout << pem << std::endl;
}
TEST(certificate, loadPemSingle) {
auto certs = ::openssl::load_certificates("client_ca_cert.pem");
ASSERT_EQ(certs.size(), 1);
auto pem = ::openssl::certificate_to_pem(certs[0].get());
EXPECT_FALSE(pem.empty());
auto pem_certs = ::openssl::load_certificates_pem(pem.c_str());
ASSERT_EQ(pem_certs.size(), 1);
EXPECT_EQ(certs[0], pem_certs[0]);
}
TEST(certificate, loadPemMulti) {
auto certs = ::openssl::load_certificates("client_chain.pem");
ASSERT_GT(certs.size(), 1);
std::string pem;
for (const auto& cert : certs) {
pem += ::openssl::certificate_to_pem(cert.get());
}
EXPECT_FALSE(pem.empty());
// std::cout << pem << std::endl << "Output" << std::endl;
auto pem_certs = ::openssl::load_certificates_pem(pem.c_str());
ASSERT_EQ(pem_certs.size(), certs.size());
for (auto i = 0; i < certs.size(); i++) {
SCOPED_TRACE(std::to_string(i));
// std::cout << ::openssl::certificate_to_pem(pem_certs[i].get()) << std::endl;
EXPECT_EQ(certs[i], pem_certs[i]);
}
}
TEST(certificate, verify) {
auto client = ::openssl::load_certificates("client_cert.pem");
auto chain = ::openssl::load_certificates("client_chain.pem");
auto root = ::openssl::load_certificates("client_root_cert.pem");
ASSERT_EQ(client.size(), 1);
EXPECT_GT(chain.size(), 0);
EXPECT_EQ(root.size(), 1);
EXPECT_EQ(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::Verified);
}
TEST(certificate, 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) {
auto client = ::openssl::load_certificates("client_cert.pem");
auto chain = ::openssl::load_certificates("client_chain.pem");
auto root = ::openssl::load_certificates("client_root_cert.pem");
ASSERT_EQ(client.size(), 1);
EXPECT_GT(chain.size(), 0);
EXPECT_EQ(root.size(), 1);
// client certificate is 1st in the list
openssl::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);
}
TEST(certificate, verifyNoClient) {
// client certificate is in the chain
auto chain = ::openssl::load_certificates("client_chain.pem");
auto root = ::openssl::load_certificates("client_root_cert.pem");
EXPECT_GT(chain.size(), 0);
EXPECT_EQ(root.size(), 1);
EXPECT_EQ(::openssl::verify_certificate(nullptr, root, chain), openssl::verify_result_t::Verified);
}
TEST(certificate, verifyFailWrongClient) {
auto client = ::openssl::load_certificates("server_cert.pem");
auto chain = ::openssl::load_certificates("client_chain.pem");
auto root = ::openssl::load_certificates("client_root_cert.pem");
ASSERT_EQ(client.size(), 1);
EXPECT_GT(chain.size(), 0);
EXPECT_EQ(root.size(), 1);
EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::Verified);
}
TEST(certificate, verifyFailWrongRoot) {
auto client = ::openssl::load_certificates("client_cert.pem");
auto chain = ::openssl::load_certificates("client_chain.pem");
auto root = ::openssl::load_certificates("server_root_cert.pem");
ASSERT_EQ(client.size(), 1);
EXPECT_GT(chain.size(), 0);
EXPECT_EQ(root.size(), 1);
EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::Verified);
}
TEST(certificate, verifyFailWrongChain) {
auto client = ::openssl::load_certificates("client_cert.pem");
auto chain = ::openssl::load_certificates("server_chain.pem");
auto root = ::openssl::load_certificates("client_root_cert.pem");
ASSERT_EQ(client.size(), 1);
EXPECT_GT(chain.size(), 0);
EXPECT_EQ(root.size(), 1);
EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::Verified);
}
TEST(certificate, subjectName) {
auto chain = ::openssl::load_certificates("client_chain.pem");
EXPECT_GT(chain.size(), 0);
for (const auto& cert : chain) {
auto subject = ::openssl::certificate_subject(cert.get());
EXPECT_GT(subject.size(), 0);
}
}
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

View File

@@ -0,0 +1,62 @@
// 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"
namespace {
// ----------------------------------------------------------------------------
// The tests - only pass on a patched OpenSSL
TEST_F(TlsTest, TLS12) {
// test using TLS 1.2
start();
connect();
// no status requested
EXPECT_TRUE(is_set(flags_t::connected));
EXPECT_TRUE(is_reset(flags_t::status_request_cb));
EXPECT_TRUE(is_reset(flags_t::status_request));
EXPECT_TRUE(is_reset(flags_t::status_request_v2));
client_config.status_request = true;
connect();
// status_request only
EXPECT_TRUE(is_set(flags_t::connected));
EXPECT_TRUE(is_set(flags_t::status_request_cb));
EXPECT_TRUE(is_set(flags_t::status_request));
EXPECT_TRUE(is_reset(flags_t::status_request_v2));
client_config.status_request = false;
client_config.status_request_v2 = true;
connect();
// status_request_v2 only
EXPECT_TRUE(is_set(flags_t::connected));
EXPECT_TRUE(is_set(flags_t::status_request_cb));
EXPECT_TRUE(is_reset(flags_t::status_request));
EXPECT_TRUE(is_set(flags_t::status_request_v2));
client_config.status_request = true;
connect();
// status_request and status_request_v2
// status_request_v2 is preferred over status_request
EXPECT_TRUE(is_set(flags_t::connected));
EXPECT_TRUE(is_set(flags_t::status_request_cb));
EXPECT_TRUE(is_reset(flags_t::status_request));
EXPECT_TRUE(is_set(flags_t::status_request_v2));
}
} // namespace

View File

@@ -0,0 +1 @@
*.pem

View File

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

View File

@@ -0,0 +1,11 @@
asn1=SEQ:pkcs8c
[pkcs8c]
ver=INT:0
algid=SEQ:algid
data=OCTWRAP,SEQ:sec1
[algid]
alg=OID:id-ecPublicKey
parm=OID:prime256v1
[sec1]
ver=INT:1
privkey=FORMAT:HEX,OCT:b9134963f51c4414738435057f97bbf1010cabcb8dbde9c5d48138396aa94b9d

View File

@@ -0,0 +1,143 @@
openssl_conf = openssl_init
[openssl_init]
providers = provider_section
[provider_section]
default = default_section
tpm2 = tpm2_section
base = base_section
[default_section]
activate = 1
[tpm2_section]
activate = 1
[base_section]
activate = 1
# server section
# ==============
[req_server_root]
distinguished_name = req_dn_server_root
utf8 = yes
prompt = no
req_extensions = v3_server_root
[req_server_ca]
distinguished_name = req_dn_server_ca
utf8 = yes
prompt = no
req_extensions = v3_server_ca
[req_server]
distinguished_name = req_dn_server
utf8 = yes
prompt = no
req_extensions = v3_server
[req_dn_server_root]
C = GB
O = Pionix
L = London
CN = Root Trust Anchor
[req_dn_server_ca]
C = GB
O = Pionix
L = London
CN = Intermediate CA
[req_dn_server]
C = GB
O = Pionix
L = London
CN = 00000000
[req_dn_client]
C = GB
O = Pionix
L = London
CN = 12345678
[v3_server_root]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = critical, CA:true, pathlen:2
keyUsage = keyCertSign, cRLSign
[v3_server_ca]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = critical, CA:true
keyUsage = keyCertSign, cRLSign
[v3_server]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always
keyUsage = digitalSignature, keyEncipherment, keyAgreement
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = IP:192.168.245.1, DNS:evse.pionix.de
# client section
# ==============
[req_client]
distinguished_name = req_dn_client
utf8 = yes
prompt = no
req_extensions = v3_client
[req_client_root]
distinguished_name = req_dn_client_root
utf8 = yes
prompt = no
req_extensions = v3_client_root
[req_client_ca]
distinguished_name = req_dn_client_ca
utf8 = yes
prompt = no
req_extensions = v3_client_ca
[req_server]
distinguished_name = req_dn_server
utf8 = yes
prompt = no
req_extensions = v3_server
[req_dn_client_root]
C = DE
O = Pionix
L = Frankfurt
CN = Root Trust Anchor
[req_dn_client_ca]
C = DE
O = Pionix
L = Frankfurt
CN = Intermediate CA
[req_dn_client]
C = DE
O = Pionix
L = Frankfurt
CN = 12345678
[v3_client_root]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = critical, CA:true, pathlen:2
keyUsage = keyCertSign, cRLSign
[v3_client_ca]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = critical, CA:true
keyUsage = keyCertSign, cRLSign
[v3_client]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always
keyUsage = digitalSignature, keyEncipherment, keyAgreement
extendedKeyUsage = clientAuth

View File

@@ -0,0 +1,57 @@
#!/bin/sh
base=.
cfg=./openssl-pki.conf
dir=tpm_pki
[ ! -f "$cfg" ] && echo "missing openssl-pki.conf" && exit 1
generate() {
local base=$1
local dir=$2
mkdir -p ${base}/${dir}
local root_priv=${base}/${dir}/server_root_priv.pem
local ca_priv=${base}/${dir}/server_ca_priv.pem
local server_priv=${base}/${dir}/server_priv.pem
local root_cert=${base}/${dir}/server_root_cert.pem
local ca_cert=${base}/${dir}/server_ca_cert.pem
local server_cert=${base}/${dir}/server_cert.pem
local cert_path=${base}/${dir}/server_chain.pem
local tpmA="-provider"
local tpmB="tpm2"
local propA="-propquery"
local propB="?provider=tpm2"
# generate keys
for i in ${root_priv} ${ca_priv} ${server_priv}
do
openssl genpkey -config ${cfg} ${tpmA} ${tpmB} ${propA} ${propB} -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out $i
done
export OPENSSL_CONF=${cfg}
# generate root cert
echo "Generate root"
openssl req ${tpmA} ${tpmB} -provider default ${propA} ${propB} \
-config ${cfg} -x509 -section req_server_root -extensions v3_server_root \
-key ${root_priv} -out ${root_cert}
# generate ca cert
echo "Generate ca"
openssl req ${tpmA} ${tpmB} -provider default ${propA} ${propB} \
-config ${cfg} -x509 -section req_server_ca -extensions v3_server_ca \
-key ${ca_priv} -CA ${root_cert} \
-CAkey ${root_priv} -out ${ca_cert}
# generate server cert
echo "Generate server"
openssl req ${tpmA} ${tpmB} -provider default ${propA} ${propB} \
-config ${cfg} -x509 -section req_server -extensions v3_server \
-key ${server_priv} -CA ${ca_cert} \
-CAkey ${ca_priv} -out ${server_cert}
# create bundle
cat ${server_cert} ${ca_cert} > ${cert_path}
}
generate $base $dir

View File

@@ -0,0 +1,80 @@
#!/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
server_ca_priv=server_ca_priv.pem
server_priv=server_priv.pem
server_root_cert=server_root_cert.pem
server_ca_cert=server_ca_cert.pem
server_cert=server_cert.pem
server_chain=server_chain.pem
client_root_priv=client_root_priv.pem
client_ca_priv=client_ca_priv.pem
client_priv=client_priv.pem
client_root_cert=client_root_cert.pem
client_ca_cert=client_ca_cert.pem
client_cert=client_cert.pem
client_chain=client_chain.pem
generate
generate alt_
# cross signed intermediate certificate
echo "Generate cross_ca"
openssl req \
-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

View File

@@ -0,0 +1,135 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include <everest/tls/openssl_util.hpp>
#include <everest/tls/tls.hpp>
#include <chrono>
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std::chrono_literals;
namespace {
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;
while ((c = getopt(argc, argv, short_opts)) != -1) {
switch (c) {
break;
case '1':
use_status_request = true;
break;
case '2':
use_status_request_v2 = true;
break;
case '3':
use_tls1_3 = true;
break;
case 'r':
trust_anchor = optarg;
break;
case 'h':
case '?':
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:
exit(2);
}
}
}
} // namespace
int main(int argc, char** argv) {
parse_options(argc, argv);
// used test client for extra output
tls::Client client;
tls::Client::config_t config;
if (use_tls1_3) {
config.cipher_list = "ECDHE-ECDSA-AES128-SHA256";
config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384";
std::cout << "use_tls1_3 true" << std::endl;
} else {
config.cipher_list = "ECDHE-ECDSA-AES128-SHA256";
config.ciphersuites = ""; // No TLS1.3
std::cout << "use_tls1_3 false" << std::endl;
}
config.certificate_chain_file = "client_chain.pem";
config.private_key_file = "client_priv.pem";
config.verify_locations_file = "server_root_cert.pem";
config.io_timeout_ms = 500;
config.verify_server = false;
if (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;
} else {
config.status_request = false;
std::cout << "use_status_request false" << std::endl;
}
if (use_status_request_v2) {
config.status_request_v2 = true;
std::cout << "use_status_request_v2 true" << std::endl;
} else {
config.status_request_v2 = false;
std::cout << "use_status_request_v2 false" << std::endl;
}
client.init(config);
// localhost works in some cases but not in the CI pipeline ip6-localhost is an option
auto connection = client.connect("localhost", "8444", false, 1000);
if (connection) {
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<std::byte, 1024> buffer{};
std::size_t readbytes = 0;
std::cout << "about to read" << std::endl;
const auto res = connection->read(buffer.data(), buffer.size(), readbytes);
std::cout << (int)res << std::endl;
std::this_thread::sleep_for(1s);
connection->shutdown();
}
}
return 0;
}

View File

@@ -0,0 +1,908 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include "tls_connection_test.hpp"
#include <memory>
#include <mutex>
#include <poll.h>
#include <thread>
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";
constexpr auto WAIT_FOR_SERVER_START_TIMEOUT = 50ms;
void do_poll(std::array<pollfd, 2>& 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<tls::Server::config_t>();
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 = "127.0.0.1";
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::timed_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);
// FIXME (aw): this is not a proper solution. It would be necessary
// to get an exception or result on whether `start()` function has
// been successful
if (not mux.try_lock_for(WAIT_FOR_SERVER_START_TIMEOUT)) {
GTEST_SKIP();
}
// 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<pollfd, 2> 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::timed_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);
if (not mux.try_lock_for(WAIT_FOR_SERVER_START_TIMEOUT)) {
GTEST_SKIP();
}
// 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<pollfd, 2> 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::timed_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);
if (not mux.try_lock_for(WAIT_FOR_SERVER_START_TIMEOUT)) {
GTEST_SKIP();
}
// 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<pollfd, 2> 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<std::byte, 1024> 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<std::byte, 1024> 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<std::byte, 1024> 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<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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, TCKeysKeyPem) {
// same as TCKeysKey but using a PEM string trust anchor rather than file
std::map<std::string, std::string> 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();
}
}
};
// convert file to PEM in config
for (auto& cfg : server_config.chains) {
const auto certs = ::openssl::load_certificates(cfg.trust_anchor_file);
std::string pem;
for (const auto& cert : certs) {
pem += ::openssl::certificate_to_pem(cert.get());
}
// std::cout << cfg.trust_anchor_file << ": " << certs.size() << std::endl;
ASSERT_FALSE(pem.empty());
cfg.trust_anchor_file = nullptr;
cfg.trust_anchor_pem = pem.c_str();
}
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<std::string, std::string> 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<std::string, std::string> 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("127.0.0.1", "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);
}
TEST_F(TlsTest, Suspend) {
using state_t = tls::Server::state_t;
start();
EXPECT_EQ(server.state(), state_t::running);
connect();
EXPECT_TRUE(is_set(flags_t::connected));
EXPECT_TRUE(server.suspend());
EXPECT_EQ(server.state(), state_t::init_socket);
connect();
EXPECT_FALSE(is_set(flags_t::connected));
EXPECT_EQ(server.state(), state_t::init_socket);
EXPECT_TRUE(server.update(server_config));
EXPECT_EQ(server.state(), state_t::init_complete);
connect();
EXPECT_TRUE(is_set(flags_t::connected));
EXPECT_EQ(server.state(), state_t::running);
}
TEST_F(TlsTest, SuspendToRunning) {
using state_t = tls::Server::state_t;
start();
EXPECT_EQ(server.state(), state_t::running);
connect();
EXPECT_TRUE(is_set(flags_t::connected));
EXPECT_TRUE(server.suspend());
EXPECT_EQ(server.state(), state_t::init_socket);
connect();
EXPECT_FALSE(is_set(flags_t::connected));
EXPECT_EQ(server.state(), state_t::init_socket);
EXPECT_TRUE(server.update(server_config));
EXPECT_EQ(server.state(), state_t::init_complete);
// should switch to running after a timeout
std::this_thread::sleep_for(std::chrono::milliseconds(tls::c_serve_timeout_ms + 100));
EXPECT_EQ(server.state(), state_t::running);
}
} // namespace

View File

@@ -0,0 +1,323 @@
// 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 "extensions/helpers.hpp"
#include <everest/tls/openssl_util.hpp>
#include <array>
#include <chrono>
#include <csignal>
#include <everest/tls/tls.hpp>
#include <gtest/gtest.h>
#include <memory>
#include <openssl/ssl.h>
#include <thread>
#include <unistd.h>
#include <everest/util/enum/EnumFlags.hpp>
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,
};
everest::lib::util::AtomicEnumFlags<flags_t>& flags;
ClientStatusRequestV2Test() = delete;
explicit ClientStatusRequestV2Test(everest::lib::util::AtomicEnumFlags<flags_t>& 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<std::int32_t>::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<std::int32_t>(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;
everest::lib::util::AtomicEnumFlags<flags_t> flags;
ClientTest() : tls::Client(std::unique_ptr<ClientStatusRequestV2>(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<std::byte, 1024> 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 = "127.0.0.1";
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<void(tls::Server::ConnectionPtr&& con)>& 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<void(tls::Client::ConnectionPtr& con)>& 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("127.0.0.1", "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));
}
}
}
};
class TlsTestTpm : public TlsTest {
protected:
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 = "tpm_pki/server_chain.pem";
ref0.private_key_file = "tpm_pki/server_priv.pem";
ref0.trust_anchor_file = "tpm_pki/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.host = "127.0.0.1";
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 = "tpm_pki/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();
}
};
} // namespace
#endif // TLS_CONNECTION_TEST_HPP_

View File

@@ -0,0 +1,34 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include "tls_connection_test.hpp"
#include <everest/tls/openssl_util.hpp>
#include <memory>
#include <mutex>
#include <poll.h>
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";
constexpr auto WAIT_FOR_SERVER_START_TIMEOUT = 50ms;
// ----------------------------------------------------------------------------
// The tests
TEST_F(TlsTestTpm, StartConnectDisconnect) {
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));
}
} // namespace

View File

@@ -0,0 +1,159 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
/*
* testing options
* openssl s_client -connect localhost:8444 -verify 2 -CAfile server_root_cert.pem -cert client_cert.pem -cert_chain
* client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname evse.pionix.de -status
*/
#include <everest/tls/tls.hpp>
#include <array>
#include <chrono>
#include <csignal>
#include <iostream>
#include <memory>
#include <thread>
#include <unistd.h>
using namespace std::chrono_literals;
namespace {
const char* short_opts = "ch36t";
bool disable_tls1_3{false};
bool enable_tpm{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 't':
enable_tpm = 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;
std::cout << " -t enable TPM" << std::endl;
exit(1);
break;
default:
exit(2);
}
}
}
void handle_connection(tls::Server::ConnectionPtr&& con) {
std::cout << "Connection" << std::endl;
if (con->accept() == tls::Connection::result_t::success) {
std::uint32_t count{0};
std::array<std::byte, 1024> 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();
}
std::cout << "Connection closed" << std::endl;
}
} // 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;
// 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";
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();
if (enable_tpm) {
ref0.certificate_chain_file = "tpm_pki/server_chain.pem";
ref0.private_key_file = "tpm_pki/server_priv.pem";
ref0.trust_anchor_file = "tpm_pki/server_root_cert.pem";
} else {
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.service = "8444";
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.wait_stopped();
stop.join();
return 0;
}

View File

@@ -0,0 +1,452 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include "extensions/trusted_ca_keys.hpp"
#include <everest/tls/openssl_util.hpp>
#include <everest/tls/tls.hpp>
#include <gtest/gtest.h>
#include <iterator>
#include <cstring>
#include <utility>
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 < 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<const std::uint8_t*>(&digest), sizeof(digest));
}
namespace {
TEST(strdup, usage) {
// auto* r1 = strdup(nullptr); need to ensure non-nullptr
auto* r2 = strdup("");
auto* r3 = strdup("hello");
// free(r1);
free(r2);
free(r3);
free(nullptr);
}
TEST(string, use) {
// was hoping to use std::string for config, but ...
std::string empty;
std::string space{""};
std::string value{"something"};
EXPECT_TRUE(empty.empty());
// EXPECT_FALSE(space.empty()); was hoping it would be true
EXPECT_FALSE(value.empty());
// EXPECT_EQ(empty.c_str(), nullptr); was hoping it would be nullptr
EXPECT_NE(space.c_str(), nullptr);
EXPECT_NE(value.c_str(), nullptr);
}
TEST(ConfigItem, test) {
// tests reduced with new ConfigStore implementation
tls::ConfigItem i1;
tls::ConfigItem i2{nullptr};
tls::ConfigItem i3{"Hello"};
tls::ConfigItem i4 = nullptr;
tls::ConfigItem i5(nullptr);
tls::ConfigItem i6("Hello");
EXPECT_EQ(i1, nullptr);
EXPECT_EQ(i4, nullptr);
EXPECT_EQ(i5, nullptr);
EXPECT_EQ(i2, i5);
EXPECT_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));
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);
std::vector<tls::ConfigItem> j3 = {"one", "two", nullptr};
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;
tls::OcspCache::digest_t digest{};
auto res = cache.lookup(digest);
EXPECT_EQ(res.get(), nullptr);
}
TEST(OcspCache, init) {
tls::OcspCache cache;
auto chain = openssl::load_certificates("client_chain.pem");
std::vector<tls::OcspCache::ocsp_entry_t> entries;
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;
entries.emplace_back(digest, "ocsp_response.der");
}
EXPECT_TRUE(cache.load(entries));
// std::cout << "digest: " << to_string(digest) << std::endl;
auto res = cache.lookup(digest);
EXPECT_NE(res.get(), nullptr);
}
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<TrustedAuthority_ptr>(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::CertificateNotAllowed);
EXPECT_EQ(openssl::verify_certificate(alt_server[0].get(), server_root, chain),
openssl::verify_result_t::CertificateNotAllowed);
}
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

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""
trusted ca keys hex string
"0069011d484406bca8888997a7462416445e7db117114c017f204de30f1cd42c9e6dae91b6a8ac9b8d481ba601597be7013ad6fc397b78b01d90cea1b7f909f145011d484406bca8888997a7462416445e7db117114c0100fae3900795c888a4d4d7bd9fdffa60418ac19f"
length 0069
"01 1d484406bca8888997a7462416445e7db117114c"
"01 7f204de30f1cd42c9e6dae91b6a8ac9b8d481ba6"
"01 597be7013ad6fc397b78b01d90cea1b7f909f145"
"01 1d484406bca8888997a7462416445e7db117114c"
"01 00fae3900795c888a4d4d7bd9fdffa60418ac19f"
key hash from certificate
openssl x509 -in cert.pem -pubkey -noout | openssl enc -base64 -d | openssl dgst -sha1
"""
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
import argparse
def certificate_key_hash(filename):
with open(filename, "rb") as fp:
cert = x509.load_pem_x509_certificate(fp.read())
pub = cert.public_key()
pub_der = pub.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
dgst = hashes.Hash(hashes.SHA1())
dgst.update(pub_der)
sha1 = dgst.finalize()
print(sha1.hex())
def certificate_hash(filename):
# note this is the hash of the whole certificate including signature
with open(filename, "rb") as fp:
cert = x509.load_pem_x509_certificate(fp.read())
pub_der = cert.public_bytes(encoding=serialization.Encoding.DER)
dgst = hashes.Hash(hashes.SHA1())
dgst.update(pub_der)
sha1 = dgst.finalize()
print(sha1.hex())
def trusted_ca_keys_decode(data):
data_len = int.from_bytes(data[:2], "big", signed=False)
data = data[2:]
assert len(data) == data_len
while data:
entry_type = data[0]
data = data[1:]
if entry_type == 0:
print("pre_agreed")
elif entry_type == 1:
sha1 = data[:20]
data = data[20:]
print("key_sha1_hash: %s" % sha1.hex())
elif entry_type == 2:
print("x509_name (not decoded yet)")
elif entry_type == 3:
sha1 = data[:20]
data = data[20:]
print("cert_sha1_hash: %s" % sha1.hex())
def trusted_ca_keys_decode_file(filename):
with open(filename, "rb") as fp:
trusted_ca_keys_decode(fp.read())
def trusted_ca_keys_decode_hex(hexstr):
trusted_ca_keys_decode(bytes.fromhex(hexstr))
# -----------------------------------------------------------------------------
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--key",
action="store",
help="filename: Print sha1 hash of certificate public key",
)
parser.add_argument(
"--cert", action="store", help="filename: Print sha1 hash of certificate"
)
parser.add_argument(
"--file", action="store", help="filename: Parse trusted ca keys"
)
parser.add_argument(
"--hex", action="store", help="parse trusted ca keys hex string"
)
args = parser.parse_args()
if args.key:
certificate_key_hash(args.key)
if args.cert:
certificate_hash(args.cert)
if args.file:
trusted_ca_keys_decode_file(args.file)
if args.hex:
trusted_ca_keys_decode_hex(args.hex)