diff --git a/config/CMakeLists.txt b/config/CMakeLists.txt index f6349c26c8..499a2d2dd6 100644 --- a/config/CMakeLists.txt +++ b/config/CMakeLists.txt @@ -3,6 +3,7 @@ generate_config_run_script(CONFIG sil-two-evse) generate_config_run_script(CONFIG sil-ocpp) generate_config_run_script(CONFIG sil-ocpp201) generate_config_run_script(CONFIG sil-dc) +generate_config_run_script(CONFIG sil-dc-tls) generate_config_run_script(CONFIG sil-dc-sae-v2g) generate_config_run_script(CONFIG sil-dc-sae-v2h) generate_config_run_script(CONFIG sil-two-evse-dc) @@ -25,7 +26,7 @@ install( install( DIRECTORY "certs" DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}/everest" - FILES_MATCHING PATTERN "*.pem" PATTERN "*.key" PATTERN "*.der" PATTERN "*.txt" PATTERN "*.jks" PATTERN "*.p12" + FILES_MATCHING PATTERN "*.pem" PATTERN "*.key" PATTERN "*.der" PATTERN "*.txt" PATTERN "*.jks" PATTERN "*.p12" ) install( diff --git a/config/config-sil-dc-tls.yaml b/config/config-sil-dc-tls.yaml new file mode 100644 index 0000000000..867983a9a9 --- /dev/null +++ b/config/config-sil-dc-tls.yaml @@ -0,0 +1,147 @@ +active_modules: + iso15118_charger: + module: EvseV2G + config_module: + device: auto + tls_security: force + connections: + security: + - module_id: evse_security + implementation_id: main + iso15118_car: + module: PyEvJosev + config_module: + device: auto + supported_DIN70121: false + supported_ISO15118_2: true + tls_active: true + enforce_tls: true + evse_manager: + module: EvseManager + config_module: + connector_id: 1 + country_code: DE + evse_id: DE*PNX*E12345*1 + evse_id_din: 49A80737A45678 + session_logging: true + session_logging_xml: false + session_logging_path: /tmp/everest-logs + charge_mode: DC + hack_allow_bpt_with_iso2: true + payment_enable_contract: false + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_car_side: + - module_id: powersupply_dc + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + powersupply_DC: + - module_id: powersupply_dc + implementation_id: main + imd: + - module_id: imd + implementation_id: main + powersupply_dc: + module: DCSupplySimulator + yeti_driver: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + imd: + config_implementation: + main: + selftest_success: true + module: IMDSimulator + ev_manager: + module: EvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + dc_target_current: 20 + dc_target_voltage: 400 + connections: + ev_board_support: + - module_id: yeti_driver + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + auth: + module: Auth + config_module: + connection_timeout: 10 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: token_provider + implementation_id: main + token_validator: + - module_id: token_validator + implementation_id: main + evse_manager: + - module_id: evse_manager + implementation_id: evse + token_provider: + module: DummyTokenProvider + config_implementation: + main: + token: TOKEN1 + connections: + evse: + - module_id: evse_manager + implementation_id: evse + token_validator: + module: DummyTokenValidator + config_implementation: + main: + validation_result: Accepted + validation_reason: Token seems valid + sleep: 0.25 + evse_security: + module: EvseSecurity + config_module: + private_key_password: "123456" + energy_manager: + module: EnergyManager + config_module: + schedule_total_duration: 1 + schedule_interval_duration: 60 + debug: false + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: evse_manager + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + api: + module: API + connections: + evse_manager: + - module_id: evse_manager + implementation_id: evse +x-module-layout: {} diff --git a/lib/staging/tls/openssl_util.cpp b/lib/staging/tls/openssl_util.cpp index 15e8cdbe24..a582813bd0 100644 --- a/lib/staging/tls/openssl_util.cpp +++ b/lib/staging/tls/openssl_util.cpp @@ -329,34 +329,34 @@ bool signature_to_bn(bn_t& r, bn_t& s, const std::uint8_t* sig_p, std::size_t le }; std::vector load_certificates(const char* filename) { - assert(filename != nullptr); - std::vector result{}; - auto* store = OSSL_STORE_open(filename, UI_null(), nullptr, nullptr, nullptr); - - if (store != nullptr) { - while (OSSL_STORE_eof(store) != 1) { - auto* info = OSSL_STORE_load(store); - - if (info != nullptr) { - if (OSSL_STORE_error(store) == 1) { - log_error("OSSL_STORE_load"); - } else { - const auto type = OSSL_STORE_INFO_get_type(info); - - if (type == OSSL_STORE_INFO_CERT) { - // get a copy of the certificate - auto cert = OSSL_STORE_INFO_get1_CERT(info); - result.push_back({cert, &X509_free}); + + if (filename != nullptr) { + auto* store = OSSL_STORE_open(filename, UI_null(), nullptr, nullptr, nullptr); + if (store != nullptr) { + while (OSSL_STORE_eof(store) != 1) { + auto* info = OSSL_STORE_load(store); + + if (info != nullptr) { + if (OSSL_STORE_error(store) == 1) { + log_error("OSSL_STORE_load"); + } else { + const auto type = OSSL_STORE_INFO_get_type(info); + + if (type == OSSL_STORE_INFO_CERT) { + // get a copy of the certificate + auto cert = OSSL_STORE_INFO_get1_CERT(info); + result.push_back({cert, &X509_free}); + } } } - } - OSSL_STORE_INFO_free(info); + OSSL_STORE_INFO_free(info); + } } - } - OSSL_STORE_close(store); + OSSL_STORE_close(store); + } return result; } diff --git a/lib/staging/tls/tests/patched_test.cpp b/lib/staging/tls/tests/patched_test.cpp index 1195335698..525396647d 100644 --- a/lib/staging/tls/tests/patched_test.cpp +++ b/lib/staging/tls/tests/patched_test.cpp @@ -172,7 +172,7 @@ class OcspTest : public testing::Test { server_config.service = "8444"; server_config.ipv6_only = false; server_config.verify_client = false; - server_config.io_timeout_ms = 100; + server_config.io_timeout_ms = 500; client_config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; // client_config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; @@ -194,8 +194,10 @@ class OcspTest : public testing::Test { } } - void start() { - if (server.init(server_config)) { + void start(const std::function& init_ssl = nullptr) { + using state_t = tls::Server::state_t; + const auto res = server.init(server_config, init_ssl); + if ((res == state_t::init_complete) || (res == state_t::init_socket)) { server_thread = std::thread(&run_server, std::ref(server)); server.wait_running(); } @@ -228,6 +230,24 @@ class OcspTest : public testing::Test { } }; +bool ssl_init(tls::Server& server) { + std::cout << "ssl_init" << std::endl; + tls::Server::config_t server_config; + server_config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + server_config.ciphersuites = ""; + server_config.certificate_chain_file = "server_chain.pem"; + server_config.private_key_file = "server_priv.pem"; + server_config.ocsp_response_files = {"ocsp_response.der", "ocsp_response.der"}; + server_config.host = "localhost"; + server_config.service = "8444"; + server_config.ipv6_only = false; + server_config.verify_client = false; + server_config.io_timeout_ms = 100; + const auto res = server.update(server_config); + EXPECT_TRUE(res); + return res; +} + // ---------------------------------------------------------------------------- // The tests @@ -246,6 +266,20 @@ TEST_F(OcspTest, NonBlockingConnect) { EXPECT_TRUE(is_reset(flags_t::status_request_v2)); } +TEST_F(OcspTest, delayedConfig) { + // partial config + server_config.certificate_chain_file = nullptr; + server_config.private_key_file = nullptr; + server_config.ocsp_response_files.clear(); + + start(ssl_init); + connect(); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_reset(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); +} + TEST_F(OcspTest, TLS12) { // test using TLS 1.2 start(); diff --git a/lib/staging/tls/tests/tls_main.cpp b/lib/staging/tls/tests/tls_main.cpp index d6734f38df..c521fe010e 100644 --- a/lib/staging/tls/tests/tls_main.cpp +++ b/lib/staging/tls/tests/tls_main.cpp @@ -86,7 +86,7 @@ int main() { server.stop(); }); - server.init(config); + server.init(config, nullptr); server.wait_stopped(); // server.serve(&handle_connection); diff --git a/lib/staging/tls/tests/tls_test.cpp b/lib/staging/tls/tests/tls_test.cpp index 2e3df0590f..5d6acb5003 100644 --- a/lib/staging/tls/tests/tls_test.cpp +++ b/lib/staging/tls/tests/tls_test.cpp @@ -18,6 +18,74 @@ std::string to_string(const openssl::sha_256_digest_t& digest) { namespace { +TEST(strdup, usage) { + // auto* r1 = strdup(nullptr); need to ensure non-nullptr + auto* r2 = strdup(""); + auto* r3 = strdup("hello"); + // free(r1); + free(r2); + free(r3); + free(nullptr); +} + +TEST(string, use) { + // was hoping to use std::string for config, but ... + std::string empty; + std::string space{""}; + std::string value{"something"}; + + EXPECT_TRUE(empty.empty()); + // EXPECT_FALSE(space.empty()); was hoping it would be true + EXPECT_FALSE(value.empty()); + + // EXPECT_EQ(empty.c_str(), nullptr); was hoping it would be nullptr + EXPECT_NE(space.c_str(), nullptr); + EXPECT_NE(value.c_str(), nullptr); +} + +TEST(ConfigItem, test) { + tls::ConfigItem i1; + tls::ConfigItem i2{nullptr}; + tls::ConfigItem i3{"Hello"}; + tls::ConfigItem i4 = nullptr; + tls::ConfigItem i5(nullptr); + tls::ConfigItem i6("Hello"); + + EXPECT_EQ(i1, nullptr); + EXPECT_EQ(i4, nullptr); + EXPECT_EQ(i5, nullptr); + + EXPECT_EQ(i2, i5); + EXPECT_EQ(i3, i6); + + EXPECT_EQ(i1, i2); + EXPECT_NE(i1, i3); + EXPECT_EQ(i1, i5); + EXPECT_NE(i1, i6); + + auto j1(std::move(i3)); + auto j2 = std::move(i6); + EXPECT_EQ(i6, i3); + EXPECT_EQ(j1, j2); + EXPECT_EQ(j1, "Hello"); + EXPECT_NE(j1, i6); + + EXPECT_NE(j1, nullptr); + EXPECT_NE(j2, nullptr); + + EXPECT_EQ(i3, nullptr); + EXPECT_EQ(i6, nullptr); + EXPECT_EQ(i6, i3); + + std::vector j3 = {"one", "two", nullptr}; + EXPECT_EQ(j3[0], "one"); + EXPECT_EQ(j3[1], "two"); + EXPECT_EQ(j3[2], nullptr); + + const char* p = j1; + EXPECT_STREQ(p, "Hello"); +} + TEST(OcspCache, initEmpty) { tls::OcspCache cache; openssl::sha_256_digest_t digest{}; diff --git a/lib/staging/tls/tls.cpp b/lib/staging/tls/tls.cpp index 7ae9a13ddd..2220fb7219 100644 --- a/lib/staging/tls/tls.cpp +++ b/lib/staging/tls/tls.cpp @@ -49,6 +49,7 @@ template <> class default_delete { } // namespace std using ::openssl::log_error; +using ::openssl::log_warning; namespace { @@ -328,7 +329,7 @@ void ssl_shutdown(SSL* ctx, std::int32_t timeout_ms) { bool configure_ssl_ctx(SSL_CTX* ctx, const char* ciphersuites, const char* cipher_list, const char* certificate_chain_file, const char* private_key_file, - const char* private_key_password) { + const char* private_key_password, bool required) { bool bRes{true}; if (ctx == nullptr) { @@ -365,6 +366,8 @@ bool configure_ssl_ctx(SSL_CTX* ctx, const char* ciphersuites, const char* ciphe log_error("SSL_CTX_use_certificate_chain_file"); bRes = false; } + } else { + bRes = !required; } if (private_key_file != nullptr) { @@ -385,6 +388,8 @@ bool configure_ssl_ctx(SSL_CTX* ctx, const char* ciphersuites, const char* ciphe log_error("SSL_CTX_check_private_key"); bRes = false; } + } else { + bRes = !required; } } @@ -413,10 +418,54 @@ OCSP_RESPONSE* load_ocsp(const char* filename) { return resp; } +constexpr char* dup(const char* value) { + char* res = nullptr; + if (value != nullptr) { + res = strdup(value); + } + return res; +} + } // namespace namespace tls { +ConfigItem::ConfigItem(const char* value) : m_ptr(dup(value)) { +} +ConfigItem& ConfigItem::operator=(const char* value) { + m_ptr = dup(value); + return *this; +} +ConfigItem::ConfigItem(const ConfigItem& obj) : m_ptr(dup(obj.m_ptr)) { +} +ConfigItem& ConfigItem::operator=(const ConfigItem& obj) { + m_ptr = dup(obj.m_ptr); + return *this; +} +ConfigItem::ConfigItem(ConfigItem&& obj) noexcept : m_ptr(obj.m_ptr) { + obj.m_ptr = nullptr; +} +ConfigItem& ConfigItem::operator=(ConfigItem&& obj) noexcept { + m_ptr = obj.m_ptr; + obj.m_ptr = nullptr; + return *this; +} +ConfigItem::~ConfigItem() { + free(m_ptr); + m_ptr = nullptr; +} + +bool ConfigItem::operator==(const char* ptr) const { + bool result{false}; + if (m_ptr == ptr) { + // both nullptr, or both point to the same string + result = true; + } else if ((m_ptr != nullptr) && (ptr != nullptr)) { + result = strcmp(m_ptr, ptr) == 0; + } + return result; +} + using SSL_ptr = std::unique_ptr; using SSL_CTX_ptr = std::unique_ptr; using OCSP_RESPONSE_ptr = std::shared_ptr; @@ -1001,7 +1050,7 @@ bool Server::init_ssl(const config_t& cfg) { const SSL_METHOD* method = TLS_server_method(); auto* ctx = SSL_CTX_new(method); auto bRes = configure_ssl_ctx(ctx, cfg.ciphersuites, cfg.cipher_list, cfg.certificate_chain_file, - cfg.private_key_file, cfg.private_key_password); + cfg.private_key_file, cfg.private_key_password, true); if (bRes) { int mode = SSL_VERIFY_NONE; @@ -1050,56 +1099,81 @@ bool Server::init_ssl(const config_t& cfg) { return ctx != nullptr; } -bool Server::init(const config_t& cfg) { +Server::state_t Server::init(const config_t& cfg, const std::function& init_ssl) { std::lock_guard lock(m_mutex); - (void)update_ocsp(cfg); m_timeout_ms = cfg.io_timeout_ms; - bool bRes = init_ssl(cfg); - bRes = bRes && init_socket(cfg); - m_state = state_t::init; - return bRes; + m_init_callback = init_ssl; + m_state = state_t::init_needed; + if (init_socket(cfg)) { + m_state = state_t::init_socket; + if (update(cfg)) { + m_state = state_t::init_complete; + } + } + return m_state; } -bool Server::update_ocsp(const config_t& cfg) { - std::vector entries; - auto chain = openssl::load_certificates(cfg.certificate_chain_file); - bool bRes = chain.size() == cfg.ocsp_response_files.size(); +bool Server::update(const config_t& cfg) { + bool bRes = init_ssl(cfg); if (bRes) { - for (std::size_t i = 0; i < chain.size(); i++) { - const auto& file = cfg.ocsp_response_files[i]; - const auto& cert = chain[i]; - - if (file != nullptr) { - openssl::sha_256_digest_t digest{}; - if (OcspCache::digest(digest, cert.get())) { - entries.emplace_back(digest, file); + std::vector entries; + auto chain = openssl::load_certificates(cfg.certificate_chain_file); + if (chain.size() == cfg.ocsp_response_files.size()) { + for (std::size_t i = 0; i < chain.size(); i++) { + const auto& file = cfg.ocsp_response_files[i]; + const auto& cert = chain[i]; + + if (file != nullptr) { + openssl::sha_256_digest_t digest{}; + if (OcspCache::digest(digest, cert.get())) { + entries.emplace_back(digest, file); + } } } - } - bRes = m_cache.load(entries); - } else { - log_error(std::string("update_ocsp: ocsp files != ") + std::to_string(chain.size())); + bRes = m_cache.load(entries); + } else { + log_warning(std::string("update_ocsp: ocsp files != ") + std::to_string(chain.size())); + } } return bRes; } -bool Server::serve(const std::function& ctx)>& handler) { +Server::state_t Server::serve(const std::function& ctx)>& handler) { assert(m_context != nullptr); // prevent init() or server() being called while serve is running std::lock_guard lock(m_mutex); bool bRes = false; + + state_t tmp = m_state; + + switch (tmp) { + case state_t::init_socket: + if (m_init_callback != nullptr) { + bRes = m_socket != INVALID_SOCKET; + } + break; + case state_t::init_complete: + bRes = m_socket != INVALID_SOCKET; + break; + case state_t::init_needed: + case state_t::running: + case state_t::stopped: + default: + break; + } + { std::lock_guard lock(m_cv_mutex); m_running = true; } m_cv.notify_all(); - if (m_socket != INVALID_SOCKET) { + if (bRes) { m_exit = false; - m_state = state_t::running; + m_state = (m_state == state_t::init_complete) ? state_t::running : state_t::init_socket; while (!m_exit) { auto* peer = BIO_ADDR_new(); if (peer == nullptr) { @@ -1122,6 +1196,15 @@ bool Server::serve(const std::function& c } }; + if ((soc >= 0) && (m_state == state_t::init_socket)) { + if (m_init_callback(*this)) { + m_state = state_t::running; + } else { + BIO_closesocket(soc); + soc = INVALID_SOCKET; + } + } + if (m_exit) { if (soc >= 0) { BIO_closesocket(soc); @@ -1155,7 +1238,7 @@ bool Server::serve(const std::function& c m_running = false; } m_cv.notify_all(); - return bRes; + return m_state; } void Server::stop() { @@ -1208,7 +1291,7 @@ bool Client::init(const config_t& cfg) { const SSL_METHOD* method = TLS_client_method(); auto* ctx = SSL_CTX_new(method); auto bRes = configure_ssl_ctx(ctx, cfg.ciphersuites, cfg.cipher_list, cfg.certificate_chain_file, - cfg.private_key_file, cfg.private_key_password); + cfg.private_key_file, cfg.private_key_password, false); if (bRes) { int mode = SSL_VERIFY_NONE; diff --git a/lib/staging/tls/tls.hpp b/lib/staging/tls/tls.hpp index f77a88bb64..60f27cb318 100644 --- a/lib/staging/tls/tls.hpp +++ b/lib/staging/tls/tls.hpp @@ -38,6 +38,52 @@ struct ocsp_cache_ctx; struct server_ctx; struct client_ctx; +// ---------------------------------------------------------------------------- +// ConfigItem - store configuration item allowing nullptr + +/** + * \brief class to hold configuration strings, behaves like const char * + * but keeps a copy + * + * unlike std::string this class allows nullptr as a valid setting. + * + * unlike const char * it doesn't have scope issues since it holds + * a copy. + */ +class ConfigItem { +private: + char* m_ptr{nullptr}; + +public: + ConfigItem() = default; + ConfigItem(const char* value); // must not be explicit + ConfigItem& operator=(const char* value); + ConfigItem(const ConfigItem& obj); + ConfigItem& operator=(const ConfigItem& obj); + ConfigItem(ConfigItem&& obj) noexcept; + ConfigItem& operator=(ConfigItem&& obj) noexcept; + + ~ConfigItem(); + + inline operator const char*() const { + return m_ptr; + } + + bool operator==(const char* ptr) const; + + inline bool operator!=(const char* ptr) const { + return !(*this == ptr); + } + + inline bool operator==(const ConfigItem& obj) const { + return *this == obj.m_ptr; + } + + inline bool operator!=(const ConfigItem& obj) const { + return !(*this == obj); + } +}; + // ---------------------------------------------------------------------------- // Cache of OCSP responses for status_request and status_request_v2 extensions @@ -371,23 +417,24 @@ class Server { * \brief server state */ enum class state_t : std::uint8_t { - need_init, //!< not initialised yet - init, //!< initialised but not running - running, //!< waiting for connections - stopped, //!< stopped + init_needed, //!< not initialised yet - call init() + init_socket, //!< TCP listen socket initialised (but not SSL) - call update() + init_complete, //!< initialised but not running - call serve() + running, //!< waiting for connections - fully initialised + stopped, //!< stopped - reinitialisation will be needed }; struct config_t { - const char* cipher_list{nullptr}; // nullptr means use default - const char* ciphersuites{nullptr}; // nullptr means use default, "" disables TSL 1.3 - const char* certificate_chain_file{nullptr}; - const char* private_key_file{nullptr}; - const char* private_key_password{nullptr}; - const char* verify_locations_file{nullptr}; // for client certificate - const char* verify_locations_path{nullptr}; // for client certificate - const char* host{nullptr}; // see BIO_lookup_ex() - const char* service{nullptr}; // TLS port number - std::vector ocsp_response_files; // in certificate chain order + ConfigItem cipher_list{nullptr}; // nullptr means use default + ConfigItem ciphersuites{nullptr}; // nullptr means use default, "" disables TSL 1.3 + ConfigItem certificate_chain_file{nullptr}; + ConfigItem private_key_file{nullptr}; + ConfigItem private_key_password{nullptr}; + ConfigItem verify_locations_file{nullptr}; // for client certificate + ConfigItem verify_locations_path{nullptr}; // for client certificate + ConfigItem host{nullptr}; // see BIO_lookup_ex() + ConfigItem service{nullptr}; // TLS port number + std::vector ocsp_response_files; // in certificate chain order int socket{INVALID_SOCKET}; // use this specific socket - bypasses socket setup in init_socket() when set std::int32_t io_timeout_ms{-1}; // socket timeout in milliseconds bool ipv6_only{true}; @@ -400,12 +447,13 @@ class Server { bool m_running{false}; std::int32_t m_timeout_ms{-1}; std::atomic_bool m_exit{false}; - std::atomic m_state{state_t::need_init}; + std::atomic m_state{state_t::init_needed}; std::mutex m_mutex; std::mutex m_cv_mutex; std::condition_variable m_cv; OcspCache m_cache; CertificateStatusRequestV2 m_status_request_v2; + std::function m_init_callback{nullptr}; /** * \brief initialise the server socket @@ -432,27 +480,43 @@ class Server { /** * \brief initialise the server socket and TLS configuration * \param[in] cfg server configuration - * \return true on success - * \note when the server certificate and key change then the server needs - * to be stopped, initialised and start serving. + * \param[in] init_ssl function to collect certificates and keys, can be nullptr + * \return need_init - initialisation failed + * socket_init - server socket created and ready for serve() + * init_complete - SSL certificates and keys loaded + * + * It is possible to initialise the server and start listening for + * connections before certificates and keys are available. + * when init() returns socket_init the server will call init_ssl() with a + * reference to the object so that update() can be called with updated + * OCSP and SSL configuration. + * + * init_ssl() should return true when SSL has been configured so that the + * incoming connection is accepted. */ - bool init(const config_t& cfg); + state_t init(const config_t& cfg, const std::function& init_ssl); /** - * \brief update the OCSP cache + * \brief update the OCSP cache and SSL certificates and keys * \param[in] cfg server configuration * \return true on success - * \note used to update OCSP caches + * \note used to update OCSP caches and SSL config */ - bool update_ocsp(const config_t& cfg); + bool update(const config_t& cfg); /** * \brief wait for incomming connections * \param[in] handler called when there is a new connection - * \return false when there was an error listening for connections - * \note this is a blocking call that will not return until stop() has been called. - */ - bool serve(const std::function& ctx)>& handler); + * \return stopped after it has been running, or init_ values when listening + * can not start + * \note this is a blocking call that will not return until stop() has been + * called (unless it couldn't start listening) + * \note changing socket configuration requires stopping the server and + * calling init() + * \note after server() returns stopped init() will need to be called + * before further connections can be managed + */ + state_t serve(const std::function& ctx)>& handler); /** * \brief stop listening for new connections diff --git a/modules/EvseV2G/connection/tls_connection.cpp b/modules/EvseV2G/connection/tls_connection.cpp index bbe3ba4a5b..ac6aeced6d 100644 --- a/modules/EvseV2G/connection/tls_connection.cpp +++ b/modules/EvseV2G/connection/tls_connection.cpp @@ -86,18 +86,13 @@ void server_loop_thread(struct v2g_context* ctx) { assert(ctx != nullptr); assert(ctx->tls_server != nullptr); const auto res = ctx->tls_server->serve([ctx](auto con) { handle_new_connection_cb(con, ctx); }); - if (!res) { + if (res != tls::Server::state_t::stopped) { dlog(DLOG_LEVEL_ERROR, "tls::Server failed to serve"); } } -} // namespace - -namespace tls { - -int connection_init(struct v2g_context* ctx) { +bool build_config(tls::Server::config_t& config, struct v2g_context* ctx) { assert(ctx != nullptr); - assert(ctx->tls_server != nullptr); assert(ctx->r_security != nullptr); using types::evse_security::CaCertificateType; @@ -111,13 +106,18 @@ int connection_init(struct v2g_context* ctx) { * hence private keys are always encrypted. */ - tls::Server::config_t config; - bool bResult = false; + bool bResult{false}; config.cipher_list = "ECDHE-ECDSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256"; config.ciphersuites = ""; // disable TLS 1.3 config.verify_client = false; // contract certificate managed in-band in 15118-2 + // use the existing configured socket + // TODO(james-ctc): switch to server socket init code otherwise there + // may be issues with reinitialisation + config.socket = ctx->tls_socket.fd; + config.io_timeout_ms = static_cast(ctx->network_read_timeout_tls); + // information from libevse-security const auto cert_info = ctx->r_security->call_get_leaf_certificate_info(LeafCertificateType::V2G, EncodingFormat::PEM, false); @@ -146,19 +146,57 @@ int connection_init(struct v2g_context* ctx) { } } - // use the existing configured socket - config.socket = ctx->tls_socket.fd; - config.io_timeout_ms = static_cast(ctx->network_read_timeout_tls); - - ctx->tls_server->stop(); - ctx->tls_server->wait_stopped(); - bResult = ctx->tls_server->init(config); + bResult = true; } else { dlog(DLOG_LEVEL_ERROR, "Failed to read cert_info! Empty response"); } } - return (bResult) ? 0 : -1; + return bResult; +} + +bool configure_ssl(tls::Server& server, struct v2g_context* ctx) { + tls::Server::config_t config; + bool bResult{false}; + + dlog(DLOG_LEVEL_WARNING, "configure_ssl"); + + // The config of interest is from Evse Security, no point in updating + // config when there is a problem + if (build_config(config, ctx)) { + bResult = server.update(config); + } + + return bResult; +} + +} // namespace + +namespace tls { + +int connection_init(struct v2g_context* ctx) { + using state_t = tls::Server::state_t; + + assert(ctx != nullptr); + assert(ctx->tls_server != nullptr); + assert(ctx->r_security != nullptr); + + int res{-1}; + tls::Server::config_t config; + + // build_config can fail due to issues with Evse Security, + // this can be retried later. Not treated as an error. + (void)build_config(config, ctx); + + // apply config + ctx->tls_server->stop(); + ctx->tls_server->wait_stopped(); + const auto result = ctx->tls_server->init(config, [ctx](auto& server) { return configure_ssl(server, ctx); }); + if ((result == state_t::init_complete) || (result == state_t::init_socket)) { + res = 0; + } + + return res; } int connection_start_server(struct v2g_context* ctx) { @@ -171,6 +209,10 @@ int connection_start_server(struct v2g_context* ctx) { try { ctx->tls_server->stop(); ctx->tls_server->wait_stopped(); + if (ctx->tls_server->state() == tls::Server::state_t::stopped) { + // need to re-initialise + tls::connection_init(ctx); + } std::thread serve_loop(server_loop_thread, ctx); serve_loop.detach(); ctx->tls_server->wait_running(); diff --git a/modules/EvseV2G/tests/README.md b/modules/EvseV2G/tests/README.md index 3168e40403..21b4cd129d 100644 --- a/modules/EvseV2G/tests/README.md +++ b/modules/EvseV2G/tests/README.md @@ -17,7 +17,7 @@ $ ninja install ## Run EVerest in SIL 1. start MQTT broker -2. from `build/run-scripts` run `./run-sil-dc.sh` +2. from `build/run-scripts` run `./run-sil-dc-tls.sh` 3. from `build/run-scripts` run `./nodered-sil-dc.sh` 4. open web browser [EVerest Node-RED dashboard](http://localhost:1880/ui/)