Skip to content

Commit

Permalink
Added libwesocket support for secprofile 0 and 1 (#508)
Browse files Browse the repository at this point in the history
Signed-off-by: AssemblyJohn <[email protected]>
  • Loading branch information
AssemblyJohn authored Mar 6, 2024
1 parent ba83f17 commit 135b385
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 47 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,9 +540,13 @@ Local testing is still in progress.
## Building with FetchContent instead of EDM
In [doc/build-with-fetchcontent](doc/build-with-fetchcontent) you can find an example how to build libocpp with FetchContent instead of EDM.

## Support for security profile 2 and 3 with TPM in OCPP 1.6 using libwebsockets
## Support for libwebsockets

If you want to try the new websocket implementation based on libwebsockets (supporting security profile 2 and 3 with TPM) you can set the following cmake option.
A new websocket implementation based on libwebsockets will deprecate the old websocket++ implmentation. It supports all security profiles, along with TPM usage.

### Support for TPM keys

In order to use the TPM keys, it is mandatory to use the libwebsocket implementation with the following cmake option.

```bash
cmake .. -DLIBOCPP_ENABLE_LIBWEBSOCKETS=ON
Expand Down
2 changes: 1 addition & 1 deletion lib/ocpp/common/websocket/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ target_sources(ocpp
if(LIBOCPP_ENABLE_LIBWEBSOCKETS)
target_sources(ocpp
PRIVATE
websocket_tls_tpm.cpp
websocket_libwebsockets.cpp
)
endif()
2 changes: 1 addition & 1 deletion lib/ocpp/common/websocket/websocket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#include <ocpp/v16/types.hpp>

#ifdef LIBOCPP_ENABLE_LIBWEBSOCKETS
#include <ocpp/common/websocket/websocket_tls_tpm.hpp>
#include <ocpp/common/websocket/websocket_libwebsockets.hpp>
#endif

#include <boost/algorithm/string.hpp>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
#include <evse_security/crypto/openssl/openssl_tpm.hpp>
#include <ocpp/common/websocket/websocket_tls_tpm.hpp>
#include <ocpp/common/websocket/websocket_libwebsockets.hpp>

#include <everest/logging.hpp>

Expand Down Expand Up @@ -151,6 +151,93 @@ struct WebsocketMessage {
std::atomic_bool message_sent;
};

static std::vector<std::string> get_subject_alt_names(const X509* x509) {
std::vector<std::string> list;
GENERAL_NAMES* subject_alt_names =
static_cast<GENERAL_NAMES*>(X509_get_ext_d2i(x509, NID_subject_alt_name, NULL, NULL));
if (subject_alt_names == nullptr) {
return list;
}
for (int i = 0; i < sk_GENERAL_NAME_num(subject_alt_names); i++) {
GENERAL_NAME* gen = sk_GENERAL_NAME_value(subject_alt_names, i);
if (gen == nullptr) {
continue;
}
if (gen->type == GEN_URI || gen->type == GEN_DNS || gen->type == GEN_EMAIL) {
ASN1_IA5STRING* asn1_str = gen->d.uniformResourceIdentifier;
std::string san = std::string(reinterpret_cast<const char*>(ASN1_STRING_get0_data(asn1_str)),
ASN1_STRING_length(asn1_str));
list.push_back(san);
} else if (gen->type == GEN_IPADD) {
unsigned char* ip = gen->d.ip->data;
if (gen->d.ip->length == 4) { // only support IPv4 for now
std::stringstream ip_stream;
ip_stream << static_cast<int>(ip[0]) << '.' << static_cast<int>(ip[1]) << '.' << static_cast<int>(ip[2])
<< '.' << static_cast<int>(ip[3]);
list.push_back(ip_stream.str());
}
}
}
GENERAL_NAMES_free(subject_alt_names);
return list;
}

static bool verify_csms_cn(const std::string& hostname, bool preverified, const X509_STORE_CTX* ctx) {

// Error depth gives the depth in the chain (with 0 = leaf certificate) where
// a potential (!) error occurred; error here means current error code and can also be "OK".
// This thus gives also the position (in the chain) of the currently to be verified certificate.
// If depth is 0, we need to check the leaf certificate;
// If depth > 0, we are verifying a CA (or SUB-CA) certificate and thus trust "preverified"
int depth = X509_STORE_CTX_get_error_depth(ctx);

if (!preverified) {
int error = X509_STORE_CTX_get_error(ctx);
EVLOG_warning << "Invalid certificate error '" << X509_verify_cert_error_string(error) << "' (at chain depth '"
<< depth << "')";
}

// only check for CSMS server certificate
if (depth == 0 and preverified) {
// Get server certificate
X509* server_cert = X509_STORE_CTX_get_current_cert(ctx);

// Extract CN from csms server's certificate
X509_NAME* subject_name = X509_get_subject_name(server_cert);
char common_name[256];
if (X509_NAME_get_text_by_NID(subject_name, NID_commonName, common_name, sizeof(common_name)) <= 0) {
EVLOG_error << "Could not extract CN from CSMS server certificate";
return false;
}

auto alt_names = get_subject_alt_names(server_cert);

// Compare the extracted CN with the expected FQDN
if (hostname == common_name) {
EVLOG_debug << "FQDN matches CN of server certificate: " << hostname;
return true;
}

// If the CN does not match, go through all alternative names
for (auto name : alt_names) {
if (hostname == name) {
EVLOG_debug << "FQDN matches alternative name of server certificate: " << hostname;
return true;
}
}

std::stringstream s;
s << "FQDN '" << hostname << "' does not match CN '" << common_name << "' or any alternative names";
for (auto alt_name : alt_names) {
s << " '" << alt_name << "'";
}
EVLOG_warning << s.str();
return false;
}

return preverified;
}

WebsocketTlsTPM::WebsocketTlsTPM(const WebsocketConnectionOptions& connection_options,
std::shared_ptr<EvseSecurity> evse_security) :
WebsocketBase(), evse_security(evse_security) {
Expand Down Expand Up @@ -178,8 +265,6 @@ void WebsocketTlsTPM::set_connection_options(const WebsocketConnectionOptions& c
switch (connection_options.security_profile) { // `switch` used to lint on missing enum-values
case security::SecurityProfile::OCPP_1_6_ONLY_UNSECURED_TRANSPORT_WITHOUT_BASIC_AUTHENTICATION:
case security::SecurityProfile::UNSECURED_TRANSPORT_WITH_BASIC_AUTHENTICATION:
throw std::invalid_argument("`security_profile` is not a TLS-profile");
[[fallthrough]];
case security::SecurityProfile::TLS_WITH_BASIC_AUTHENTICATION:
case security::SecurityProfile::TLS_WITH_CLIENT_SIDE_CERTIFICATES:
break;
Expand Down Expand Up @@ -347,39 +432,35 @@ void WebsocketTlsTPM::client_loop() {

info.fd_limit_per_thread = 1 + 1 + 1;

// Setup context - need to know the key type first

std::string path_key;
std::string path_chain;
std::optional<std::string> password;
if (this->connection_options.security_profile == 2 || this->connection_options.security_profile == 3) {
// Setup context - need to know the key type first
std::string path_key;
std::string path_chain;
std::optional<std::string> password;

if (this->connection_options.security_profile == 3) {
if (this->connection_options.security_profile == 3) {

const auto certificate_key_pair =
this->evse_security->get_key_pair(CertificateSigningUseEnum::ChargingStationCertificate);
const auto certificate_key_pair =
this->evse_security->get_key_pair(CertificateSigningUseEnum::ChargingStationCertificate);

if (!certificate_key_pair.has_value()) {
EVLOG_AND_THROW(std::runtime_error(
"Connecting with security profile 3 but no client side certificate is present or valid"));
}
if (!certificate_key_pair.has_value()) {
EVLOG_AND_THROW(std::runtime_error(
"Connecting with security profile 3 but no client side certificate is present or valid"));
}

path_chain = certificate_key_pair.value().certificate_path;
if (path_chain.empty()) {
path_chain = certificate_key_pair.value().certificate_single_path;
path_chain = certificate_key_pair.value().certificate_path;
if (path_chain.empty()) {
path_chain = certificate_key_pair.value().certificate_single_path;
}
path_key = certificate_key_pair.value().key_path;
password = certificate_key_pair.value().password;
}
path_key = certificate_key_pair.value().key_path;
password = certificate_key_pair.value().password;
}

SSL_CTX* ssl_ctx = nullptr;
bool tpm_key = false;
SSL_CTX* ssl_ctx = nullptr;
bool tpm_key = false;

{
if (!path_key.empty()) {
tpm_key = is_tpm_key_filename(path_key);
#ifdef DEBUG
EVLOG_info << "TPM Key: " << tpm_key;
#endif
}

OpenSSLProvider provider;
Expand All @@ -397,16 +478,16 @@ void WebsocketTlsTPM::client_loop() {
ERR_print_errors_fp(stderr);
EVLOG_AND_THROW(std::runtime_error("Unable to create ssl ctx."));
}
}

// Init TLS data
tls_init(ssl_ctx, path_chain, path_key, tpm_key, password);
// Init TLS data
tls_init(ssl_ctx, path_chain, path_key, tpm_key, password);

// Setup our context
info.provided_client_ssl_ctx = ssl_ctx;
// Setup our context
info.provided_client_ssl_ctx = ssl_ctx;

// Connection acquire the contexts
conn_data->sec_context = std::unique_ptr<SSL_CTX>(ssl_ctx);
// Connection acquire the contexts
conn_data->sec_context = std::unique_ptr<SSL_CTX>(ssl_ctx);
}

lws_context* lws_ctx = lws_create_context(&info);
if (nullptr == lws_ctx) {
Expand All @@ -420,13 +501,18 @@ void WebsocketTlsTPM::client_loop() {
lws_client_connect_info i;
memset(&i, 0, sizeof(lws_client_connect_info));

int ssl_connection = LCCSCF_USE_SSL;
// No SSL
int ssl_connection = 0;

// TODO: Completely remove after test
// ssl_connection |= LCCSCF_ALLOW_SELFSIGNED;
// ssl_connection |= LCCSCF_ALLOW_INSECURE;
// ssl_connection |= LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK;
// ssl_connection |= LCCSCF_ALLOW_EXPIRED;
if (this->connection_options.security_profile == 2 || this->connection_options.security_profile == 3) {
ssl_connection = LCCSCF_USE_SSL;

// TODO: Completely remove after test
// ssl_connection |= LCCSCF_ALLOW_SELFSIGNED;
// ssl_connection |= LCCSCF_ALLOW_INSECURE;
// ssl_connection |= LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK;
// ssl_connection |= LCCSCF_ALLOW_EXPIRED;
}

auto& uri = this->connection_options.csms_uri;

Expand Down Expand Up @@ -916,39 +1002,66 @@ int WebsocketTlsTPM::process_callback(void* wsi_ptr, int callback_reason, void*
}

switch (reason) {
// TODO: If required in the future
case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION:
// user is X509_STORE and and len is preverify_ok
if (false == verify_csms_cn(this->connection_options.csms_uri.get_hostname(), (len == 1),
reinterpret_cast<X509_STORE_CTX*>(user))) {
EVLOG_error << "Failed to verify server certificate cn!";
// Return 1 to fail the cert
return 1;
}

break;
case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS:
break;

case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: {
EVLOG_info << "Handshake with security profile: " << this->connection_options.security_profile;

unsigned char **ptr = reinterpret_cast<unsigned char**>(in), *end_header = (*ptr) + len;

if (this->connection_options.hostName.has_value()) {
auto& str = this->connection_options.hostName.value();
EVLOG_info << "User-Host is set to " << str;

if (0 != lws_add_http_header_by_token(wsi, lws_token_indexes::WSI_TOKEN_HOST,
reinterpret_cast<const unsigned char*>(str.c_str()), str.length(),
ptr, end_header)) {
EVLOG_AND_THROW(std::runtime_error("Could not append authorization header."));
}

/*
if (0 != lws_add_http_header_by_name(wsi, reinterpret_cast<const unsigned char*>("User-Host"),
reinterpret_cast<const unsigned char*>(str.c_str()), str.length(), ptr,
end_header)) {
EVLOG_AND_THROW(std::runtime_error("Could not append authorization header."));
}
*/
}

if (this->connection_options.security_profile == 2) {
if (this->connection_options.security_profile == 1 || this->connection_options.security_profile == 2) {
std::optional<std::string> authorization_header = this->getAuthorizationHeader();

if (authorization_header != std::nullopt) {
auto& str = authorization_header.value();

if (0 != lws_add_http_header_by_token(wsi, lws_token_indexes::WSI_TOKEN_HTTP_AUTHORIZATION,
reinterpret_cast<const unsigned char*>(str.c_str()), str.length(),
ptr, end_header)) {
EVLOG_AND_THROW(std::runtime_error("Could not append authorization header."));
}

/*
// TODO: See if we need to switch back here
if (0 != lws_add_http_header_by_name(wsi, reinterpret_cast<const unsigned char*>("Authorization"),
reinterpret_cast<const unsigned char*>(str.c_str()), str.length(),
ptr, end_header)) {
EVLOG_AND_THROW(std::runtime_error("Could not append authorization header."));
}
*/
} else {
EVLOG_AND_THROW(
std::runtime_error("No authorization key provided when connecting with security profile 2 or 3."));
std::runtime_error("No authorization key provided when connecting with security profile 1 or 2."));
}
}

Expand Down

0 comments on commit 135b385

Please sign in to comment.