diff --git a/config/v201/component_schemas/standardized/InternalCtrlr.json b/config/v201/component_schemas/standardized/InternalCtrlr.json index 72bceb646..87da33458 100644 --- a/config/v201/component_schemas/standardized/InternalCtrlr.json +++ b/config/v201/component_schemas/standardized/InternalCtrlr.json @@ -387,6 +387,70 @@ "minimum": 512, "default": "51200", "type": "integer" + }, + "V2GCertificateExpireCheckInitialDelaySeconds": { + "description": "Seconds to wait after boot notification to first check the V2G leaf certificate for expiration and potential renewal", + "variable_name": "V2GCertificateExpireCheckInitialDelaySeconds", + "characteristics": { + "supportsMonitoring": true, + "dataType": "integer" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadOnly" + } + ], + "default": "60", + "type": "integer" + }, + "V2GCertificateExpireCheckIntervalSeconds": { + "description": "Seconds between two checks for V2G leaf certificate expiration and potential renewal", + "variable_name": "V2GCertificateExpireCheckIntervalSeconds", + "characteristics": { + "supportsMonitoring": true, + "dataType": "integer" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadOnly" + } + ], + "default": "43200", + "type": "integer" + }, + "ClientCertificateExpireCheckInitialDelaySeconds": { + "description": "Seconds to wait after boot notification to first check the client certificate for expiration and potential renewal", + "variable_name": "ClientCertificateExpireCheckInitialDelaySeconds", + "characteristics": { + "supportsMonitoring": true, + "dataType": "integer" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadOnly" + } + ], + "default": "60", + "type": "integer" + }, + "ClientCertificateExpireCheckIntervalSeconds": { + "description": "Seconds between two checks for client certificate expiration and potential renewal", + "variable_name": "ClientCertificateExpireCheckIntervalSeconds", + "characteristics": { + "supportsMonitoring": true, + "dataType": "integer" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadOnly" + } + ], + "default": "43200", + "type": "integer" } }, "required": [ diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index e8a131d7b..b4b33371a 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -159,6 +159,8 @@ class ChargePoint : ocpp::ChargingStationBase { // timers Everest::SteadyTimer heartbeat_timer; Everest::SteadyTimer boot_notification_timer; + Everest::SteadyTimer client_certificate_expiration_check_timer; + Everest::SteadyTimer v2g_certificate_expiration_check_timer; ClockAlignedTimer aligned_meter_values_timer; // time keeping @@ -218,6 +220,9 @@ class ChargePoint : ocpp::ChargingStationBase { // internal helper functions void init_websocket(); WebsocketConnectionOptions get_ws_connection_options(const int32_t configuration_slot); + void init_certificate_expiration_check_timers(); + void scheduled_check_client_certificate_expiration(); + void scheduled_check_v2g_certificate_expiration(); /// \brief Gets the configured NetworkConnectionProfile based on the given \p configuration_slot . The /// central system uri ofthe connection options will not contain ws:// or wss:// because this method removes it if diff --git a/include/ocpp/v201/ctrlr_component_variables.hpp b/include/ocpp/v201/ctrlr_component_variables.hpp index 45d44671e..8e36fbdc1 100644 --- a/include/ocpp/v201/ctrlr_component_variables.hpp +++ b/include/ocpp/v201/ctrlr_component_variables.hpp @@ -60,6 +60,10 @@ extern const ComponentVariable& OcspRequestInterval; extern const ComponentVariable& WebsocketPingPayload; extern const ComponentVariable& WebsocketPongTimeout; extern const ComponentVariable& MaxCustomerInformationDataLength; +extern const ComponentVariable& V2GCertificateExpireCheckInitialDelaySeconds; +extern const ComponentVariable& V2GCertificateExpireCheckIntervalSeconds; +extern const ComponentVariable& ClientCertificateExpireCheckInitialDelaySeconds; +extern const ComponentVariable& ClientCertificateExpireCheckIntervalSeconds; extern const ComponentVariable& AlignedDataCtrlrEnabled; extern const ComponentVariable& AlignedDataCtrlrAvailable; extern const ComponentVariable& AlignedDataInterval; diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index c96177d84..530c6fd16 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -15,7 +15,6 @@ namespace v201 { const auto DEFAULT_BOOT_NOTIFICATION_RETRY_INTERVAL = std::chrono::seconds(30); const auto WEBSOCKET_INIT_DELAY = std::chrono::seconds(2); -const auto INITIAL_CERTIFICATE_REQUESTS_DELAY = std::chrono::seconds(60); bool Callbacks::all_callbacks_valid() const { return this->is_reset_allowed_callback != nullptr and this->reset_callback != nullptr and @@ -58,6 +57,8 @@ ChargePoint::ChargePoint(const std::map& evse_connector_struct upload_log_status(UploadLogStatusEnum::Idle), bootreason(BootReasonEnum::PowerUp), csr_attempt(1), + client_certificate_expiration_check_timer([this]() { this->scheduled_check_client_certificate_expiration(); }), + v2g_certificate_expiration_check_timer([this]() { this->scheduled_check_v2g_certificate_expiration(); }), callbacks(callbacks) { // Make sure the received callback struct is completely filled early before we actually start running if (!this->callbacks.all_callbacks_valid()) { @@ -156,6 +157,8 @@ void ChargePoint::stop() { this->boot_notification_timer.stop(); this->certificate_signed_timer.stop(); this->websocket_timer.stop(); + this->client_certificate_expiration_check_timer.stop(); + this->v2g_certificate_expiration_check_timer.stop(); this->disconnect_websocket(websocketpp::close::status::going_away); this->message_queue->stop(); } @@ -666,6 +669,7 @@ void ChargePoint::init_websocket() { } } } + this->init_certificate_expiration_check_timers(); // re-init as timers are stopped on disconnect } this->time_disconnected = std::chrono::time_point(); }); @@ -691,6 +695,9 @@ void ChargePoint::init_websocket() { // Get the current time point using steady_clock this->time_disconnected = std::chrono::steady_clock::now(); } + + this->client_certificate_expiration_check_timer.stop(); + this->v2g_certificate_expiration_check_timer.stop(); }); this->websocket->register_closed_callback( @@ -714,6 +721,27 @@ void ChargePoint::init_websocket() { this->websocket->register_message_callback([this](const std::string& message) { this->message_callback(message); }); } +void ChargePoint::init_certificate_expiration_check_timers() { + + // Timers started with initial delays; callback functions are supposed to re-schedule on their own! + + // Client Certificate only needs to be checked for SecurityProfile 3; if SecurityProfile changes, timers get + // re-initialized at reconnect + if (this->device_model->get_value(ControllerComponentVariables::SecurityProfile) == 3) { + this->client_certificate_expiration_check_timer.timeout(std::chrono::seconds( + this->device_model + ->get_optional_value(ControllerComponentVariables::ClientCertificateExpireCheckInitialDelaySeconds) + .value_or(60))); + } + + // V2G Certificate timer is started in any case; condition (V2GCertificateInstallationEnabled) is validated in + // callback (ChargePoint::scheduled_check_v2g_certificate_expiration) + this->v2g_certificate_expiration_check_timer.timeout(std::chrono::seconds( + this->device_model + ->get_optional_value(ControllerComponentVariables::V2GCertificateExpireCheckInitialDelaySeconds) + .value_or(60))); +} + WebsocketConnectionOptions ChargePoint::get_ws_connection_options(const int32_t configuration_slot) { const auto network_connection_profile_opt = this->get_network_connection_profile(configuration_slot); @@ -1664,6 +1692,7 @@ void ChargePoint::handle_boot_notification_response(CallResult 0) { this->heartbeat_timer.interval([this]() { this->heartbeat_req(); }, std::chrono::seconds(msg.interval)); } + this->init_certificate_expiration_check_timers(); this->update_aligned_data_interval(); // B01.FR.06 Only use boot timestamp if TimeSource contains Heartbeat if (this->callbacks.time_sync_callback.has_value() && @@ -2689,5 +2718,50 @@ void ChargePoint::handle_get_local_authorization_list_version_req(Callsend(call_result); } +void ChargePoint::scheduled_check_client_certificate_expiration() { + + EVLOG_info << "Checking if CSMS client certificate has expired"; + int expiry_days_count = + this->evse_security->get_leaf_expiry_days_count(ocpp::CertificateSigningUseEnum::ChargingStationCertificate); + if (expiry_days_count < 30) { + EVLOG_info << "CSMS client certificate is invalid in " << expiry_days_count + << " days. Requesting new certificate with certificate signing request"; + this->sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate); + } else { + EVLOG_info << "CSMS client certificate is still valid."; + } + + this->client_certificate_expiration_check_timer.interval(std::chrono::seconds( + this->device_model + ->get_optional_value(ControllerComponentVariables::ClientCertificateExpireCheckIntervalSeconds) + .value_or(12 * 60 * 60))); +} + +void ChargePoint::scheduled_check_v2g_certificate_expiration() { + if (this->device_model->get_optional_value(ControllerComponentVariables::V2GCertificateInstallationEnabled) + .value_or(false)) { + EVLOG_info << "Checking if V2GCertificate has expired"; + int expiry_days_count = + this->evse_security->get_leaf_expiry_days_count(ocpp::CertificateSigningUseEnum::V2GCertificate); + if (expiry_days_count < 30) { + EVLOG_info << "V2GCertificate is invalid in " << expiry_days_count + << " days. Requesting new certificate with certificate signing request"; + this->sign_certificate_req(ocpp::CertificateSigningUseEnum::V2GCertificate); + } else { + EVLOG_info << "V2GCertificate is still valid."; + } + } else { + if (this->device_model->get_optional_value(ControllerComponentVariables::PnCEnabled).value_or(false)) { + EVLOG_warning << "PnC is enabled but V2G certificate installation is not, so no certificate expiration " + "check is performed."; + } + } + + this->v2g_certificate_expiration_check_timer.interval(std::chrono::seconds( + this->device_model + ->get_optional_value(ControllerComponentVariables::V2GCertificateExpireCheckIntervalSeconds) + .value_or(12 * 60 * 60))); +} + } // namespace v201 } // namespace ocpp diff --git a/lib/ocpp/v201/ctrlr_component_variables.cpp b/lib/ocpp/v201/ctrlr_component_variables.cpp index 05087535e..c437514af 100644 --- a/lib/ocpp/v201/ctrlr_component_variables.cpp +++ b/lib/ocpp/v201/ctrlr_component_variables.cpp @@ -207,6 +207,34 @@ const ComponentVariable& MaxCustomerInformationDataLength = { "MaxCustomerInformationDataLength", }), }; +const ComponentVariable& V2GCertificateExpireCheckInitialDelaySeconds = { + ControllerComponents::InternalCtrlr, + std::nullopt, + std::optional({ + "V2GCertificateExpireCheckInitialDelaySeconds", + }), +}; +const ComponentVariable& V2GCertificateExpireCheckIntervalSeconds = { + ControllerComponents::InternalCtrlr, + std::nullopt, + std::optional({ + "V2GCertificateExpireCheckIntervalSeconds", + }), +}; +const ComponentVariable& ClientCertificateExpireCheckInitialDelaySeconds = { + ControllerComponents::InternalCtrlr, + std::nullopt, + std::optional({ + "ClientCertificateExpireCheckInitialDelaySeconds", + }), +}; +const ComponentVariable& ClientCertificateExpireCheckIntervalSeconds = { + ControllerComponents::InternalCtrlr, + std::nullopt, + std::optional({ + "ClientCertificateExpireCheckIntervalSeconds", + }), +}; const ComponentVariable& AlignedDataCtrlrEnabled = { ControllerComponents::AlignedDataCtrlr, std::nullopt,