diff --git a/include/ocpp/common/evse_security.hpp b/include/ocpp/common/evse_security.hpp index b7690b5ef..0af6acdaf 100644 --- a/include/ocpp/common/evse_security.hpp +++ b/include/ocpp/common/evse_security.hpp @@ -46,8 +46,8 @@ class EvseSecurity { /// \param certificate_chain PEM formatted certificate or certificate chain /// \param certificate_type type of the leaf certificate /// \return result of the operation - virtual InstallCertificateResult verify_certificate(const std::string& certificate_chain, - const CertificateSigningUseEnum& certificate_type) = 0; + virtual CertificateValidationResult verify_certificate(const std::string& certificate_chain, + const CaCertificateType& certificate_type) = 0; /// \brief Retrieves all certificates installed on the filesystem applying the \p certificate_types filter. This /// function respects the requirements of OCPP specified for the CSMS initiated message @@ -62,6 +62,13 @@ class EvseSecurity { /// \return contains OCSP request data virtual std::vector get_ocsp_request_data() = 0; + /// \brief Retrieves the OCSP request data of a certificate chain. + /// \param certificate_chain PEM formatted certificate or certificate chain + /// \param certificate_type type of the leaf certificate + /// \return contains OCSP request data + virtual std::vector get_ocsp_request_data(const std::string& certificate_chain, + const CaCertificateType& certificate_type) = 0; + /// \brief Updates the OCSP cache for the given \p certificate_hash_data with the given \p ocsp_response /// \param certificate_hash_data identifies the certificate for which the \p ocsp_response is specified /// \param ocsp_response the actual OCSP data diff --git a/include/ocpp/common/evse_security_impl.hpp b/include/ocpp/common/evse_security_impl.hpp index ddc989486..4d2b00d54 100644 --- a/include/ocpp/common/evse_security_impl.hpp +++ b/include/ocpp/common/evse_security_impl.hpp @@ -39,11 +39,13 @@ class EvseSecurityImpl : public EvseSecurity { DeleteCertificateResult delete_certificate(const CertificateHashDataType& certificate_hash_data) override; InstallCertificateResult update_leaf_certificate(const std::string& certificate_chain, const CertificateSigningUseEnum& certificate_type) override; - InstallCertificateResult verify_certificate(const std::string& certificate_chain, - const CertificateSigningUseEnum& certificate_type) override; + CertificateValidationResult verify_certificate(const std::string& certificate_chain, + const CaCertificateType& certificate_type) override; std::vector get_installed_certificates(const std::vector& certificate_types) override; std::vector get_ocsp_request_data() override; + std::vector get_ocsp_request_data(const std::string& certificate_chain, + const CaCertificateType& certificate_type) override; void update_ocsp_cache(const CertificateHashDataType& certificate_hash_data, const std::string& ocsp_response) override; bool is_ca_certificate_installed(const CaCertificateType& certificate_type) override; @@ -62,6 +64,7 @@ CaCertificateType to_ocpp(evse_security::CaCertificateType other); CertificateSigningUseEnum to_ocpp(evse_security::LeafCertificateType other); CertificateType to_ocpp(evse_security::CertificateType other); HashAlgorithmEnumType to_ocpp(evse_security::HashAlgorithm other); +CertificateValidationResult to_ocpp(evse_security::CertificateValidationError other); InstallCertificateResult to_ocpp(evse_security::InstallCertificateResult other); DeleteCertificateResult to_ocpp(evse_security::DeleteCertificateResult other); diff --git a/include/ocpp/common/types.hpp b/include/ocpp/common/types.hpp index 137381b2d..eaea1d21c 100644 --- a/include/ocpp/common/types.hpp +++ b/include/ocpp/common/types.hpp @@ -335,6 +335,31 @@ CaCertificateType string_to_ca_certificate_type(const std::string& s); /// \returns an output stream with the CaCertificateType written to std::ostream& operator<<(std::ostream& os, const CaCertificateType& ca_certificate_type); +enum class CertificateValidationResult { + Accepted, + Expired, + InvalidSignature, + IssuerNotFound, + InvalidLeafSignature, + InvalidChain, + Unknown, +}; + +namespace conversions { +/// \brief Converts the given InstallCertificateResult \p e to human readable string +/// \returns a string representation of the InstallCertificateResult +std::string certificate_validation_result_to_string(CertificateValidationResult e); + +/// \brief Converts the given std::string \p s to InstallCertificateResult +/// \returns a InstallCertificateResult from a string representation +CertificateValidationResult string_to_certificate_validation_result(const std::string& s); +} // namespace conversions + +/// \brief Writes the string representation of the given InstallCertificateResult \p +/// install_certificate_result to the given output stream \p os \returns an output stream with the +/// InstallCertificateResult written to +std::ostream& operator<<(std::ostream& os, const CertificateValidationResult& certificate_validation_result); + enum class InstallCertificateResult { InvalidSignature, InvalidCertificateChain, diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index 94f0b4288..70c7b3741 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -654,6 +654,11 @@ 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_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_impl.cpp b/lib/ocpp/common/evse_security_impl.cpp index cfd7c6dd5..19b1884af 100644 --- a/lib/ocpp/common/evse_security_impl.cpp +++ b/lib/ocpp/common/evse_security_impl.cpp @@ -40,8 +40,8 @@ InstallCertificateResult EvseSecurityImpl::update_leaf_certificate(const std::st this->evse_security->update_leaf_certificate(certificate_chain, conversions::from_ocpp(certificate_type))); } -InstallCertificateResult EvseSecurityImpl::verify_certificate(const std::string& certificate_chain, - const CertificateSigningUseEnum& certificate_type) { +CertificateValidationResult EvseSecurityImpl::verify_certificate(const std::string& certificate_chain, + const CaCertificateType& certificate_type) { return conversions::to_ocpp( this->evse_security->verify_certificate(certificate_chain, conversions::from_ocpp(certificate_type))); } @@ -74,6 +74,20 @@ std::vector EvseSecurityImpl::get_ocsp_request_data() { return result; } + +std::vector EvseSecurityImpl::get_ocsp_request_data(const std::string& certificate_chain, + const CaCertificateType& certificate_type) { + std::vector result; + + const auto ocsp_request_data = + this->evse_security->get_ocsp_request_data(certificate_chain, conversions::from_ocpp(certificate_type)); + for (const auto& ocsp_request_entry : ocsp_request_data.ocsp_request_data_list) { + result.push_back(conversions::to_ocpp(ocsp_request_entry)); + } + + return result; +} + void EvseSecurityImpl::update_ocsp_cache(const CertificateHashDataType& certificate_hash_data, const std::string& ocsp_response) { this->evse_security->update_ocsp_cache(conversions::from_ocpp(certificate_hash_data), ocsp_response); @@ -174,6 +188,28 @@ HashAlgorithmEnumType to_ocpp(evse_security::HashAlgorithm other) { } } +CertificateValidationResult to_ocpp(evse_security::CertificateValidationError other) { + switch (other) { + case evse_security::CertificateValidationError::NoError: + return CertificateValidationResult::Accepted; + case evse_security::CertificateValidationError::Expired: + return CertificateValidationResult::Expired; + case evse_security::CertificateValidationError::InvalidSignature: + return CertificateValidationResult::InvalidSignature; + case evse_security::CertificateValidationError::IssuerNotFound: + return CertificateValidationResult::IssuerNotFound; + case evse_security::CertificateValidationError::InvalidLeafSignature: + return CertificateValidationResult::InvalidLeafSignature; + case evse_security::CertificateValidationError::InvalidChain: + return CertificateValidationResult::InvalidChain; + case evse_security::CertificateValidationError::Unknown: + return CertificateValidationResult::Unknown; + default: + throw std::runtime_error( + "Could not convert evse_security::CertificateValidationError to CertificateValidationResult"); + } +} + InstallCertificateResult to_ocpp(evse_security::InstallCertificateResult other) { switch (other) { case evse_security::InstallCertificateResult::InvalidSignature: diff --git a/lib/ocpp/common/types.cpp b/lib/ocpp/common/types.cpp index 82d0b79c9..cce8d2925 100644 --- a/lib/ocpp/common/types.cpp +++ b/lib/ocpp/common/types.cpp @@ -602,6 +602,61 @@ std::ostream& operator<<(std::ostream& os, const CaCertificateType& ca_certifica os << conversions::ca_certificate_type_to_string(ca_certificate_type); return os; } + +namespace conversions { +std::string certificate_validation_result_to_string(CertificateValidationResult e) { + switch (e) { + case CertificateValidationResult::Accepted: + return "Accepted"; + case CertificateValidationResult::Expired: + return "Expired"; + case CertificateValidationResult::InvalidSignature: + return "InvalidSignature"; + case CertificateValidationResult::IssuerNotFound: + return "IssuerNotFound"; + case CertificateValidationResult::InvalidLeafSignature: + return "InvalidLeafSignature"; + case CertificateValidationResult::InvalidChain: + return "InvalidChain"; + case CertificateValidationResult::Unknown: + return "Unknown"; + } + + throw std::out_of_range("No known string conversion for provided enum of type CertificateValidationResult"); +} + +CertificateValidationResult string_to_certificate_validation_result(const std::string& s) { + if (s == "Accepted") { + return CertificateValidationResult::Accepted; + } + if (s == "Expired") { + return CertificateValidationResult::Expired; + } + if (s == "InvalidSignature") { + return CertificateValidationResult::InvalidSignature; + } + if (s == "IssuerNotFound") { + return CertificateValidationResult::IssuerNotFound; + } + if (s == "InvalidLeafSignature") { + return CertificateValidationResult::InvalidLeafSignature; + } + if (s == "InvalidChain") { + return CertificateValidationResult::InvalidChain; + } + if (s == "Unknown") { + return CertificateValidationResult::Unknown; + } + throw std::out_of_range("Provided string " + s + + " could not be converted to enum of type CertificateValidationResult"); +} +} // namespace conversions + +std::ostream& operator<<(std::ostream& os, const CertificateValidationResult& certificate_validation_result) { + os << conversions::certificate_validation_result_to_string(certificate_validation_result); + return os; +} + namespace conversions { std::string install_certificate_result_to_string(InstallCertificateResult e) { switch (e) { diff --git a/lib/ocpp/v16/charge_point_impl.cpp b/lib/ocpp/v16/charge_point_impl.cpp index c5d0b2eac..fde34d412 100644 --- a/lib/ocpp/v16/charge_point_impl.cpp +++ b/lib/ocpp/v16/charge_point_impl.cpp @@ -2378,8 +2378,8 @@ void ChargePointImpl::handleSignedUpdateFirmware(ocpp::Callevse_security->verify_certificate(call.msg.firmware.signingCertificate.get(), - ocpp::CertificateSigningUseEnum::ManufacturerCertificate) != - ocpp::InstallCertificateResult::Accepted) { + ocpp::CaCertificateType::MF) != + ocpp::CertificateValidationResult::Accepted) { response.status = UpdateFirmwareStatusEnumType::InvalidCertificate; ocpp::CallResult call_result(response, call.uniqueId); this->send(call_result); diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index abd54d660..553ac4a49 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -544,6 +544,32 @@ bool ChargePoint::on_charging_state_changed(const uint32_t evse_id, ChargingStat return false; } +std::vector ChargePoint::generate_ocsp_data(const CiString<5500>& certificate) { + std::vector ocsp_request_data_list; + const auto ocsp_data_list = + this->evse_security->get_ocsp_request_data(certificate.get(), ocpp::CaCertificateType::MO); + 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 @@ -563,6 +589,113 @@ AuthorizeResponse ChargePoint::validate_token(const IdToken id_token, const std: return response; } + // C07: Authorization using contract certificates + if (id_token.type == IdTokenEnum::eMAID) { + // Temporary variable that is set to true to avoid immediate response to allow the local auth list + // or auth cache to be tried + bool tryLocalAuthListOrCache = false; + bool forwardedToCsms = false; + + // If OCSP data is provided as argument, use it + if (this->websocket->is_connected() and ocsp_request_data.has_value()) { + EVLOG_info << "Online: Pass provided OCSP data to CSMS"; + response = this->authorize_req(id_token, std::nullopt, ocsp_request_data); + forwardedToCsms = true; + } else if (certificate.has_value()) { + // First try to validate the contract certificate locally + CertificateValidationResult localVerifyResult = + this->evse_security->verify_certificate(certificate.value().get(), ocpp::CaCertificateType::MO); + EVLOG_info << "Local contract validation result: " << localVerifyResult; + + bool CentralContractValidationAllowed = + this->device_model + ->get_optional_value(ControllerComponentVariables::CentralContractValidationAllowed) + .value_or(true); + bool ContractValidationOffline = + this->device_model->get_optional_value(ControllerComponentVariables::ContractValidationOffline) + .value_or(true); + bool LocalAuthorizeOffline = + this->device_model->get_optional_value(ControllerComponentVariables::LocalAuthorizeOffline) + .value_or(true); + + // C07.FR.01: When CS is online, it shall send an AuthorizeRequest + // C07.FR.02: The AuthorizeRequest shall at least contain the OCSP data + // TODO: local validation results are ignored if response is based only on OCSP data, is that acceptable? + if (this->websocket->is_connected()) { + // If no OCSP data was provided, check for a contract root + if (localVerifyResult == CertificateValidationResult::IssuerNotFound) { + // C07.FR.06: Pass contract validation to CSMS when no contract root is found + if (CentralContractValidationAllowed) { + EVLOG_info << "Online: No local contract root found. Pass contract validation to CSMS"; + response = this->authorize_req(id_token, certificate, std::nullopt); + forwardedToCsms = true; + } else { + EVLOG_warning << "Online: Central Contract Validation not allowed"; + response.idTokenInfo.status = AuthorizationStatusEnum::Invalid; + } + } else { + // Try to generate the OCSP data from the certificate chain and use that + std::vector generated_ocsp_request_data_list = + generate_ocsp_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); + forwardedToCsms = true; + } else { + EVLOG_warning << "Online: OCSP data could not be generated"; + response.idTokenInfo.status = AuthorizationStatusEnum::Invalid; + } + } + } else { // Offline + // C07.FR.08: CS shall try to validate the contract locally + if (ContractValidationOffline) { + EVLOG_info << "Offline: contract " << localVerifyResult; + switch (localVerifyResult) { + // C07.FR.09: CS shall lookup the eMAID in Local Auth List or Auth Cache when + // local validation succeeded + case CertificateValidationResult::Accepted: + // 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 (LocalAuthorizeOffline) { + tryLocalAuthListOrCache = true; + } else { + // No requirement states what to do when ContractValidationOffline is true + // and LocalAuthorizeOffline is false + response.idTokenInfo.status = AuthorizationStatusEnum::Unknown; + response.certificateStatus = AuthorizeCertificateStatusEnum::Accepted; + } + break; + case CertificateValidationResult::Expired: + response.idTokenInfo.status = AuthorizationStatusEnum::Expired; + response.certificateStatus = AuthorizeCertificateStatusEnum::CertificateExpired; + break; + default: + response.idTokenInfo.status = AuthorizationStatusEnum::Unknown; + break; + } + } else { + // C07.FR.07: CS shall not allow charging + response.idTokenInfo.status = AuthorizationStatusEnum::NotAtThisTime; + } + } + } else { + EVLOG_warning << "Can not validate eMAID without certificate chain"; + response.idTokenInfo.status = AuthorizationStatusEnum::Invalid; + } + if (forwardedToCsms) { + // AuthorizeRequest sent to CSMS, let's show the results + EVLOG_info << "CSMS idToken status: " << response.idTokenInfo.status; + if (response.certificateStatus.has_value()) { + EVLOG_info << "CSMS certificate status: " << response.certificateStatus.value(); + } + } + // For eMAID, we will respond here, unless the local auth list or auth cache is tried + if (!tryLocalAuthListOrCache) { + return response; + } + } + if (this->device_model->get_optional_value(ControllerComponentVariables::LocalAuthListCtrlrEnabled) .value_or(false)) { const auto id_token_info = this->database_handler->get_local_authorization_list_entry(id_token); @@ -1594,7 +1727,7 @@ void ChargePoint::sign_certificate_req(const ocpp::CertificateSigningUseEnum& ce std::optional organization; if (certificate_signing_use == ocpp::CertificateSigningUseEnum::ChargingStationCertificate) { - req.certificateType = CertificateSigningUseEnum::ChargingStationCertificate; + req.certificateType = ocpp::v201::CertificateSigningUseEnum::ChargingStationCertificate; common = this->device_model->get_optional_value(ControllerComponentVariables::ChargeBoxSerialNumber); organization = @@ -1602,7 +1735,7 @@ void ChargePoint::sign_certificate_req(const ocpp::CertificateSigningUseEnum& ce country = this->device_model->get_optional_value(ControllerComponentVariables::ISO15118CtrlrCountryName); } else { - req.certificateType = CertificateSigningUseEnum::V2GCertificate; + req.certificateType = ocpp::v201::CertificateSigningUseEnum::V2GCertificate; common = this->device_model->get_optional_value(ControllerComponentVariables::ISO15118CtrlrSeccId); organization = this->device_model->get_optional_value( ControllerComponentVariables::ISO15118CtrlrOrganizationName);