From e1ae0bcdb7d4bbfc980f3056f91b8e129147c204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piet=20G=C3=B6mpel?= <37657534+Pietfried@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:05:53 +0200 Subject: [PATCH] OCPP1.6 P&C Handling adjustments (#538) * ocpp16 adjustments for plug&charge token validation * update libevse-security dependency Signed-off-by: pietfried * Bump libocpp version to 0.10.0 Signed-off-by: Kai-Uwe Hermann * Update libevse-security to v0.5.0 tag This points to the same commit as the git rev used before Signed-off-by: Kai-Uwe Hermann --------- Signed-off-by: pietfried Signed-off-by: Kai-Uwe Hermann Co-authored-by: Kai-Uwe Hermann --- CMakeLists.txt | 2 +- dependencies.yaml | 2 +- include/ocpp/common/evse_security.hpp | 1 + include/ocpp/v16/charge_point_impl.hpp | 5 +- include/ocpp/v201/charge_point.hpp | 5 - lib/ocpp/common/evse_security.cpp | 9 ++ lib/ocpp/v16/charge_point_impl.cpp | 167 ++++++++++++++++++------- lib/ocpp/v201/charge_point.cpp | 29 +---- 8 files changed, 141 insertions(+), 79 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ef2f79567..f9800b9e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) project(ocpp - VERSION 0.9.8 + VERSION 0.10.0 DESCRIPTION "A C++ implementation of the Open Charge Point Protocol" LANGUAGES CXX ) diff --git a/dependencies.yaml b/dependencies.yaml index 25c369004..ea0d1a857 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -30,7 +30,7 @@ websocketpp: git_tag: 0.8.2 libevse-security: git: https://github.com/EVerest/libevse-security.git - git_tag: bce1ba4 + git_tag: v0.5.0 libwebsockets: git: https://github.com/warmcat/libwebsockets.git git_tag: v4.3.3 diff --git a/include/ocpp/common/evse_security.hpp b/include/ocpp/common/evse_security.hpp index 60e8557cf..f0e8d6af2 100644 --- a/include/ocpp/common/evse_security.hpp +++ b/include/ocpp/common/evse_security.hpp @@ -128,6 +128,7 @@ ocpp::v201::DeleteCertificateStatusEnum to_ocpp_v201(ocpp::DeleteCertificateResu ocpp::v201::CertificateHashDataType to_ocpp_v201(ocpp::CertificateHashDataType other); ocpp::v201::CertificateHashDataChain to_ocpp_v201(ocpp::CertificateHashDataChain other); ocpp::v201::OCSPRequestData to_ocpp_v201(ocpp::OCSPRequestData other); +std::vector to_ocpp_v201(const std::vector& ocsp_request_data); ocpp::CertificateType from_ocpp_v201(ocpp::v201::GetCertificateIdUseEnum other); std::vector from_ocpp_v201(const std::vector& other); diff --git a/include/ocpp/v16/charge_point_impl.hpp b/include/ocpp/v16/charge_point_impl.hpp index dbad28d79..a9c304d80 100644 --- a/include/ocpp/v16/charge_point_impl.hpp +++ b/include/ocpp/v16/charge_point_impl.hpp @@ -415,9 +415,10 @@ class ChargePointImpl : ocpp::ChargingStationBase { /// \brief Authorizes the provided \p id_token against the central system, LocalAuthorizationList or /// AuthorizationCache depending on the values of the ConfigurationKeys LocalPreAuthorize, LocalAuthorizeOffline, - /// LocalAuthListEnabled and AuthorizationCacheEnabled + /// LocalAuthListEnabled and AuthorizationCacheEnabled. If \p authorize_only_locally is true, no Authorize.req will + /// be sent to the CSMS but only LocalAuthorizationList and LocalAuthorizationCache will be used for the validation /// \returns the IdTagInfo - IdTagInfo authorize_id_token(CiString<20> id_token); + IdTagInfo authorize_id_token(CiString<20> id_token, const bool authorize_only_locally = false); // for plug&charge 1.6 whitepaper diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index 7d2749776..ef69ad9ae 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -664,11 +664,6 @@ class ChargePoint : ocpp::ChargingStationBase { /// \return True on success. False if evse id does not exist. bool on_charging_state_changed(const uint32_t evse_id, ChargingStateEnum charging_state); - /// \brief Generates OCSP request data from a (contract) certificate chain - /// \param certificate - /// \return vector with OCSP request data - std::vector generate_mo_ocsp_data(const CiString<5500>& certificate); - /// \brief Validates provided \p id_token \p certificate and \p ocsp_request_data using CSMS, AuthCache or AuthList /// \param id_token /// \param certificate diff --git a/lib/ocpp/common/evse_security.cpp b/lib/ocpp/common/evse_security.cpp index d4ec485f2..1e82713a9 100644 --- a/lib/ocpp/common/evse_security.cpp +++ b/lib/ocpp/common/evse_security.cpp @@ -134,6 +134,15 @@ ocpp::v201::OCSPRequestData to_ocpp_v201(ocpp::OCSPRequestData other) { return lhs; } +std::vector to_ocpp_v201(const std::vector& ocsp_request_data) { + std::vector ocsp_request_data_list; + for (const auto& ocsp_data : ocsp_request_data) { + ocpp::v201::OCSPRequestData request = to_ocpp_v201(ocsp_data); + ocsp_request_data_list.push_back(request); + } + return ocsp_request_data_list; +} + ocpp::CertificateType from_ocpp_v201(ocpp::v201::GetCertificateIdUseEnum other) { switch (other) { case ocpp::v201::GetCertificateIdUseEnum::V2GRootCertificate: diff --git a/lib/ocpp/v16/charge_point_impl.cpp b/lib/ocpp/v16/charge_point_impl.cpp index 6260357d8..b58a3755e 100644 --- a/lib/ocpp/v16/charge_point_impl.cpp +++ b/lib/ocpp/v16/charge_point_impl.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -2629,7 +2630,7 @@ void ChargePointImpl::status_notification(const int32_t connector, const ChargeP // public API for Core profile -IdTagInfo ChargePointImpl::authorize_id_token(CiString<20> idTag) { +IdTagInfo ChargePointImpl::authorize_id_token(CiString<20> idTag, const bool authorize_only_locally) { // only do authorize req when authorization locally not enabled or fails // proritize auth list over auth cache for same idTags @@ -2656,6 +2657,10 @@ IdTagInfo ChargePointImpl::authorize_id_token(CiString<20> idTag) { } } + if (authorize_only_locally) { + return {AuthorizationStatus::Invalid}; + } + AuthorizeRequest req; req.idTag = idTag; @@ -2743,8 +2748,6 @@ ocpp::v201::AuthorizeResponse ChargePointImpl::data_transfer_pnc_authorize( return authorize_response; } - // FIXME(piet): Handle C07.FR.06 - C07.FR.12 - DataTransferRequest req; req.vendorId = ISO15118_PNC_VENDOR_ID; req.messageId.emplace(CiString<50>(std::string("Authorize"))); @@ -2754,58 +2757,136 @@ ocpp::v201::AuthorizeResponse ChargePointImpl::data_transfer_pnc_authorize( id_token.type = ocpp::v201::IdTokenEnum::eMAID; id_token.idToken = emaid; - authorize_req.iso15118CertificateHashData = iso15118_certificate_hash_data; authorize_req.idToken = id_token; - // C07.FR.06: If Charging Station is not able to validate a contract certificate, because it does not have the - // associated root certificate AND CentralContractValidationAllowed is true - // certificate.has_value() implies that ISO module could not validate certificate, otherwise certificate would not - // be set - if (certificate.has_value() and this->configuration->getCentralContractValidationAllowed().has_value() and - this->configuration->getCentralContractValidationAllowed().value()) { - authorize_req.certificate = certificate; - } - - req.data.emplace(json(authorize_req).dump()); - - Call call(req, this->message_queue->createMessageId()); - auto authorize_future = this->send_async(call); - - auto enhanced_message = authorize_future.get(); - - if (enhanced_message.messageType == MessageType::DataTransferResponse) { - try { - // parse and return authorize response - ocpp::CallResult call_result = enhanced_message.message; - if (call_result.msg.data.has_value()) { - authorize_response = json::parse(call_result.msg.data.value()); - return authorize_response; + // Temporary variable that is set to true to avoid immediate response to allow the local auth list + // or auth cache to be tried + bool try_local_auth_list_or_cache = false; + bool forward_to_csms = false; + + if (this->websocket->is_connected() and iso15118_certificate_hash_data.has_value()) { + authorize_req.iso15118CertificateHashData = iso15118_certificate_hash_data; + forward_to_csms = true; + } else if (certificate.has_value()) { + // First try to validate the contract certificate locally + CertificateValidationResult local_verify_result = + this->evse_security->verify_certificate(certificate.value(), ocpp::LeafCertificateType::MO); + EVLOG_info << "Local contract validation result: " << local_verify_result; + const auto central_contract_validation_allowed = + this->configuration->getCentralContractValidationAllowed().value_or(false); + const auto contract_validation_offline = this->configuration->getContractValidationOffline(); + const auto local_authorize_offline = this->configuration->getLocalAuthorizeOffline(); + + // C07.FR.01: When CS is online, it shall send an AuthorizeRequest + // C07.FR.02: The AuthorizeRequest shall at least contain the OCSP data + if (this->websocket->is_connected()) { + if (local_verify_result == CertificateValidationResult::IssuerNotFound) { + // C07.FR.06: Pass contract validation to CSMS when no contract root is found + if (central_contract_validation_allowed) { + EVLOG_info << "Online: No local contract root found. Pass contract validation to CSMS"; + authorize_req.certificate = certificate.value(); + forward_to_csms = true; + } else { + EVLOG_warning << "Online: Central Contract Validation not allowed"; + authorize_response.idTokenInfo.status = ocpp::v201::AuthorizationStatusEnum::Invalid; + } } else { - EVLOG_warning << "CSMS response of DataTransferRequest(Authorize) did not include data"; + // Try to generate the OCSP data from the certificate chain and use that + const auto generated_ocsp_request_data_list = ocpp::evse_security_conversions::to_ocpp_v201( + this->evse_security->get_mo_ocsp_request_data(certificate.value())); + if (generated_ocsp_request_data_list.size() > 0) { + EVLOG_info << "Online: Pass generated OCSP data to CSMS"; + authorize_req.iso15118CertificateHashData = generated_ocsp_request_data_list; + forward_to_csms = true; + } else { + EVLOG_warning << "Online: OCSP data could not be generated"; + authorize_response.idTokenInfo.status = ocpp::v201::AuthorizationStatusEnum::Invalid; + } + } + } else { // Offline + // C07.FR.08: CS shall try to validate the contract locally + if (contract_validation_offline) { + EVLOG_info << "Offline: contract " << local_verify_result; + switch (local_verify_result) { + // C07.FR.09: CS shall lookup the eMAID in Local Auth List or Auth Cache when + // local validation succeeded + case CertificateValidationResult::Valid: + // In C07.FR.09 LocalAuthorizeOffline is mentioned, this seems to be a generic config item + // that applies to Local Auth List and Auth Cache, but since there are no requirements about + // it, lets check it here + if (local_authorize_offline) { + try_local_auth_list_or_cache = true; + } else { + // No requirement states what to do when ContractValidationOffline is true + // and LocalAuthorizeOffline is false + authorize_response.idTokenInfo.status = ocpp::v201::AuthorizationStatusEnum::Unknown; + authorize_response.certificateStatus = ocpp::v201::AuthorizeCertificateStatusEnum::Accepted; + } + break; + case CertificateValidationResult::Expired: + authorize_response.idTokenInfo.status = ocpp::v201::AuthorizationStatusEnum::Expired; + authorize_response.certificateStatus = + ocpp::v201::AuthorizeCertificateStatusEnum::CertificateExpired; + break; + default: + authorize_response.idTokenInfo.status = ocpp::v201::AuthorizationStatusEnum::Unknown; + break; + } + } else { + EVLOG_warning << "Offline: ContractValidationOffline is disabled. Validation not allowed"; + // C07.FR.07: CS shall not allow charging + authorize_response.idTokenInfo.status = ocpp::v201::AuthorizationStatusEnum::NotAtThisTime; } - } catch (const json::exception& e) { - EVLOG_warning << "Could not parse data of DataTransfer message Authorize.conf: " << e.what(); - } catch (const std::exception& e) { - EVLOG_error << "Unknown Error while handling DataTransfer message Authorize.conf: " << e.what(); } + } else { + EVLOG_warning << "Can not validate eMAID without certificate chain"; + authorize_response.idTokenInfo.status = ocpp::v201::AuthorizationStatusEnum::Invalid; + } - } else if (enhanced_message.offline) { - if (this->configuration->getContractValidationOffline()) { - // C07.FR.08 - // The Charging Station SHALL try to validate the contract certificate locally. + if (forward_to_csms) { + authorize_response.idTokenInfo.status = ocpp::v201::AuthorizationStatusEnum::Unknown; + // AuthorizeRequest sent to CSMS, let's show the results + req.data.emplace(json(authorize_req).dump()); + + // Send the DataTransfer(Authorize) to the CSMS + Call call(req, this->message_queue->createMessageId()); + auto authorize_future = this->send_async(call); - // if valid: C07.FR.09 - if (this->configuration->getLocalAuthorizeOffline()) { - // use auth cache to look up emaid + auto enhanced_message = authorize_future.get(); + + if (enhanced_message.messageType == MessageType::DataTransferResponse) { + try { + // parse and return authorize response + ocpp::CallResult call_result = enhanced_message.message; + if (call_result.msg.data.has_value()) { + authorize_response = json::parse(call_result.msg.data.value()); + } else { + EVLOG_warning << "CSMS response of DataTransferRequest(Authorize) did not include data"; + } + } catch (const json::exception& e) { + EVLOG_warning << "Could not parse data of DataTransfer message Authorize.conf: " << e.what(); + } catch (const std::exception& e) { + EVLOG_error << "Unknown Error while handling DataTransfer message Authorize.conf: " << e.what(); } + } else if (enhanced_message.offline) { + EVLOG_warning << "No response received for DataTransfer.req(Authorize) from CSMS"; } else { - // C07.FR.07 - // The Charging Station SHALL NOT allow charging. + EVLOG_warning << "CSMS response of DataTransferRequest(Authorize) was not DataTransferResponse"; } + return authorize_response; + } + // For eMAID, we will respond here, unless the local auth list or auth cache is tried + if (!try_local_auth_list_or_cache) { + return authorize_response; } else { - EVLOG_warning << "CSMS response of DataTransferRequest(Authorize) was not DataTransferResponse"; + const auto local_authorize_result = this->authorize_id_token(emaid, true); + if (local_authorize_result.status == AuthorizationStatus::Accepted) { + authorize_response.idTokenInfo.status = ocpp::v201::AuthorizationStatusEnum::Accepted; + } else { + authorize_response.idTokenInfo.status = ocpp::v201::AuthorizationStatusEnum::Invalid; + } + return authorize_response; } - return authorize_response; // FIXME(piet) } void ChargePointImpl::data_transfer_pnc_sign_certificate() { diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index c116d35e3..6ebb455aa 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -544,31 +544,6 @@ bool ChargePoint::on_charging_state_changed(const uint32_t evse_id, ChargingStat return false; } -std::vector ChargePoint::generate_mo_ocsp_data(const CiString<5500>& certificate) { - std::vector ocsp_request_data_list; - const auto ocsp_data_list = this->evse_security->get_mo_ocsp_request_data(certificate.get()); - for (const auto& ocsp_data : ocsp_data_list) { - OCSPRequestData request; - switch (ocsp_data.hashAlgorithm) { - case HashAlgorithmEnumType::SHA256: - request.hashAlgorithm = ocpp::v201::HashAlgorithmEnum::SHA256; - break; - case HashAlgorithmEnumType::SHA384: - request.hashAlgorithm = ocpp::v201::HashAlgorithmEnum::SHA384; - break; - case HashAlgorithmEnumType::SHA512: - request.hashAlgorithm = ocpp::v201::HashAlgorithmEnum::SHA512; - break; - } - request.issuerKeyHash = ocsp_data.issuerKeyHash; - request.issuerNameHash = ocsp_data.issuerNameHash; - request.responderURL = ocsp_data.responderUrl; - request.serialNumber = ocsp_data.serialNumber; - ocsp_request_data_list.push_back(request); - } - return ocsp_request_data_list; -} - AuthorizeResponse ChargePoint::validate_token(const IdToken id_token, const std::optional>& certificate, const std::optional>& ocsp_request_data) { // TODO(piet): C01.FR.14 @@ -634,8 +609,8 @@ AuthorizeResponse ChargePoint::validate_token(const IdToken id_token, const std: } } else { // Try to generate the OCSP data from the certificate chain and use that - std::vector generated_ocsp_request_data_list = - generate_mo_ocsp_data(certificate.value()); + const auto generated_ocsp_request_data_list = ocpp::evse_security_conversions::to_ocpp_v201( + this->evse_security->get_mo_ocsp_request_data(certificate.value())); if (generated_ocsp_request_data_list.size() > 0) { EVLOG_info << "Online: Pass generated OCSP data to CSMS"; response = this->authorize_req(id_token, std::nullopt, generated_ocsp_request_data_list);