diff --git a/.ci/build-kit/run_unit_tests.sh b/.ci/build-kit/run_unit_tests.sh index d73a4e53c5..d0ef689540 100755 --- a/.ci/build-kit/run_unit_tests.sh +++ b/.ci/build-kit/run_unit_tests.sh @@ -2,4 +2,6 @@ set -e +trap "cp $EXT_MOUNT/build/Testing/Temporary/LastTest.log $EXT_MOUNT/ctest-report" EXIT + ninja -C "$EXT_MOUNT/build" test diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b58553f274..d43ddb76bb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,7 @@ /lib/staging/gpio/ @corneliusclaussen @hikinggrass @hikinggrass /lib/staging/ocpp/ @hikinggrass @pietfried @corneliusclaussen /lib/staging/slac/ @a-w50 @corneliusclaussen @SebaLukas +/lib/staging/tls/ @james-ctc @AssemblyJohn @corneliusclaussen @SebaLukas # modules /modules/API/ @hikinggrass @pietfried @corneliusclaussen @@ -20,6 +21,7 @@ /modules/Energymanager/ @corneliusclaussen @hikinggrass @pietfried /modules/EnergyNode/ @corneliusclaussen @hikinggrass @pietfried /modules/EvseManager/ @corneliusclaussen @SebaLukas @hikinggrass @pietfried +/modules/EvManager/ @SebaLukas @pietfried @MarzellT /modules/EvseSecurity/ @AssemblyJohn @pietfried @hikinggrass /modules/EvseV2G/ @corneliusclaussen @SebaLukas @james-ctc /modules/EvseSlac/ @a-w50 @corneliusclaussen @SebaLukas diff --git a/.github/workflows/bazel_build_and_test.yaml b/.github/workflows/bazel_build_and_test.yaml index 9eeeb0560e..002218c037 100644 --- a/.github/workflows/bazel_build_and_test.yaml +++ b/.github/workflows/bazel_build_and_test.yaml @@ -15,9 +15,6 @@ jobs: key: ${{ runner.os }}-bazel-${{ hashFiles('dependencies.yaml', '.bazelversion', '.bazelrc', 'WORKSPACE.bazel', 'third-party/bazel/*') }} restore-keys: | ${{ runner.os }}-bazel- - - name: Setup edm - run: | - pip install git+https://github.com/Everest/everest-dev-environment.git#subdirectory=dependency_manager - name: Build all run: > bazelisk build //... diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 7f7caee51c..577f7dd143 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -94,6 +94,12 @@ jobs: with: path: dist-wheels name: wheels + - name: Archive unit test results + if: always() + uses: actions/upload-artifact@v4.3.3 + with: + name: ctest-report + path: ${{ github.workspace }}/ctest-report integration-tests: name: Integration Tests needs: build diff --git a/BUILD.bazel b/BUILD.bazel index f8bb5b1af2..8839e5aa90 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -7,4 +7,7 @@ filegroup( srcs = glob(["errors/**/*.yaml"]), ) -exports_files(["dependencies.yaml"]) +exports_files([ + "dependencies.yaml", + "WORKSPACE.bazel", +]) diff --git a/README.md b/README.md index 989ae9c07a..486edb138b 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ More CPU cores will optionally boost the build process, while requiring more RAM ```bash sudo apt update -sudo apt install -y python3-pip git rsync wget cmake doxygen graphviz build-essential clang-tidy cppcheck openjdk-17-jdk npm docker docker-compose libboost-all-dev nodejs libssl-dev libsqlite3-dev clang-format curl rfkill libpcap-dev libevent-dev pkg-config libcap-dev +sudo apt install -y python3-pip python3-venv git rsync wget cmake doxygen graphviz build-essential clang-tidy cppcheck openjdk-17-jdk npm docker docker-compose libboost-all-dev nodejs libssl-dev libsqlite3-dev clang-format curl rfkill libpcap-dev libevent-dev pkg-config libcap-dev ``` #### OpenSuse diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 26895908cd..cbe55b61a8 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -40,9 +40,6 @@ load("@rules_rust//crate_universe:repositories.bzl", "crate_universe_dependencie crate_universe_dependencies() - - - load("//third-party/bazel:repos.bzl", "everest_core_repos") everest_core_repos() @@ -51,7 +48,6 @@ load("//third-party/bazel:defs.bzl", "everest_core_defs") everest_core_defs() - load("@everest-framework//third-party/bazel:repos.bzl", "everest_framework_repos") everest_framework_repos() @@ -62,4 +58,4 @@ everest_framework_deps() load("@everest-utils//third-party/bazel:defs.bzl", "everest_utils_defs") -everest_utils_defs() \ No newline at end of file +everest_utils_defs() diff --git a/cmake/assets/BUILD.bazel b/cmake/assets/BUILD.bazel new file mode 100644 index 0000000000..bc45670197 --- /dev/null +++ b/cmake/assets/BUILD.bazel @@ -0,0 +1,2 @@ +exports_files(["logging.ini"]) + diff --git a/cmake/ev-project-bootstrap.cmake b/cmake/ev-project-bootstrap.cmake index 324c9d3c1e..8730905c46 100644 --- a/cmake/ev-project-bootstrap.cmake +++ b/cmake/ev-project-bootstrap.cmake @@ -4,11 +4,10 @@ include(${CMAKE_CURRENT_LIST_DIR}/ev-cli.cmake) include(${CMAKE_CURRENT_LIST_DIR}/config-run-script.cmake) include(${CMAKE_CURRENT_LIST_DIR}/config-run-nodered-script.cmake) -# dependencies -setup_ev_cli() -if(NOT ${${PROJECT_NAME}_INSTALL_EV_CLI_IN_PYTHON_VENV}) - require_ev_cli_version("0.2.0") -endif() +set_property( + GLOBAL + PROPERTY EVEREST_REQUIRED_EV_CLI_VERSION "0.2.0" +) # source generate scripts / setup include(${CMAKE_CURRENT_LIST_DIR}/everest-generate.cmake) diff --git a/cmake/everest-generate.cmake b/cmake/everest-generate.cmake index 6068243abf..c5fe381d32 100644 --- a/cmake/everest-generate.cmake +++ b/cmake/everest-generate.cmake @@ -26,7 +26,7 @@ set_target_properties(generate_cpp_files # # out-of-tree interfaces/types/modules support # -function(ev_add_project) +function(_ev_add_project) # FIXME (aw): resort to proper argument handling! if (ARGC EQUAL 2) set (EVEREST_PROJECT_DIR ${ARGV0}) @@ -125,6 +125,35 @@ function(ev_add_project) ) endfunction() +macro(ev_add_project) + ev_setup_cmake_variables_python_wheel() + set(${PROJECT_NAME}_PYTHON_VENV_PATH "${CMAKE_BINARY_DIR}/venv" CACHE PATH "Path to python venv") + + ev_setup_python_executable( + USE_PYTHON_VENV ${${PROJECT_NAME}_USE_PYTHON_VENV} + PYTHON_VENV_PATH ${${PROJECT_NAME}_PYTHON_VENV_PATH} + ) + + setup_ev_cli() + if(NOT ${${PROJECT_NAME}_USE_PYTHON_VENV}) + message(STATUS "Using system ev-cli instead of installing it in the build venv.") + get_property(EVEREST_REQUIRED_EV_CLI_VERSION + GLOBAL + PROPERTY EVEREST_REQUIRED_EV_CLI_VERSION + ) + require_ev_cli_version(${EVEREST_REQUIRED_EV_CLI_VERSION}) + else() + message(STATUS "Installing ev-cli in the build venv.") + endif() + + # FIXME (aw): resort to proper argument handling! + if (${ARGC} EQUAL 2) + _ev_add_project(${ARGV0} ${ARGV1}) + else() + _ev_add_project() + endif () +endmacro() + # # rust support # 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-sae-v2g.yaml b/config/config-sil-dc-sae-v2g.yaml index f1f57d57c3..78a4a161d2 100644 --- a/config/config-sil-dc-sae-v2g.yaml +++ b/config/config-sil-dc-sae-v2g.yaml @@ -48,7 +48,7 @@ active_modules: - module_id: imd implementation_id: main powersupply_dc: - module: JsDCSupplySimulator + module: DCSupplySimulator yeti_driver: module: JsYetiSimulator config_module: @@ -61,7 +61,7 @@ active_modules: main: selftest_success: true ev_manager: - module: JsEvManager + module: EvManager config_module: connector_id: 1 auto_enable: true diff --git a/config/config-sil-dc-sae-v2h.yaml b/config/config-sil-dc-sae-v2h.yaml index 793abd6e4e..747887200a 100644 --- a/config/config-sil-dc-sae-v2h.yaml +++ b/config/config-sil-dc-sae-v2h.yaml @@ -48,7 +48,7 @@ active_modules: - module_id: imd implementation_id: main powersupply_dc: - module: JsDCSupplySimulator + module: DCSupplySimulator yeti_driver: module: JsYetiSimulator config_module: @@ -61,7 +61,7 @@ active_modules: main: selftest_success: true ev_manager: - module: JsEvManager + module: EvManager config_module: connector_id: 1 auto_enable: true 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/config/config-sil-dc.yaml b/config/config-sil-dc.yaml index 0002d575b4..0d30393aff 100644 --- a/config/config-sil-dc.yaml +++ b/config/config-sil-dc.yaml @@ -46,7 +46,7 @@ active_modules: - module_id: imd implementation_id: main powersupply_dc: - module: JsDCSupplySimulator + module: DCSupplySimulator yeti_driver: module: JsYetiSimulator config_module: @@ -59,7 +59,7 @@ active_modules: selftest_success: true module: IMDSimulator ev_manager: - module: JsEvManager + module: EvManager config_module: connector_id: 1 auto_enable: true diff --git a/config/config-sil-energy-management.yaml b/config/config-sil-energy-management.yaml index 14da40ab51..36e10f6acc 100644 --- a/config/config-sil-energy-management.yaml +++ b/config/config-sil-energy-management.yaml @@ -76,7 +76,7 @@ active_modules: slac: module: JsSlacSimulator ev_manager_1: - module: JsEvManager + module: EvManager config_module: connector_id: 1 auto_enable: true @@ -93,7 +93,7 @@ active_modules: - module_id: slac implementation_id: ev ev_manager_2: - module: JsEvManager + module: EvManager config_module: connector_id: 2 auto_enable: true diff --git a/config/config-sil-ocpp-custom-extension.yaml b/config/config-sil-ocpp-custom-extension.yaml index 4d344f5c64..2dd9cbcd41 100644 --- a/config/config-sil-ocpp-custom-extension.yaml +++ b/config/config-sil-ocpp-custom-extension.yaml @@ -78,7 +78,7 @@ active_modules: slac: module: JsSlacSimulator ev_manager_1: - module: JsEvManager + module: EvManager config_module: connector_id: 1 auto_enable: true @@ -95,7 +95,7 @@ active_modules: - module_id: slac implementation_id: ev ev_manager_2: - module: JsEvManager + module: EvManager config_module: connector_id: 2 auto_enable: true diff --git a/config/config-sil-ocpp-pnc.yaml b/config/config-sil-ocpp-pnc.yaml index 2ddd4d20ad..3c1a818a6a 100644 --- a/config/config-sil-ocpp-pnc.yaml +++ b/config/config-sil-ocpp-pnc.yaml @@ -81,7 +81,7 @@ active_modules: slac: module: JsSlacSimulator ev_manager_1: - module: JsEvManager + module: EvManager config_module: connector_id: 1 auto_enable: true @@ -98,7 +98,7 @@ active_modules: - module_id: slac implementation_id: ev ev_manager_2: - module: JsEvManager + module: EvManager config_module: connector_id: 2 auto_enable: true diff --git a/config/config-sil-ocpp.yaml b/config/config-sil-ocpp.yaml index 6b614885fe..529eb8b531 100644 --- a/config/config-sil-ocpp.yaml +++ b/config/config-sil-ocpp.yaml @@ -80,7 +80,7 @@ active_modules: slac: module: JsSlacSimulator ev_manager_1: - module: JsEvManager + module: EvManager config_module: connector_id: 1 auto_enable: true @@ -97,7 +97,7 @@ active_modules: - module_id: slac implementation_id: ev ev_manager_2: - module: JsEvManager + module: EvManager config_module: connector_id: 2 auto_enable: true diff --git a/config/config-sil-ocpp201-pnc.yaml b/config/config-sil-ocpp201-pnc.yaml index 481a3bf6c8..19ad85155a 100644 --- a/config/config-sil-ocpp201-pnc.yaml +++ b/config/config-sil-ocpp201-pnc.yaml @@ -81,7 +81,7 @@ active_modules: slac: module: JsSlacSimulator ev_manager_1: - module: JsEvManager + module: EvManager config_module: connector_id: 1 auto_enable: true @@ -98,7 +98,7 @@ active_modules: - module_id: slac implementation_id: ev ev_manager_2: - module: JsEvManager + module: EvManager config_module: connector_id: 2 auto_enable: true diff --git a/config/config-sil-ocpp201.yaml b/config/config-sil-ocpp201.yaml index cdf9dfa746..ce2b12fa4e 100644 --- a/config/config-sil-ocpp201.yaml +++ b/config/config-sil-ocpp201.yaml @@ -78,7 +78,7 @@ active_modules: slac: module: JsSlacSimulator ev_manager_1: - module: JsEvManager + module: EvManager config_module: connector_id: 1 auto_enable: true @@ -95,7 +95,7 @@ active_modules: - module_id: slac implementation_id: ev ev_manager_2: - module: JsEvManager + module: EvManager config_module: connector_id: 2 auto_enable: true diff --git a/config/config-sil-two-evse-dc.yaml b/config/config-sil-two-evse-dc.yaml index c411325194..84a39b00fa 100644 --- a/config/config-sil-two-evse-dc.yaml +++ b/config/config-sil-two-evse-dc.yaml @@ -76,14 +76,14 @@ active_modules: slac_1: module: JsSlacSimulator powersupply_dc: - module: JsDCSupplySimulator + module: DCSupplySimulator imd: module: IMDSimulator config_implementation: main: selftest_success: true ev_manager_1: - module: JsEvManager + module: EvManager config_module: connector_id: 1 auto_enable: true @@ -102,7 +102,7 @@ active_modules: - module_id: slac_1 implementation_id: ev ev_manager_2: - module: JsEvManager + module: EvManager config_module: connector_id: 2 auto_enable: true diff --git a/config/config-sil-two-evse.yaml b/config/config-sil-two-evse.yaml index ec929c01c3..54fc86b1e3 100644 --- a/config/config-sil-two-evse.yaml +++ b/config/config-sil-two-evse.yaml @@ -72,7 +72,7 @@ active_modules: slac: module: JsSlacSimulator ev_manager_1: - module: JsEvManager + module: EvManager config_module: connector_id: 1 auto_enable: true @@ -89,7 +89,7 @@ active_modules: - module_id: slac implementation_id: ev ev_manager_2: - module: JsEvManager + module: EvManager config_module: connector_id: 2 auto_enable: true diff --git a/config/config-sil.yaml b/config/config-sil.yaml index fc1b561fb0..b1d3bbaaf6 100644 --- a/config/config-sil.yaml +++ b/config/config-sil.yaml @@ -40,7 +40,7 @@ active_modules: slac: - implementation_id: ev module_id: slac - module: JsEvManager + module: EvManager energy_manager: connections: energy_trunk: diff --git a/dependencies.yaml b/dependencies.yaml index f3e2bf5414..a928009152 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -54,13 +54,13 @@ libcurl: # and would otherwise be overwritten by the version used there libevse-security: git: https://github.com/EVerest/libevse-security.git - git_tag: v0.7.0 + git_tag: b140c17b0a5eaf09b60035605ed8aeb84627eb78 cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBEVSE_SECURITY" # OCPP libocpp: git: https://github.com/EVerest/libocpp.git - git_tag: v0.13.0 + git_tag: 673bc5bf5db3a02d03f4f06cc2d9a575cbe53f39 cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBOCPP" # Josev Josev: diff --git a/errors/BUILD.bazel b/errors/BUILD.bazel new file mode 100644 index 0000000000..6f8c6e168e --- /dev/null +++ b/errors/BUILD.bazel @@ -0,0 +1,5 @@ +filegroup( + name = "errors", + srcs = glob(["*.yaml"]), + visibility = ["//visibility:public"], +) diff --git a/interfaces/BUILD.bazel b/interfaces/BUILD.bazel index 5c38ba0c5f..1a5963a602 100644 --- a/interfaces/BUILD.bazel +++ b/interfaces/BUILD.bazel @@ -23,15 +23,17 @@ genrule( outs = cpp_headers, srcs = interface_srcs + [ "@everest-framework//schemas:schemas", - "//types:types", - "//:errors", + "@everest-core//types", + "@everest-core//errors", + "@everest-core//:WORKSPACE.bazel", ], tools = [ "@everest-utils//ev-dev-tools:ev-cli", ], cmd = """ $(location @everest-utils//ev-dev-tools:ev-cli) interface generate-headers \ - --everest-dir . \ + --work-dir `dirname $(location @everest-core//:WORKSPACE.bazel)` \ + --everest-dir `dirname $(location @everest-core//:WORKSPACE.bazel)` \ --schemas-dir external/everest-framework/schemas \ --disable-clang-format \ --output-dir `dirname $(location {some_output})`/.. diff --git a/interfaces/car_simulator.yaml b/interfaces/car_simulator.yaml index 4138318f05..741865ea85 100644 --- a/interfaces/car_simulator.yaml +++ b/interfaces/car_simulator.yaml @@ -5,14 +5,12 @@ description: >- cmds: enable: description: >- - Sets the ID that uniquely identifies the EVSE. The EVSEID shall - match the following structure: = + Enables or disables the simulation. arguments: value: description: Enable/Disable simulation mode type: boolean - executeChargingSession: + execute_charging_session: description: Executes a charging simulation string arguments: value: diff --git a/interfaces/evse_board_support.yaml b/interfaces/evse_board_support.yaml index 80f49b1949..81c82b0f81 100644 --- a/interfaces/evse_board_support.yaml +++ b/interfaces/evse_board_support.yaml @@ -135,3 +135,4 @@ vars: $ref: /evse_manager#/StopTransactionRequest errors: - reference: /errors/evse_board_support + - reference: /errors/ac_rcd diff --git a/interfaces/ocpp.yaml b/interfaces/ocpp.yaml index a4b0ce0877..aa4f0083e6 100644 --- a/interfaces/ocpp.yaml +++ b/interfaces/ocpp.yaml @@ -66,6 +66,9 @@ cmds: minimum: 0 type: object $ref: /ocpp#/SetVariableRequest + source: + description: Source of variable values + type: string result: description: >- List of SetVariableResult containing the result for every requested set operation diff --git a/interfaces/powermeter.yaml b/interfaces/powermeter.yaml index 0c8f5a9a82..f47da967f0 100644 --- a/interfaces/powermeter.yaml +++ b/interfaces/powermeter.yaml @@ -26,5 +26,8 @@ vars: description: Measured dataset type: object $ref: /powermeter#/Powermeter + public_key_ocmf: + description: The public key for OCMF + type: string errors: - reference: /errors/powermeter diff --git a/lib/staging/CMakeLists.txt b/lib/staging/CMakeLists.txt index f186caf24b..4caf814931 100644 --- a/lib/staging/CMakeLists.txt +++ b/lib/staging/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(can_dpm1000) add_subdirectory(evse_security) +add_subdirectory(tls) if(EVEREST_DEPENDENCY_ENABLED_LIBSLAC AND EVEREST_DEPENDENCY_ENABLED_LIBFSM) add_subdirectory(slac) endif() diff --git a/lib/staging/tls/CMakeLists.txt b/lib/staging/tls/CMakeLists.txt new file mode 100644 index 0000000000..5fb832a7ba --- /dev/null +++ b/lib/staging/tls/CMakeLists.txt @@ -0,0 +1,29 @@ +add_library(tls STATIC) +add_library(everest::tls ALIAS tls) + +find_package(OpenSSL 3) + +target_sources(tls + PRIVATE + openssl_conv.cpp + openssl_util.cpp + tls.cpp +) + +target_include_directories(tls + PUBLIC + $ + $ +) + +target_link_libraries(tls + PUBLIC + OpenSSL::SSL + OpenSSL::Crypto + everest::evse_security + everest::framework +) + +if(EVEREST_CORE_BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/lib/staging/tls/openssl-3.0.8-feat-updates-to-support-status_request_v2.patch b/lib/staging/tls/openssl-3.0.8-feat-updates-to-support-status_request_v2.patch new file mode 100644 index 0000000000..49f380646a --- /dev/null +++ b/lib/staging/tls/openssl-3.0.8-feat-updates-to-support-status_request_v2.patch @@ -0,0 +1,139 @@ +From 92125584f2fe87023cbfe96bba06358111ed8c13 Mon Sep 17 00:00:00 2001 +From: James Chapman +Date: Fri, 21 Jun 2024 10:29:44 +0100 +Subject: [PATCH 1/1] feat: updates to support status_request_v2 + +Signed-off-by: James Chapman +--- + include/openssl/ssl.h.in | 2 ++ + include/openssl/tls1.h | 7 +++++++ + ssl/s3_lib.c | 8 ++++++++ + ssl/statem/extensions_clnt.c | 3 ++- + ssl/statem/extensions_srvr.c | 4 ++++ + ssl/statem/statem_clnt.c | 3 ++- + 6 files changed, 25 insertions(+), 2 deletions(-) + +diff --git a/include/openssl/ssl.h.in b/include/openssl/ssl.h.in +index 105b4a4a3c..b29f65fbfa 100644 +--- a/include/openssl/ssl.h.in ++++ b/include/openssl/ssl.h.in +@@ -1251,6 +1251,8 @@ DECLARE_PEM_rw(SSL_SESSION, SSL_SESSION) + # define SSL_CTRL_SET_TLSEXT_STATUS_REQ_IDS 69 + # define SSL_CTRL_GET_TLSEXT_STATUS_REQ_OCSP_RESP 70 + # define SSL_CTRL_SET_TLSEXT_STATUS_REQ_OCSP_RESP 71 ++# define SSL_CTRL_GET_TLSEXT_STATUS_EXPECTED 270 ++# define SSL_CTRL_SET_TLSEXT_STATUS_EXPECTED 271 + # ifndef OPENSSL_NO_DEPRECATED_3_0 + # define SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB 72 + # endif +diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h +index d6e9331fa1..f0a8413703 100644 +--- a/include/openssl/tls1.h ++++ b/include/openssl/tls1.h +@@ -160,6 +160,7 @@ extern "C" { + # define TLSEXT_NAMETYPE_host_name 0 + /* status request value from RFC3546 */ + # define TLSEXT_STATUSTYPE_ocsp 1 ++# define TLSEXT_STATUSTYPE_ocsp_multi 2 + + /* ECPointFormat values from RFC4492 */ + # define TLSEXT_ECPOINTFORMAT_first 0 +@@ -291,6 +292,12 @@ __owur int SSL_check_chain(SSL *s, X509 *x, EVP_PKEY *pk, STACK_OF(X509) *chain) + # define SSL_set_tlsext_status_ocsp_resp(ssl, arg, arglen) \ + SSL_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_REQ_OCSP_RESP,arglen,arg) + ++# define SSL_get_tlsext_status_expected(ssl) \ ++ SSL_ctrl(ssl,SSL_CTRL_GET_TLSEXT_STATUS_EXPECTED,0,NULL) ++ ++# define SSL_set_tlsext_status_expected(ssl, arg) \ ++ SSL_ctrl(ssl,SSL_CTRL_SET_TLSEXT_STATUS_EXPECTED,arg,NULL) ++ + # define SSL_CTX_set_tlsext_servername_callback(ctx, cb) \ + SSL_CTX_callback_ctrl(ctx,SSL_CTRL_SET_TLSEXT_SERVERNAME_CB,\ + (void (*)(void))cb) +diff --git a/ssl/s3_lib.c b/ssl/s3_lib.c +index 78d4f04056..ede3a56f2f 100644 +--- a/ssl/s3_lib.c ++++ b/ssl/s3_lib.c +@@ -3556,6 +3556,14 @@ long ssl3_ctrl(SSL *s, int cmd, long larg, void *parg) + ret = 1; + break; + ++ case SSL_CTRL_GET_TLSEXT_STATUS_EXPECTED: ++ return (long)s->ext.status_expected; ++ ++ case SSL_CTRL_SET_TLSEXT_STATUS_EXPECTED: ++ s->ext.status_expected = larg; ++ ret = 1; ++ break; ++ + case SSL_CTRL_CHAIN: + if (larg) + return ssl_cert_set1_chain(s, NULL, (STACK_OF(X509) *)parg); +diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c +index 842be0722b..b9d5493e72 100644 +--- a/ssl/statem/extensions_clnt.c ++++ b/ssl/statem/extensions_clnt.c +@@ -8,6 +8,7 @@ + */ + + #include ++#include + #include "../ssl_local.h" + #include "internal/cryptlib.h" + #include "statem_local.h" +@@ -1397,7 +1398,7 @@ int tls_parse_stoc_status_request(SSL *s, PACKET *pkt, unsigned int context, + * MUST only be sent if we've requested a status + * request message. In TLS <= 1.2 it must also be empty. + */ +- if (s->ext.status_type != TLSEXT_STATUSTYPE_ocsp) { ++ if ((s->ext.status_type != TLSEXT_STATUSTYPE_ocsp) && (s->ext.status_type != TLSEXT_STATUSTYPE_ocsp_multi)) { + SSLfatal(s, SSL_AD_UNSUPPORTED_EXTENSION, SSL_R_BAD_EXTENSION); + return 0; + } +diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c +index 16765a5a5b..7fb67937bf 100644 +--- a/ssl/statem/extensions_srvr.c ++++ b/ssl/statem/extensions_srvr.c +@@ -8,6 +8,7 @@ + */ + + #include ++#include + #include "../ssl_local.h" + #include "statem_local.h" + #include "internal/cryptlib.h" +@@ -1421,6 +1422,9 @@ EXT_RETURN tls_construct_stoc_status_request(SSL *s, WPACKET *pkt, + if (!s->ext.status_expected) + return EXT_RETURN_NOT_SENT; + ++ if (s->ext.status_type == TLSEXT_STATUSTYPE_ocsp_multi) ++ return EXT_RETURN_NOT_SENT; ++ + if (SSL_IS_TLS13(s) && chainidx != 0) + return EXT_RETURN_NOT_SENT; + +diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c +index 3cd1ee2d3d..29a07bd413 100644 +--- a/ssl/statem/statem_clnt.c ++++ b/ssl/statem/statem_clnt.c +@@ -9,6 +9,7 @@ + * https://www.openssl.org/source/license.html + */ + ++#include + #include + #include + #include +@@ -2636,7 +2637,7 @@ int tls_process_cert_status_body(SSL *s, PACKET *pkt) + unsigned int type; + + if (!PACKET_get_1(pkt, &type) +- || type != TLSEXT_STATUSTYPE_ocsp) { ++ || (type != TLSEXT_STATUSTYPE_ocsp) && (type != TLSEXT_STATUSTYPE_ocsp_multi)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_UNSUPPORTED_STATUS_TYPE); + return 0; + } +-- +2.34.1 + diff --git a/lib/staging/tls/openssl-patch.md b/lib/staging/tls/openssl-patch.md new file mode 100644 index 0000000000..c5477b6e43 --- /dev/null +++ b/lib/staging/tls/openssl-patch.md @@ -0,0 +1,98 @@ + +# OpenSSL 3.0.8 patch + +The file `openssl-3.0.8-feat-updates-to-support-status_request_v2.patch` is a +patch to OpenSSL 3.0.8 to support the `status_request_v2` TLS extension defined +in [RFC 6961](https://datatracker.ietf.org/doc/html/rfc6961). + +## Apply the patch + +Assuming `openssl-3.0.8-feat-updates-to-support-status_request_v2.patch` is in +the current directory: + +```sh +$ git clone --branch openssl-3.0.8 https://github.com/openssl/openssl.git +$ cd openssl +$ patch -p1 < ../openssl-3.0.8-feat-updates-to-support-status_request_v2.patch +$ ./Configure +$ make +$ sudo make install +``` + +The patch can also be added to `SRC_URI` in a yocto bbappend file +`openssl_3.0.8.bbappend`: + +```bitbake +SRC_URI:append = " file://openssl-3.0.8-feat-updates-to-support-status_request_v2.patch" +``` + +## Notes + +The patch is designed to be a minimal change so that `status_request_v2` can be +supported with the emphasis on TLS server support. TLS client support exists to +facilitate testing. + +`status_request_v2` is deprecated for TLS 1.3 and must not be used. The code +ignores `status_request_v2` extensions when TLS 1.3 has been negotiated. + +When a client requests `status_request` and `status_request_v2` then +`status_request_v2` is used and `status_request` ignored. + +## Implementation + +`status_request_v2` is implemented in `tls.cpp` and relies on OCSP responses +being available in separate files that are associated with the server +certificate and chain. + +The patch defines `TLSEXT_STATUSTYPE_ocsp_multi` which is used in `tls.cpp` to +detect a patched version of OpenSSL. + +### OpenSSL + +OpenSSL contains a framework for adding handlers for TLS extensions that are not +natively handled. `status_request` is supported and the same mechanism is used +to to build the `status_request_v2` response. + +Unfortunately both `status_request` and `status_request_v2` add an additional +TLS handshake record `Certificate Status` containing the OCSP responses rather +than including them as part of the extension. The OpenSSL extension framework +doesn't provide a mechanism to add a `Certificate Status` record. + +The solution is to reuse the support for `status_request` and provide the +`status_request_v2` data for the `Certificate Status` record in application +code. + +The patch adds the additional status type `TLSEXT_STATUSTYPE_ocsp_multi` for use +with `SSL_set_tlsext_status_type()` and updates checks on `ext.status_type` so +that it isn't rejected. + +Additional functions `SSL_get_tlsext_status_expected()` and +`SSL_set_tlsext_status_expected()` are added so that application code can +indicate to OpenSSL that the `Certificate Status` record needs to be added. + +`SSL_set_tlsext_status_ocsp_resp()` is used by both `status_request` and +`status_request_v2` to populate the response. + +An early `Client Hello` handler is used to detect `status_request` and +`status_request_v2` extensions so that the `status_request` handler can ignore +the request (unless TLS 1.3 had been negotiated). + +### OcspCache + +Contains a digest method that produces a digest of a certificate. This digest +is paired with the OCSP response filename which provides the association used +in the OCSP cache. + +When responding to a `status_request_v2` the server iterates through the server +certificates and builds the response including the cached OCSP response for each +certificate where available. + +## Testing + +The primary testing has been performed using `Wireshark` to ensure that the +`Server Hello` and `Certificate Status` records are correctly formed. + +There is a googletest test suite `patched_test` that checks operation via the +OpenSSL APIs but it isn't able to check the handshake records directly. + +There are a test TLS server and client that can be used to check operation. diff --git a/lib/staging/tls/openssl_conv.cpp b/lib/staging/tls/openssl_conv.cpp new file mode 100644 index 0000000000..9deebea8b0 --- /dev/null +++ b/lib/staging/tls/openssl_conv.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include + +#include +#include +#include + +using evse_security::X509Handle_ptr; +using evse_security::X509HandleOpenSSL; +using evse_security::X509Wrapper; + +namespace openssl::conversions { + +X509Handle_ptr to_X509Handle_ptr(x509_st* cert) { + X509Handle_ptr ptr; + if (X509_up_ref(cert) == 1) { + ptr = std::make_unique(cert); + } + return ptr; +} + +X509Wrapper to_X509Wrapper(x509_st* cert) { + return X509Wrapper(to_X509Handle_ptr(cert)); +} + +} // namespace openssl::conversions diff --git a/lib/staging/tls/openssl_conv.hpp b/lib/staging/tls/openssl_conv.hpp new file mode 100644 index 0000000000..82322535ab --- /dev/null +++ b/lib/staging/tls/openssl_conv.hpp @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef OPENSSL_CONV_HPP_ +#define OPENSSL_CONV_HPP_ + +#include +#include +#include + +namespace openssl::conversions { + +evse_security::X509Handle_ptr to_X509Handle_ptr(x509_st* cert); +evse_security::X509Wrapper to_X509Wrapper(x509_st* cert); + +} // namespace openssl::conversions + +#endif // OPENSSL_CONV_HPP_ diff --git a/lib/staging/tls/openssl_util.cpp b/lib/staging/tls/openssl_util.cpp new file mode 100644 index 0000000000..a582813bd0 --- /dev/null +++ b/lib/staging/tls/openssl_util.cpp @@ -0,0 +1,523 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "openssl_util.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +openssl::log_handler_t s_log_handler{nullptr}; + +int add_error_str(const char* str, std::size_t len, void* u) { + assert(u != nullptr); + auto* list = reinterpret_cast(u); + *list += '\n' + std::string(str, len); + return 0; +} +} // namespace + +namespace openssl { + +void log(log_level_t level, const std::string& str) { + std::string messages = {str}; + ERR_print_errors_cb(&add_error_str, &messages); + if (s_log_handler == nullptr) { + std::cerr << messages << std::endl; + } else { + s_log_handler(level, messages); + } +} + +log_handler_t set_log_handler(log_handler_t handler) { + const auto tmp = s_log_handler; + s_log_handler = handler; + return tmp; +} + +} // namespace openssl + +namespace { + +void DER_Signature_free(std::uint8_t* ptr) { + OPENSSL_free(ptr); +} + +template bool sha_impl(const void* data, std::size_t len, DIGEST& digest, const EVP_MD* HASH) { + std::array buffer{}; + unsigned int digestlen{0}; + const auto res = EVP_Digest(data, len, buffer.data(), &digestlen, HASH, nullptr); + if (res == 1) { + if (digestlen == digest.size()) { + std::memcpy(digest.data(), buffer.data(), digest.size()); + } else { + openssl::log_error("EVP_Digest - size"); + } + } else { + openssl::log_error("EVP_Digest"); + } + return res == 1; +} + +template bool sha(const void* data, std::size_t len, DIGEST& digest); + +template <> bool sha(const void* data, std::size_t len, openssl::sha_256_digest_t& digest) { + return sha_impl(data, len, digest, EVP_sha256()); +} +template <> bool sha(const void* data, std::size_t len, openssl::sha_384_digest_t& digest) { + return sha_impl(data, len, digest, EVP_sha384()); +} +template <> bool sha(const void* data, std::size_t len, openssl::sha_512_digest_t& digest) { + return sha_impl(data, len, digest, EVP_sha512()); +} + +} // namespace + +namespace openssl { + +bool sign(evp_pkey_st* pkey, bn_t& r, bn_t& s, const sha_256_digest_t& digest) { + bool bRes{false}; + std::array signature{}; + auto len = signature.size(); + bRes = sign(pkey, signature.data(), len, digest.data(), sha_256_digest_size); + if (bRes) { + bRes = signature_to_bn(r, s, signature.data(), len); + } + return bRes; +} + +bool sign(EVP_PKEY* pkey, unsigned char* sig, std::size_t& siglen, const unsigned char* tbs, std::size_t tbslen) { + bool bRes{true}; + + auto* ctx = EVP_PKEY_CTX_new(pkey, nullptr); + if (ctx == nullptr) { + log_error("EVP_PKEY_CTX_new"); + bRes = false; + } + if (bRes && (EVP_PKEY_sign_init(ctx) != 1)) { + log_error("EVP_PKEY_sign_init"); + bRes = false; + } + if (bRes && (EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha256()) != 1)) { + log_error("EVP_PKEY_CTX_set_signature_md"); + bRes = false; + } + if (bRes) { + // calculate signature size + std::size_t length{0}; + if (EVP_PKEY_sign(ctx, nullptr, &length, tbs, tbslen) != 1) { + log_error("EVP_PKEY_sign - length"); + bRes = false; + } else if (siglen < length) { + log_error("EVP_PKEY_sign - length too small: " + std::to_string(length)); + bRes = false; + } + if (bRes) { + const auto res = EVP_PKEY_sign(ctx, sig, &siglen, tbs, tbslen); + if (res != 1) { + log_error("EVP_PKEY_sign" + std::to_string(res)); + bRes = false; + } + } + } + EVP_PKEY_CTX_free(ctx); + return bRes; +} + +bool verify(evp_pkey_st* pkey, const bn_t& r, const bn_t& s, const sha_256_digest_t& digest) { + return verify(pkey, r.data(), s.data(), digest); +} + +bool verify(evp_pkey_st* pkey, const std::uint8_t* r, const std::uint8_t* s, const sha_256_digest_t& digest) { + bool bRes{false}; + auto [signature, len] = bn_to_signature(r, s); + if ((signature != nullptr) && (len > 0)) { + bRes = verify(pkey, signature.get(), len, digest.data(), sha_256_digest_size); + } + return bRes; +} + +bool verify(EVP_PKEY* pkey, const unsigned char* sig, std::size_t siglen, const unsigned char* tbs, + std::size_t tbslen) { + bool bRes{true}; + + auto* ctx = EVP_PKEY_CTX_new(pkey, nullptr); + if (ctx == nullptr) { + log_error("EVP_PKEY_CTX_new"); + bRes = false; + } + if (bRes && (EVP_PKEY_verify_init(ctx) != 1)) { + log_error("EVP_PKEY_verify_init"); + bRes = false; + } + if (bRes && (EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha256()) != 1)) { + log_error("EVP_PKEY_CTX_set_signature_md"); + bRes = false; + } + if (bRes) { + const auto res = EVP_PKEY_verify(ctx, sig, siglen, tbs, tbslen); + if (res != 1) { + log_error("EVP_PKEY_verify: " + std::to_string(res)); + bRes = false; + } + } + EVP_PKEY_CTX_free(ctx); + return bRes; +} + +bool sha_256(const void* data, std::size_t len, sha_256_digest_t& digest) { + return sha(data, len, digest); +} + +bool sha_384(const void* data, std::size_t len, sha_384_digest_t& digest) { + return sha(data, len, digest); +} + +bool sha_512(const void* data, std::size_t len, sha_512_digest_t& digest) { + return sha(data, len, digest); +} + +std::vector base64_decode(const char* text, std::size_t len) { + assert(text != nullptr); + assert(len > 0); + + // remove \n + auto input = std::make_unique(len); + std::size_t input_len{0}; + + for (std::size_t i = 0; i < len; i++) { + const auto item = text[i]; + if (item != '\n') { + input.get()[input_len++] = item; + } + } + + auto* b64 = BIO_new(BIO_f_base64()); + auto* mem = BIO_new_mem_buf(input.get(), static_cast(input_len)); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + BIO_push(b64, mem); + + std::size_t output_len{0}; + int read_len{0}; + std::array buffer{}; + std::vector result(len); + + while ((read_len = BIO_read(b64, buffer.data(), buffer.size())) > 0) { + if ((output_len + read_len) <= result.size()) { + std::memcpy(&result[output_len], buffer.data(), read_len); + output_len += static_cast(read_len); + } else { + // decoded data is larger than the input - can't happen! + output_len = 0; + break; + } + } + + result.resize(output_len); + BIO_free_all(b64); + return result; +} + +bool base64_decode(const char* text, std::size_t len, std::uint8_t* out_data, std::size_t& out_len) { + assert(out_data != nullptr); + + bool bResult = false; + auto res = base64_decode(text, len); + if ((res.size() > 0) && (res.size() <= out_len)) { + std::memcpy(out_data, res.data(), res.size()); + out_len = res.size(); + bResult = true; + } + return bResult; +} + +std::string base64_encode(const std::uint8_t* data, std::size_t len, bool newLine) { + assert(data != nullptr); + assert(len > 0); + + auto* b64 = BIO_new(BIO_f_base64()); + auto* mem = BIO_new(BIO_s_mem()); + BIO_push(b64, mem); + if (!newLine) { + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + } + BIO_write(b64, data, static_cast(len)); + BIO_flush(b64); + + char* ptr{nullptr}; + const auto size = BIO_get_mem_data(mem, &ptr); + + std::string result(ptr, size); + BIO_free_all(b64); + return result; +} + +std::tuple bn_to_signature(const bn_t& r, const bn_t& s) { + return bn_to_signature(r.data(), s.data()); +}; + +std::tuple bn_to_signature(const std::uint8_t* r, const std::uint8_t* s) { + std::uint8_t* sig_p{nullptr}; + std::size_t signature_len{0}; + BIGNUM* rbn{nullptr}; + BIGNUM* sbn{nullptr}; + + auto* signature = ECDSA_SIG_new(); + if (signature == nullptr) { + log_error("ECDSA_SIG_new"); + } else { + rbn = BN_bin2bn(r, signature_n_size, nullptr); + sbn = BN_bin2bn(s, signature_n_size, nullptr); + } + + if (rbn != nullptr && sbn != nullptr) { + if (ECDSA_SIG_set0(signature, rbn, sbn) == 1) { + /* Set these to NULL since they are now owned by obj */ + rbn = sbn = nullptr; + signature_len = i2d_ECDSA_SIG(signature, &sig_p); + if (signature_len == 0) { + log_error("i2d_ECDSA_SIG"); + } + } else { + log_error("ECDSA_SIG_set0"); + } + } + + BN_free(rbn); + BN_free(sbn); + ECDSA_SIG_free(signature); + return {DER_Signature_ptr(sig_p, &DER_Signature_free), signature_len}; +}; + +bool signature_to_bn(bn_t& r, bn_t& s, const std::uint8_t* sig_p, std::size_t len) { + bool bRes{false}; + + auto* signature = d2i_ECDSA_SIG(nullptr, &sig_p, static_cast(len)); + if (signature == nullptr) { + log_error("d2i_ECDSA_SIG"); + } else { + const auto* rbn = ECDSA_SIG_get0_r(signature); + const auto* sbn = ECDSA_SIG_get0_s(signature); + + bRes = BN_bn2binpad(rbn, r.data(), static_cast(r.size())) != -1; + bRes = bRes && BN_bn2binpad(sbn, s.data(), static_cast(s.size())) != -1; + if (!bRes) { + log_error("BN_bn2binpad"); + } + } + + ECDSA_SIG_free(signature); + return bRes; +}; + +std::vector load_certificates(const char* filename) { + std::vector result{}; + + 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_close(store); + } + return result; +} + +std::string certificate_to_pem(const x509_st* cert) { + assert(cert != nullptr); + + auto* mem = BIO_new(BIO_s_mem()); + std::string result; + if (PEM_write_bio_X509(mem, cert) != 1) { + log_error("PEM_write_bio_X509"); + } else { + BIO_flush(mem); + + char* ptr{nullptr}; + const auto size = BIO_get_mem_data(mem, &ptr); + + result = std::string(ptr, size); + } + BIO_free(mem); + return result; +} + +Certificate_ptr der_to_certificate(const std::uint8_t* der, std::size_t len) { + Certificate_ptr result{nullptr, nullptr}; + const auto* ptr = der; + auto* cert = d2i_X509(nullptr, &ptr, static_cast(len)); + if (cert == nullptr) { + log_error("d2i_X509"); + } else { + result = Certificate_ptr{cert, &X509_free}; + } + return result; +} + +verify_result_t verify_certificate(const x509_st* cert, const CertificateList& trust_anchors, + const CertificateList& untrusted) { + verify_result_t result = verify_result_t::verified; + auto* store_ctx = X509_STORE_CTX_new(); + auto* ta_store = X509_STORE_new(); + auto* chain = sk_X509_new_null(); + X509* target{nullptr}; + + if (store_ctx == nullptr) { + log_error("X509_STORE_CTX_new"); + result = verify_result_t::OtherError; + } + + if (ta_store == nullptr) { + log_error("X509_STORE_new"); + result = verify_result_t::OtherError; + } + + if (chain == nullptr) { + log_error("sk_X509_new_null"); + result = verify_result_t::OtherError; + } + + if (cert != nullptr) { + target = X509_dup(cert); + if (target == nullptr) { + log_error("X509_dup"); + result = verify_result_t::OtherError; + } + } + + if (result == verify_result_t::verified) { + result = verify_result_t::OtherError; + + for (const auto& i : trust_anchors) { + if (X509_STORE_add_cert(ta_store, i.get()) != 1) { + log_error("X509_STORE_add_cert"); + } + } + + for (const auto& j : untrusted) { + if (X509_add_cert(chain, j.get(), X509_ADD_FLAG_UP_REF | X509_ADD_FLAG_NO_DUP | X509_ADD_FLAG_NO_SS) != 1) { + log_error("X509_add_cert"); + } + } + + if (X509_STORE_CTX_init(store_ctx, ta_store, target, chain) != 1) { + log_error("X509_STORE_CTX_init"); + } else { + if (X509_STORE_CTX_verify(store_ctx) != 1) { + const auto err = X509_STORE_CTX_get_error(store_ctx); + if (err != X509_V_OK) { + log_error("X509_STORE_CTX_verify (" + std::to_string(X509_STORE_CTX_get_error_depth(store_ctx)) + + ") " + X509_verify_cert_error_string(err)); + } + + switch (err) { + case X509_V_ERR_CERT_CHAIN_TOO_LONG: + case X509_V_ERR_CERT_SIGNATURE_FAILURE: + case X509_V_ERR_CERT_UNTRUSTED: + case X509_V_ERR_PATH_LENGTH_EXCEEDED: + case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN: + case X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE: + case X509_V_ERR_UNSPECIFIED: + result = verify_result_t::CertChainError; + break; + case X509_V_ERR_CERT_HAS_EXPIRED: + case X509_V_ERR_CERT_NOT_YET_VALID: + result = verify_result_t::CertificateExpired; + break; + case X509_V_ERR_CERT_REVOKED: + result = verify_result_t::CertificateRevoked; + break; + case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: + case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: + result = verify_result_t::NoCertificateAvailable; + break; + default: + break; + } + } else { + result = verify_result_t::verified; + } + } + } + + X509_STORE_CTX_free(store_ctx); + X509_STORE_free(ta_store); + sk_X509_pop_free(chain, X509_free); + X509_free(target); + return result; +} + +std::map certificate_subject(const x509_st* cert) { + assert(cert != nullptr); + std::map result; + + // DO NOT FREE - internal pointers to certificate + const auto* subject = X509_get_subject_name(cert); + if (subject != nullptr) { + for (int i = 0; i < X509_NAME_entry_count(subject); i++) { + const auto* name_entry = X509_NAME_get_entry(subject, i); + if (name_entry != nullptr) { + const auto* object = X509_NAME_ENTRY_get_object(name_entry); + const auto* data = X509_NAME_ENTRY_get_data(name_entry); + if ((object != nullptr) && (data != nullptr)) { + std::string name(OBJ_nid2sn(OBJ_obj2nid(object))); + std::string value(reinterpret_cast(ASN1_STRING_get0_data(data)), + ASN1_STRING_length(data)); + result[name] = value; + } + } + } + } + + return result; +} + +PKey_ptr certificate_public_key(x509_st* cert) { + PKey_ptr result{nullptr, nullptr}; + auto* pkey = X509_get_pubkey(cert); + if (pkey == nullptr) { + log_error("X509_get_pubkey"); + } else { + result = PKey_ptr(pkey, &EVP_PKEY_free); + } + return result; +} + +} // namespace openssl diff --git a/lib/staging/tls/openssl_util.hpp b/lib/staging/tls/openssl_util.hpp new file mode 100644 index 0000000000..661a36bef8 --- /dev/null +++ b/lib/staging/tls/openssl_util.hpp @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef OPENSSL_UTIL_HPP_ +#define OPENSSL_UTIL_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct evp_pkey_st; +struct x509_st; + +namespace openssl { + +enum class verify_result_t : std::uint8_t { + verified, + CertChainError, + CertificateExpired, + CertificateRevoked, + NoCertificateAvailable, + OtherError, +}; + +constexpr std::size_t signature_size = 64; +constexpr std::size_t signature_n_size = 32; +constexpr std::size_t signature_der_size = 128; +constexpr std::size_t sha_256_digest_size = 32; +constexpr std::size_t sha_384_digest_size = 48; +constexpr std::size_t sha_512_digest_size = 64; + +enum class digest_alg_t : std::uint8_t { + sha256, + sha384, + sha512, +}; + +using sha_256_digest_t = std::array; +using sha_384_digest_t = std::array; +using sha_512_digest_t = std::array; +using bn_t = std::array; +using bn_const_t = std::array; + +using Certificate_ptr = std::unique_ptr; +using CertificateList = std::vector; +using DER_Signature_ptr = std::unique_ptr; +using PKey_ptr = std::unique_ptr; + +/** + * \brief sign using ECDSA on curve secp256r1/prime256v1/P-256 of a SHA 256 digest + * \param[in] pkey the private key + * \param[out] r the R component of the signature as a BIGNUM + * \param[out] s the S component of the signature as a BIGNUM + * \param[out] digest the SHA256 digest to sign + * \return true when successful + */ +bool sign(evp_pkey_st* pkey, bn_t& r, bn_t& s, const sha_256_digest_t& digest); + +/** + * \brief sign using ECDSA on curve secp256r1/prime256v1/P-256 of a SHA 256 digest + * \param[in] pkey the private key + * \param[out] sig the buffer where the DER encoded signature will be placed + * \param[inout] siglen the size of the signature buffer, updated to be the size of the signature + * \param[in] tbs a pointer to the SHA256 digest + * \param[in] tbslen the size of the SHA256 digest + * \return true when successful + */ +bool sign(evp_pkey_st* pkey, unsigned char* sig, std::size_t& siglen, const unsigned char* tbs, std::size_t tbslen); + +/** + * \brief verify a signature against a SHA 256 digest using ECDSA on curve secp256r1/prime256v1/P-256 + * \param[in] pkey the public key + * \param[in] r the R component of the signature as a BIGNUM + * \param[in] s the S component of the signature as a BIGNUM + * \param[in] digest the SHA256 digest to sign + * \return true when successful + */ +bool verify(evp_pkey_st* pkey, const bn_t& r, const bn_t& s, const sha_256_digest_t& digest); + +/** + * \brief verify a signature against a SHA 256 digest using ECDSA on curve secp256r1/prime256v1/P-256 + * \param[in] pkey the public key + * \param[in] r the R component of the signature as a BIGNUM (0-padded 32 bytes) + * \param[in] s the S component of the signature as a BIGNUM (0-padded 32 bytes) + * \param[in] digest the SHA256 digest to sign + * \return true when successful + */ +bool verify(evp_pkey_st* pkey, const std::uint8_t* r, const std::uint8_t* s, const sha_256_digest_t& digest); + +/** + * \brief verify a signature against a SHA 256 digest using ECDSA on curve secp256r1/prime256v1/P-256 + * \param[in] pkey the public key + * \param[in] sig the DER encoded signature + * \param[in] siglen the size of the DER encoded signature + * \param[in] tbs a pointer to the SHA256 digest + * \param[in] tbslen the size of the SHA256 digest + * \return true when successful + */ +bool verify(evp_pkey_st* pkey, const unsigned char* sig, std::size_t siglen, const unsigned char* tbs, + std::size_t tbslen); + +/** + * \brief calculate the SHA256 digest over an array of bytes + * \param[in] data the start of the data + * \param[in] len the length of the data + * \param[out] the SHA256 digest + * \return true on success + */ +bool sha_256(const void* data, std::size_t len, sha_256_digest_t& digest); + +/** + * \brief calculate the SHA384 digest over an array of bytes + * \param[in] data the start of the data + * \param[in] len the length of the data + * \param[out] the SHA384 digest + * \return true on success + */ +bool sha_384(const void* data, std::size_t len, sha_384_digest_t& digest); + +/** + * \brief calculate the SHA512 digest over an array of bytes + * \param[in] data the start of the data + * \param[in] len the length of the data + * \param[out] the SHA512 digest + * \return true on success + */ +bool sha_512(const void* data, std::size_t len, sha_512_digest_t& digest); + +/** + * \brief decode a base64 string into it's binary form + * \param[in] text the base64 string (does not need to be \0 terminated) + * \param[in] len the length of the string (excluding any terminating \0) + * \return binary array or empty on error + */ +std::vector base64_decode(const char* text, std::size_t len); + +/** + * \brief decode a base64 string into it's binary form + * \param[in] text the base64 string (does not need to be \0 terminated) + * \param[in] len the length of the string (excluding any terminating \0) + * \param[out] out_data where to place the decoded data + * \param[inout] out_len the size of out_data, updated to be the length of the decoded data + * \return true on success + */ +bool base64_decode(const char* text, std::size_t len, std::uint8_t* out_data, std::size_t& out_len); + +/** + * \brief encode data into a base64 text string + * \param[in] data the data to encode + * \param[in] len the length of the data + * \param[in] newLine when true add a \n to break the result into multiple lines + * \return base64 string or empty on error + */ +std::string base64_encode(const std::uint8_t* data, std::size_t len, bool newLine); + +/** + * \brief encode data into a base64 text string + * \param[in] data the data to encode + * \param[in] len the length of the data + * \return base64 string or empty on error + * \note the return string doesn't include line breaks + */ +inline std::string base64_encode(const std::uint8_t* data, std::size_t len) { + return base64_encode(data, len, false); +} + +/** + * \brief zero a structure + * \param mem the structure to zero + */ +template constexpr void zero(T& mem) { + std::memset(mem.data(), 0, mem.size()); +} + +/** + * \brief convert R, S BIGNUM to DER signature + * \param[in] r the BIGNUM R component of the signature + * \param[in] s the BIGNUM S component of the signature + * \return The DER signature and it's length + */ +std::tuple bn_to_signature(const bn_t& r, const bn_t& s); + +/** + * \brief convert R, S BIGNUM to DER signature + * \param[in] r the BIGNUM R component of the signature (0-padded 32 bytes) + * \param[in] s the BIGNUM S component of the signature (0-padded 32 bytes) + * \return The DER signature and it's length + */ +std::tuple bn_to_signature(const std::uint8_t* r, const std::uint8_t* s); + +/** + * \brief convert DER signature into BIGNUM R and S components + * \param[out] r the BIGNUM R component of the signature + * \param[out] s the BIGNUM S component of the signature + * \param[in] sig_p a pointer to the DER encoded signature + * \param[in] len the length of the DER encoded signature + * \return true when successful + */ +bool signature_to_bn(openssl::bn_t& r, openssl::bn_t& s, const std::uint8_t* sig_p, std::size_t len); + +/** + * \brief load any PEM encoded certificates from a file + * \param[in] filename + * \return a list of 0 or more certificates + */ +CertificateList load_certificates(const char* filename); + +/** + * \brief convert a certificate to a PEM string + * \param[in] cert the certificate + * \return the PEM string or empty on error + */ +std::string certificate_to_pem(const x509_st* cert); + +/** + * \brief parse a DER (ASN.1) encoded certificate + * \param[in] der a pointer to the DER encoded certificate + * \param[in] len the length of the DER encoded certificate + * \return the certificate or empty unique_ptr on error + */ +Certificate_ptr der_to_certificate(const std::uint8_t* der, std::size_t len); + +/** + * \brief verify a certificate against a certificate chain and trust anchors + * \param[in] cert the certificate to verify - when nullptr the certificate must + * be the first certificate in the untrusted list + * \param[in] trust_anchors a list of trust anchors. Must not contain any + * intermediate CAs + * \param[in] untrusted intermediate CAs needed to form a chain from the leaf + * certificate to one of the supplied trust anchors + */ +verify_result_t verify_certificate(const x509_st* cert, const CertificateList& trust_anchors, + const CertificateList& untrusted); + +/** + * \brief extract the certificate subject as a dictionary of name/value pairs + * \param cert the certificate + * \return dictionary of the (short name, value) pairs + * \note short name examples "CN" for CommonName "OU" for OrganizationalUnit + * "C" for Country ... + */ +std::map certificate_subject(const x509_st* cert); + +/** + * \brief extract the subject public key from the certificate + * \param[in] cert the certificate + * \return a unique_ptr holding the key or empty on error + */ +PKey_ptr certificate_public_key(x509_st* cert); + +enum class log_level_t : std::uint8_t { + debug, + warning, + error, +}; + +/** + * \brief log an OpenSSL event + * \param[in] level the event level + * \param[in] str string to display + * \note any OpenSSL error is displayed after the string + */ +void log(log_level_t level, const std::string& str); + +static inline void log_error(const std::string& str) { + log(log_level_t::error, str); +} + +static inline void log_warning(const std::string& str) { + log(log_level_t::warning, str); +} + +static inline void log_debug(const std::string& str) { + log(log_level_t::debug, str); +} + +using log_handler_t = void (*)(log_level_t level, const std::string& err); + +/** + * \brief set log handler function + * \param[in] handler a pointer to the function + * \return the pointer to the previous handler or nullptr + * where there is no previous handler + */ +log_handler_t set_log_handler(log_handler_t handler); + +} // namespace openssl + +#endif // OPENSSL_UTIL_HPP_ diff --git a/lib/staging/tls/tests/CMakeLists.txt b/lib/staging/tls/tests/CMakeLists.txt new file mode 100644 index 0000000000..2639026674 --- /dev/null +++ b/lib/staging/tls/tests/CMakeLists.txt @@ -0,0 +1,114 @@ +find_package(OpenSSL 3) + +set(TLS_GTEST_NAME tls_test) +add_executable(${TLS_GTEST_NAME}) + +target_include_directories(${TLS_GTEST_NAME} PRIVATE + . .. ../../util +) + +target_compile_definitions(${TLS_GTEST_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${TLS_GTEST_NAME} PRIVATE + gtest_main.cpp + crypto_test.cpp + openssl_util_test.cpp + tls_test.cpp + ../openssl_conv.cpp + ../openssl_util.cpp + ../tls.cpp +) + +target_link_libraries(${TLS_GTEST_NAME} PRIVATE + GTest::gtest + OpenSSL::SSL + OpenSSL::Crypto + everest::evse_security +) + +set(TLS_MAIN_NAME tls_server) +add_executable(${TLS_MAIN_NAME}) + +target_include_directories(${TLS_MAIN_NAME} PRIVATE + . .. ../../util +) + +target_compile_definitions(${TLS_MAIN_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${TLS_MAIN_NAME} PRIVATE + tls_main.cpp + ../openssl_util.cpp + ../tls.cpp +) + +target_link_libraries(${TLS_MAIN_NAME} PRIVATE + OpenSSL::SSL + OpenSSL::Crypto +) + +set(TLS_CLIENT_NAME tls_client) +add_executable(${TLS_CLIENT_NAME}) + +target_include_directories(${TLS_CLIENT_NAME} PRIVATE + . .. ../../util +) + +target_compile_definitions(${TLS_CLIENT_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${TLS_CLIENT_NAME} PRIVATE + tls_client_main.cpp + ../openssl_util.cpp + ../tls.cpp +) + +target_link_libraries(${TLS_CLIENT_NAME} PRIVATE + OpenSSL::SSL + OpenSSL::Crypto +) + +set(TLS_PATCH_NAME patched_test) +add_executable(${TLS_PATCH_NAME}) + +target_include_directories(${TLS_PATCH_NAME} PRIVATE + . .. ../../util +) + +target_compile_definitions(${TLS_PATCH_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${TLS_PATCH_NAME} PRIVATE + patched_test.cpp + ../openssl_util.cpp + ../tls.cpp +) + +target_link_libraries(${TLS_PATCH_NAME} PRIVATE + GTest::gtest_main + OpenSSL::SSL + OpenSSL::Crypto +) + +install( + FILES + pki/iso_pkey.asn1 + pki/openssl-pki.conf + pki/ocsp_response.der + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" +) + +install( + PROGRAMS + pki/pki.sh + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" +) + +# tests don't run successfully in CI pipeline +# pki.sh not installed or run from wrong directory +# add_test(${TLS_GTEST_NAME} ${TLS_GTEST_NAME}) diff --git a/lib/staging/tls/tests/README.md b/lib/staging/tls/tests/README.md new file mode 100644 index 0000000000..e9b25ad0c4 --- /dev/null +++ b/lib/staging/tls/tests/README.md @@ -0,0 +1,43 @@ + +# Tests + +Building tests: + +```sh +$ cd everest-core +$ mkdir build +$ cd build +$ cmake -GNinja -DEVEREST_CORE_BUILD_TESTING=ON .. +$ ninja install +``` + +`touch release.json` may be needed if it hasn't been created +(then re-run `ninja install`). + +## Unit tests + +- `./tls_test` and `./patched_test` +- automatically runs `pki.sh` +- run from the directory containing the executable + +## Standalone server + +- Run `pki.sh` to build the test certificates and keys +- use openssl_s_client to make test connections +- run from the directory containing the executable + +### Standalone TLS server + +Tests the Server class in isolation. + +- `./tls_server` +- connects to IPv4 and IPv6 +- only one connection at a time +- gracefully terminates after 30 seconds +- `valgrind` can be used to check memory allocations (should be none) +- requires client certificate and supports `status_request` extension +- s_client echos back what is typed + +```sh +openssl s_client -connect localhost:8444 -verify 2 -CAfile server_root_cert.pem -cert client_cert.pem -cert_chain client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname evse.pionix.de -status +``` diff --git a/lib/staging/tls/tests/crypto_test.cpp b/lib/staging/tls/tests/crypto_test.cpp new file mode 100644 index 0000000000..b3b0245769 --- /dev/null +++ b/lib/staging/tls/tests/crypto_test.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "gtest/gtest.h" + +#include +#include + +#include +#include + +#include +#include + +namespace { + +using evse_security::HashAlgorithm; +using evse_security::X509CertificateHierarchy; +using evse_security::X509Wrapper; +using openssl::load_certificates; +using openssl::conversions::to_X509Wrapper; + +TEST(evseSecurity, certificateHash) { + auto chain = load_certificates("client_chain.pem"); + ASSERT_GT(chain.size(), 0); + + std::vector certs; + + for (const auto& cert : chain) { + certs.push_back(to_X509Wrapper(cert.get())); + } + + for (std::uint8_t i = 0; i < certs.size() - 1; i++) { + SCOPED_TRACE("i=" + std::to_string(i)); + const auto& cert = certs[i]; + const auto& issuer = certs[i + 1]; + const auto resA = cert.get_certificate_hash_data(issuer); + EXPECT_EQ(resA.hash_algorithm, HashAlgorithm::SHA256); + } +} + +} // namespace diff --git a/lib/staging/tls/tests/gtest_main.cpp b/lib/staging/tls/tests/gtest_main.cpp new file mode 100644 index 0000000000..477e18bba5 --- /dev/null +++ b/lib/staging/tls/tests/gtest_main.cpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include +#include +#include +#include + +#include + +int main(int argc, char** argv) { + // create test certificates and keys + if (std::system("./pki.sh") != 0) { + std::cerr << "Problem creating test certificates and keys" << std::endl; + char buf[PATH_MAX]; + if (getcwd(&buf[0], sizeof(buf)) != nullptr) { + std::cerr << "./pki.sh not found in " << buf << std::endl; + } + return 1; + } + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/lib/staging/tls/tests/openssl_util_test.cpp b/lib/staging/tls/tests/openssl_util_test.cpp new file mode 100644 index 0000000000..970f863d52 --- /dev/null +++ b/lib/staging/tls/tests/openssl_util_test.cpp @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include + +namespace { + +template constexpr void setCharacters(T& dest, const std::string& s) { + dest.charactersLen = s.size(); + std::memcpy(&dest.characters[0], s.c_str(), s.size()); +} + +template constexpr void setBytes(T& dest, const std::uint8_t* b, std::size_t len) { + dest.bytesLen = len; + std::memcpy(&dest.bytes[0], b, len); +} + +struct test_vectors_t { + const char* input; + const std::uint8_t digest[32]; +}; + +constexpr std::uint8_t sign_test[] = {0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, + 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}; + +constexpr test_vectors_t sha_256_test[] = { + {"", {0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}}, + {"abc", {0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, + 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}}}; + +// Test vectors from ISO 15118-2 Section J.2 +// checked okay (see iso_priv.pem) +constexpr std::uint8_t iso_private_key[] = {0xb9, 0x13, 0x49, 0x63, 0xf5, 0x1c, 0x44, 0x14, 0x73, 0x84, 0x35, + 0x05, 0x7f, 0x97, 0xbb, 0xf1, 0x01, 0x0c, 0xab, 0xcb, 0x8d, 0xbd, + 0xe9, 0xc5, 0xd4, 0x81, 0x38, 0x39, 0x6a, 0xa9, 0x4b, 0x9d}; +// checked okay (see iso_priv.pem) +constexpr std::uint8_t iso_public_key[] = {0x43, 0xe4, 0xfc, 0x4c, 0xcb, 0x64, 0x39, 0x04, 0x27, 0x9c, 0x7a, 0x5e, 0x65, + 0x76, 0xb3, 0x23, 0xe5, 0x5e, 0xc7, 0x9f, 0xf0, 0xe5, 0xa4, 0x05, 0x6e, 0x33, + 0x40, 0x84, 0xcb, 0xc3, 0x36, 0xff, 0x46, 0xe4, 0x4c, 0x1a, 0xdd, 0xf6, 0x91, + 0x62, 0xe5, 0x19, 0x2c, 0x2a, 0x83, 0xfc, 0x2b, 0xca, 0x9d, 0x8f, 0x46, 0xec, + 0xf4, 0xb7, 0x80, 0x67, 0xc2, 0x47, 0x6f, 0x6b, 0x3f, 0x34, 0x60, 0x0e}; + +// EXI AuthorizationReq: checked okay (hash computes correctly) +constexpr std::uint8_t iso_exi_a[] = {0x80, 0x04, 0x01, 0x52, 0x51, 0x0c, 0x40, 0x82, 0x9b, 0x7b, 0x6b, 0x29, 0x02, + 0x93, 0x0b, 0x73, 0x23, 0x7b, 0x69, 0x02, 0x23, 0x0b, 0xa3, 0x09, 0xe8}; + +// checked okay +constexpr std::uint8_t iso_exi_a_hash[] = {0xd1, 0xb5, 0xe0, 0x3d, 0x00, 0x65, 0xbe, 0xe5, 0x6b, 0x31, 0x79, + 0x84, 0x45, 0x30, 0x51, 0xeb, 0x54, 0xca, 0x18, 0xfc, 0x0e, 0x09, + 0x16, 0x17, 0x4f, 0x8b, 0x3c, 0x77, 0xa9, 0x8f, 0x4a, 0xa9}; + +// EXI AuthorizationReq signature block: checked okay (hash computes correctly) +constexpr std::uint8_t iso_exi_b[] = { + 0x80, 0x81, 0x12, 0xb4, 0x3a, 0x3a, 0x38, 0x1d, 0x17, 0x97, 0xbb, 0xbb, 0xbb, 0x97, 0x3b, 0x99, 0x97, 0x37, 0xb9, + 0x33, 0x97, 0xaa, 0x29, 0x17, 0xb1, 0xb0, 0xb7, 0x37, 0xb7, 0x34, 0xb1, 0xb0, 0xb6, 0x16, 0xb2, 0xbc, 0x34, 0x97, + 0xa1, 0xab, 0x43, 0xa3, 0xa3, 0x81, 0xd1, 0x79, 0x7b, 0xbb, 0xbb, 0xb9, 0x73, 0xb9, 0x99, 0x73, 0x7b, 0x93, 0x39, + 0x79, 0x91, 0x81, 0x81, 0x89, 0x79, 0x81, 0xa1, 0x7b, 0xc3, 0x6b, 0x63, 0x23, 0x9b, 0x4b, 0x39, 0x6b, 0x6b, 0x7b, + 0x93, 0x29, 0x1b, 0x2b, 0x1b, 0x23, 0x9b, 0x09, 0x6b, 0x9b, 0x43, 0x09, 0x91, 0xa9, 0xb2, 0x20, 0x62, 0x34, 0x94, + 0x43, 0x10, 0x25, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72, + 0x67, 0x2f, 0x54, 0x52, 0x2f, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x2d, 0x65, 0x78, 0x69, 0x2f, + 0x48, 0x52, 0xd0, 0xe8, 0xe8, 0xe0, 0x74, 0x5e, 0x5e, 0xee, 0xee, 0xee, 0x5c, 0xee, 0x66, 0x5c, 0xde, 0xe4, 0xce, + 0x5e, 0x64, 0x60, 0x60, 0x62, 0x5e, 0x60, 0x68, 0x5e, 0xf0, 0xda, 0xd8, 0xca, 0xdc, 0xc6, 0x46, 0xe6, 0xd0, 0xc2, + 0x64, 0x6a, 0x6c, 0x84, 0x1a, 0x36, 0xbc, 0x07, 0xa0, 0x0c, 0xb7, 0xdc, 0xad, 0x66, 0x2f, 0x30, 0x88, 0xa6, 0x0a, + 0x3d, 0x6a, 0x99, 0x43, 0x1f, 0x81, 0xc1, 0x22, 0xc2, 0xe9, 0xf1, 0x67, 0x8e, 0xf5, 0x31, 0xe9, 0x55, 0x23, 0x70}; + +// checked okay +constexpr std::uint8_t iso_exi_b_hash[] = {0xa4, 0xe9, 0x03, 0xe1, 0x82, 0x43, 0x04, 0x1b, 0x55, 0x4e, 0x11, + 0x64, 0x7e, 0x10, 0x1e, 0xd2, 0x5f, 0xc9, 0xf2, 0x15, 0x2a, 0xf4, + 0x67, 0x40, 0x14, 0xfe, 0x2a, 0xde, 0xac, 0x1e, 0x1c, 0xf7}; + +// checked okay (verifies iso_exi_b_hash with iso_priv.pem) +constexpr std::uint8_t iso_exi_sig[] = {0x4c, 0x8f, 0x20, 0xc1, 0x40, 0x0b, 0xa6, 0x76, 0x06, 0xaa, 0x48, 0x11, 0x57, + 0x2a, 0x2f, 0x1a, 0xd3, 0xc1, 0x50, 0x89, 0xd9, 0x54, 0x20, 0x36, 0x34, 0x30, + 0xbb, 0x26, 0xb4, 0x9d, 0xb1, 0x04, 0xf0, 0x8d, 0xfa, 0x8b, 0xf8, 0x05, 0x5e, + 0x63, 0xa4, 0xb7, 0x5a, 0x8d, 0x31, 0x69, 0x20, 0x6f, 0xa8, 0xd5, 0x43, 0x08, + 0xba, 0x58, 0xf0, 0x56, 0x6b, 0x96, 0xba, 0xf6, 0x92, 0xce, 0x59, 0x50}; + +const char iso_exi_a_hash_b64[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk="; +const char iso_exi_a_hash_b64_nl[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=\n"; + +const char iso_exi_sig_b64[] = + "TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBvqNVDCLpY8FZrlrr2ks5ZUA=="; +const char iso_exi_sig_b64_nl[] = + "TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBv\nqNVDCLpY8FZrlrr2ks5ZUA==\n"; + +TEST(util, removeHyphen) { + const std::string expected{"UKSWI123456791A"}; + std::string cert_emaid{"UKSWI123456791A"}; + + EXPECT_EQ(cert_emaid, expected); + cert_emaid.erase(std::remove(cert_emaid.begin(), cert_emaid.end(), '-'), cert_emaid.end()); + EXPECT_EQ(cert_emaid, expected); + + cert_emaid = std::string{"-UKSWI-123456791-A-"}; + cert_emaid.erase(std::remove(cert_emaid.begin(), cert_emaid.end(), '-'), cert_emaid.end()); + EXPECT_EQ(cert_emaid, expected); +} + +TEST(openssl, base64Encode) { + auto res = openssl::base64_encode(&iso_exi_a_hash[0], sizeof(iso_exi_a_hash)); + EXPECT_EQ(res, iso_exi_a_hash_b64); + res = openssl::base64_encode(&iso_exi_sig[0], sizeof(iso_exi_sig)); + EXPECT_EQ(res, iso_exi_sig_b64); +} + +TEST(openssl, base64EncodeNl) { + auto res = openssl::base64_encode(&iso_exi_a_hash[0], sizeof(iso_exi_a_hash), true); + EXPECT_EQ(res, iso_exi_a_hash_b64_nl); + res = openssl::base64_encode(&iso_exi_sig[0], sizeof(iso_exi_sig), true); + EXPECT_EQ(res, iso_exi_sig_b64_nl); +} + +TEST(openssl, base64Decode) { + auto res = openssl::base64_decode(&iso_exi_a_hash_b64[0], sizeof(iso_exi_a_hash_b64)); + ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash)); + EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0); + res = openssl::base64_decode(&iso_exi_sig_b64[0], sizeof(iso_exi_sig_b64)); + ASSERT_EQ(res.size(), sizeof(iso_exi_sig)); + EXPECT_EQ(std::memcmp(res.data(), &iso_exi_sig[0], res.size()), 0); + + std::array buffer{}; + std::size_t buffer_len = buffer.size(); + + EXPECT_TRUE(openssl::base64_decode(&iso_exi_a_hash_b64[0], sizeof(iso_exi_a_hash_b64), buffer.data(), buffer_len)); + ASSERT_EQ(buffer_len, sizeof(iso_exi_a_hash)); + EXPECT_EQ(std::memcmp(buffer.data(), &iso_exi_a_hash[0], buffer_len), 0); +} + +TEST(openssl, base64DecodeNl) { + auto res = openssl::base64_decode(&iso_exi_a_hash_b64_nl[0], sizeof(iso_exi_a_hash_b64_nl)); + ASSERT_EQ(res.size(), sizeof(iso_exi_a_hash)); + EXPECT_EQ(std::memcmp(res.data(), &iso_exi_a_hash[0], res.size()), 0); + res = openssl::base64_decode(&iso_exi_sig_b64_nl[0], sizeof(iso_exi_sig_b64_nl)); + ASSERT_EQ(res.size(), sizeof(iso_exi_sig)); + EXPECT_EQ(std::memcmp(res.data(), &iso_exi_sig[0], res.size()), 0); + + std::array buffer{}; + std::size_t buffer_len = buffer.size(); + + EXPECT_TRUE( + openssl::base64_decode(&iso_exi_a_hash_b64_nl[0], sizeof(iso_exi_a_hash_b64_nl), buffer.data(), buffer_len)); + ASSERT_EQ(buffer_len, sizeof(iso_exi_a_hash)); + EXPECT_EQ(std::memcmp(buffer.data(), &iso_exi_a_hash[0], buffer_len), 0); +} + +TEST(openssl, sha256) { + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(sha_256_test[0].input, 0, digest)); + EXPECT_EQ(std::memcmp(digest.data(), &sha_256_test[0].digest[0], 32), 0); + EXPECT_TRUE(openssl::sha_256(sha_256_test[1].input, 3, digest)); + EXPECT_EQ(std::memcmp(digest.data(), &sha_256_test[1].digest[0], 32), 0); +} + +TEST(openssl, sha256Exi) { + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(&iso_exi_a[0], sizeof(iso_exi_a), digest)); + EXPECT_EQ(std::memcmp(digest.data(), &iso_exi_a_hash[0], 32), 0); + + EXPECT_TRUE(openssl::sha_256(&iso_exi_b[0], sizeof(iso_exi_b), digest)); + EXPECT_EQ(std::memcmp(digest.data(), &iso_exi_b_hash[0], 32), 0); +} + +TEST(openssl, signVerify) { + auto* bio = BIO_new_file("server_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + + std::array sig_der{}; + std::size_t sig_der_len{sig_der.size()}; + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(&sign_test[0], openssl::sha_256_digest_size, digest)); + + EXPECT_TRUE(openssl::sign(pkey, sig_der.data(), sig_der_len, digest.data(), digest.size())); + EXPECT_TRUE(openssl::verify(pkey, sig_der.data(), sig_der_len, digest.data(), digest.size())); + + BIO_free(bio); + EVP_PKEY_free(pkey); +} + +TEST(openssl, signVerifyBn) { + auto* bio = BIO_new_file("server_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + + openssl::bn_t r; + openssl::bn_t s; + + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(&sign_test[0], openssl::sha_256_digest_size, digest)); + + EXPECT_TRUE(openssl::sign(pkey, r, s, digest)); + EXPECT_TRUE(openssl::verify(pkey, r, s, digest)); + + BIO_free(bio); + EVP_PKEY_free(pkey); +} + +TEST(openssl, signVerifyMix) { + auto* bio = BIO_new_file("server_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + + std::array sig_der; + std::size_t sig_der_len{sig_der.size()}; + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(&sign_test[0], openssl::sha_256_digest_size, digest)); + + EXPECT_TRUE(openssl::sign(pkey, sig_der.data(), sig_der_len, digest.data(), digest.size())); + + openssl::bn_t r; + openssl::bn_t s; + EXPECT_TRUE(openssl::signature_to_bn(r, s, sig_der.data(), sig_der_len)); + EXPECT_TRUE(openssl::verify(pkey, r, s, digest)); + + BIO_free(bio); + EVP_PKEY_free(pkey); +} + +TEST(openssl, signVerifyFail) { + auto bio = BIO_new_file("server_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + BIO_free(bio); + + bio = BIO_new_file("client_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey_inv = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + BIO_free(bio); + + std::array sig_der; + std::size_t sig_der_len{sig_der.size()}; + openssl::sha_256_digest_t digest; + EXPECT_TRUE(openssl::sha_256(&sign_test[0], openssl::sha_256_digest_size, digest)); + + EXPECT_TRUE(openssl::sign(pkey, sig_der.data(), sig_der_len, digest.data(), digest.size())); + EXPECT_FALSE(openssl::verify(pkey_inv, sig_der.data(), sig_der_len, digest.data(), digest.size())); + + EVP_PKEY_free(pkey); + EVP_PKEY_free(pkey_inv); +} + +TEST(openssl, verifyIso) { + auto* bio = BIO_new_file("iso_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + BIO_free(bio); + + auto [sig, siglen] = openssl::bn_to_signature(&iso_exi_sig[0], &iso_exi_sig[32]); + EXPECT_TRUE(openssl::verify(pkey, sig.get(), siglen, &iso_exi_b_hash[0], sizeof(iso_exi_b_hash))); + EVP_PKEY_free(pkey); +} + +TEST(certificateLoad, single) { + auto certs = ::openssl::load_certificates("server_cert.pem"); + EXPECT_EQ(certs.size(), 1); +} + +TEST(certificateLoad, chain) { + auto certs = ::openssl::load_certificates("server_chain.pem"); + EXPECT_EQ(certs.size(), 2); +} + +TEST(certificateLoad, key) { + auto certs = ::openssl::load_certificates("server_priv.pem"); + EXPECT_EQ(certs.size(), 0); +} + +TEST(certificate, toPem) { + auto certs = ::openssl::load_certificates("client_ca_cert.pem"); + ASSERT_EQ(certs.size(), 1); + auto pem = ::openssl::certificate_to_pem(certs[0].get()); + EXPECT_FALSE(pem.empty()); + // std::cout << pem << std::endl; +} + +TEST(certificate, verify) { + auto client = ::openssl::load_certificates("client_cert.pem"); + auto chain = ::openssl::load_certificates("client_chain.pem"); + auto root = ::openssl::load_certificates("client_root_cert.pem"); + + ASSERT_EQ(client.size(), 1); + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + EXPECT_EQ(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::verified); +} + +TEST(certificate, verifyRemoveClientFromChain) { + auto client = ::openssl::load_certificates("client_cert.pem"); + auto chain = ::openssl::load_certificates("client_chain.pem"); + auto root = ::openssl::load_certificates("client_root_cert.pem"); + + ASSERT_EQ(client.size(), 1); + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + // client certificate is 1st in the list + openssl::CertificateList new_chain; + for (auto itt = std::next(chain.begin()); itt != chain.end(); itt++) { + new_chain.push_back(std::move(*itt)); + } + + EXPECT_EQ(::openssl::verify_certificate(client[0].get(), root, new_chain), openssl::verify_result_t::verified); +} + +TEST(certificate, verifyNoClient) { + // client certificate is in the chain + auto chain = ::openssl::load_certificates("client_chain.pem"); + auto root = ::openssl::load_certificates("client_root_cert.pem"); + + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + EXPECT_EQ(::openssl::verify_certificate(nullptr, root, chain), openssl::verify_result_t::verified); +} + +TEST(certificate, verifyFailWrongClient) { + auto client = ::openssl::load_certificates("server_cert.pem"); + auto chain = ::openssl::load_certificates("client_chain.pem"); + auto root = ::openssl::load_certificates("client_root_cert.pem"); + + ASSERT_EQ(client.size(), 1); + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::verified); +} + +TEST(certificate, verifyFailWrongRoot) { + auto client = ::openssl::load_certificates("client_cert.pem"); + auto chain = ::openssl::load_certificates("client_chain.pem"); + auto root = ::openssl::load_certificates("server_root_cert.pem"); + + ASSERT_EQ(client.size(), 1); + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::verified); +} + +TEST(certificate, verifyFailWrongChain) { + auto client = ::openssl::load_certificates("client_cert.pem"); + auto chain = ::openssl::load_certificates("server_chain.pem"); + auto root = ::openssl::load_certificates("client_root_cert.pem"); + + ASSERT_EQ(client.size(), 1); + EXPECT_GT(chain.size(), 0); + EXPECT_EQ(root.size(), 1); + + EXPECT_NE(::openssl::verify_certificate(client[0].get(), root, chain), openssl::verify_result_t::verified); +} + +TEST(certificate, subjectName) { + auto chain = ::openssl::load_certificates("client_chain.pem"); + EXPECT_GT(chain.size(), 0); + + for (const auto& cert : chain) { + auto subject = ::openssl::certificate_subject(cert.get()); + EXPECT_GT(subject.size(), 0); +#if 0 + for (const auto& itt : subject) { + std::cout << itt.first << ": " << itt.second << std::endl; + } +#endif + } +} + +} // namespace diff --git a/lib/staging/tls/tests/patched_test.cpp b/lib/staging/tls/tests/patched_test.cpp new file mode 100644 index 0000000000..525396647d --- /dev/null +++ b/lib/staging/tls/tests/patched_test.cpp @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +/** + * \file testing patched version of OpenSSL + * + * These tests will only pass on a patched version of OpenSSL. + * (they should compile and run fine with some test failures) + * + * It is recommended to also run tests alongside Wireshark + * e.g. `./patched_test --gtest_filter=OcspTest.TLS12` + * to check that the Server Hello record is correctly formed: + * - no status_request or status_request_v2 then no Certificate Status record + * - status_request or status_request_v2 then there is a Certificate Status record + * - never both status_request and status_request_v2 + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// ---------------------------------------------------------------------------- +// set up code + +void log_handler(openssl::log_level_t level, const std::string& str) { + switch (level) { + case openssl::log_level_t::debug: + std::cout << "DEBUG: " << str << std::endl; + break; + case openssl::log_level_t::warning: + std::cout << "WARN: " << str << std::endl; + break; + case openssl::log_level_t::error: + std::cerr << "ERROR: " << str << std::endl; + break; + default: + std::cerr << "Unknown: " << str << std::endl; + break; + } +} + +struct ClientTest : public tls::Client { + enum class flags_t { + status_request_cb, + status_request, + status_request_v2, + connected, + last = connected, + }; + util::AtomicEnumFlags flags; + + void reset() { + flags.reset(); + } + + virtual int status_request_cb(tls::Ssl* ctx) { + /* + * This callback is called when status_request or status_request_v2 extensions + * were present in the Client Hello. It doesn't mean that the extension is in + * the Server Hello SSL_get_tlsext_status_ocsp_resp() returns -1 in that case + */ + const unsigned char* response{nullptr}; + const auto total_length = SSL_get_tlsext_status_ocsp_resp(ctx, &response); +#if 0 + // could set a different flag to spot no extension + if (total_length != -1) { + // -1 is the extension isn't present + flags.set(flags_t::status_request_cb); + } +#else + flags.set(flags_t::status_request_cb); +#endif + if ((response != nullptr) && (total_length > 0)) { + switch (response[0]) { + case 0x30: + flags.set(flags_t::status_request); + break; + case 0x00: + flags.set(flags_t::status_request_v2); + break; + default: + break; + } + } + return 1; + // return tls::Client::status_request_cb(ctx); + } +}; + +void handler(std::shared_ptr& con) { + if (con->accept()) { + std::uint32_t count{0}; + std::array buffer{}; + bool bExit = false; + while (!bExit) { + std::size_t readbytes = 0; + std::size_t writebytes = 0; + + switch (con->read(buffer.data(), buffer.size(), readbytes)) { + case tls::Connection::result_t::success: + switch (con->write(buffer.data(), readbytes, writebytes)) { + case tls::Connection::result_t::success: + break; + case tls::Connection::result_t::timeout: + case tls::Connection::result_t::error: + default: + bExit = true; + break; + } + break; + case tls::Connection::result_t::timeout: + count++; + if (count > 10) { + bExit = true; + } + break; + case tls::Connection::result_t::error: + default: + bExit = true; + break; + } + } + con->shutdown(); + } +} + +void run_server(tls::Server& server) { + server.serve(&handler); +} + +class OcspTest : public testing::Test { +protected: + using flags_t = ClientTest::flags_t; + + tls::Server server; + tls::Server::config_t server_config; + std::thread server_thread; + ClientTest client; + tls::Client::config_t client_config; + + static void SetUpTestSuite() { + openssl::set_log_handler(log_handler); + struct sigaction action; + std::memset(&action, 0, sizeof(action)); + action.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &action, nullptr); + if (std::system("./pki.sh > /dev/null") != 0) { + std::cerr << "Problem creating test certificates and keys" << std::endl; + char buf[PATH_MAX]; + if (getcwd(&buf[0], sizeof(buf)) != nullptr) { + std::cerr << "./pki.sh not found in " << buf << std::endl; + } + exit(1); + } + } + + void SetUp() override { + server_config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + // server_config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; + server_config.ciphersuites = ""; + server_config.certificate_chain_file = "server_chain.pem"; + server_config.private_key_file = "server_priv.pem"; + // server_config.verify_locations_file = "client_root_cert.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 = 500; + + client_config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + // client_config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; + // client_config.certificate_chain_file = "client_chain.pem"; + // client_config.private_key_file = "client_priv.pem"; + client_config.verify_locations_file = "server_root_cert.pem"; + client_config.io_timeout_ms = 100; + client_config.verify_server = false; + client_config.status_request = false; + client_config.status_request_v2 = false; + client.reset(); + } + + void TearDown() override { + server.stop(); + server.wait_stopped(); + if (server_thread.joinable()) { + server_thread.join(); + } + } + + 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(); + } + } + + void connect() { + client.init(client_config); + client.reset(); + // localhost works in some cases but not in the CI pipeline for IPv6 + // use ip6-localhost + auto connection = client.connect("localhost", "8444", false); + if (connection) { + if (connection->connect()) { + set(ClientTest::flags_t::connected); + connection->shutdown(); + } + } + } + + void set(flags_t flag) { + client.flags.set(flag); + } + + [[nodiscard]] bool is_set(flags_t flag) const { + return client.flags.is_set(flag); + } + + [[nodiscard]] bool is_reset(flags_t flag) const { + return client.flags.is_reset(flag); + } +}; + +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 + +TEST_F(OcspTest, NonBlocking) { + // test shouldn't hang + start(); +} + +TEST_F(OcspTest, NonBlockingConnect) { + // test shouldn't hang + start(); + 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, 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(); + connect(); + // no status requested + 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)); + + client_config.status_request = true; + connect(); + // status_request only + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_set(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = false; + client_config.status_request_v2 = true; + connect(); + // status_request_v2 only + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_set(flags_t::status_request_v2)); + + client_config.status_request = true; + connect(); + // status_request and status_request_v2 + // status_request_v2 is preferred over status_request + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_set(flags_t::status_request_v2)); +} + +TEST_F(OcspTest, TLS13) { + // test using TLS 1.3 + // there shouldn't be status_request_v2 responses + // TLS 1.3 still supports status_request however it is handled differently + // (which is handled within the OpenSSL API) + server_config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; + start(); + connect(); + // no status requested + 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)); + + client_config.status_request = true; + connect(); + // status_request only + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_set(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = false; + client_config.status_request_v2 = true; + connect(); + // status_request_v2 only - ignored by server + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = true; + connect(); + // status_request and status_request_v2 + // status_request_v2 is ignored by server and status_request used + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_set(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); +} + +TEST_F(OcspTest, NoOcspFiles) { + // test using TLS 1.2 + server_config.ocsp_response_files.clear(); + + start(); + connect(); + // no status requested + 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)); + + client_config.status_request = true; + connect(); + // status_request only + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = false; + client_config.status_request_v2 = true; + connect(); + // status_request_v2 only + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); + + client_config.status_request = true; + connect(); + // status_request and status_request_v2 + // status_request_v2 is preferred over status_request + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_TRUE(is_set(flags_t::status_request_cb)); + EXPECT_TRUE(is_reset(flags_t::status_request)); + EXPECT_TRUE(is_reset(flags_t::status_request_v2)); +} + +} // namespace diff --git a/lib/staging/tls/tests/pki/.gitignore b/lib/staging/tls/tests/pki/.gitignore new file mode 100644 index 0000000000..cfaad76118 --- /dev/null +++ b/lib/staging/tls/tests/pki/.gitignore @@ -0,0 +1 @@ +*.pem diff --git a/lib/staging/tls/tests/pki/iso_pkey.asn1 b/lib/staging/tls/tests/pki/iso_pkey.asn1 new file mode 100644 index 0000000000..4d657c3b4e --- /dev/null +++ b/lib/staging/tls/tests/pki/iso_pkey.asn1 @@ -0,0 +1,11 @@ +asn1=SEQ:pkcs8c +[pkcs8c] +ver=INT:0 +algid=SEQ:algid +data=OCTWRAP,SEQ:sec1 +[algid] +alg=OID:id-ecPublicKey +parm=OID:prime256v1 +[sec1] +ver=INT:1 +privkey=FORMAT:HEX,OCT:b9134963f51c4414738435057f97bbf1010cabcb8dbde9c5d48138396aa94b9d diff --git a/lib/staging/tls/tests/pki/ocsp_response.der b/lib/staging/tls/tests/pki/ocsp_response.der new file mode 100644 index 0000000000..c79ef5c78f Binary files /dev/null and b/lib/staging/tls/tests/pki/ocsp_response.der differ diff --git a/lib/staging/tls/tests/pki/openssl-pki.conf b/lib/staging/tls/tests/pki/openssl-pki.conf new file mode 100644 index 0000000000..90147e4d87 --- /dev/null +++ b/lib/staging/tls/tests/pki/openssl-pki.conf @@ -0,0 +1,143 @@ +openssl_conf = openssl_init + +[openssl_init] +providers = provider_section + +[provider_section] +default = default_section +tpm2 = tpm2_section +base = base_section + +[default_section] +activate = 1 + +[tpm2_section] +activate = 1 + +[base_section] +activate = 1 + +# server section +# ============== +[req_server_root] +distinguished_name = req_dn_server_root +utf8 = yes +prompt = no +req_extensions = v3_server_root + +[req_server_ca] +distinguished_name = req_dn_server_ca +utf8 = yes +prompt = no +req_extensions = v3_server_ca + +[req_server] +distinguished_name = req_dn_server +utf8 = yes +prompt = no +req_extensions = v3_server + +[req_dn_server_root] +C = GB +O = Pionix +L = London +CN = Root Trust Anchor + +[req_dn_server_ca] +C = GB +O = Pionix +L = London +CN = Intermediate CA + +[req_dn_server] +C = GB +O = Pionix +L = London +CN = 00000000 + +[req_dn_client] +C = GB +O = Pionix +L = London +CN = 12345678 + +[v3_server_root] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always +basicConstraints = critical, CA:true, pathlen:2 +keyUsage = keyCertSign, cRLSign + +[v3_server_ca] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always +basicConstraints = critical, CA:true +keyUsage = keyCertSign, cRLSign + +[v3_server] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always +keyUsage = digitalSignature, keyEncipherment, keyAgreement +extendedKeyUsage = serverAuth, clientAuth +subjectAltName = IP:192.168.245.1, DNS:evse.pionix.de + +# client section +# ============== +[req_client] +distinguished_name = req_dn_client +utf8 = yes +prompt = no +req_extensions = v3_client + +[req_client_root] +distinguished_name = req_dn_client_root +utf8 = yes +prompt = no +req_extensions = v3_client_root + +[req_client_ca] +distinguished_name = req_dn_client_ca +utf8 = yes +prompt = no +req_extensions = v3_client_ca + +[req_server] +distinguished_name = req_dn_server +utf8 = yes +prompt = no +req_extensions = v3_server + +[req_dn_client_root] +C = DE +O = Pionix +L = Frankfurt +CN = Root Trust Anchor + +[req_dn_client_ca] +C = DE +O = Pionix +L = Frankfurt +CN = Intermediate CA + +[req_dn_client] +C = DE +O = Pionix +L = Frankfurt +CN = 12345678 + +[v3_client_root] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always +basicConstraints = critical, CA:true, pathlen:2 +keyUsage = keyCertSign, cRLSign + +[v3_client_ca] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always +basicConstraints = critical, CA:true +keyUsage = keyCertSign, cRLSign + +[v3_client] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always +keyUsage = digitalSignature, keyEncipherment, keyAgreement +extendedKeyUsage = clientAuth diff --git a/lib/staging/tls/tests/pki/pki.sh b/lib/staging/tls/tests/pki/pki.sh new file mode 100755 index 0000000000..feec62e92d --- /dev/null +++ b/lib/staging/tls/tests/pki/pki.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +cfg=openssl-pki.conf + +server_root_priv=server_root_priv.pem +server_ca_priv=server_ca_priv.pem +server_priv=server_priv.pem + +server_root_cert=server_root_cert.pem +server_ca_cert=server_ca_cert.pem +server_cert=server_cert.pem +server_chain=server_chain.pem + +client_root_priv=client_root_priv.pem +client_ca_priv=client_ca_priv.pem +client_priv=client_priv.pem + +client_root_cert=client_root_cert.pem +client_ca_cert=client_ca_cert.pem +client_cert=client_cert.pem +client_chain=client_chain.pem + +# generate keys +for i in ${server_root_priv} ${server_ca_priv} ${server_priv} \ + ${client_root_priv} ${client_ca_priv} ${client_priv} +do + openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out $i + chmod 644 $i +done + +export OPENSSL_CONF=${cfg} + +echo "Generate server root" +openssl req \ + -config ${cfg} -x509 -section req_server_root -extensions v3_server_root \ + -key ${server_root_priv} -out ${server_root_cert} +echo "Generate server ca" +openssl req \ + -config ${cfg} -x509 -section req_server_ca -extensions v3_server_ca \ + -key ${server_ca_priv} -CA ${server_root_cert} -CAkey ${server_root_priv} -out ${server_ca_cert} +echo "Generate server" +openssl req \ + -config ${cfg} -x509 -section req_server -extensions v3_server \ + -key ${server_priv} -CA ${server_ca_cert} -CAkey ${server_ca_priv} -out ${server_cert} +cat ${server_cert} ${server_ca_cert} > ${server_chain} + +echo "Generate client root" +openssl req \ + -config ${cfg} -x509 -section req_client_root -extensions v3_client_root \ + -key ${client_root_priv} -out ${client_root_cert} +echo "Generate client ca" +openssl req \ + -config ${cfg} -x509 -section req_client_ca -extensions v3_client_ca \ + -key ${client_ca_priv} -CA ${client_root_cert} -CAkey ${client_root_priv} -out ${client_ca_cert} +echo "Generate client" +openssl req \ + -config ${cfg} -x509 -section req_client -extensions v3_client \ + -key ${client_priv} -CA ${client_ca_cert} -CAkey ${client_ca_priv} -out ${client_cert} + +cat ${client_cert} ${client_ca_cert} > ${client_chain} + +# convert iso key to PEM +openssl asn1parse -genconf iso_pkey.asn1 -noout -out -| openssl pkey -inform der -out iso_priv.pem diff --git a/lib/staging/tls/tests/tls_client_main.cpp b/lib/staging/tls/tests/tls_client_main.cpp new file mode 100644 index 0000000000..0baa90713e --- /dev/null +++ b/lib/staging/tls/tests/tls_client_main.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include + +#include +#include +#include +#include + +using namespace std::chrono_literals; + +namespace { +const char* short_opts = "h123"; +bool use_tls1_3{false}; +bool use_status_request{false}; +bool use_status_request_v2{false}; + +void parse_options(int argc, char** argv) { + int c; + + while ((c = getopt(argc, argv, short_opts)) != -1) { + switch (c) { + break; + case '1': + use_status_request = true; + break; + case '2': + use_status_request_v2 = true; + break; + case '3': + use_tls1_3 = true; + break; + case 'h': + case '?': + std::cout << "Usage: " << argv[0] << " [-1|-2|-3]" << std::endl; + std::cout << " -1 request status_request" << std::endl; + std::cout << " -2 request status_request_v2" << std::endl; + std::cout << " -3 use TLS 1.3 (TLS 1.2 otherwise)" << std::endl; + exit(1); + break; + default: + exit(2); + } + } +} +} // namespace + +int main(int argc, char** argv) { + parse_options(argc, argv); + + tls::Client client; + tls::Client::config_t config; + + if (use_tls1_3) { + config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; + std::cout << "use_tls1_3 true" << std::endl; + } else { + config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + config.ciphersuites = ""; // No TLS1.3 + std::cout << "use_tls1_3 false" << std::endl; + } + + config.certificate_chain_file = "client_chain.pem"; + config.private_key_file = "client_priv.pem"; + config.verify_locations_file = "server_root_cert.pem"; + config.io_timeout_ms = 500; + config.verify_server = false; + + if (use_status_request) { + config.status_request = true; + std::cout << "use_status_request true" << std::endl; + } else { + config.status_request = false; + std::cout << "use_status_request false" << std::endl; + } + + if (use_status_request_v2) { + config.status_request_v2 = true; + std::cout << "use_status_request_v2 true" << std::endl; + } else { + config.status_request_v2 = false; + std::cout << "use_status_request_v2 false" << std::endl; + } + + client.init(config); + + // localhost works in some cases but not in the CI pipeline + auto connection = client.connect("ip6-localhost", "8444", true); + if (connection) { + if (connection->connect()) { + std::array buffer{}; + std::size_t readbytes = 0; + std::cout << "about to read" << std::endl; + const auto res = connection->read(buffer.data(), buffer.size(), readbytes); + std::cout << (int)res << std::endl; + std::this_thread::sleep_for(1s); + connection->shutdown(); + } + } + + return 0; +} diff --git a/lib/staging/tls/tests/tls_main.cpp b/lib/staging/tls/tests/tls_main.cpp new file mode 100644 index 0000000000..c521fe010e --- /dev/null +++ b/lib/staging/tls/tests/tls_main.cpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +/* + * testing options + * openssl s_client -connect localhost:8444 -verify 2 -CAfile server_root_cert.pem -cert client_cert.pem -cert_chain + * client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname evse.pionix.de -status + */ + +#include + +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +void handle_connection(std::shared_ptr& con) { + std::cout << "Connection" << std::endl; + if (con->accept()) { + std::uint32_t count{0}; + std::array buffer{}; + bool bExit = false; + while (!bExit) { + std::size_t readbytes = 0; + std::size_t writebytes = 0; + + switch (con->read(buffer.data(), buffer.size(), readbytes)) { + case tls::Connection::result_t::success: + switch (con->write(buffer.data(), readbytes, writebytes)) { + case tls::Connection::result_t::success: + break; + case tls::Connection::result_t::timeout: + case tls::Connection::result_t::error: + default: + bExit = true; + break; + } + break; + case tls::Connection::result_t::timeout: + count++; + if (count > 10) { + bExit = true; + } + break; + case tls::Connection::result_t::error: + default: + bExit = true; + break; + } + } + + con->shutdown(); + } + std::cout << "Connection closed" << std::endl; +} + +int main() { + tls::Server server; + tls::Server::config_t config; + +#if 0 + config.cipher_list = + "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384"; + config.ciphersuites = "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"; +#else + // config.cipher_list = "ECDHE-ECDSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256"; + config.cipher_list = "ECDHE-ECDSA-AES128-SHA256"; + config.ciphersuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"; + // config.ciphersuites = ""; +#endif + config.certificate_chain_file = "server_chain.pem"; + config.private_key_file = "server_priv.pem"; + config.verify_locations_file = "client_root_cert.pem"; + // config.ocsp_response_files = {"ocsp_response.der", nullptr}; + config.ocsp_response_files = {"ocsp_response.der", "ocsp_response.der"}; + config.service = "8444"; + config.ipv6_only = false; + config.verify_client = true; + config.io_timeout_ms = 1000; + + std::thread stop([&server]() { + std::this_thread::sleep_for(30s); + server.stop(); + }); + + server.init(config, nullptr); + server.wait_stopped(); + + // server.serve(&handle_connection); + server.serve([](auto con) { handle_connection(con); }); + server.wait_stopped(); + + stop.join(); + + return 0; +} diff --git a/lib/staging/tls/tests/tls_test.cpp b/lib/staging/tls/tests/tls_test.cpp new file mode 100644 index 0000000000..5d6acb5003 --- /dev/null +++ b/lib/staging/tls/tests/tls_test.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include +#include + +#include + +std::string to_string(const openssl::sha_256_digest_t& digest) { + std::stringstream string_stream; + string_stream << std::hex; + + for (int idx = 0; idx < digest.size(); ++idx) + string_stream << std::setw(2) << std::setfill('0') << (int)digest[idx]; + + return string_stream.str(); +} + +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{}; + auto res = cache.lookup(digest); + EXPECT_EQ(res.get(), nullptr); +} + +TEST(OcspCache, init) { + tls::OcspCache cache; + + auto chain = openssl::load_certificates("client_chain.pem"); + std::vector entries; + + openssl::sha_256_digest_t digest{}; + for (const auto& cert : chain) { + ASSERT_TRUE(tls::OcspCache::digest(digest, cert.get())); + // std::cout << "digest: " << to_string(digest) << std::endl; + entries.emplace_back(digest, "ocsp_response.der"); + } + + EXPECT_TRUE(cache.load(entries)); + // std::cout << "digest: " << to_string(digest) << std::endl; + auto res = cache.lookup(digest); + EXPECT_NE(res.get(), nullptr); +} + +} // namespace diff --git a/lib/staging/tls/tls.cpp b/lib/staging/tls/tls.cpp new file mode 100644 index 0000000000..2220fb7219 --- /dev/null +++ b/lib/staging/tls/tls.cpp @@ -0,0 +1,1518 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "tls.hpp" +#include "openssl_util.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef TLSEXT_STATUSTYPE_ocsp_multi +#define OPENSSL_PATCHED +#endif + +namespace std { +template <> class default_delete { +public: + void operator()(SSL* ptr) const { + ::SSL_free(ptr); + } +}; +template <> class default_delete { +public: + void operator()(SSL_CTX* ptr) const { + ::SSL_CTX_free(ptr); + } +}; +} // namespace std + +using ::openssl::log_error; +using ::openssl::log_warning; + +namespace { + +/** + * \brief convert a big endian 3 byte (24 bit) unsigned value to uint32 + * \param[in] ptr the pointer to the most significant byte + * \return the interpreted value + */ +constexpr std::uint32_t uint24(const std::uint8_t* ptr) { + return (static_cast(ptr[0]) << 16U) | (static_cast(ptr[1]) << 8U) | + static_cast(ptr[2]); +} + +/** + * \brief convert a uint32 to big endian 3 byte (24 bit) value + * \param[in] ptr the pointer to the most significant byte + * \param[in] value the 24 bit value + */ +constexpr void uint24(std::uint8_t* ptr, std::uint32_t value) { + ptr[0] = (value >> 16U) & 0xffU; + ptr[1] = (value >> 8U) & 0xffU; + ptr[2] = value & 0xffU; +} + +// see https://datatracker.ietf.org/doc/html/rfc6961 +constexpr int TLSEXT_TYPE_status_request_v2 = 17; + +std::string to_string(const openssl::sha_256_digest_t& digest) { + std::stringstream string_stream; + string_stream << std::hex; + for (const auto& c : digest) { + string_stream << std::setw(2) << std::setfill('0') << static_cast(c); + } + return string_stream.str(); +} + +constexpr std::uint32_t c_shutdown_timeout_ms = 5000; // 5 seconds + +enum class ssl_error_t : std::uint8_t { + error, + error_ssl, + error_syscall, + none, + want_accept, + want_async, + want_async_job, + want_connect, + want_hello_cb, + want_read, + want_write, + want_x509_lookup, + zero_return, + timeout, // not an OpenSSL result +}; + +constexpr ssl_error_t convert(const int err) { + ssl_error_t res{ssl_error_t::error}; + switch (err) { + case SSL_ERROR_NONE: + res = ssl_error_t::none; + break; + case SSL_ERROR_ZERO_RETURN: + res = ssl_error_t::zero_return; + break; + case SSL_ERROR_WANT_READ: + res = ssl_error_t::want_read; + break; + case SSL_ERROR_WANT_WRITE: + res = ssl_error_t::want_write; + break; + case SSL_ERROR_WANT_CONNECT: + res = ssl_error_t::want_connect; + break; + case SSL_ERROR_WANT_ACCEPT: + res = ssl_error_t::want_accept; + break; + case SSL_ERROR_WANT_X509_LOOKUP: + res = ssl_error_t::want_x509_lookup; + break; + case SSL_ERROR_WANT_ASYNC: + res = ssl_error_t::want_async; + break; + case SSL_ERROR_WANT_ASYNC_JOB: + res = ssl_error_t::want_async_job; + break; + case SSL_ERROR_WANT_CLIENT_HELLO_CB: + res = ssl_error_t::want_hello_cb; + break; + case SSL_ERROR_SYSCALL: + res = ssl_error_t::error_syscall; + break; + case SSL_ERROR_SSL: + res = ssl_error_t::error_ssl; + break; + default: + log_error(std::string("Unexpected SSL_get_error: ") + std::to_string(static_cast(res))); + break; + }; + return res; +} + +enum class ssl_result_t : std::uint8_t { + error, + error_syscall, + success, + closed, + timeout, +}; + +constexpr ssl_result_t convert(ssl_error_t err) { + switch (err) { + case ssl_error_t::none: + return ssl_result_t::success; + case ssl_error_t::timeout: + return ssl_result_t::timeout; + case ssl_error_t::error_syscall: + case ssl_error_t::error_ssl: + return ssl_result_t::error_syscall; + case ssl_error_t::zero_return: + return ssl_result_t::closed; + case ssl_error_t::error: + case ssl_error_t::want_accept: + case ssl_error_t::want_async: + case ssl_error_t::want_async_job: + case ssl_error_t::want_connect: + case ssl_error_t::want_hello_cb: + case ssl_error_t::want_read: + case ssl_error_t::want_write: + case ssl_error_t::want_x509_lookup: + default: + return ssl_result_t::error; + } +} + +constexpr tls::Connection::result_t convert(ssl_result_t err) { + switch (err) { + case ssl_result_t::success: + return tls::Connection::result_t::success; + case ssl_result_t::timeout: + return tls::Connection::result_t::timeout; + case ssl_result_t::closed: + case ssl_result_t::error: + case ssl_result_t::error_syscall: + default: + return tls::Connection::result_t::error; + } +} + +int wait_for(int soc, bool forWrite, std::int32_t timeout_ms) { + std::int16_t event = POLLIN; + if (forWrite) { + event = POLLOUT; + } + std::array fds = {{{soc, event, 0}}}; + int poll_res{0}; + + for (;;) { + poll_res = poll(fds.data(), fds.size(), timeout_ms); + if (poll_res == -1) { + if (errno != EINTR) { + log_error(std::string("wait_for poll: ") + std::to_string(errno)); + break; + } + } + // timeout or event(s) + break; + } + + return poll_res; +} + +[[nodiscard]] ssl_result_t ssl_read(SSL* ctx, std::byte* buf, std::size_t num, std::size_t& readbytes, + std::int32_t timeout_ms); +[[nodiscard]] ssl_result_t ssl_write(SSL* ctx, const std::byte* buf, std::size_t num, std::size_t& writebytes, + std::int32_t timeout_ms); +[[nodiscard]] ssl_result_t ssl_accept(SSL* ctx, std::int32_t timeout_ms); +[[nodiscard]] ssl_result_t ssl_connect(SSL* ctx, std::int32_t timeout_ms); +void ssl_shutdown(SSL* ctx, std::int32_t timeout_ms); + +bool process_result(SSL* ctx, const std::string& operation, const int res, ssl_error_t& result, + std::int32_t timeout_ms) { + bool bLoop = false; + + if (res <= 0) { + const auto sslerr_raw = SSL_get_error(ctx, res); + result = convert(sslerr_raw); + switch (result) { + case ssl_error_t::none: + case ssl_error_t::zero_return: + break; + case ssl_error_t::want_accept: + case ssl_error_t::want_connect: + case ssl_error_t::want_read: + case ssl_error_t::want_write: + if (wait_for(SSL_get_fd(ctx), result == ssl_error_t::want_write, timeout_ms) > 0) { + bLoop = true; + } + result = ssl_error_t::timeout; + break; + case ssl_error_t::error_syscall: + if (errno != 0) { + log_error(operation + "SSL_ERROR_SYSCALL " + std::to_string(errno)); + } + break; + case ssl_error_t::error: + case ssl_error_t::error_ssl: + case ssl_error_t::want_async: + case ssl_error_t::want_async_job: + case ssl_error_t::want_hello_cb: + case ssl_error_t::want_x509_lookup: + default: + log_error(operation + std::to_string(res) + " " + std::to_string(sslerr_raw)); + break; + } + } else { + result = ssl_error_t::none; + } + + return bLoop; +} + +ssl_result_t ssl_read(SSL* ctx, std::byte* buf, const std::size_t num, std::size_t& readbytes, + std::int32_t timeout_ms) { + ssl_error_t result = ssl_error_t::error; + bool bLoop = ctx != nullptr; + while (bLoop) { + const auto res = SSL_read_ex(ctx, buf, num, &readbytes); + bLoop = process_result(ctx, "SSL_read: ", res, result, timeout_ms); + } + return convert(result); +}; + +ssl_result_t ssl_write(SSL* ctx, const std::byte* buf, const std::size_t num, std::size_t& writebytes, + std::int32_t timeout_ms) { + ssl_error_t result = ssl_error_t::error; + bool bLoop = ctx != nullptr; + while (bLoop) { + const auto res = SSL_write_ex(ctx, buf, num, &writebytes); + bLoop = process_result(ctx, "SSL_write: ", res, result, timeout_ms); + } + return convert(result); +} + +ssl_result_t ssl_accept(SSL* ctx, std::int32_t timeout_ms) { + ssl_error_t result = ssl_error_t::error; + bool bLoop = ctx != nullptr; + while (bLoop) { + const auto res = SSL_accept(ctx); + // 0 is handshake not successful + // < 0 is other error + bLoop = process_result(ctx, "SSL_accept: ", res, result, timeout_ms); + } + return convert(result); +} + +ssl_result_t ssl_connect(SSL* ctx, std::int32_t timeout_ms) { + ssl_error_t result = ssl_error_t::error; + bool bLoop = ctx != nullptr; + + while (bLoop) { + const auto res = SSL_connect(ctx); + // 0 is handshake not successful + // < 0 is other error + bLoop = process_result(ctx, "SSL_connect: ", res, result, timeout_ms); + } + return convert(result); +} + +void ssl_shutdown(SSL* ctx, std::int32_t timeout_ms) { + ssl_error_t result = ssl_error_t::error; + bool bLoop = ctx != nullptr; + while (bLoop) { + const auto res = SSL_shutdown(ctx); + bLoop = process_result(ctx, "SSL_shutdown: ", res, result, 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, bool required) { + bool bRes{true}; + + if (ctx == nullptr) { + log_error("server_init::SSL_CTX_new"); + bRes = false; + } else { + if (SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 0) { + log_error("SSL_CTX_set_min_proto_version"); + bRes = false; + } + if ((ciphersuites != nullptr) && (ciphersuites[0] == '\0')) { + // no cipher suites configured - don't use TLS 1.3 + // nullptr means use the defaults + if (SSL_CTX_set_max_proto_version(ctx, TLS1_2_VERSION) == 0) { + log_error("SSL_CTX_set_max_proto_version"); + bRes = false; + } + } + if (cipher_list != nullptr) { + if (SSL_CTX_set_cipher_list(ctx, cipher_list) == 0) { + log_error("SSL_CTX_set_cipher_list"); + bRes = false; + } + } + if (ciphersuites != nullptr) { + if (SSL_CTX_set_ciphersuites(ctx, ciphersuites) == 0) { + log_error("SSL_CTX_set_ciphersuites"); + bRes = false; + } + } + + if (certificate_chain_file != nullptr) { + if (SSL_CTX_use_certificate_chain_file(ctx, certificate_chain_file) != 1) { + log_error("SSL_CTX_use_certificate_chain_file"); + bRes = false; + } + } else { + bRes = !required; + } + + if (private_key_file != nullptr) { + // the password callback uses a non-const argument + void* pass_ptr{nullptr}; + std::string pass_str; + if (private_key_password != nullptr) { + pass_str = private_key_password; + pass_ptr = pass_str.data(); + } + SSL_CTX_set_default_passwd_cb_userdata(ctx, pass_ptr); + + if (SSL_CTX_use_PrivateKey_file(ctx, private_key_file, SSL_FILETYPE_PEM) != 1) { + log_error("SSL_CTX_use_PrivateKey_file"); + bRes = false; + } + if (SSL_CTX_check_private_key(ctx) != 1) { + log_error("SSL_CTX_check_private_key"); + bRes = false; + } + } else { + bRes = !required; + } + } + + return bRes; +} + +OCSP_RESPONSE* load_ocsp(const char* filename) { + // update the cache + OCSP_RESPONSE* resp{nullptr}; + + if (filename != nullptr) { + + BIO* bio_file = BIO_new_file(filename, "r"); + if (bio_file == nullptr) { + log_error(std::string("BIO_new_file: ") + filename); + } else { + resp = d2i_OCSP_RESPONSE_bio(bio_file, nullptr); + BIO_free(bio_file); + } + + if (resp == nullptr) { + log_error("d2i_OCSP_RESPONSE_bio"); + } + } + + 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; + +struct connection_ctx { + SSL_ptr ctx; + BIO* soc_bio{nullptr}; + int soc{0}; +}; + +struct ocsp_cache_ctx { + std::map cache; +}; + +struct server_ctx { + SSL_CTX_ptr ctx; +}; + +struct client_ctx { + SSL_CTX_ptr ctx; +}; + +// ---------------------------------------------------------------------------- +// OcspCache +OcspCache::OcspCache() : m_context(std::make_unique()) { +} + +OcspCache::~OcspCache() = default; + +bool OcspCache::load(const std::vector& filenames) { + assert(m_context != nullptr); + + bool bResult{true}; + + if (filenames.empty()) { + // clear the cache + std::lock_guard lock(mux); + m_context->cache.clear(); + } else { + std::map updates; + for (const auto& entry : filenames) { + const auto& digest = std::get(entry); + const auto* filename = std::get(entry); + + OCSP_RESPONSE* resp{nullptr}; + + if (filename != nullptr) { + resp = load_ocsp(filename); + if (resp == nullptr) { + bResult = false; + } + } + + if (resp != nullptr) { + updates[digest] = std::shared_ptr(resp, &::OCSP_RESPONSE_free); + } + } + + { + std::lock_guard lock(mux); + m_context->cache.swap(updates); + } + } + + return bResult; +} + +std::shared_ptr OcspCache::lookup(const openssl::sha_256_digest_t& digest) { + assert(m_context != nullptr); + + std::shared_ptr resp; + std::lock_guard lock(mux); + if (const auto itt = m_context->cache.find(digest); itt != m_context->cache.end()) { + resp = itt->second; + } else { + log_error("OcspCache::lookup: not in cache: " + to_string(digest)); + } + + return resp; +} + +bool OcspCache::digest(openssl::sha_256_digest_t& digest, const x509_st* cert) { + assert(cert != nullptr); + + bool bResult{false}; + const ASN1_BIT_STRING* signature{nullptr}; + const X509_ALGOR* alg{nullptr}; + X509_get0_signature(&signature, &alg, cert); + if (signature != nullptr) { + unsigned char* data{nullptr}; + const auto len = i2d_ASN1_BIT_STRING(signature, &data); + if (len > 0) { + bResult = openssl::sha_256(data, len, digest); + } + OPENSSL_free(data); + } + + return bResult; +} + +// ---------------------------------------------------------------------------- +// CertificateStatusRequestV2 + +bool CertificateStatusRequestV2::set_ocsp_response(const openssl::sha_256_digest_t& digest, SSL* ctx) { + bool bResult{false}; + auto response = m_cache.lookup(digest); + if (response) { + unsigned char* der{nullptr}; + auto len = i2d_OCSP_RESPONSE(response.get(), &der); + if (len > 0) { + bResult = SSL_set_tlsext_status_ocsp_resp(ctx, der, len) == 1; + if (bResult) { + SSL_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp); + } else { + log_error((std::string("SSL_set_tlsext_status_ocsp_resp"))); + OPENSSL_free(der); + } + } + } + return bResult; +} + +int CertificateStatusRequestV2::status_request_cb(SSL* ctx, void* object) { + // returns: + // - SSL_TLSEXT_ERR_OK response to client via SSL_set_tlsext_status_ocsp_resp + // - SSL_TLSEXT_ERR_NOACK no response to client + // - SSL_TLSEXT_ERR_ALERT_FATAL abort connection + bool bSet{false}; + bool tls_1_3{false}; + int result = SSL_TLSEXT_ERR_NOACK; + openssl::sha_256_digest_t digest{}; + + if (ctx != nullptr) { + const auto* cert = SSL_get_certificate(ctx); + bSet = OcspCache::digest(digest, cert); + } + + const auto* session = SSL_get0_session(ctx); + if (session != nullptr) { + tls_1_3 = SSL_SESSION_get_protocol_version(session) == TLS1_3_VERSION; + } + + if (!tls_1_3) { + auto* connection = reinterpret_cast(SSL_get_app_data(ctx)); + if (connection != nullptr) { + /* + * if there is a status_request_v2 then don't provide a status_request response + * unless this is TLS 1.3 where status_request_v2 is deprecated (not to be used) + */ + if (connection->has_status_request_v2()) { + bSet = false; + result = SSL_TLSEXT_ERR_NOACK; + } + } + } + + auto* ptr = reinterpret_cast(object); + if (bSet && (ptr != nullptr)) { + if (ptr->set_ocsp_response(digest, ctx)) { + result = SSL_TLSEXT_ERR_OK; + } + } + return result; +} + +bool CertificateStatusRequestV2::set_ocsp_v2_response(const std::vector& digests, SSL* ctx) { + /* + * There is no response in the extension. An additional handshake message is + * sent after the certificate (certificate status) that includes the + * actual response. + */ + + /* + * s->ext.status_expected, set to 1 to include the certificate status message + * s->ext.status_type, ocsp(1), ocsp_multi(2) + * s->ext.ocsp.resp, set by SSL_set_tlsext_status_ocsp_resp + * s->ext.ocsp.resp_len, set by SSL_set_tlsext_status_ocsp_resp + */ + + bool bResult{false}; + +#ifdef OPENSSL_PATCHED + if (ctx != nullptr) { + std::vector> response_list; + std::size_t total_size{0}; + + for (const auto& digest : digests) { + auto response = m_cache.lookup(digest); + if (response) { + unsigned char* der{nullptr}; + auto len = i2d_OCSP_RESPONSE(response.get(), &der); + if (len > 0) { + const std::size_t adjusted_len = len + 3; + total_size += adjusted_len; + // prefix the length of the DER encoded OCSP response + auto* der_len = static_cast(OPENSSL_malloc(adjusted_len)); + if (der_len != nullptr) { + uint24(der_len, len); + std::memcpy(&der_len[3], der, len); + response_list.emplace_back(adjusted_len, der_len); + } + OPENSSL_free(der); + } + } + } + + // don't include the extension when there are no OCSP responses + if (total_size > 0) { + std::size_t resp_len = total_size; + auto* resp = static_cast(OPENSSL_malloc(resp_len)); + if (resp == nullptr) { + resp_len = 0; + } else { + std::size_t idx{0}; + + for (auto& entry : response_list) { + auto len = entry.first; + auto* der = entry.second; + std::memcpy(&resp[idx], der, len); + OPENSSL_free(der); + idx += len; + } + } + + // SSL_set_tlsext_status_ocsp_resp sets the correct overall length + bResult = SSL_set_tlsext_status_ocsp_resp(ctx, resp, resp_len) == 1; + if (bResult) { + SSL_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp_multi); + SSL_set_tlsext_status_expected(ctx, 1); + } else { + log_error((std::string("SSL_set_tlsext_status_ocsp_resp"))); + } + } + } +#endif // OPENSSL_PATCHED + + return bResult; +} + +int CertificateStatusRequestV2::status_request_v2_add(Ssl* ctx, unsigned int ext_type, unsigned int context, + const unsigned char** out, std::size_t* outlen, Certificate* cert, + std::size_t chainidx, int* alert, void* object) { + /* + * return values: + * - fatal, abort handshake and sent TLS Alert: result = -1 and *alert = alert value + * - do not include extension: result = 0 + * - include extension: result = 1 + */ + + *out = nullptr; + *outlen = 0; + + int result = 0; + +#ifdef OPENSSL_PATCHED + openssl::sha_256_digest_t digest{}; + std::vector digest_chain; + + if (ctx != nullptr) { + const auto* cert = SSL_get_certificate(ctx); + const auto* name = X509_get_subject_name(cert); + if (OcspCache::digest(digest, cert)) { + digest_chain.push_back(digest); + } + + STACK_OF(X509) * chain{nullptr}; + + if (SSL_get0_chain_certs(ctx, &chain) != 1) { + log_error((std::string("SSL_get0_chain_certs"))); + } else { + const auto num = sk_X509_num(chain); + for (std::size_t i = 0; i < num; i++) { + cert = sk_X509_value(chain, i); + name = X509_get_subject_name(cert); + if (OcspCache::digest(digest, cert)) { + digest_chain.push_back(digest); + } + } + } + } + + auto* ptr = reinterpret_cast(object); + if (!digest_chain.empty() && (ptr != nullptr)) { + if (ptr->set_ocsp_v2_response(digest_chain, ctx)) { + result = 1; + } + } +#endif // OPENSSL_PATCHED + + return result; +} + +void CertificateStatusRequestV2::status_request_v2_free(Ssl* ctx, unsigned int ext_type, unsigned int context, + const unsigned char* out, void* object) { + OPENSSL_free(const_cast(out)); +} + +int CertificateStatusRequestV2::status_request_v2_cb(Ssl* ctx, unsigned int ext_type, unsigned int context, + const unsigned char* data, std::size_t datalen, X509* cert, + std::size_t chainidx, int* alert, void* object) { + /* + * return values: + * - fatal, abort handshake and sent TLS Alert: result = 0 or negative and *alert = alert value + * - success: result = 1 + */ + + // TODO(james-ctc): check requested type std, or multi + return 1; +} + +int CertificateStatusRequestV2::client_hello_cb(Ssl* ctx, int* alert, void* object) { + /* + * return values: + * - fatal, abort handshake and sent TLS Alert: result = 0 or negative and *alert = alert value + * - success: result = 1 + */ + + auto* connection = reinterpret_cast(SSL_get_app_data(ctx)); + if (connection != nullptr) { + int* extensions{nullptr}; + std::size_t length{0}; + if (SSL_client_hello_get1_extensions_present(ctx, &extensions, &length) == 1) { + for (std::size_t i = 0; i < length; i++) { + if (extensions[i] == TLSEXT_TYPE_status_request) { + connection->status_request_received(); + } else if (extensions[i] == TLSEXT_TYPE_status_request_v2) { + connection->status_request_v2_received(); + } + } + OPENSSL_free(extensions); + } + } + return 1; +} + +// ---------------------------------------------------------------------------- +// Connection represents a TLS connection + +Connection::Connection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms) : + m_context(std::make_unique()), m_ip(ip_in), m_service(service_in), m_timeout_ms(timeout_ms) { + m_context->ctx = SSL_ptr(SSL_new(ctx)); + m_context->soc = soc; + + if (m_context->ctx == nullptr) { + log_error("Connection::SSL_new"); + } else { + m_context->soc_bio = BIO_new_socket(soc, BIO_CLOSE); + } + + if (m_context->soc_bio == nullptr) { + log_error("Connection::BIO_new_socket"); + } +} + +Connection::~Connection() = default; + +Connection::result_t Connection::read(std::byte* buf, std::size_t num, std::size_t& readbytes) { + assert(m_context != nullptr); + ssl_result_t result{ssl_result_t::error}; + if (m_state == state_t::connected) { + result = ssl_read(m_context->ctx.get(), buf, num, readbytes, m_timeout_ms); + switch (result) { + case ssl_result_t::success: + case ssl_result_t::timeout: + break; + case ssl_result_t::error_syscall: + m_state = state_t::fault; + break; + case ssl_result_t::closed: + shutdown(); + break; + case ssl_result_t::error: + default: + shutdown(); + m_state = state_t::fault; + break; + } + } + return convert(result); +} + +Connection::result_t Connection::write(const std::byte* buf, std::size_t num, std::size_t& writebytes) { + assert(m_context != nullptr); + ssl_result_t result{ssl_result_t::error}; + if (m_state == state_t::connected) { + result = ssl_write(m_context->ctx.get(), buf, num, writebytes, m_timeout_ms); + switch (result) { + case ssl_result_t::success: + case ssl_result_t::timeout: + break; + case ssl_result_t::error_syscall: + m_state = state_t::fault; + break; + case ssl_result_t::closed: + shutdown(); + break; + case ssl_result_t::error: + default: + shutdown(); + m_state = state_t::fault; + break; + } + } + return convert(result); +} + +void Connection::shutdown() { + assert(m_context != nullptr); + if (m_state == state_t::connected) { + ssl_shutdown(m_context->ctx.get(), c_shutdown_timeout_ms); + m_state = state_t::closed; + } +} + +int Connection::socket() const { + return m_context->soc; +} + +// ---------------------------------------------------------------------------- +// ServerConnection represents a TLS server connection + +std::uint32_t ServerConnection::m_count{0}; +std::mutex ServerConnection::m_cv_mutex; +std::condition_variable ServerConnection::m_cv; + +ServerConnection::ServerConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, + std::int32_t timeout_ms) : + Connection(ctx, soc, ip_in, service_in, timeout_ms) { + { + std::lock_guard lock(m_cv_mutex); + m_count++; + } + if (m_context->soc_bio != nullptr) { + // BIO_free is handled when SSL_free is done (SSL_ptr) + SSL_set_bio(m_context->ctx.get(), m_context->soc_bio, m_context->soc_bio); + SSL_set_accept_state(m_context->ctx.get()); + SSL_set_app_data(m_context->ctx.get(), this); + } +} + +ServerConnection::~ServerConnection() { + { + std::lock_guard lock(m_cv_mutex); + m_count--; + } + m_cv.notify_all(); +}; + +bool ServerConnection::accept() { + assert(m_context != nullptr); + ssl_result_t result{ssl_result_t::error}; + if (m_state == state_t::idle) { + result = ssl_accept(m_context->ctx.get(), m_timeout_ms); + switch (result) { + case ssl_result_t::success: + m_state = state_t::connected; + break; + case ssl_result_t::error_syscall: + m_state = state_t::fault; + break; + case ssl_result_t::closed: + shutdown(); + break; + case ssl_result_t::error: + default: + shutdown(); + m_state = state_t::fault; + break; + } + } + return result == ssl_result_t::success; +} + +void ServerConnection::wait_all_closed() { + std::unique_lock lock(m_cv_mutex); + m_cv.wait(lock, [] { return m_count == 0; }); + lock.unlock(); +} + +// ---------------------------------------------------------------------------- +// ClientConnection represents a TLS server connection + +ClientConnection::ClientConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, + std::int32_t timeout_ms) : + Connection(ctx, soc, ip_in, service_in, timeout_ms) { + if (m_context->soc_bio != nullptr) { + BIO_set_nbio(m_context->soc_bio, 1); + // BIO_free is handled when SSL_free is done (SSL_ptr) + SSL_set_bio(m_context->ctx.get(), m_context->soc_bio, m_context->soc_bio); + SSL_set_connect_state(m_context->ctx.get()); + } +} + +ClientConnection::~ClientConnection() = default; + +bool ClientConnection::connect() { + assert(m_context != nullptr); + ssl_result_t result{ssl_result_t::error}; + if (m_state == state_t::idle) { + result = ssl_connect(m_context->ctx.get(), m_timeout_ms); + switch (result) { + case ssl_result_t::success: + m_state = state_t::connected; + break; + case ssl_result_t::error_syscall: + m_state = state_t::fault; + break; + case ssl_result_t::closed: + shutdown(); + break; + case ssl_result_t::error: + default: + shutdown(); + m_state = state_t::fault; + break; + } + } + return result == ssl_result_t::success; +} + +// ---------------------------------------------------------------------------- +// TLS Server + +Server::Server() : m_context(std::make_unique()), m_status_request_v2(m_cache) { +} + +Server::~Server() { + stop(); + wait_stopped(); +} + +bool Server::init_socket(const config_t& cfg) { + bool bRes = false; + if (cfg.socket == INVALID_SOCKET) { + BIO_ADDRINFO* addrinfo{nullptr}; + + bRes = BIO_lookup_ex(cfg.host, cfg.service, BIO_LOOKUP_SERVER, AF_UNSPEC, SOCK_STREAM, IPPROTO_TCP, + &addrinfo) != 0; + + if (!bRes) { + log_error("init_socket::BIO_lookup_ex"); + } else { + const auto sock_family = BIO_ADDRINFO_family(addrinfo); + const auto sock_type = BIO_ADDRINFO_socktype(addrinfo); + const auto sock_protocol = BIO_ADDRINFO_protocol(addrinfo); + const auto* sock_address = BIO_ADDRINFO_address(addrinfo); + int sock_options{BIO_SOCK_REUSEADDR | BIO_SOCK_NONBLOCK}; + if (cfg.ipv6_only) { + sock_options = BIO_SOCK_REUSEADDR | BIO_SOCK_V6_ONLY | BIO_SOCK_NONBLOCK; + } + + m_socket = BIO_socket(sock_family, sock_type, sock_protocol, 0); + + if (m_socket == INVALID_SOCKET) { + log_error("init_socket::BIO_socket"); + } else { + bRes = BIO_listen(m_socket, sock_address, sock_options) != 0; + if (!bRes) { + log_error("init_socket::BIO_listen"); + BIO_closesocket(m_socket); + m_socket = INVALID_SOCKET; + } + } + } + + BIO_ADDRINFO_free(addrinfo); + } else { + // the code that sets cfg.socket is responsible for + // all socket initialisation + m_socket = cfg.socket; + bRes = true; + } + + return bRes; +} + +bool Server::init_ssl(const config_t& cfg) { + assert(m_context != nullptr); + + // TODO(james-ctc) TPM2 support + + 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, true); + if (bRes) { + int mode = SSL_VERIFY_NONE; + + if (cfg.verify_client) { + mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + if (SSL_CTX_load_verify_locations(ctx, cfg.verify_locations_file, cfg.verify_locations_path) != 1) { + log_error("SSL_CTX_load_verify_locations"); + } + } else { + if (SSL_CTX_set_default_verify_paths(ctx) != 1) { + log_error("SSL_CTX_set_default_verify_paths"); + bRes = false; + } + } + + SSL_CTX_set_verify(ctx, mode, nullptr); + SSL_CTX_set_client_hello_cb(ctx, &CertificateStatusRequestV2::client_hello_cb, nullptr); + + if (SSL_CTX_set_tlsext_status_cb(ctx, &CertificateStatusRequestV2::status_request_cb) != 1) { + log_error("SSL_CTX_set_tlsext_status_cb"); + bRes = false; + } + + if (SSL_CTX_set_tlsext_status_arg(ctx, &m_status_request_v2) != 1) { + log_error("SSL_CTX_set_tlsext_status_arg"); + bRes = false; + } + + constexpr int context = SSL_EXT_TLS_ONLY | SSL_EXT_TLS1_2_AND_BELOW_ONLY | SSL_EXT_IGNORE_ON_RESUMPTION | + SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO; + if (SSL_CTX_add_custom_ext(ctx, TLSEXT_TYPE_status_request_v2, context, + &CertificateStatusRequestV2::status_request_v2_add, + &CertificateStatusRequestV2::status_request_v2_free, &m_status_request_v2, + &CertificateStatusRequestV2::status_request_v2_cb, nullptr) != 1) { + log_error("SSL_CTX_add_custom_ext"); + bRes = false; + } + } + + if (!bRes) { + SSL_CTX_free(ctx); + ctx = nullptr; + } + + m_context->ctx = SSL_CTX_ptr(ctx); + return ctx != nullptr; +} + +Server::state_t Server::init(const config_t& cfg, const std::function& init_ssl) { + std::lock_guard lock(m_mutex); + m_timeout_ms = cfg.io_timeout_ms; + 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(const config_t& cfg) { + bool bRes = init_ssl(cfg); + + if (bRes) { + 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_warning(std::string("update_ocsp: ocsp files != ") + std::to_string(chain.size())); + } + } + + return bRes; +} + +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 (bRes) { + m_exit = false; + 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) { + log_error("serve::BIO_ADDR_new"); + m_exit = true; + } else { + int soc{INVALID_SOCKET}; + while ((soc < 0) && !m_exit) { + auto poll_res = wait_for(m_socket, false, m_timeout_ms); + if (poll_res == -1) { + log_error(std::string("Server::serve poll: ") + std::to_string(errno)); + m_exit = true; + } else if (poll_res == 0) { + // timeout + } else { + soc = BIO_accept_ex(m_socket, peer, BIO_SOCK_NONBLOCK); + if (BIO_sock_should_retry(soc) == 0) { + break; + } + } + }; + + 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); + } + } else { + if (soc < 0) { + log_error("serve::BIO_accept_ex"); + } else { + auto* ip = BIO_ADDR_hostname_string(peer, 1); + auto* service = BIO_ADDR_service_string(peer, 1); + auto connection = + std::make_shared(m_context->ctx.get(), soc, ip, service, m_timeout_ms); + handler(connection); + OPENSSL_free(ip); + OPENSSL_free(service); + } + } + } + + BIO_ADDR_free(peer); + } + + BIO_closesocket(m_socket); + m_socket = INVALID_SOCKET; + bRes = true; + m_state = state_t::stopped; + } + + { + std::lock_guard lock(m_cv_mutex); + m_running = false; + } + m_cv.notify_all(); + return m_state; +} + +void Server::stop() { + m_exit = true; +} + +void Server::wait_running() { + std::unique_lock lock(m_cv_mutex); + m_cv.wait(lock, [this] { return m_running; }); + lock.unlock(); +} + +void Server::wait_stopped() { + std::unique_lock lock(m_cv_mutex); + m_cv.wait(lock, [this] { return !m_running; }); + lock.unlock(); +} + +// ---------------------------------------------------------------------------- +// Client + +Client::Client() : m_context(std::make_unique()) { +} + +Client::~Client() = default; + +bool Client::print_ocsp_response(FILE* stream, const unsigned char*& response, std::size_t length) { + OCSP_RESPONSE* ocsp{nullptr}; + + if (response != nullptr) { + ocsp = d2i_OCSP_RESPONSE(nullptr, &response, static_cast(length)); + if (ocsp == nullptr) { + std::cerr << "d2i_OCSP_RESPONSE: decode error" << std::endl; + } else { + BIO* bio_out = BIO_new_fp(stream, BIO_NOCLOSE); + OCSP_RESPONSE_print(bio_out, ocsp, 0); + OCSP_RESPONSE_free(ocsp); + BIO_free(bio_out); + } + } + + return ocsp != nullptr; +} + +bool Client::init(const config_t& cfg) { + assert(m_context != nullptr); + + // TODO(james-ctc) TPM2 support + + 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, false); + if (bRes) { + int mode = SSL_VERIFY_NONE; + + if (cfg.verify_server) { + mode = SSL_VERIFY_PEER; + if (SSL_CTX_load_verify_locations(ctx, cfg.verify_locations_file, cfg.verify_locations_path) != 1) { + log_error("SSL_CTX_load_verify_locations"); + } + } else { + if (SSL_CTX_set_default_verify_paths(ctx) != 1) { + log_error("SSL_CTX_set_default_verify_paths"); + bRes = false; + } + } + + SSL_CTX_set_verify(ctx, mode, nullptr); + + if (cfg.status_request) { + if (SSL_CTX_set_tlsext_status_cb(ctx, &Client::status_request_v2_multi_cb) != 1) { + log_error("SSL_CTX_set_tlsext_status_cb"); + bRes = false; + } + if (SSL_CTX_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp) != 1) { + log_error("SSL_CTX_set_tlsext_status_type"); + bRes = false; + } + } + + if (cfg.status_request_v2) { + constexpr int context = SSL_EXT_TLS_ONLY | SSL_EXT_TLS1_2_AND_BELOW_ONLY | SSL_EXT_IGNORE_ON_RESUMPTION | + SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO; + if (SSL_CTX_add_custom_ext(ctx, TLSEXT_TYPE_status_request_v2, context, &Client::status_request_v2_add, + nullptr, nullptr, &Client::status_request_v2_cb, this) != 1) { + log_error("SSL_CTX_add_custom_ext"); + bRes = false; + } + if (SSL_CTX_set_tlsext_status_cb(ctx, &Client::status_request_v2_multi_cb) != 1) { + log_error("SSL_CTX_set_tlsext_status_cb"); + bRes = false; + } + } + + if (cfg.status_request || cfg.status_request_v2) { + if (SSL_CTX_set_tlsext_status_arg(ctx, this) != 1) { + log_error("SSL_CTX_set_tlsext_status_arg"); + bRes = false; + } + } + } + + if (bRes) { + m_context->ctx = SSL_CTX_ptr(ctx); + m_state = state_t::init; + } else { + SSL_CTX_free(ctx); + ctx = nullptr; + } + + return ctx != nullptr; +} + +std::unique_ptr Client::connect(const char* host, const char* service, bool ipv6_only) { + BIO_ADDRINFO* addrinfo{nullptr}; + std::unique_ptr result; + + const int family = (ipv6_only) ? AF_INET6 : AF_UNSPEC; + bool bRes = BIO_lookup_ex(host, service, BIO_LOOKUP_CLIENT, family, SOCK_STREAM, IPPROTO_TCP, &addrinfo) != 0; + + if (!bRes) { + log_error("connect::BIO_lookup_ex"); + } else { + const auto sock_family = BIO_ADDRINFO_family(addrinfo); + const auto sock_type = BIO_ADDRINFO_socktype(addrinfo); + const auto sock_protocol = BIO_ADDRINFO_protocol(addrinfo); + const auto* sock_address = BIO_ADDRINFO_address(addrinfo); + + // set non-blocking after a successful connection + // using BIO_SOCK_NONBLOCK on connect is problematic + // int sock_options{BIO_SOCK_NONBLOCK}; + + auto socket = BIO_socket(sock_family, sock_type, sock_protocol, 0); + + if (socket == INVALID_SOCKET) { + log_error("connect::BIO_socket"); + } else { + if (BIO_connect(socket, sock_address, 0) != 1) { + log_error("connect::BIO_connect"); + } else { + result = std::make_unique(m_context->ctx.get(), socket, host, service, m_timeout_ms); + } + } + } + + BIO_ADDRINFO_free(addrinfo); + return result; +} + +int Client::status_request_cb(Ssl* ctx) { + /* + * This callback is called when status_request or status_request_v2 extensions + * were present in the Client Hello. It doesn't mean that the extension is in + * the Server Hello SSL_get_tlsext_status_ocsp_resp() returns -1 in that case + */ + + /* + * The callback when used on the client side should return + * a negative value on error, + * 0 if the response is not acceptable (in which case the handshake will fail), or + * a positive value if it is acceptable. + */ + + int result{1}; + + const unsigned char* response{nullptr}; + const auto total_length = SSL_get_tlsext_status_ocsp_resp(ctx, &response); + // length == -1 on no response and response will be nullptr + + if ((response != nullptr) && (total_length > 0)) { + // there is a response + + if (response[0] == 0x30) { + // not a multi response + if (!print_ocsp_response(stdout, response, total_length)) { + result = 0; + } + } else { + // multiple responses + auto remaining{total_length}; + const unsigned char* ptr{response}; + + while (remaining > 0) { + bool b_okay = remaining > 3; + std::uint32_t len{0}; + + if (b_okay) { + len = uint24(ptr); + remaining -= len + 3; + b_okay = remaining >= 0; + } + + if (b_okay) { + ptr += 3; + b_okay = print_ocsp_response(stdout, ptr, len); + } + + if (!b_okay) { + result = 0; + remaining = -1; + } + } + } + } + + return result; +} + +int Client::status_request_v2_multi_cb(Ssl* ctx, void* object) { + /* + * This callback is called when status_request or status_request_v2 extensions + * were present in the Client Hello. It doesn't mean that the extension is in + * the Server Hello SSL_get_tlsext_status_ocsp_resp() returns -1 in that case + */ + + /* + * The callback when used on the client side should return + * a negative value on error, + * 0 if the response is not acceptable (in which case the handshake will fail), or + * a positive value if it is acceptable. + */ + + auto* client_ptr = reinterpret_cast(object); + + int result{1}; + if (client_ptr != nullptr) { + result = client_ptr->status_request_cb(ctx); + } else { + log_error("Client::status_request_v2_multi_cb missing Client *"); + } + return result; +} + +int Client::status_request_v2_add(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char** out, + std::size_t* outlen, Certificate* cert, std::size_t chainidx, int* alert, + void* object) { + if (context == SSL_EXT_CLIENT_HELLO) { + /* + * CertificateStatusRequestListV2: + * 0x0007 struct CertificateStatusRequestItemV2 + length + * 0x02 CertificateStatusType - OCSP multi + * 0x0004 request_length (uint 16) + * 0x0000 struct ResponderID list + length + * 0x0000 struct Extensions + length + */ + // don't use constexpr + static const std::uint8_t asn1[] = {0x00, 0x07, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00}; + *out = &asn1[0]; + *outlen = sizeof(asn1); +#ifdef OPENSSL_PATCHED + /* + * ensure client callback is called - SSL_set_tlsext_status_type() needs to have a value + * TLSEXT_STATUSTYPE_ocsp_multi for status_request_v2, or + * TLSEXT_STATUSTYPE_ocsp for status_request and status_request_v2 + */ + + if (SSL_get_tlsext_status_type(ctx) != TLSEXT_STATUSTYPE_ocsp) { + SSL_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp_multi); + } +#endif // OPENSSL_PATCHED + return 1; + } + return 0; +} + +int Client::status_request_v2_cb(SSL* ctx, unsigned int ext_type, unsigned int context, const unsigned char* data, + std::size_t datalen, X509* cert, std::size_t chainidx, int* alert, void* object) { +#ifdef OPENSSL_PATCHED + SSL_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp_multi); + SSL_set_tlsext_status_expected(ctx, 1); +#endif // OPENSSL_PATCHED + + return 1; +} + +} // namespace tls diff --git a/lib/staging/tls/tls.hpp b/lib/staging/tls/tls.hpp new file mode 100644 index 0000000000..60f27cb318 --- /dev/null +++ b/lib/staging/tls/tls.hpp @@ -0,0 +1,673 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef TLS_HPP_ +#define TLS_HPP_ + +#include "openssl_util.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +struct ocsp_response_st; +struct ssl_ctx_st; +struct ssl_st; +struct x509_st; + +namespace tls { + +constexpr int INVALID_SOCKET{-1}; + +using Certificate = struct ::x509_st; +using OcspResponse = struct ::ocsp_response_st; +using Ssl = struct ::ssl_st; +using SslContext = struct ::ssl_ctx_st; + +struct connection_ctx; +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 + +/** + * \brief cache of OCSP responses + * \note responses can be updated at any time via load() + */ +class OcspCache { +public: + using ocsp_entry_t = std::tuple; + +private: + std::unique_ptr m_context; + std::mutex mux; //!< protects the cached OCSP responses + +public: + OcspCache(); + OcspCache(const OcspCache&) = delete; + OcspCache(OcspCache&&) = delete; + OcspCache& operator=(const OcspCache&) = delete; + OcspCache& operator=(OcspCache&&) = delete; + ~OcspCache(); + + bool load(const std::vector& filenames); + std::shared_ptr lookup(const openssl::sha_256_digest_t& digest); + static bool digest(openssl::sha_256_digest_t& digest, const x509_st* cert); +}; + +// ---------------------------------------------------------------------------- +// TLS handshake extension status_request amd status_request_v2 support + +/** + * \brief TLS status_request and status_request_v2 support + */ +class CertificateStatusRequestV2 { +private: + OcspCache& m_cache; + +public: + explicit CertificateStatusRequestV2(OcspCache& cache) : m_cache(cache) { + } + CertificateStatusRequestV2() = delete; + CertificateStatusRequestV2(const CertificateStatusRequestV2&) = delete; + CertificateStatusRequestV2(CertificateStatusRequestV2&&) = delete; + CertificateStatusRequestV2& operator=(const CertificateStatusRequestV2&) = delete; + CertificateStatusRequestV2& operator=(CertificateStatusRequestV2&&) = delete; + ~CertificateStatusRequestV2() = default; + + /** + * \brief set the OCSP reponse for the SSL context + * \param[in] digest the certificate requested + * \param[in] ctx the connection context + * \return true on success + * \return for status_request extension + */ + bool set_ocsp_response(const openssl::sha_256_digest_t& digest, Ssl* ctx); + + /** + * \brief the OpenSSL callback for the status_request extension + * \param[in] ctx the connection context + * \param[in] object the instance of a CertificateStatusRequest + * \return SSL_TLSEXT_ERR_OK on success and SSL_TLSEXT_ERR_NOACK on error + */ + static int status_request_cb(Ssl* ctx, void* object); + + /** + * \brief set the OCSP reponse for the SSL context + * \param[in] digest the certificate requested + * \param[in] ctx the connection context + * \return true on success + * \return for status_request_v2 extension + */ + bool set_ocsp_v2_response(const std::vector& digests, Ssl* ctx); + + /** + * \brief add status_request_v2 extension to server hello + * \param[in] ctx the connection context + * \param[in] ext_type the TLS extension + * \param[in] context the extension context flags + * \param[in] out pointer to the extension data + * \param[in] outlen size of extension data + * \param[in] cert certificate + * \param[in] chainidx certificate chain index + * \param[in] alert the alert to send on error + * \param[in] object the instance of a CertificateStatusRequestV2 + * \return success = 1, do not include = 0, error == -1 + */ + static int status_request_v2_add(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char** out, + std::size_t* outlen, Certificate* cert, std::size_t chainidx, int* alert, + void* object); + + /** + * \brief free status_request_v2 extension added to server hello + * \param[in] ctx the connection context + * \param[in] ext_type the TLS extension + * \param[in] context the extension context flags + * \param[in] out pointer to the extension data + * \param[in] object the instance of a CertificateStatusRequestV2 + */ + static void status_request_v2_free(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char* out, + void* object); + + /** + * \brief the OpenSSL callback for the status_request_v2 extension + * \param[in] ctx the connection context + * \param[in] ext_type the TLS extension + * \param[in] context the extension context flags + * \param[in] data pointer to the extension data + * \param[in] datalen size of extension data + * \param[in] cert certificate + * \param[in] chainidx certificate chain index + * \param[in] alert the alert to send on error + * \param[in] object the instance of a CertificateStatusRequestV2 + * \return success = 1, error = zero or negative + */ + static int status_request_v2_cb(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char* data, + std::size_t datalen, Certificate* cert, std::size_t chainidx, int* alert, + void* object); + + /** + * \brief the OpenSSL callback for the client hello record + * \param[in] ctx the connection context + * \param[in] alert the alert to send on error + * \param[in] object the instance of a CertificateStatusRequestV2 + * \return success = 1, error = zero or negative + * + * This callback has early access to the extensions requested by the client. + * It is used to determine whether status_request and status_request_v2 + * have been requested so that status_request_v2 can take priority. + */ + static int client_hello_cb(Ssl* ctx, int* alert, void* object); +}; + +// ---------------------------------------------------------------------------- +// Connection represents a TLS connection + +/** + * \brief class representing a TLS connection + */ +class Connection { +public: + /** + * \brief connection state + */ + enum class state_t : std::uint8_t { + idle, //!< no connection in progress + connected, //!< active connection + closed, //!< connection has closed + fault, //!< connection has faulted + }; + + enum class result_t : std::uint8_t { + success, + error, + timeout, + }; + +protected: + std::unique_ptr m_context; + state_t m_state{state_t::idle}; + std::string m_ip; + std::string m_service; + std::int32_t m_timeout_ms; + +public: + Connection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms); + Connection() = delete; + Connection(const Connection&) = delete; + Connection(Connection&&) = delete; + Connection& operator=(const Connection&) = delete; + Connection& operator=(Connection&&) = delete; + ~Connection(); + + /** + * \brief read bytes from the TLS connection + * \param[out] buf pointer to output buffer + * \param[in] num size of output buffer + * \param[out] readBytes number of received bytes. May be less than num + * when there has been a timeout + * \return success, error, or timeout. On error the connection will have been closed + */ + [[nodiscard]] result_t read(std::byte* buf, std::size_t num, std::size_t& readbytes); + + /** + * \brief write bytes to the TLS connection + * \param[in] buf pointer to input buffer + * \param[in] num size of input buffer + * \param[out] writeBytes number of sent bytes. May be less than num + * when there has been a timeout + * \return success, error, or timeout. On error the connection will have been closed + */ + [[nodiscard]] result_t write(const std::byte* buf, std::size_t num, std::size_t& writebytes); + + /** + * \brief close the TLS connection + */ + void shutdown(); + + /** + * IP address of the connection's peer + */ + [[nodiscard]] const std::string& ip_address() const { + return m_ip; + } + + /** + * Service (port number) of the connection's peer + */ + [[nodiscard]] const std::string& service() const { + return m_service; + } + + /** + * \brief return the current state + * \return the current state + */ + [[nodiscard]] state_t state() const { + return m_state; + } + + /** + * \brief obtain the underlying socket for use with poll or select + * \returns the underlying socket or INVALID_SOCKET on error + */ + [[nodiscard]] int socket() const; +}; + +/** + * \brief class representing a TLS connection + */ +class ServerConnection : public Connection { +private: + static std::uint32_t m_count; + static std::mutex m_cv_mutex; + static std::condition_variable m_cv; + + enum class flags_t : std::uint8_t { + status_request, + status_request_v2, + last = status_request_v2, + }; + + util::AtomicEnumFlags flags; + +public: + ServerConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms); + ServerConnection() = delete; + ServerConnection(const ServerConnection&) = delete; + ServerConnection(ServerConnection&&) = delete; + ServerConnection& operator=(const ServerConnection&) = delete; + ServerConnection& operator=(ServerConnection&&) = delete; + ~ServerConnection(); + + void status_request_received() { + flags.set(flags_t::status_request); + } + void status_request_v2_received() { + flags.set(flags_t::status_request_v2); + } + [[nodiscard]] bool has_status_request() const { + return flags.is_set(flags_t::status_request); + } + [[nodiscard]] bool has_status_request_v2() const { + return flags.is_set(flags_t::status_request_v2); + } + + /** + * \brief accept the incoming connection and run the TLS handshake + * \return true when the TLS connection has been established + */ + [[nodiscard]] bool accept(); + + /** + * \brief wait for all connections to be closed + */ + static void wait_all_closed(); + + /** + * \brief return number of active connections (indicative only) + * \return number of active connections + */ + static std::uint32_t active_connections() { + return m_count; + } +}; + +/** + * \brief class representing a TLS connection + */ +class ClientConnection : public Connection { +public: + ClientConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms); + ClientConnection() = delete; + ClientConnection(const ClientConnection&) = delete; + ClientConnection(ClientConnection&&) = delete; + ClientConnection& operator=(const ClientConnection&) = delete; + ClientConnection& operator=(ClientConnection&&) = delete; + ~ClientConnection(); + + /** + * \brief run the TLS handshake + * \return true when the TLS connection has been established + */ + [[nodiscard]] bool connect(); +}; + +// ---------------------------------------------------------------------------- +// TLS server + +/** + * \brief represents a TLS server + * + * The TLS server does not make use of any threads. This is to support multiple + * use cases giving developers the option on how best to support multiple + * incoming connections. + * + * Example code in tests has some examples on how this can be done. + * + * One option is to have a server thread calling Server.serve() with the supplied + * handler creating a new connection thread when a connection arrives. + * + * For unit tests the connection handler can perform the test directly which + * has the advantage of preventing new connections from being accepted. + * + * Another option is for the connection handler to add the new connection to a list + * which is serviced by an event handler - i.e. one thread could manage all connections. + */ +class Server { +public: + /** + * \brief server state + */ + enum class state_t : std::uint8_t { + 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 { + 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}; + bool verify_client{true}; + }; + +private: + std::unique_ptr m_context; + int m_socket{INVALID_SOCKET}; + bool m_running{false}; + std::int32_t m_timeout_ms{-1}; + std::atomic_bool m_exit{false}; + 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 + * \param[in] cfg server configuration + * \return true on success + */ + bool init_socket(const config_t& cfg); + + /** + * \brief initialise TLS configuration + * \param[in] cfg server configuration + * \return true on success + */ + bool init_ssl(const config_t& cfg); + +public: + Server(); + Server(const Server&) = delete; + Server(Server&&) = delete; + Server& operator=(const Server&) = delete; + Server& operator=(Server&&) = delete; + ~Server(); + + /** + * \brief initialise the server socket and TLS configuration + * \param[in] cfg server configuration + * \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. + */ + state_t init(const config_t& cfg, const std::function& init_ssl); + + /** + * \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 and SSL config + */ + bool update(const config_t& cfg); + + /** + * \brief wait for incomming connections + * \param[in] handler called when there is a new connection + * \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 + * \note returns immediately + */ + void stop(); + + /** + * \brief wait for server to start listening for connections + * \note blocks until server is running + */ + void wait_running(); + + /** + * \brief wait for server to stop + * \note blocks until server has stopped + */ + void wait_stopped(); + + /** + * \brief return the current server state (indicative only) + * \return current server state + */ + [[nodiscard]] state_t state() const { + return m_state; + } +}; + +// ---------------------------------------------------------------------------- +// TLS client + +/** + * \brief represents a TLS client + */ +class Client { +public: + /** + * \brief client state + */ + enum class state_t : std::uint8_t { + need_init, //!< not initialised yet + init, //!< initialised but not connected + connected, //!< connected to server + stopped, //!< stopped + }; + + 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 + std::int32_t io_timeout_ms{-1}; // socket timeout in milliseconds + bool verify_server{true}; + bool status_request{false}; + bool status_request_v2{false}; + }; + +private: + std::unique_ptr m_context; + std::int32_t m_timeout_ms{-1}; + std::atomic m_state{state_t::need_init}; + +public: + Client(); + Client(const Client&) = delete; + Client(Client&&) = delete; + Client& operator=(const Client&) = delete; + Client& operator=(Client&&) = delete; + virtual ~Client(); + + static bool print_ocsp_response(FILE* stream, const unsigned char*& response, std::size_t length); + + /** + * \brief initialise the client socket and TLS configuration + * \param[in] cfg server configuration + * \return true on success + * \note when the server certificate and key change then the client needs + * to be stopped, initialised and start serving. + */ + bool init(const config_t& cfg); + + /** + * \brief connect to server + * \param[in] host host to connect to + * \param[in] service port to connect to + * \param[in] ipv6_only false - also support IPv4 + * \return a connection pointer (nullptr on error) + */ + std::unique_ptr connect(const char* host, const char* service, bool ipv6_only); + + /** + * \brief return the current server state (indicative only) + * \return current server state + */ + [[nodiscard]] state_t state() const { + return m_state; + } + + /** + * \brief the OpenSSL callback for the status_request and status_request_v2 extensions + * \param[in] ctx the connection context + * \return SSL_TLSEXT_ERR_OK on success and SSL_TLSEXT_ERR_NOACK on error + */ + virtual int status_request_cb(Ssl* ctx); + + /** + * \brief the OpenSSL callback for the status_request_v2 extension + * \param[in] ctx the connection context + * \param[in] object the instance of a CertificateStatusRequest + * \return SSL_TLSEXT_ERR_OK on success and SSL_TLSEXT_ERR_NOACK on error + */ + static int status_request_v2_multi_cb(Ssl* ctx, void* object); + + /** + * \brief add status_request_v2 extension to client hello + * \param[in] ctx the connection context + * \param[in] ext_type the TLS extension + * \param[in] context the extension context flags + * \param[in] out pointer to the extension data + * \param[in] outlen size of extension data + * \param[in] cert certificate + * \param[in] chainidx certificate chain index + * \param[in] alert the alert to send on error + * \param[in] object the instance of a CertificateStatusRequestV2 + * \return success = 1, do not include = 0, error == -1 + */ + static int status_request_v2_add(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char** out, + std::size_t* outlen, Certificate* cert, std::size_t chainidx, int* alert, + void* object); + + /** + * \brief the OpenSSL callback for the status_request_v2 extension + * \param[in] ctx the connection context + * \param[in] ext_type the TLS extension + * \param[in] context the extension context flags + * \param[in] data pointer to the extension data + * \param[in] datalen size of extension data + * \param[in] cert certificate + * \param[in] chainidx certificate chain index + * \param[in] alert the alert to send on error + * \param[in] object the instance of a CertificateStatusRequestV2 + * \return success = 1, error = zero or negative + */ + static int status_request_v2_cb(Ssl* ctx, unsigned int ext_type, unsigned int context, const unsigned char* data, + std::size_t datalen, Certificate* cert, std::size_t chainidx, int* alert, + void* object); +}; + +} // namespace tls + +#endif // TLS_HPP_ diff --git a/lib/staging/util/EnumFlags.hpp b/lib/staging/util/EnumFlags.hpp new file mode 100644 index 0000000000..af892001e2 --- /dev/null +++ b/lib/staging/util/EnumFlags.hpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef ENUMFLAGS_HPP +#define ENUMFLAGS_HPP + +#include +#include +#include + +namespace util { + +template struct AtomicEnumFlags { + static_assert(std::is_enum() == true, "Not enum"); + static_assert(std::is_integral() == true, "Not integer"); + static_assert((sizeof(B) * 8) >= static_cast(T::last) + 1, "Underlying flag type too small"); + std::atomic _value{0ULL}; + + constexpr std::size_t bit(const T& flag) const { + return 1ULL << static_cast>(flag); + } + + constexpr void set(const T& flag, bool value) { + if (value) { + set(flag); + } else { + reset(flag); + } + } + + constexpr void set(const T& flag) { + _value |= bit(flag); + } + + constexpr void reset(const T& flag) { + _value &= ~bit(flag); + } + + constexpr void reset() { + _value = 0ULL; + } + + [[nodiscard]] constexpr bool all_reset() const { + return _value == 0ULL; + } + + constexpr bool is_set(const T& flag) const { + return (_value & bit(flag)) != 0; + } + + constexpr bool is_reset(const T& flag) const { + return (_value & bit(flag)) == 0; + } +}; + +} // namespace util +#endif diff --git a/modules/API/API.cpp b/modules/API/API.cpp index fc0f65ff7a..e4821fc8f2 100644 --- a/modules/API/API.cpp +++ b/modules/API/API.cpp @@ -111,6 +111,7 @@ void SessionInfo::update_state(const types::evse_manager::SessionEventEnum event this->state = State::WaitingForEnergy; break; case Event::ChargingFinished: + case Event::PluginTimeout: case Event::StoppingCharging: case Event::TransactionFinished: this->state = State::Finished; @@ -138,7 +139,6 @@ void SessionInfo::update_state(const types::evse_manager::SessionEventEnum event break; case Event::ReplugStarted: case Event::ReplugFinished: - case Event::PluginTimeout: default: break; } @@ -525,7 +525,13 @@ void API::init() { << ", error: " << e.what(); } } - evse->call_force_unlock(connector_id); // + // match processing in ChargePointImpl::handleUnlockConnectorRequest + // so that OCPP UnlockConnector and everest_api/evse_manager/cmd/force_unlock + // perform the same action + types::evse_manager::StopTransactionRequest req; + req.reason = types::evse_manager::StopTransactionReason::UnlockCommand; + evse->call_stop_transaction(req); + evse->call_force_unlock(connector_id); }); // Check if a uk_random_delay is connected that matches this evse_manager diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 6a644589c7..98a06d1a32 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -2,6 +2,7 @@ ev_add_module(API) ev_add_module(Auth) ev_add_module(EnergyManager) ev_add_module(EnergyNode) +ev_add_module(EvManager) ev_add_module(ErrorHistory) ev_add_module(EvseManager) ev_add_module(EvseSecurity) diff --git a/modules/DummyTokenValidator/BUILD.bazel b/modules/DummyTokenValidator/BUILD.bazel new file mode 100644 index 0000000000..04637151e9 --- /dev/null +++ b/modules/DummyTokenValidator/BUILD.bazel @@ -0,0 +1,11 @@ +load("//modules:module.bzl", "cc_everest_module") + +IMPLS = [ + "main", +] + +cc_everest_module( + name = "DummyTokenValidator", + deps = [], + impls = IMPLS, +) \ No newline at end of file diff --git a/modules/EnergyNode/BUILD.bazel b/modules/EnergyNode/BUILD.bazel new file mode 100644 index 0000000000..8c36bef978 --- /dev/null +++ b/modules/EnergyNode/BUILD.bazel @@ -0,0 +1,20 @@ +load("//modules:module.bzl", "cc_everest_module") + +IMPLS = [ + "energy_grid", + "external_limits", +] + +cc_everest_module( + name = "EnergyNode", + impls = IMPLS, + deps = [ + "@sigslot//:sigslot", + ], + srcs = glob( + [ + "*.cpp", + "*.hpp", + ], + ), +) diff --git a/modules/EvManager/CMakeLists.txt b/modules/EvManager/CMakeLists.txt new file mode 100644 index 0000000000..d5aea153d0 --- /dev/null +++ b/modules/EvManager/CMakeLists.txt @@ -0,0 +1,32 @@ +# +# AUTO GENERATED - MARKED REGIONS WILL BE KEPT +# template version 3 +# + +# module setup: +# - ${MODULE_NAME}: module name +ev_setup_cpp_module() + +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 +target_sources(${MODULE_NAME} + PRIVATE + "main/car_simulatorImpl.cpp" + "main/car_simulation.cpp" + "main/simulation_command.cpp" +) +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 + +target_sources(${MODULE_NAME} + PRIVATE + "main/car_simulatorImpl.cpp" +) + +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 +# insert other things like install cmds etc here +target_compile_features(${MODULE_NAME} PRIVATE cxx_std_17) +set_target_properties(${MODULE_NAME} PROPERTIES CXX_EXTENSIONS OFF) + +if (EVEREST_CORE_BUILD_TESTING) + add_subdirectory(tests) +endif () +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/EvManager/EvManager.cpp b/modules/EvManager/EvManager.cpp new file mode 100644 index 0000000000..69769d3a96 --- /dev/null +++ b/modules/EvManager/EvManager.cpp @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "EvManager.hpp" +#include "main/car_simulatorImpl.hpp" + +namespace module { + +void EvManager::init() { + invoke_init(*p_main); +} + +void EvManager::ready() { + invoke_ready(*p_main); +} + +} // namespace module diff --git a/modules/EvManager/EvManager.hpp b/modules/EvManager/EvManager.hpp new file mode 100644 index 0000000000..41c6dc877e --- /dev/null +++ b/modules/EvManager/EvManager.hpp @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef EV_MANAGER_HPP +#define EV_MANAGER_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#include + +// headers for required interface implementations +#include +#include +#include +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf { + int connector_id; + bool auto_enable; + bool auto_exec; + bool auto_exec_infinite; + std::string auto_exec_commands; + int dc_max_current_limit; + int dc_max_power_limit; + int dc_max_voltage_limit; + int dc_energy_capacity; + int dc_target_current; + int dc_target_voltage; + bool support_sae_j2847; + int dc_discharge_max_current_limit; + int dc_discharge_max_power_limit; + int dc_discharge_target_current; + int dc_discharge_v2g_minimal_soc; + double max_current; + bool three_phases; +}; + +class EvManager : public Everest::ModuleBase { +public: + EvManager() = delete; + EvManager(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, + std::unique_ptr p_main, std::unique_ptr r_ev_board_support, + std::vector> r_ev, std::vector> r_slac, + std::vector> r_powermeter, Conf& config) : + ModuleBase(info), + mqtt(mqtt_provider), + p_main(std::move(p_main)), + r_ev_board_support(std::move(r_ev_board_support)), + r_ev(std::move(r_ev)), + r_slac(std::move(r_slac)), + r_powermeter(std::move(r_powermeter)), + config(config){}; + + Everest::MqttProvider& mqtt; + const std::unique_ptr p_main; + const std::unique_ptr r_ev_board_support; + const std::vector> r_ev; + const std::vector> r_slac; + const std::vector> r_powermeter; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + // insert your public definitions here + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + +protected: + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + // insert your protected definitions here + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + +private: + friend class LdEverest; + void init(); + void ready(); + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 + // insert your private definitions here + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 +}; + +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 + +} // namespace module + +#endif // EV_MANAGER_HPP diff --git a/modules/EvManager/doc.rst b/modules/EvManager/doc.rst new file mode 100644 index 0000000000..1a5e35b3c4 --- /dev/null +++ b/modules/EvManager/doc.rst @@ -0,0 +1,45 @@ +.. _everest_modules_handwritten_EvManager: + +===== +EvManager +===== + +This Module implements the car simulator for a charging session. + +Configuration +_____________ + +``connector_id`` + The connector id of the EVSE Manager + to which the simulator connects to. + +External MQTT +------------- + +The module listens to the following MQTT topics: + +``everest_external/nodered/{connector_id}/carsim/cmd/enable`` + | Used to enable the car simulator. + | Possible values are: + + - ``true`` + - ``false`` + +``everest_external/nodered/{connector_id}/carsim/cmd/execute_charging_session`` + | Used to execute a charging session based on the semicolon separated provided command string. + :: + + "sleep 1;iso_wait_slac_matched;iso_start_v2g_session DC_extended;iso_wait_pwr_ready;sleep 36000" + + | (For all available commands see: `Simulator Commands`_) + +``everest_external/nodered/{connector_id}/carsim/cmd/modify_charging_session`` + | Used to modify the current charging session. + | Follows the same format as ``execute_charging_session``. + +Simulator Commands +------------------ +``sleep {time in seconds}`` + | Sleeps for the specified time. + | Example: ``sleep 10`` +``test`` diff --git a/modules/EvManager/main/car_simulation.cpp b/modules/EvManager/main/car_simulation.cpp new file mode 100644 index 0000000000..a85832738e --- /dev/null +++ b/modules/EvManager/main/car_simulation.cpp @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "car_simulation.hpp" + +#include "constants.hpp" + +#include + +void CarSimulation::state_machine() { + using types::ev_board_support::EvCpState; + + const auto state_has_changed = sim_data.state != sim_data.last_state; + sim_data.last_state = sim_data.state; + + switch (sim_data.state) { + case SimState::UNPLUGGED: + if (state_has_changed) { + + r_ev_board_support->call_set_cp_state(EvCpState::A); + r_ev_board_support->call_allow_power_on(false); + // Wait for physical plugin (ev BSP sees state A on CP and not Disconnected) + + sim_data.slac_state = "UNMATCHED"; + r_ev[0]->call_stop_charging(); + } + break; + case SimState::PLUGGED_IN: + if (state_has_changed) { + r_ev_board_support->call_set_cp_state(EvCpState::B); + r_ev_board_support->call_allow_power_on(false); + } + break; + case SimState::CHARGING_REGULATED: + if (state_has_changed || sim_data.pwm_duty_cycle != sim_data.last_pwm_duty_cycle) { + sim_data.last_pwm_duty_cycle = sim_data.pwm_duty_cycle; + // do not draw power if EVSE paused by stopping PWM + if (sim_data.pwm_duty_cycle > 7.0 && sim_data.pwm_duty_cycle < 97.0) { + r_ev_board_support->call_set_cp_state(EvCpState::C); + } else { + r_ev_board_support->call_set_cp_state(EvCpState::B); + } + } + break; + case SimState::CHARGING_FIXED: + // Todo(sl): What to do here + if (state_has_changed) { + // Also draw power if EVSE stopped PWM - this is a break the rules simulator->mode to test the charging + // implementation! + r_ev_board_support->call_set_cp_state(EvCpState::C); + } + break; + + case SimState::ERROR_E: + if (state_has_changed) { + r_ev_board_support->call_set_cp_state(EvCpState::E); + r_ev_board_support->call_allow_power_on(false); + } + break; + case SimState::DIODE_FAIL: + if (state_has_changed) { + r_ev_board_support->call_diode_fail(true); + r_ev_board_support->call_allow_power_on(false); + } + break; + case SimState::ISO_POWER_READY: + if (state_has_changed) { + r_ev_board_support->call_set_cp_state(EvCpState::C); + } + break; + case SimState::ISO_CHARGING_REGULATED: + if (state_has_changed) { + r_ev_board_support->call_set_cp_state(EvCpState::C); + } + break; + case SimState::BCB_TOGGLE: + if (sim_data.bcb_toggle_C) { + r_ev_board_support->call_set_cp_state(EvCpState::C); + sim_data.bcb_toggle_C = false; + } else { + r_ev_board_support->call_set_cp_state(EvCpState::B); + sim_data.bcb_toggle_C = true; + ++sim_data.bcb_toggles; + } + break; + default: + sim_data.state = SimState::UNPLUGGED; + break; + } +}; + +bool CarSimulation::sleep(const CmdArguments& arguments, size_t loop_interval_ms) { + if (!sim_data.sleep_ticks_left.has_value()) { + const auto sleep_time = std::stold(arguments[0]); + const auto sleep_time_ms = sleep_time * 1000; + sim_data.sleep_ticks_left = static_cast(sleep_time_ms / loop_interval_ms) + 1; + } + sim_data.sleep_ticks_left = sim_data.sleep_ticks_left.value() - 1; + if (!(sim_data.sleep_ticks_left > 0)) { + sim_data.sleep_ticks_left.reset(); + return true; + } else { + return false; + } +} + +bool CarSimulation::iec_wait_pwr_ready(const CmdArguments& arguments) { + sim_data.state = SimState::PLUGGED_IN; + return (sim_data.pwm_duty_cycle > 7.0f && sim_data.pwm_duty_cycle < 97.0f); +} + +bool CarSimulation::iso_wait_pwm_is_running(const CmdArguments& arguments) { + sim_data.state = SimState::PLUGGED_IN; + return (sim_data.pwm_duty_cycle > 4.0f && sim_data.pwm_duty_cycle < 97.0f); +} + +bool CarSimulation::draw_power_regulated(const CmdArguments& arguments) { + r_ev_board_support->call_set_ac_max_current(std::stod(arguments[0])); + if (arguments[1] == constants::THREE_PHASES) { + r_ev_board_support->call_set_three_phases(true); + } else { + r_ev_board_support->call_set_three_phases(false); + } + sim_data.state = SimState::CHARGING_REGULATED; + return true; +} + +bool CarSimulation::draw_power_fixed(const CmdArguments& arguments) { + r_ev_board_support->call_set_ac_max_current(std::stod(arguments[0])); + if (arguments[1] == constants::THREE_PHASES) { + r_ev_board_support->call_set_three_phases(true); + } else { + r_ev_board_support->call_set_three_phases(false); + } + sim_data.state = SimState::CHARGING_FIXED; + return true; +} + +bool CarSimulation::pause(const CmdArguments& arguments) { + sim_data.state = SimState::PLUGGED_IN; + return true; +} + +bool CarSimulation::unplug(const CmdArguments& arguments) { + sim_data.state = SimState::UNPLUGGED; + return true; +} + +bool CarSimulation::error_e(const CmdArguments& arguments) { + sim_data.state = SimState::ERROR_E; + return true; +} + +bool CarSimulation::diode_fail(const CmdArguments& arguments) { + sim_data.state = SimState::DIODE_FAIL; + return true; +} + +bool CarSimulation::rcd_current(const CmdArguments& arguments) { + sim_data.rcd_current_ma = std::stof(arguments[0]); + r_ev_board_support->call_set_rcd_error(sim_data.rcd_current_ma); + return true; +} + +bool CarSimulation::iso_wait_slac_matched(const CmdArguments& arguments) { + sim_data.state = SimState::PLUGGED_IN; + + if (sim_data.slac_state == "UNMATCHED") { + EVLOG_debug << "Slac UNMATCHED"; + if (!r_slac.empty()) { + EVLOG_debug << "Slac trigger matching"; + r_slac[0]->call_reset(); + r_slac[0]->call_trigger_matching(); + sim_data.slac_state = "TRIGGERED"; + } + } + if (sim_data.slac_state == "MATCHED") { + EVLOG_debug << "Slac Matched"; + return true; + } + return false; +} + +bool CarSimulation::iso_wait_pwr_ready(const CmdArguments& arguments) { + if (sim_data.iso_pwr_ready) { + sim_data.state = SimState::ISO_POWER_READY; + return true; + } + return false; +} + +bool CarSimulation::iso_dc_power_on(const CmdArguments& arguments) { + sim_data.state = SimState::ISO_POWER_READY; + if (sim_data.dc_power_on) { + sim_data.state = SimState::ISO_CHARGING_REGULATED; + r_ev_board_support->call_allow_power_on(true); + return true; + } + return false; +} + +bool CarSimulation::iso_start_v2g_session(const CmdArguments& arguments, bool three_phases) { + const auto& energy_mode = arguments[0]; + + if (energy_mode == constants::AC) { + if (three_phases == false) { + r_ev[0]->call_start_charging(types::iso15118_ev::EnergyTransferMode::AC_single_phase_core); + } else { + r_ev[0]->call_start_charging(types::iso15118_ev::EnergyTransferMode::AC_three_phase_core); + } + } else if (energy_mode == constants::DC) { + r_ev[0]->call_start_charging(types::iso15118_ev::EnergyTransferMode::DC_extended); + } else { + return false; + } + return true; +} + +bool CarSimulation::iso_draw_power_regulated(const CmdArguments& arguments) { + r_ev_board_support->call_set_ac_max_current(std::stod(arguments[0])); + if (arguments[1] == constants::THREE_PHASES) { + r_ev_board_support->call_set_three_phases(true); + } else { + r_ev_board_support->call_set_three_phases(false); + } + sim_data.state = SimState::ISO_CHARGING_REGULATED; + return true; +} + +bool CarSimulation::iso_stop_charging(const CmdArguments& arguments) { + r_ev[0]->call_stop_charging(); + r_ev_board_support->call_allow_power_on(false); + sim_data.state = SimState::PLUGGED_IN; + return true; +} + +bool CarSimulation::iso_wait_for_stop(const CmdArguments& arguments, size_t loop_interval_ms) { + if (!sim_data.sleep_ticks_left.has_value()) { + sim_data.sleep_ticks_left = + std::stoll(arguments[0]) * static_cast(1 / static_cast(loop_interval_ms)) + 1; + } + sim_data.sleep_ticks_left = sim_data.sleep_ticks_left.value() - 1; + if (!sim_data.sleep_ticks_left > 0) { + r_ev[0]->call_stop_charging(); + r_ev_board_support->call_allow_power_on(false); + sim_data.state = SimState::PLUGGED_IN; + return true; + } + if (sim_data.iso_stopped) { + EVLOG_info << "POWER OFF iso stopped"; + r_ev_board_support->call_allow_power_on(false); + sim_data.state = SimState::PLUGGED_IN; + return true; + } + return false; +} + +bool CarSimulation::iso_wait_v2g_session_stopped(const CmdArguments& arguments) { + if (sim_data.v2g_finished) { + return true; + } + return false; +} + +bool CarSimulation::iso_pause_charging(const CmdArguments& arguments) { + r_ev[0]->call_pause_charging(); + sim_data.state = SimState::PLUGGED_IN; + sim_data.iso_pwr_ready = false; + return true; +} + +bool CarSimulation::iso_wait_for_resume(const CmdArguments& arguments) { + return false; +} + +bool CarSimulation::iso_start_bcb_toggle(const CmdArguments& arguments) { + sim_data.v2g_finished = false; + sim_data.state = SimState::BCB_TOGGLE; + if (sim_data.bcb_toggles >= std::stoul(arguments[0]) || sim_data.bcb_toggles == 3) { + sim_data.bcb_toggles = 0; + sim_data.state = SimState::PLUGGED_IN; + return true; + } + return false; +} + +bool CarSimulation::wait_for_real_plugin(const CmdArguments& arguments) { + using types::board_support_common::Event; + if (sim_data.actual_bsp_event == Event::A) { + EVLOG_info << "Real plugin detected"; + sim_data.state = SimState::PLUGGED_IN; + return true; + } + return false; +} diff --git a/modules/EvManager/main/car_simulation.hpp b/modules/EvManager/main/car_simulation.hpp new file mode 100644 index 0000000000..120ff3a100 --- /dev/null +++ b/modules/EvManager/main/car_simulation.hpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include "simulation_data.hpp" + +#include +#include +#include +#include + +using CmdArguments = std::vector; + +class CarSimulation { +public: + CarSimulation(const std::unique_ptr& r_ev_board_support_, + const std::vector>& r_ev_, + const std::vector>& r_slac_) : + r_ev_board_support(r_ev_board_support_), r_ev(r_ev_), r_slac(r_slac_){}; + ~CarSimulation() = default; + + void reset() { + sim_data = SimulationData(); + } + + const SimState& get_state() const { + return sim_data.state; + } + + void set_state(SimState state) { + sim_data.state = state; + } + + void set_bsp_event(types::board_support_common::Event event) { + sim_data.actual_bsp_event = event; + } + + void set_pp(types::board_support_common::Ampacity pp) { + sim_data.pp = pp; + } + + void set_rcd_current(float rcd_current) { + sim_data.rcd_current_ma = rcd_current; + } + + void set_pwm_duty_cycle(float pwm_duty_cycle) { + sim_data.pwm_duty_cycle = pwm_duty_cycle; + } + + void set_slac_state(std::string slac_state) { + sim_data.slac_state = std::move(slac_state); + } + + void set_iso_pwr_ready(bool iso_pwr_ready) { + sim_data.iso_pwr_ready = iso_pwr_ready; + } + + void set_evse_max_current(size_t evse_max_current) { + sim_data.evse_maxcurrent = evse_max_current; + } + + void set_iso_stopped(bool iso_stopped) { + sim_data.iso_stopped = iso_stopped; + } + + void set_v2g_finished(bool v2g_finished) { + sim_data.v2g_finished = v2g_finished; + } + + void set_dc_power_on(bool dc_power_on) { + sim_data.dc_power_on = dc_power_on; + } + + void state_machine(); + bool sleep(const CmdArguments&, size_t); + bool iec_wait_pwr_ready(const CmdArguments&); + bool iso_wait_pwm_is_running(const CmdArguments&); + bool draw_power_regulated(const CmdArguments&); + bool draw_power_fixed(const CmdArguments&); + bool pause(const CmdArguments&); + bool unplug(const CmdArguments&); + bool error_e(const CmdArguments&); + bool diode_fail(const CmdArguments&); + bool rcd_current(const CmdArguments&); + bool iso_wait_slac_matched(const CmdArguments&); + bool iso_wait_pwr_ready(const CmdArguments&); + bool iso_dc_power_on(const CmdArguments&); + bool iso_start_v2g_session(const CmdArguments&, bool); + bool iso_draw_power_regulated(const CmdArguments&); + bool iso_stop_charging(const CmdArguments&); + bool iso_wait_for_stop(const CmdArguments&, size_t); + bool iso_wait_v2g_session_stopped(const CmdArguments&); + bool iso_pause_charging(const CmdArguments&); + bool iso_wait_for_resume(const CmdArguments&); + bool iso_start_bcb_toggle(const CmdArguments&); + bool wait_for_real_plugin(const CmdArguments&); + +private: + SimulationData sim_data; + + const std::unique_ptr& r_ev_board_support; + const std::vector>& r_ev; + const std::vector>& r_slac; +}; diff --git a/modules/EvManager/main/car_simulatorImpl.cpp b/modules/EvManager/main/car_simulatorImpl.cpp new file mode 100644 index 0000000000..f587bc9a0b --- /dev/null +++ b/modules/EvManager/main/car_simulatorImpl.cpp @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "car_simulatorImpl.hpp" +#include "constants.hpp" +#include "simulation_command.hpp" +#include + +namespace module::main { + +void car_simulatorImpl::init() { + loop_interval_ms = constants::DEFAULT_LOOP_INTERVAL_MS; + register_all_commands(); + subscribe_to_external_mqtt(); + subscribe_to_variables_on_init(); + + std::thread(&car_simulatorImpl::run, this).detach(); +} + +void car_simulatorImpl::ready() { + + car_simulation = std::make_unique(mod->r_ev_board_support, mod->r_ev, mod->r_slac); + + setup_ev_parameters(); + + if (mod->config.auto_enable) { + auto enable_copy = mod->config.auto_enable; + handle_enable(enable_copy); + } + if (mod->config.auto_exec) { + auto value_copy = mod->config.auto_exec_commands; + handle_execute_charging_session(value_copy); + } +} + +void car_simulatorImpl::handle_enable(bool& value) { + if (enabled == value) { + // ignore if value is the same + EVLOG_warning << "Enabled value didn't change, ignoring enable!"; + return; + } + + reset_car_simulation_defaults(); + + call_ev_board_support_functions(); + + enabled = value; + + mod->r_ev_board_support->call_enable(value); + publish_enabled(value); +} + +void car_simulatorImpl::handle_execute_charging_session(std::string& value) { + if (!check_can_execute()) { + return; + } + + set_execution_active(false); + reset_car_simulation_defaults(); + + update_command_queue(value); + + const std::lock_guard lock{car_simulation_mutex}; + if (!command_queue.empty()) { + set_execution_active(true); + } +} + +void car_simulatorImpl::handle_modify_charging_session(std::string& value) { + if (!enabled) { + EVLOG_warning << "Simulation disabled, cannot execute charging simulation."; + return; + } + + set_execution_active(false); + + update_command_queue(value); + + const std::lock_guard lock{car_simulation_mutex}; + if (!command_queue.empty()) { + set_execution_active(true); + } +} + +void car_simulatorImpl::run() { + while (true) { + if (enabled == true && execution_active == true) { + + const auto finished = run_simulation_loop(); + + if (finished) { + EVLOG_info << "Finished simulation."; + set_execution_active(false); + + reset_car_simulation_defaults(); + + // If we have auto_exec_infinite configured, restart simulation when it is done + if (mod->config.auto_exec && mod->config.auto_exec_infinite) { + auto value_copy = mod->config.auto_exec_commands; + handle_execute_charging_session(value_copy); + } + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(loop_interval_ms)); + } +} + +void car_simulatorImpl::register_all_commands() { + command_registry = std::make_unique(); + + command_registry->register_command("sleep", 1, [this](const CmdArguments& arguments) { + return this->car_simulation->sleep(arguments, loop_interval_ms); + }); + command_registry->register_command("iec_wait_pwr_ready", 0, [this](const CmdArguments& arguments) { + return this->car_simulation->iec_wait_pwr_ready(arguments); + }); + command_registry->register_command("iso_wait_pwm_is_running", 0, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_wait_pwm_is_running(arguments); + }); + command_registry->register_command("draw_power_regulated", 2, [this](const CmdArguments& arguments) { + return this->car_simulation->draw_power_regulated(arguments); + }); + command_registry->register_command("draw_power_fixed", 2, [this](const CmdArguments& arguments) { + return this->car_simulation->draw_power_fixed(arguments); + }); + command_registry->register_command( + "pause", 0, [this](const CmdArguments& arguments) { return this->car_simulation->pause(arguments); }); + command_registry->register_command( + "unplug", 0, [this](const CmdArguments& arguments) { return this->car_simulation->unplug(arguments); }); + command_registry->register_command( + "error_e", 0, [this](const CmdArguments& arguments) { return this->car_simulation->error_e(arguments); }); + command_registry->register_command( + "diode_fail", 0, [this](const CmdArguments& arguments) { return this->car_simulation->diode_fail(arguments); }); + command_registry->register_command("rcd_current", 1, [this](const CmdArguments& arguments) { + return this->car_simulation->rcd_current(arguments); + }); + command_registry->register_command("iso_draw_power_regulated", 2, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_draw_power_regulated(arguments); + }); + command_registry->register_command("wait_for_real_plugin", 0, [this](const CmdArguments& arguments) { + return this->car_simulation->wait_for_real_plugin(arguments); + }); + + if (!mod->r_slac.empty()) { + command_registry->register_command("iso_wait_slac_matched", 0, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_wait_slac_matched(arguments); + }); + } + + if (!mod->r_ev.empty()) { + command_registry->register_command("iso_wait_pwr_ready", 0, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_wait_pwr_ready(arguments); + }); + command_registry->register_command("iso_dc_power_on", 0, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_dc_power_on(arguments); + }); + command_registry->register_command("iso_start_v2g_session", 1, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_start_v2g_session(arguments, mod->config.three_phases); + }); + command_registry->register_command("iso_stop_charging", 0, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_stop_charging(arguments); + }); + command_registry->register_command("iso_wait_for_stop", 1, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_wait_for_stop(arguments, loop_interval_ms); + }); + command_registry->register_command("iso_wait_v2g_session_stopped", 0, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_wait_v2g_session_stopped(arguments); + }); + command_registry->register_command("iso_pause_charging", 0, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_pause_charging(arguments); + }); + command_registry->register_command("iso_wait_for_resume", 0, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_wait_for_resume(arguments); + }); + command_registry->register_command("iso_start_bcb_toggle", 1, [this](const CmdArguments& arguments) { + return this->car_simulation->iso_start_bcb_toggle(arguments); + }); + } +} + +bool car_simulatorImpl::run_simulation_loop() { + // Execute sim commands until a command blocks, or we are finished + const std::lock_guard lock{car_simulation_mutex}; + while (execution_active && !command_queue.empty()) { + auto& current_command = command_queue.front(); + + auto command_blocked = false; + + try { + command_blocked = !current_command.execute(); + } catch (const std::exception& e) { + EVLOG_error << e.what(); + } + + if (!command_blocked) { + command_queue.pop(); + } else { + break; // command blocked, wait for timer to run this function again + } + } + + car_simulation->state_machine(); + + if (command_queue.empty()) { + return true; + } + return false; +} + +bool car_simulatorImpl::check_can_execute() { + if (!enabled) { + EVLOG_warning << "Simulation disabled, cannot execute charging simulation."; + return false; + } + if (execution_active) { + EVLOG_warning << "Execution of charging session simulation already running, cannot start new one."; + return false; + } + + return true; +} + +void car_simulatorImpl::subscribe_to_variables_on_init() { + // subscribe bsp_event + const std::lock_guard lock{car_simulation_mutex}; + using types::board_support_common::BspEvent; + mod->r_ev_board_support->subscribe_bsp_event([this](const auto& bsp_event) { + car_simulation->set_bsp_event(bsp_event.event); + if (bsp_event.event == types::board_support_common::Event::Disconnected && + car_simulation->get_state() != SimState::UNPLUGGED) { + set_execution_active(false); + car_simulation->set_state(SimState::UNPLUGGED); + } + }); + + // subscribe bsp_measurement + using types::board_support_common::BspMeasurement; + mod->r_ev_board_support->subscribe_bsp_measurement([this](const auto& measurement) { + car_simulation->set_pp(measurement.proximity_pilot.ampacity); + car_simulation->set_rcd_current(measurement.rcd_current_mA.value()); + car_simulation->set_pwm_duty_cycle(measurement.cp_pwm_duty_cycle); + }); + + // subscribe slac_state + if (!mod->r_slac.empty()) { + const auto& slac = mod->r_slac.at(0); + slac->subscribe_state([this](const auto& state) { car_simulation->set_slac_state(state); }); + } + + // subscribe ev events + if (!mod->r_ev.empty()) { + const auto& _ev = mod->r_ev.at(0); + _ev->subscribe_AC_EVPowerReady([this](auto value) { car_simulation->set_iso_pwr_ready(value); }); + _ev->subscribe_AC_EVSEMaxCurrent([this](auto value) { car_simulation->set_evse_max_current(value); }); + _ev->subscribe_AC_StopFromCharger([this]() { car_simulation->set_iso_stopped(true); }); + _ev->subscribe_V2G_Session_Finished([this]() { car_simulation->set_v2g_finished(true); }); + _ev->subscribe_DC_PowerOn([this]() { car_simulation->set_dc_power_on(true); }); + } +} + +void car_simulatorImpl::setup_ev_parameters() { + if (!mod->r_ev.empty()) { + mod->r_ev[0]->call_set_dc_params({mod->config.dc_max_current_limit, mod->config.dc_max_power_limit, + mod->config.dc_max_voltage_limit, mod->config.dc_energy_capacity, + mod->config.dc_target_current, mod->config.dc_target_voltage}); + if (mod->config.support_sae_j2847) { + mod->r_ev[0]->call_enable_sae_j2847_v2g_v2h(); + mod->r_ev[0]->call_set_bpt_dc_params( + {mod->config.dc_discharge_max_current_limit, mod->config.dc_discharge_max_power_limit, + mod->config.dc_discharge_target_current, mod->config.dc_discharge_v2g_minimal_soc}); + } + } +} + +void car_simulatorImpl::call_ev_board_support_functions() { + mod->r_ev_board_support->call_allow_power_on(false); + + mod->r_ev_board_support->call_set_ac_max_current(mod->config.max_current); + mod->r_ev_board_support->call_set_three_phases(mod->config.three_phases); +} + +void car_simulatorImpl::subscribe_to_external_mqtt() { + const auto& mqtt = mod->mqtt; + mqtt.subscribe("everest_external/nodered/" + std::to_string(mod->config.connector_id) + "/carsim/cmd/enable", + [this](const std::string& message) { + if (message == "true") { + auto enable = true; + handle_enable(enable); + } else { + auto enable = false; + handle_enable(enable); + } + }); + mqtt.subscribe("everest_external/nodered/" + std::to_string(mod->config.connector_id) + + "/carsim/cmd/execute_charging_session", + [this](const auto data) { + auto data_copy{data}; + handle_execute_charging_session(data_copy); + }); + mqtt.subscribe("everest_external/nodered/" + std::to_string(mod->config.connector_id) + + "/carsim/cmd/modify_charging_session", + [this](auto data) { + auto data_copy = data; + handle_modify_charging_session(data_copy); + }); +} + +void car_simulatorImpl::reset_car_simulation_defaults() { + const std::lock_guard lock{car_simulation_mutex}; + car_simulation->reset(); +} + +void car_simulatorImpl::update_command_queue(std::string& value) { + const std::lock_guard lock{car_simulation_mutex}; + command_queue = SimulationCommand::parse_sim_commands(value, *command_registry); +} + +void car_simulatorImpl::set_execution_active(bool value) { + execution_active = value; +} + +} // namespace module::main diff --git a/modules/EvManager/main/car_simulatorImpl.hpp b/modules/EvManager/main/car_simulatorImpl.hpp new file mode 100644 index 0000000000..01da29056d --- /dev/null +++ b/modules/EvManager/main/car_simulatorImpl.hpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef MAIN_CAR_SIMULATOR_IMPL_HPP +#define MAIN_CAR_SIMULATOR_IMPL_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 3 +// + +#include + +#include "../EvManager.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +// insert your custom include headers here +#include "car_simulation.hpp" +#include "command_registry.hpp" +#include +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 + +namespace module::main { + +struct Conf {}; + +class car_simulatorImpl : public car_simulatorImplBase { +public: + car_simulatorImpl() = delete; + car_simulatorImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : + car_simulatorImplBase(ev, "main"), mod(mod), config(config){}; + + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + // insert your public definitions here + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + +protected: + // command handler functions (virtual) + virtual void handle_enable(bool& value) override; + virtual void handle_execute_charging_session(std::string& value) override; + + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + // insert your protected definitions here + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + +private: + const Everest::PtrContainer& mod; + const Conf& config; + + virtual void init() override; + virtual void ready() override; + + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 + + void run(); + void handle_modify_charging_session(std::string& value); + bool check_can_execute(); + bool run_simulation_loop(); + void register_all_commands(); + void subscribe_to_variables_on_init(); + void setup_ev_parameters(); + void call_ev_board_support_functions(); + void subscribe_to_external_mqtt(); + void reset_car_simulation_defaults(); + void update_command_queue(std::string& value); + void set_execution_active(bool value); + + std::unique_ptr command_registry; + + std::mutex car_simulation_mutex; + std::unique_ptr car_simulation; + + bool enabled{false}; + std::atomic execution_active{false}; + size_t loop_interval_ms{}; + + std::queue command_queue; + + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 +}; + +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 +// insert other definitions here +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 + +} // namespace module::main + +#endif // MAIN_CAR_SIMULATOR_IMPL_HPP diff --git a/modules/EvManager/main/command_registry.hpp b/modules/EvManager/main/command_registry.hpp new file mode 100644 index 0000000000..ebd89c9b37 --- /dev/null +++ b/modules/EvManager/main/command_registry.hpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class RegisteredCommandBase { +public: + virtual ~RegisteredCommandBase() = default; + virtual bool operator()(const std::vector& /*arguments*/) const = 0; +}; + +class RegisteredCommand : public RegisteredCommandBase { +public: + RegisteredCommand(std::string command_, std::size_t argument_count_, + std::function)> function_) : + command_name{std::move(command_)}, argument_count(argument_count_), function{std::move(function_)} { + } + + ~RegisteredCommand() override = default; + + bool operator()(const std::vector& arguments) const override { + if (arguments.size() != argument_count) { + throw std::invalid_argument{"Invalid number of arguments for: " + command_name}; + } + return function(arguments); + } + +private: + std::string command_name; + std::size_t argument_count; + std::function)> function; +}; + +class CommandRegistry { +public: + CommandRegistry() = default; + + void register_command(std::string command_name, size_t argument_count, + const std::function)>& function) { + registered_commands.try_emplace(command_name, + std::make_unique(command_name, argument_count, function)); + } + + const RegisteredCommandBase& get_registered_command(const std::string& command_name) const { + try { + const auto& registered_command = registered_commands.at(command_name); + return *registered_command.get(); + } catch (const std::out_of_range&) { + throw std::invalid_argument{"Command not found: " + command_name}; + } + } + +private: + using RegisteredCommands = std::unordered_map>; + + RegisteredCommands registered_commands; +}; diff --git a/modules/EvManager/main/constants.hpp b/modules/EvManager/main/constants.hpp new file mode 100644 index 0000000000..e300696f7c --- /dev/null +++ b/modules/EvManager/main/constants.hpp @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#pragma once + +namespace constants { +static constexpr auto THREE_PHASES{"3"}; +static constexpr auto DEFAULT_LOOP_INTERVAL_MS{250}; +static constexpr auto AC{"ac"}; +static constexpr auto DC{"dc"}; +} // namespace constants diff --git a/modules/EvManager/main/simulation_command.cpp b/modules/EvManager/main/simulation_command.cpp new file mode 100644 index 0000000000..2b0c36c13e --- /dev/null +++ b/modules/EvManager/main/simulation_command.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "simulation_command.hpp" +#include "command_registry.hpp" +#include +#include +#include +#include +#include + +SimulationCommand::SimulationCommand(const RegisteredCommandBase* registered_command_in, + const CmdArguments& arguments_in) : + arguments{arguments_in}, registered_command{registered_command_in} { +} + +bool SimulationCommand::execute() const { + return (*registered_command)(arguments); +} + +std::queue SimulationCommand::parse_sim_commands(const std::string& value, + const CommandRegistry& command_registry) { + auto commands_vector{convert_commands_string_to_vector(value)}; + + auto commands_with_arguments{split_into_commands_with_arguments(commands_vector)}; + + return compile_commands(commands_with_arguments, command_registry); +} + +SimulationCommand::RawCommands SimulationCommand::convert_commands_string_to_vector(const std::string& commands_view) { + + auto commands = std::string{commands_view}; + + // convert to lower case inplace + std::transform(commands.begin(), commands.end(), commands.begin(), + [](const auto& character) { return std::tolower(character); }); + + // replace newlines with semicolons + std::replace(commands.begin(), commands.end(), '\n', ';'); + + // split by semicolons + std::stringstream commands_stream{commands}; + auto command = std::string{}; + auto commands_vector = std::vector{}; + + while (std::getline(commands_stream, command, ';')) { + commands_vector.push_back(command); + } + return commands_vector; +} +SimulationCommand::CommandsWithArguments +SimulationCommand::split_into_commands_with_arguments(std::vector& commands_vector) { + auto commands_with_arguments = std::vector>>{}; + + for (auto& command : commands_vector) { + commands_with_arguments.push_back(split_into_command_with_arguments(command)); + } + return commands_with_arguments; +} + +SimulationCommand::CommandWithArguments SimulationCommand::split_into_command_with_arguments(std::string& command) { + // replace commas with spaces + std::replace(command.begin(), command.end(), ',', ' '); + + // get command name and arguments + auto command_stream = std::stringstream{command}; + auto command_name = std::string{}; + auto argument = std::string{}; + auto arguments = std::vector{}; + + // get command name + std::getline(command_stream, command_name, ' '); + + // get arguments + while (std::getline(command_stream, argument, ' ')) { + arguments.push_back(argument); + } + + return {command_name, arguments}; +} +std::queue SimulationCommand::compile_commands(CommandsWithArguments& commands_with_arguments, + const CommandRegistry& command_registry) { + auto compiled_commands = std::queue{}; + + for (auto& [command, arguments] : commands_with_arguments) { + compiled_commands.emplace(&command_registry.get_registered_command(command), arguments); + } + + return compiled_commands; +} diff --git a/modules/EvManager/main/simulation_command.hpp b/modules/EvManager/main/simulation_command.hpp new file mode 100644 index 0000000000..a10f6f9220 --- /dev/null +++ b/modules/EvManager/main/simulation_command.hpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include "command_registry.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using CmdArguments = std::vector; + +class SimulationCommand { +public: + SimulationCommand(const RegisteredCommandBase* registered_command_in, const CmdArguments& arguments_in); + + bool execute() const; + + static std::queue parse_sim_commands(const std::string& value, + const CommandRegistry& command_registry); + +private: + using RawCommands = std::vector; + using CommandWithArguments = std::pair; + using CommandsWithArguments = std::vector; + + static RawCommands convert_commands_string_to_vector(const std::string& commands_view); + static CommandsWithArguments split_into_commands_with_arguments(std::vector& commands_vector); + static CommandWithArguments split_into_command_with_arguments(std::string& command); + static std::queue compile_commands(CommandsWithArguments& commands_with_arguments, + const CommandRegistry& command_registry); + std::vector arguments; + + const RegisteredCommandBase* registered_command; +}; diff --git a/modules/EvManager/main/simulation_data.hpp b/modules/EvManager/main/simulation_data.hpp new file mode 100644 index 0000000000..5f633ec372 --- /dev/null +++ b/modules/EvManager/main/simulation_data.hpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include "command_registry.hpp" +#include "simulation_command.hpp" +#include +#include +#include +#include +#include +#include + +enum class SimState { + UNPLUGGED, + PLUGGED_IN, + CHARGING_REGULATED, + CHARGING_FIXED, + ERROR_E, + DIODE_FAIL, + ISO_POWER_READY, + ISO_CHARGING_REGULATED, + BCB_TOGGLE, + UNDEFINED, +}; + +struct SimulationData { + + SimulationData() = default; + + SimState state{SimState::UNPLUGGED}; + SimState last_state{SimState::UNDEFINED}; + std::string slac_state{"UNMATCHED"}; + std::optional sleep_ticks_left{}; + + bool v2g_finished{false}; + bool iso_stopped{false}; + size_t evse_maxcurrent{0}; + size_t max_current{0}; + std::string payment{"ExternalPayment"}; + + bool iso_pwr_ready{false}; + + size_t bcb_toggles{0}; + bool bcb_toggle_C{true}; + + types::board_support_common::Ampacity pp; + float rcd_current_ma{0.0f}; + float pwm_duty_cycle{0.0f}; + float last_pwm_duty_cycle{0.0f}; + + bool dc_power_on{false}; + + types::board_support_common::Event actual_bsp_event{}; +}; diff --git a/modules/EvManager/manifest.yaml b/modules/EvManager/manifest.yaml new file mode 100644 index 0000000000..d725f30b6a --- /dev/null +++ b/modules/EvManager/manifest.yaml @@ -0,0 +1,104 @@ +description: >- + This module implements a Car simulator that can execute charging sessions using the car_simulator interface. +config: + connector_id: + description: Connector id of the evse manager to which this simulator is connected to + type: integer + auto_enable: + description: >- + Enable this simulation directly at start. Set to true for pure SIL configs, set to false for HIL. + type: boolean + default: false + auto_exec: + description: >- + Enable automatic execution of simulation commands at startup from auto_exec_commands config option. + type: boolean + default: false + auto_exec_infinite: + description: >- + If enabled the simulation commands executes infinitely. + type: boolean + default: false + auto_exec_commands: + description: >- + Simulation commands, e.g. sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + type: string + default: "" + dc_max_current_limit: + description: Maximum current allowed by the EV + type: integer + default: 300 + dc_max_power_limit: + description: Maximum power allowed by the EV + type: integer + default: 150000 + dc_max_voltage_limit: + description: Maximum voltage allowed by the EV + type: integer + default: 900 + dc_energy_capacity: + description: Energy capacity of the EV + type: integer + default: 60000 + dc_target_current: + description: Target current requested by the EV + type: integer + default: 5 + dc_target_voltage: + description: Target voltage requested by the EV + type: integer + default: 200 + support_sae_j2847: + description: Supporting SAE J2847 ISO 2 bidi version + type: boolean + default: false + dc_discharge_max_current_limit: + description: Maximum discharge current allowed by the EV + type: integer + default: 300 + dc_discharge_max_power_limit: + description: Maximum discharge power allowed by the EV + type: integer + default: 150000 + dc_discharge_target_current: + description: Discharge target current requested by the EV + type: integer + default: 5 + dc_discharge_v2g_minimal_soc: + description: Discharge minimal soc at which the evse should shutdown + type: integer + default: 20 + max_current: + description: Ac max current in Ampere + type: number + default: 16 + three_phases: + description: Support three phase + type: boolean + default: true +provides: + main: + interface: car_simulator + description: This implements the car simulator +requires: + ev_board_support: + interface: ev_board_support + ev: + interface: ISO15118_ev + min_connections: 0 + max_connections: 1 + slac: + interface: ev_slac + min_connections: 0 + max_connections: 1 + powermeter: + interface: powermeter + min_connections: 0 + max_connections: 1 +enable_external_mqtt: true +metadata: + license: https://opensource.org/licenses/Apache-2.0 + authors: + - Cornelius Claussen + - Sebastian Lukas + - Tobias Marzell diff --git a/modules/EvManager/tests/CMakeLists.txt b/modules/EvManager/tests/CMakeLists.txt new file mode 100644 index 0000000000..f5868f509a --- /dev/null +++ b/modules/EvManager/tests/CMakeLists.txt @@ -0,0 +1,36 @@ +set(TEST_TARGET_NAME ${PROJECT_NAME}_EvManager_tests) +add_executable(${TEST_TARGET_NAME}) + +add_dependencies(${TEST_TARGET_NAME} ${MODULE_NAME}) + +get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR) +target_include_directories(${TEST_TARGET_NAME} + PRIVATE + "$" +) + +target_sources(${TEST_TARGET_NAME} + PRIVATE + CommandRegistryTest.cpp + SimCommandTest.cpp + ../main/simulation_command.cpp +) + +target_compile_definitions(${TEST_TARGET_NAME} PRIVATE + BUILD_TESTING_MODULE_EV_MANAGER +) + +target_link_libraries(${TEST_TARGET_NAME} + PRIVATE + everest::framework + everest::log + Catch2::Catch2WithMain +) + +if (NOT DISABLE_EDM) + list(APPEND CMAKE_MODULE_PATH ${CPM_PACKAGE_catch2_SOURCE_DIR}/extras) + include(Catch) + catch_discover_tests(${TEST_TARGET_NAME}) +endif () + +add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME}) diff --git a/modules/EvManager/tests/CommandRegistryTest.cpp b/modules/EvManager/tests/CommandRegistryTest.cpp new file mode 100644 index 0000000000..fcdc491ace --- /dev/null +++ b/modules/EvManager/tests/CommandRegistryTest.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "../main/command_registry.hpp" +#include +#include +#include +#include + +SCENARIO("Commands can be registered", "[RegisteredCommand]") { + GIVEN("A command with 0 arguments") { + auto command_registry = CommandRegistry(); + const auto command_name = std::string{"test_command0"}; + const auto argument_count = 0; + const auto test_comand_0_function = [](const std::vector& arguments) { return arguments.empty(); }; + + WHEN("The command is registered") { + command_registry.register_command(command_name, argument_count, test_comand_0_function); + + THEN("The command can be retrieved") { + const auto& registered_command = command_registry.get_registered_command(command_name); + THEN("The command can be executed") { + CHECK(registered_command({}) == true); + } + THEN("The command throws when the number of arguments is invalid") { + CHECK(registered_command({}) == true); + CHECK_THROWS_WITH(registered_command({"arg1"}), "Invalid number of arguments for: test_command0"); + CHECK_THROWS_WITH(registered_command({"arg1", "arg2"}), + "Invalid number of arguments for: test_command0"); + CHECK_THROWS_WITH(registered_command({"arg1", "arg2", "arg3"}), + "Invalid number of arguments for: test_command0"); + } + } + } + } + + GIVEN("A command with 1 argument") { + auto command_registry = CommandRegistry(); + const auto command_name = std::string{"test_command1"}; + const auto argument_count = 1; + const auto test_comand_1_function = [](const std::vector& arguments) { + return arguments.size() == 1; + }; + + WHEN("The command is registered") { + command_registry.register_command(command_name, argument_count, test_comand_1_function); + + THEN("The command can be retrieved") { + const auto& registered_command = command_registry.get_registered_command(command_name); + THEN("The command can be executed") { + CHECK(registered_command({"arg1"}) == true); + } + THEN("The command throws when the number of arguments is invalid") { + CHECK_THROWS_WITH(registered_command({}), "Invalid number of arguments for: test_command1"); + CHECK_THROWS_WITH(registered_command({"arg1", "arg2"}), + "Invalid number of arguments for: test_command1"); + } + } + } + } + + GIVEN("A command with 2 arguments") { + auto command_registry = CommandRegistry(); + const auto command_name = std::string{"test_command2"}; + const auto argument_count = 2; + const auto test_comand_2_function = [](const std::vector& arguments) { + return arguments.size() == 2; + }; + + WHEN("The command is registered") { + command_registry.register_command(command_name, argument_count, test_comand_2_function); + + THEN("The command can be retrieved") { + const auto& registered_command = command_registry.get_registered_command(command_name); + THEN("The command can be executed") { + CHECK(registered_command({"arg1", "arg2"}) == true); + } + THEN("The command throws when the number of arguments is invalid") { + CHECK_THROWS_WITH(registered_command({}), "Invalid number of arguments for: test_command2"); + CHECK_THROWS_WITH(registered_command({"arg1"}), "Invalid number of arguments for: test_command2"); + CHECK_THROWS_WITH(registered_command({"arg1", "arg2", "arg3"}), + "Invalid number of arguments for: test_command2"); + } + } + } + } +} diff --git a/modules/EvManager/tests/SimCommandTest.cpp b/modules/EvManager/tests/SimCommandTest.cpp new file mode 100644 index 0000000000..3c79ef1173 --- /dev/null +++ b/modules/EvManager/tests/SimCommandTest.cpp @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "../main/command_registry.hpp" +#include "../main/simulation_command.hpp" +#include +#include +#include +#include + +SCENARIO("SimCommands can be created", "[SimCommand]") { + GIVEN("A command with 0 arguments called test_command is registered") { + const auto command_name = std::string{"test_command"}; + const auto argument_count = 0; + const auto test_command_function = [](const std::vector& arguments) { return arguments.empty(); }; + auto command_registry = CommandRegistry(); + command_registry.register_command(command_name, argument_count, test_command_function); + + WHEN("The SimCommand is created") { + const auto sim_command = SimulationCommand{&command_registry.get_registered_command(command_name), {}}; + + THEN("The command can be executed") { + CHECK(sim_command.execute() == true); + } + } + + WHEN("The SimCommand is created with the wrong number of arguments") { + const auto sim_command = + SimulationCommand{&command_registry.get_registered_command(command_name), {"arg1"}}; + + THEN("The command throws") { + CHECK_THROWS_WITH(sim_command.execute(), "Invalid number of arguments for: test_command"); + } + } + } +} + +SCENARIO("SimCommands can be parsed", "[SimCommand]") { + GIVEN("A few commands registered and a command string") { + auto command_registry = CommandRegistry(); + const auto command_name_a = std::string{"commanda"}; + const auto agrument_count_a = 0; + const auto command_function_a = [](const std::vector& arguments) { return arguments.empty(); }; + command_registry.register_command(command_name_a, agrument_count_a, command_function_a); + + const auto command_name_b = std::string{"commandb"}; + const auto argument_count_b = 1; + const auto command_function_b = [](const std::vector& arguments) { return arguments.size() == 1; }; + command_registry.register_command(command_name_b, argument_count_b, command_function_b); + + const auto command_name_c = std::string{"commandc"}; + const auto argument_count_c = 2; + const auto command_function_c = [](const std::vector& arguments) { return arguments.size() == 2; }; + command_registry.register_command(command_name_c, argument_count_c, command_function_c); + + WHEN("A correct command string is to be parsed") { + const auto command_string = "commanda;commandb 0;commandc abc 0.0"; + auto parsed_commands = SimulationCommand::parse_sim_commands(command_string, command_registry); + + THEN("A queue of executable SimCommands exists.") { + CHECK(parsed_commands.front().execute()); + parsed_commands.pop(); + CHECK(parsed_commands.front().execute()); + parsed_commands.pop(); + CHECK(parsed_commands.front().execute()); + parsed_commands.pop(); + CHECK(parsed_commands.empty()); + } + + THEN("A queue of executable SimCommands exists.") { + CHECK(parsed_commands.front().execute()); + parsed_commands.pop(); + CHECK(parsed_commands.front().execute()); + parsed_commands.pop(); + CHECK(parsed_commands.front().execute()); + parsed_commands.pop(); + CHECK(parsed_commands.empty()); + } + } + + WHEN("A command string with wrong arguments is to be parsed") { + const auto command_string = "commanda 1;commandb;commandc abc 0.0 def"; + auto parsed_commands = SimulationCommand::parse_sim_commands(command_string, command_registry); + + THEN("The execution of the commands should fail.") { + CHECK_THROWS_WITH(parsed_commands.front().execute(), "Invalid number of arguments for: commanda"); + parsed_commands.pop(); + CHECK_THROWS_WITH(parsed_commands.front().execute(), "Invalid number of arguments for: commandb"); + parsed_commands.pop(); + CHECK_THROWS_WITH(parsed_commands.front().execute(), "Invalid number of arguments for: commandc"); + parsed_commands.pop(); + CHECK(parsed_commands.empty()); + } + } + + WHEN("A command string with unregistered arguments is to be parsed") { + const auto command_string = "commandd;commande;commandf"; + + THEN("Parsing should fail") { + CHECK_THROWS_WITH(SimulationCommand::parse_sim_commands(command_string, command_registry), + "Command not found: commandd"); + } + } + + WHEN("An empty string is to be parsed") { + const auto command_string = ""; + const auto parsed_commands = SimulationCommand::parse_sim_commands(command_string, command_registry); + + THEN("It should create an empty queue") { + CHECK(parsed_commands.empty()); + } + } + } +} diff --git a/modules/EvseManager/Charger.cpp b/modules/EvseManager/Charger.cpp index f59dd7f6ec..875c582cb6 100644 --- a/modules/EvseManager/Charger.cpp +++ b/modules/EvseManager/Charger.cpp @@ -482,7 +482,7 @@ void Charger::run_state_machine() { // make sure we are enabling PWM if (not hlc_use_5percent_current_session) { auto m = get_max_current_internal(); - update_pwm_now_if_changed(ampere_to_duty_cycle(m)); + update_pwm_now_if_changed_ampere(m); } else { update_pwm_now_if_changed(PWM_5_PERCENT); } @@ -513,6 +513,13 @@ void Charger::run_state_machine() { internal_context.t_step_EF_return_pwm = ampere_to_duty_cycle(get_max_current_internal()); shared_context.current_state = EvseState::T_step_EF; } + + // We are still here after the wakeup plus some extra delay, so probably the EV really does not + // want to charge. Switch to ChargingPausedEV state. + if (not shared_context.hlc_charging_active and shared_context.legacy_wakeup_done and + time_in_current_state > PREPARING_TIMEOUT_PAUSED_BY_EV) { + shared_context.current_state = EvseState::ChargingPausedEV; + } } } } @@ -562,12 +569,12 @@ void Charger::run_state_machine() { if (hlc_use_5percent_current_session) { update_pwm_now_if_changed(PWM_5_PERCENT); } else { - update_pwm_now_if_changed(ampere_to_duty_cycle(get_max_current_internal())); + update_pwm_now_if_changed_ampere(get_max_current_internal()); } } else { // update PWM if it has changed and 5 seconds have passed since last update if (not hlc_use_5percent_current_session) { - update_pwm_max_every_5seconds(ampere_to_duty_cycle(get_max_current_internal())); + update_pwm_max_every_5seconds_ampere(get_max_current_internal()); } } } @@ -636,7 +643,7 @@ void Charger::run_state_machine() { } else { // update PWM if it has changed and 5 seconds have passed since last update if (not errors_prevent_charging_internal()) { - update_pwm_max_every_5seconds(ampere_to_duty_cycle(get_max_current_internal())); + update_pwm_max_every_5seconds_ampere(get_max_current_internal()); } } } @@ -880,13 +887,15 @@ void Charger::process_cp_events_independent(CPEvent cp_event) { } } -void Charger::update_pwm_max_every_5seconds(float dc) { +void Charger::update_pwm_max_every_5seconds_ampere(float ampere) { + float dc = ampere_to_duty_cycle(ampere); if (dc not_eq internal_context.update_pwm_last_dc) { auto now = std::chrono::steady_clock::now(); - auto timeSinceLastUpdate = + auto time_since_last_update = std::chrono::duration_cast(now - internal_context.last_pwm_update).count(); - if (timeSinceLastUpdate >= 5000) { + if (time_since_last_update >= IEC_PWM_MAX_UPDATE_INTERVAL) { update_pwm_now(dc); + internal_context.pwm_set_last_ampere = ampere; } } } @@ -912,10 +921,19 @@ void Charger::update_pwm_now_if_changed(float dc) { } } +void Charger::update_pwm_now_if_changed_ampere(float ampere) { + float dc = ampere_to_duty_cycle(ampere); + if (internal_context.update_pwm_last_dc not_eq dc) { + update_pwm_now(dc); + internal_context.pwm_set_last_ampere = ampere; + } +} + void Charger::pwm_off() { session_log.evse(false, "Set PWM Off"); shared_context.pwm_running = false; internal_context.update_pwm_last_dc = 1.; + internal_context.pwm_set_last_ampere = 0.; bsp->set_pwm_off(); } @@ -923,6 +941,7 @@ void Charger::pwm_F() { session_log.evse(false, "Set PWM F"); shared_context.pwm_running = false; internal_context.update_pwm_last_dc = 0.; + internal_context.pwm_set_last_ampere = 0.; bsp->set_pwm_F(); } @@ -1479,6 +1498,16 @@ float Charger::get_max_current_internal() { return maxc; } +float Charger::get_max_current_signalled_to_ev_internal() { + // For basic charging, the max current signalled to the EV may be different from the actual current limit + // for up to 5 seconds as the PWM may only be updated every 5 seconds according to IEC61851-1. + if (not shared_context.hlc_charging_active) { + return internal_context.pwm_set_last_ampere; + } else { + return get_max_current_internal(); + } +} + void Charger::set_current_drawn_by_vehicle(float l1, float l2, float l3) { Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_set_current_drawn_by_vehicle); @@ -1488,8 +1517,9 @@ void Charger::set_current_drawn_by_vehicle(float l1, float l2, float l3) { } void Charger::check_soft_over_current() { + // Allow some tolerance - float limit = (get_max_current_internal() + soft_over_current_measurement_noise_A) * + float limit = (get_max_current_signalled_to_ev_internal() + soft_over_current_measurement_noise_A) * (1. + soft_over_current_tolerance_percent / 100.); if (shared_context.current_drawn_by_vehicle[0] > limit or shared_context.current_drawn_by_vehicle[1] > limit or @@ -1508,9 +1538,9 @@ void Charger::check_soft_over_current() { internal_context.over_current = false; } auto now = std::chrono::steady_clock::now(); - auto timeSinceOverCurrentStarted = + auto time_since_over_current_started = std::chrono::duration_cast(now - internal_context.last_over_current_event).count(); - if (internal_context.over_current and timeSinceOverCurrentStarted >= SOFT_OVER_CURRENT_TIMEOUT) { + if (internal_context.over_current and time_since_over_current_started >= SOFT_OVER_CURRENT_TIMEOUT) { auto errstr = fmt::format("Soft overcurrent event (L1:{}, L2:{}, L3:{}, limit {}) triggered", shared_context.current_drawn_by_vehicle[0], shared_context.current_drawn_by_vehicle[1], diff --git a/modules/EvseManager/Charger.hpp b/modules/EvseManager/Charger.hpp index 20f24c6357..3068f8bb10 100644 --- a/modules/EvseManager/Charger.hpp +++ b/modules/EvseManager/Charger.hpp @@ -95,6 +95,7 @@ class Charger { // external input to charger: update max_current and new validUntil bool set_max_current(float ampere, std::chrono::time_point validUntil); float get_max_current(); + sigslot::signal signal_max_current; void setup(bool three_phases, bool has_ventilation, const std::string& country_code, const ChargeMode charge_mode, @@ -206,6 +207,7 @@ class Charger { bool errors_prevent_charging_internal(); float get_max_current_internal(); + float get_max_current_signalled_to_ev_internal(); bool deauthorize_internal(); bool pause_charging_wait_for_power_internal(); @@ -218,7 +220,8 @@ class Charger { void update_pwm_now(float dc); void update_pwm_now_if_changed(float dc); - void update_pwm_max_every_5seconds(float dc); + void update_pwm_now_if_changed_ampere(float dc); + void update_pwm_max_every_5seconds_ampere(float dc); void pwm_off(); void pwm_F(); @@ -327,6 +330,7 @@ class Charger { bool pp_warning_printed{false}; bool no_energy_warning_printed{false}; + float pwm_set_last_ampere{0}; } internal_context; // main Charger thread @@ -350,6 +354,7 @@ class Charger { // constants static constexpr float CHARGER_ABSOLUTE_MAX_CURRENT{1000.}; constexpr static int LEGACY_WAKEUP_TIMEOUT{30000}; + constexpr static int PREPARING_TIMEOUT_PAUSED_BY_EV{10000}; // valid Length of BCB toggles static constexpr auto TP_EV_VALD_STATE_DURATION_MIN = std::chrono::milliseconds(200 - 50); // We give 50 msecs tolerance to the norm values (table 3 ISO15118-3) @@ -367,6 +372,7 @@ class Charger { // 4 seconds according to table 3 of ISO15118-3 static constexpr int T_STEP_EF = 4000; static constexpr int SOFT_OVER_CURRENT_TIMEOUT = 7000; + static constexpr int IEC_PWM_MAX_UPDATE_INTERVAL = 5000; types::evse_manager::EnableDisableSource active_enable_disable_source{ types::evse_manager::Enable_source::Unspecified, types::evse_manager::Enable_state::Unassigned, 10000}; diff --git a/modules/EvseManager/EvseManager.cpp b/modules/EvseManager/EvseManager.cpp index 00a8e54114..506d33a099 100644 --- a/modules/EvseManager/EvseManager.cpp +++ b/modules/EvseManager/EvseManager.cpp @@ -677,6 +677,12 @@ void EvseManager::ready() { latest_powermeter_data_billing = p; } + { + std::scoped_lock lk(powermeter_mutex); + initial_powermeter_value_received = true; + } + powermeter_cv.notify_one(); + // External Nodered interface if (p.phase_seq_error) { mqtt.publish(fmt::format("everest_external/nodered/{}/powermeter/phaseSeqError", config.connector_id), @@ -893,6 +899,13 @@ void EvseManager::ready() { } }); + { + // wait for first powermeter value + std::unique_lock lk(powermeter_mutex); + this->powermeter_cv.wait_for(lk, std::chrono::milliseconds(this->config.initial_meter_value_timeout_ms), + [this] { return initial_powermeter_value_received; }); + } + // start with a limit of 0 amps. We will get a budget from EnergyManager that is locally limited by hw // caps. charger->set_max_current(0.0F, date::utc_clock::now() + std::chrono::seconds(120)); @@ -919,6 +932,9 @@ void EvseManager::ready_to_start_charging() { this->p_evse->publish_ready(true); EVLOG_info << fmt::format(fmt::emphasis::bold | fg(fmt::terminal_color::green), "🌀🌀🌀 Ready to start charging 🌀🌀🌀"); + if (!initial_powermeter_value_received) { + EVLOG_warning << "No powermeter value received yet!"; + } } types::powermeter::Powermeter EvseManager::get_latest_powermeter_data_billing() { diff --git a/modules/EvseManager/EvseManager.hpp b/modules/EvseManager/EvseManager.hpp index a0b6c057ee..0f9dd3d20c 100644 --- a/modules/EvseManager/EvseManager.hpp +++ b/modules/EvseManager/EvseManager.hpp @@ -94,6 +94,7 @@ struct Conf { bool uk_smartcharging_random_delay_enable; int uk_smartcharging_random_delay_max_duration; bool uk_smartcharging_random_delay_at_any_change; + int initial_meter_value_timeout_ms; }; class EvseManager : public Everest::ModuleBase { @@ -324,6 +325,9 @@ class EvseManager : public Everest::ModuleBase { static constexpr int CABLECHECK_SELFTEST_TIMEOUT{30}; std::atomic_bool current_demand_active{false}; + std::mutex powermeter_mutex; + std::condition_variable powermeter_cv; + bool initial_powermeter_value_received{false}; // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 }; diff --git a/modules/EvseManager/manifest.yaml b/modules/EvseManager/manifest.yaml index 57cdc31114..7fe2e0c180 100644 --- a/modules/EvseManager/manifest.yaml +++ b/modules/EvseManager/manifest.yaml @@ -230,6 +230,12 @@ config: "True: use random delays on any current change during charging. False: Only use if current is reduced to zero or increased from zero." type: boolean default: true + initial_meter_value_timeout_ms: + description: >- + This timeout in ms defines for how long the EvseManager waits for an initial meter value from a powermeter before it becomes ready to start charging. + If configured to 0, the EvseManager will not wait for an initial meter value before it becomes ready to start charging. + type: integer + default: 5000 provides: evse: interface: evse_manager diff --git a/modules/EvseV2G/CMakeLists.txt b/modules/EvseV2G/CMakeLists.txt index f2756c8303..da6f883fc6 100644 --- a/modules/EvseV2G/CMakeLists.txt +++ b/modules/EvseV2G/CMakeLists.txt @@ -9,6 +9,13 @@ ev_setup_cpp_module() # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 # insert your custom targets and additional config variables here +option(USING_MBED_TLS "Use MbedTLS for V2G" OFF) + +if(USING_MBED_TLS) +target_compile_definitions(${MODULE_NAME} PRIVATE + EVEREST_MBED_TLS +) +endif() # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 target_sources(${MODULE_NAME} @@ -23,13 +30,15 @@ find_package(PkgConfig REQUIRED) # search for libevent.pc pkg_search_module(EVENT REQUIRED libevent) +target_include_directories(${MODULE_NAME} PRIVATE + crypto + connection +) + target_link_libraries(${MODULE_NAME} PUBLIC ${EVENT_LIBRARIES} -levent -lpthread -levent_pthreads) target_link_libraries(${MODULE_NAME} PRIVATE - mbedcrypto - mbedtls - mbedx509 cbv2g::din cbv2g::iso2 cbv2g::tp @@ -37,13 +46,44 @@ target_link_libraries(${MODULE_NAME} target_sources(${MODULE_NAME} PRIVATE - "v2g_ctx.cpp" + "connection/connection.cpp" + "iso_server.cpp" + "din_server.cpp" "log.cpp" "sdp.cpp" - "connection.cpp" "tools.cpp" + "v2g_ctx.cpp" "v2g_server.cpp" - "iso_server.cpp" - "din_server.cpp" ) + +if(USING_MBED_TLS) +# needed for header file enum definition +target_include_directories(${MODULE_NAME} PRIVATE + ../../lib/staging/tls +) +target_link_libraries(${MODULE_NAME} + PRIVATE + mbedcrypto + mbedtls + mbedx509 +) +target_sources(${MODULE_NAME} + PRIVATE + "crypto/crypto_mbedtls.cpp" +) +else() +target_link_libraries(${MODULE_NAME} + PRIVATE + everest::tls +) +target_sources(${MODULE_NAME} + PRIVATE + "crypto/crypto_openssl.cpp" + "connection/tls_connection.cpp" +) +endif() + +if(EVEREST_CORE_BUILD_TESTING) + add_subdirectory(tests) +endif() # ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/EvseV2G/EvseV2G.cpp b/modules/EvseV2G/EvseV2G.cpp index 98c6a35f41..67e4b77b12 100644 --- a/modules/EvseV2G/EvseV2G.cpp +++ b/modules/EvseV2G/EvseV2G.cpp @@ -3,22 +3,51 @@ // Copyright (C) 2022-2023 Contributors to EVerest #include "EvseV2G.hpp" #include "connection.hpp" +#include "everest/logging.hpp" #include "log.hpp" #include "sdp.hpp" -struct v2g_context* v2g_ctx = NULL; +#ifndef EVEREST_MBED_TLS +#include +namespace { +void log_handler(openssl::log_level_t level, const std::string& str) { + switch (level) { + case openssl::log_level_t::debug: + // ignore debug logs + break; + case openssl::log_level_t::warning: + EVLOG_warning << str; + break; + case openssl::log_level_t::error: + default: + EVLOG_error << str; + break; + } +} +} // namespace +#endif // EVEREST_MBED_TLS + +struct v2g_context* v2g_ctx = nullptr; namespace module { void EvseV2G::init() { - int rv = 0; /* create v2g context */ v2g_ctx = v2g_ctx_create(&(*p_charger), &(*r_security)); - if (v2g_ctx == NULL) + if (v2g_ctx == nullptr) return; +#ifndef EVEREST_MBED_TLS + (void)openssl::set_log_handler(log_handler); + v2g_ctx->tls_server = &tls_server; +#endif // EVEREST_MBED_TLS + invoke_init(*p_charger); +} + +void EvseV2G::ready() { + int rv = 0; dlog(DLOG_LEVEL_DEBUG, "Starting SDP responder"); @@ -42,14 +71,6 @@ void EvseV2G::init() { goto err_out; } - return; -err_out: - v2g_ctx_free(v2g_ctx); -} - -void EvseV2G::ready() { - int rv = 0; - invoke_ready(*p_charger); rv = sdp_listen(v2g_ctx); @@ -59,6 +80,8 @@ void EvseV2G::ready() { goto err_out; } + return; + err_out: v2g_ctx_free(v2g_ctx); } diff --git a/modules/EvseV2G/EvseV2G.hpp b/modules/EvseV2G/EvseV2G.hpp index 7835f8bb05..851dd5d6ef 100644 --- a/modules/EvseV2G/EvseV2G.hpp +++ b/modules/EvseV2G/EvseV2G.hpp @@ -19,6 +19,9 @@ // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 // insert your custom include headers here #include "v2g_ctx.hpp" +#ifndef EVEREST_MBED_TLS +#include +#endif // EVEREST_MBED_TLS // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 namespace module { @@ -70,6 +73,9 @@ class EvseV2G : public Everest::ModuleBase { // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 // insert your private definitions here +#ifndef EVEREST_MBED_TLS + tls::Server tls_server; +#endif // EVEREST_MBED_TLS // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 }; diff --git a/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp b/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp index 20cf7b89ad..aebd98e028 100644 --- a/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp +++ b/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp @@ -10,7 +10,7 @@ #include -#include "../EvseV2G.hpp" +#include "EvseV2G.hpp" // ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 #include "v2g.hpp" diff --git a/modules/EvseV2G/connection.cpp b/modules/EvseV2G/connection/connection.cpp similarity index 95% rename from modules/EvseV2G/connection.cpp rename to modules/EvseV2G/connection/connection.cpp index 9a0d5b9533..ca110a5d54 100644 --- a/modules/EvseV2G/connection.cpp +++ b/modules/EvseV2G/connection/connection.cpp @@ -4,21 +4,18 @@ #include "connection.hpp" #include "log.hpp" +#include "tls_connection.hpp" #include "tools.hpp" #include "v2g_server.hpp" #include +#include #include #include #include #include #include #include -#include -#include -#include -#include -#include #include #include #include @@ -29,15 +26,20 @@ #include #include -#ifndef SYSCONFDIR -#define SYSCONFDIR "/etc" -#endif +#ifdef EVEREST_MBED_TLS +#include +#include +#include +#include +#include +#endif // EVEREST_MBED_TLS #define DEFAULT_SOCKET_BACKLOG 3 #define DEFAULT_TCP_PORT 61341 #define DEFAULT_TLS_PORT 64109 #define ERROR_SESSION_ALREADY_STARTED 2 +#ifdef EVEREST_MBED_TLS #define MBEDTLS_DEBUG_LEVEL_VERBOSE 4 #define MBEDTLS_DEBUG_LEVEL_NO_DEBUG 0 @@ -69,6 +71,7 @@ static const int v2g_ssl_allowed_hashes[] = { MBEDTLS_MD_SHA256, MBEDTLS_MD_SHA224, #endif MBEDTLS_MD_SHA1, MBEDTLS_MD_NONE}; +#endif // EVEREST_MBED_TLS /*! * \brief connection_create_socket This function creates a tcp/tls socket @@ -103,6 +106,8 @@ static int connection_create_socket(struct sockaddr_in6* sockaddr) { if (bind(s, reinterpret_cast(sockaddr), addrlen) == -1) { if (!error_once) { dlog(DLOG_LEVEL_WARNING, "bind() failed: %s", strerror(errno)); + dlog(DLOG_LEVEL_WARNING, + "Verify that the configured interface has a valid IPv6 link local address configured."); error_once = true; } close(s); @@ -132,7 +137,8 @@ static int connection_create_socket(struct sockaddr_in6* sockaddr) { return s; } -static int connection_ssl_initialize(void) { +static int connection_ssl_initialize() { +#ifdef EVEREST_MBED_TLS unsigned char random_data[64]; int rv; @@ -166,6 +172,7 @@ static int connection_ssl_initialize(void) { #if defined(MBEDTLS_SSL_CACHE_C) mbedtls_ssl_cache_init(&cache); #endif +#endif // EVEREST_MBED_TLS return 0; } @@ -178,7 +185,8 @@ static int connection_ssl_initialize(void) { */ int check_interface(struct v2g_context* v2g_ctx) { - struct ipv6_mreq mreq = {{0}, 0}; + struct ipv6_mreq mreq = {}; + std::memset(&mreq, 0, sizeof(mreq)); if (strcmp(v2g_ctx->if_name, "auto") == 0) { v2g_ctx->if_name = choose_first_ipv6_interface(); @@ -190,7 +198,7 @@ int check_interface(struct v2g_context* v2g_ctx) { return -1; } - return (v2g_ctx->if_name == NULL) ? -1 : 0; + return (v2g_ctx->if_name == nullptr) ? -1 : 0; } /*! @@ -205,7 +213,7 @@ int connection_init(struct v2g_context* v2g_ctx) { if (v2g_ctx->tls_security != TLS_SECURITY_FORCE) { v2g_ctx->local_tcp_addr = static_cast(calloc(1, sizeof(*v2g_ctx->local_tcp_addr))); - if (v2g_ctx->local_tcp_addr == NULL) { + if (v2g_ctx->local_tcp_addr == nullptr) { dlog(DLOG_LEVEL_ERROR, "Failed to allocate memory for TCP address"); return -1; } @@ -252,7 +260,7 @@ int connection_init(struct v2g_context* v2g_ctx) { sleep(1); continue; } - if (inet_ntop(AF_INET6, &v2g_ctx->local_tcp_addr->sin6_addr, buffer, sizeof(buffer)) != NULL) { + if (inet_ntop(AF_INET6, &v2g_ctx->local_tcp_addr->sin6_addr, buffer, sizeof(buffer)) != nullptr) { dlog(DLOG_LEVEL_INFO, "TCP server on %s is listening on port [%s%%%" PRIu32 "]:%" PRIu16, v2g_ctx->if_name, buffer, v2g_ctx->local_tcp_addr->sin6_scope_id, ntohs(v2g_ctx->local_tcp_addr->sin6_port)); @@ -280,7 +288,7 @@ int connection_init(struct v2g_context* v2g_ctx) { continue; } - if (inet_ntop(AF_INET6, &v2g_ctx->local_tls_addr->sin6_addr, buffer, sizeof(buffer)) != NULL) { + if (inet_ntop(AF_INET6, &v2g_ctx->local_tls_addr->sin6_addr, buffer, sizeof(buffer)) != nullptr) { dlog(DLOG_LEVEL_INFO, "TLS server on %s is listening on port [%s%%%" PRIu32 "]:%" PRIu16, v2g_ctx->if_name, buffer, v2g_ctx->local_tls_addr->sin6_scope_id, ntohs(v2g_ctx->local_tls_addr->sin6_port)); @@ -293,6 +301,12 @@ int connection_init(struct v2g_context* v2g_ctx) { /* Sockets should be ready, leave the loop */ break; } + +#ifndef EVEREST_MBED_TLS + if (v2g_ctx->local_tls_addr) { + return tls::connection_init(v2g_ctx); + } +#endif // EVEREST_MBED_TLS return 0; } @@ -302,7 +316,7 @@ int connection_init(struct v2g_context* v2g_ctx) { * \param ctx is the V2G context. * \return Returns \c true if a timeout has occured, otherwise \c false */ -static bool is_sequence_timeout(struct timespec ts_start, struct v2g_context* ctx) { +bool is_sequence_timeout(struct timespec ts_start, struct v2g_context* ctx) { struct timespec ts_current; int sequence_timeout = V2G_SEQUENCE_TIMEOUT_60S; @@ -338,6 +352,7 @@ ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t int num_of_bytes; if (conn->is_tls_connection) { +#ifdef EVEREST_MBED_TLS num_of_bytes = mbedtls_ssl_read(&conn->conn.ssl.ssl_context, &buf[bytes_read], count - bytes_read); if (num_of_bytes == MBEDTLS_ERR_SSL_WANT_READ || num_of_bytes == MBEDTLS_ERR_SSL_WANT_WRITE || @@ -354,6 +369,10 @@ ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t return -1; } +#else + dlog(DLOG_LEVEL_ERROR, "mbedtls_ssl_read() not configured"); + return -1; +#endif // EVEREST_MBED_TLS } else { /* use select for timeout handling */ struct timeval tv; @@ -365,7 +384,7 @@ ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t tv.tv_sec = conn->ctx->network_read_timeout / 1000; tv.tv_usec = (conn->ctx->network_read_timeout % 1000) * 1000; - num_of_bytes = select(conn->conn.socket_fd + 1, &read_fds, NULL, NULL, &tv); + num_of_bytes = select(conn->conn.socket_fd + 1, &read_fds, nullptr, nullptr, &tv); if (num_of_bytes == -1) { if (errno == EINTR) @@ -420,6 +439,7 @@ ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, size_t int num_of_bytes; if (conn->is_tls_connection) { +#ifdef EVEREST_MBED_TLS num_of_bytes = mbedtls_ssl_write(&conn->conn.ssl.ssl_context, &buf[bytes_written], count - bytes_written); if (num_of_bytes == MBEDTLS_ERR_SSL_WANT_READ || num_of_bytes == MBEDTLS_ERR_SSL_WANT_WRITE) @@ -427,7 +447,10 @@ ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, size_t if (num_of_bytes < 0) return -1; - +#else + dlog(DLOG_LEVEL_ERROR, "mbedtls_ssl_write() not configured"); + return -1; // shouldn't be using this function +#endif // EVEREST_MBED_TLS } else { num_of_bytes = (int)write(conn->conn.socket_fd, &buf[bytes_written], count - bytes_written); @@ -453,7 +476,7 @@ ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, size_t * \brief connection_teardown This function must be called on connection teardown. * \param conn is the V2G connection context */ -static void connection_teardown(struct v2g_connection* conn) { +void connection_teardown(struct v2g_connection* conn) { if (conn->ctx->session.is_charging == true) { conn->ctx->p_charger->publish_currentDemand_Finished(nullptr); @@ -468,7 +491,7 @@ static void connection_teardown(struct v2g_connection* conn) { v2g_ctx_init_charging_session(conn->ctx, true); /* stop timer */ - stop_timer(&conn->ctx->com_setup_timeout, NULL, conn->ctx); + stop_timer(&conn->ctx->com_setup_timeout, nullptr, conn->ctx); /* print dlink status */ switch (conn->dlink_action) { @@ -486,6 +509,7 @@ static void connection_teardown(struct v2g_connection* conn) { } } +#ifdef EVEREST_MBED_TLS static bool connection_init_tls(struct v2g_context* ctx) { int rv; @@ -518,7 +542,7 @@ static bool connection_init_tls(struct v2g_context* ctx) { mbedtls_pk_init(&ctx->evse_tls_crt_key[idx]); } - if (ctx->evseTlsCrt == NULL || ctx->evse_tls_crt_key == NULL) { + if (ctx->evseTlsCrt == nullptr || ctx->evse_tls_crt_key == nullptr) { dlog(DLOG_LEVEL_ERROR, "Failed to allocate memory!"); goto error_out; } @@ -671,6 +695,7 @@ static void ssl_key_log_debug_callback(void* ACtx, int ALevel, const char* AFile send_udp_message(); ctx->udp_buffer = {}; } +#endif // EVEREST_MBED_TLS /** * This is the 'main' function of a thread, which handles a TCP connection. @@ -717,13 +742,14 @@ static void* connection_handle_tcp(void* data) { free(conn); - return NULL; + return nullptr; } /** * This is the 'main' function of a thread, which handles a TLS connection. */ static void* connection_handle_tls(void* data) { +#ifdef EVEREST_MBED_TLS struct v2g_connection* conn = static_cast(data); struct v2g_context* v2g_ctx = conn->ctx; mbedtls_ssl_config* ssl_config = conn->conn.ssl.ssl_config; @@ -903,8 +929,9 @@ static void* connection_handle_tls(void* data) { connection_teardown(conn); free(conn); +#endif // EVEREST_MBED_TLS - return NULL; + return nullptr; } static void* connection_server(void* data) { @@ -937,14 +964,21 @@ static void* connection_server(void* data) { /* setup common stuff */ conn->ctx = ctx; + conn->read = &connection_read; + conn->write = &connection_write; /* if this thread is the TLS thread, then connections are TLS secured; * return code is non-zero if equal so align it */ +#ifdef EVEREST_MBED_TLS conn->is_tls_connection = !!pthread_equal(pthread_self(), ctx->tls_thread); +#else + conn->is_tls_connection = false; +#endif // EVEREST_MBED_TLS /* wait for an incoming connection */ if (conn->is_tls_connection) { +#ifdef EVEREST_MBED_TLS conn->conn.ssl.ssl_config = &ctx->ssl_config; /* at the moment, this is simply resetting the fd to -1; kept for upwards compatibility */ @@ -955,6 +989,7 @@ static void* connection_server(void* data) { dlog(DLOG_LEVEL_ERROR, "Accept(tls) failed: %s", strerror(errno)); continue; } +#endif // EVEREST_MBED_TLS } else { conn->conn.socket_fd = accept(ctx->tcp_socket, (struct sockaddr*)&addr, &addrlen); if (conn->conn.socket_fd == -1) { @@ -1008,7 +1043,11 @@ int connection_start_servers(struct v2g_context* ctx) { } if (ctx->tls_socket.fd != -1) { +#ifdef EVEREST_MBED_TLS rv = pthread_create(&ctx->tls_thread, NULL, connection_server, ctx); +#else + rv = tls::connection_start_server(ctx); +#endif // EVEREST_MBED_TLS if (rv != 0) { if (tcp_started) { pthread_cancel(ctx->tcp_thread); diff --git a/modules/EvseV2G/connection.hpp b/modules/EvseV2G/connection/connection.hpp similarity index 69% rename from modules/EvseV2G/connection.hpp rename to modules/EvseV2G/connection/connection.hpp index cbfb6de377..3361835556 100644 --- a/modules/EvseV2G/connection.hpp +++ b/modules/EvseV2G/connection/connection.hpp @@ -5,15 +5,40 @@ #ifndef CONNECTION_H #define CONNECTION_H -#include "v2g_ctx.hpp" - +#include #include -#include +#include "v2g_ctx.hpp" + +/*! + * \brief initialise TCP/TLS connections + * \param ctx the V2G context + * \return 0 on success + */ int connection_init(struct v2g_context* ctx); + +/*! + * \brief start TCP/TLS servers + * \param ctx the V2G context + * \return 0 on success + */ int connection_start_servers(struct v2g_context* ctx); int create_udp_socket(const uint16_t udp_port, const char* interface_name); +/*! + * \brief check for V2G message sequence timeout + * \param ts_start start time + * \param ctx the V2G context + * \return true on timeout + */ +bool is_sequence_timeout(struct timespec ts_start, struct v2g_context* ctx); + +/*! + * \brief actions to take on connection close + * \param conn v2g connection context + */ +void connection_teardown(struct v2g_connection* conn); + /*! * \brief connection_read This abstracts a read from the connection socket, so that higher level functions * are not required to distinguish between TCP and TLS connections. @@ -22,7 +47,7 @@ int create_udp_socket(const uint16_t udp_port, const char* interface_name); * \param count number of read bytes. * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and * -2 for closed connection */ -ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t count); +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, std::size_t count); /*! * \brief connection_write This abstracts a write to the connection socket, so that higher level functions @@ -32,6 +57,6 @@ ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t * \param count size of the buffer * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and * -2 for closed connection */ -ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, size_t count); +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count); #endif /* CONNECTION_H */ diff --git a/modules/EvseV2G/connection/tls_connection.cpp b/modules/EvseV2G/connection/tls_connection.cpp new file mode 100644 index 0000000000..ac6aeced6d --- /dev/null +++ b/modules/EvseV2G/connection/tls_connection.cpp @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "tls_connection.hpp" +#include "connection.hpp" +#include "log.hpp" +#include "v2g.hpp" +#include "v2g_server.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef EVEREST_MBED_TLS +namespace tls { +int connection_init(struct v2g_context* ctx) { + return -1; +} +int connection_start_servers(struct v2g_context* ctx) { + return -1; +} +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, std::size_t count) { + return -1; +} +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count) { + return -1; +} +} // namespace tls + +#else // EVEREST_MBED_TLS + +namespace { + +void process_connection_thread(std::shared_ptr con, struct v2g_context* ctx) { + assert(con != nullptr); + assert(ctx != nullptr); + + openssl::PKey_ptr contract_public_key{nullptr, nullptr}; + auto connection = std::make_unique(); + connection->ctx = ctx; + connection->is_tls_connection = true; + connection->read = &tls::connection_read; + connection->write = &tls::connection_write; + connection->tls_connection = con.get(); + connection->pubkey = &contract_public_key; + + dlog(DLOG_LEVEL_INFO, "Incoming TLS connection"); + + if (con->accept()) { + // TODO(james-ctc) v2g_ctx->tls_key_logging + + if (ctx->state == 0) { + const auto rv = ::v2g_handle_connection(connection.get()); + dlog(DLOG_LEVEL_INFO, "v2g_dispatch_connection exited with %d", rv); + } else { + dlog(DLOG_LEVEL_INFO, "%s", "Closing tls-connection. v2g-session is already running"); + } + + con->shutdown(); + } + + ::connection_teardown(connection.get()); +} + +void handle_new_connection_cb(std::shared_ptr con, struct v2g_context* ctx) { + assert(con != nullptr); + assert(ctx != nullptr); + // create a thread to process this connection + try { + std::thread connection_loop(process_connection_thread, con, ctx); + connection_loop.detach(); + } catch (const std::system_error&) { + // unable to start thread + dlog(DLOG_LEVEL_ERROR, "pthread_create() failed: %s", strerror(errno)); + con->shutdown(); + } +} + +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 != tls::Server::state_t::stopped) { + dlog(DLOG_LEVEL_ERROR, "tls::Server failed to serve"); + } +} + +bool build_config(tls::Server::config_t& config, struct v2g_context* ctx) { + assert(ctx != nullptr); + assert(ctx->r_security != nullptr); + + using types::evse_security::CaCertificateType; + using types::evse_security::EncodingFormat; + using types::evse_security::GetCertificateInfoStatus; + using types::evse_security::LeafCertificateType; + + /* + * libevse-security checks for an optional password and when one + * isn't set is uses an empty string as the password rather than nullptr. + * hence private keys are always encrypted. + */ + + 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); + if (cert_info.status != GetCertificateInfoStatus::Accepted) { + dlog(DLOG_LEVEL_ERROR, "Failed to read cert_info! Not Accepted"); + } else { + if (cert_info.info) { + const auto& info = cert_info.info.value(); + const auto cert_path = info.certificate.value_or(""); + const auto key_path = info.key; + + // workaround (see above libevse-security comment) + const auto key_password = info.password.value_or(""); + + config.certificate_chain_file = cert_path.c_str(); + config.private_key_file = key_path.c_str(); + config.private_key_password = key_password.c_str(); + + if (info.ocsp) { + for (const auto& ocsp : info.ocsp.value()) { + const char* file{nullptr}; + if (ocsp.ocsp_path) { + file = ocsp.ocsp_path.value().c_str(); + } + config.ocsp_response_files.push_back(file); + } + } + + bResult = true; + } else { + dlog(DLOG_LEVEL_ERROR, "Failed to read cert_info! Empty response"); + } + } + + 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) { + assert(ctx != nullptr); + assert(ctx->tls_server != nullptr); + + // only starts the TLS server + + int res = 0; + 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(); + } catch (const std::system_error&) { + // unable to start thread (caller logs failure) + res = -1; + } + return res; +} + +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, const std::size_t count) { + assert(conn != nullptr); + assert(conn->tls_connection != nullptr); + + ssize_t result{0}; + std::size_t bytes_read{0}; + timespec ts_start{}; + + if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == -1) { + dlog(DLOG_LEVEL_ERROR, "clock_gettime(ts_start) failed: %s", strerror(errno)); + result = -1; + } + + while ((bytes_read < count) && (result >= 0)) { + const std::size_t remaining = count - bytes_read; + std::size_t bytes_in{0}; + auto* ptr = reinterpret_cast(&buf[bytes_read]); + + switch (conn->tls_connection->read(ptr, remaining, bytes_in)) { + case tls::Connection::result_t::success: + bytes_read += bytes_in; + break; + case tls::Connection::result_t::timeout: + break; + case tls::Connection::result_t::error: + default: + result = -1; + break; + } + + if (conn->ctx->is_connection_terminated) { + dlog(DLOG_LEVEL_ERROR, "Reading from tcp-socket aborted"); + conn->tls_connection->shutdown(); + result = -2; + } + + if (::is_sequence_timeout(ts_start, conn->ctx)) { + break; + } + } + + return (result < 0) ? result : static_cast(bytes_read); +} + +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count) { + assert(conn != nullptr); + assert(conn->tls_connection != nullptr); + + ssize_t result{0}; + std::size_t bytes_written{0}; + + while ((bytes_written < count) && (result >= 0)) { + const std::size_t remaining = count - bytes_written; + std::size_t bytes_out{0}; + const auto* ptr = reinterpret_cast(&buf[bytes_written]); + + switch (conn->tls_connection->write(ptr, remaining, bytes_out)) { + case tls::Connection::result_t::success: + bytes_written += bytes_out; + break; + case tls::Connection::result_t::timeout: + break; + case tls::Connection::result_t::error: + default: + result = -1; + break; + } + } + + if ((result == -1) && (conn->tls_connection->state() == tls::Connection::state_t::closed)) { + // if the connection has closed - return the number of bytes sent + result = 0; + } + + return (result < 0) ? result : static_cast(bytes_written); +} + +} // namespace tls + +#endif // EVEREST_MBED_TLS diff --git a/modules/EvseV2G/connection/tls_connection.hpp b/modules/EvseV2G/connection/tls_connection.hpp new file mode 100644 index 0000000000..d7bacdb878 --- /dev/null +++ b/modules/EvseV2G/connection/tls_connection.hpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef TLS_CONNECTION_HPP_ +#define TLS_CONNECTION_HPP_ + +#include +#include + +struct v2g_context; +struct v2g_connection; + +namespace tls { + +/*! + * \param ctx v2g connection context + * \return returns 0 on succss and -1 on error + */ +int connection_init(struct v2g_context* ctx); + +/*! + * \param ctx v2g connection context + * \return returns 0 on succss and -1 on error + */ +int connection_start_server(struct v2g_context* ctx); + +/*! + * \brief connection_read This abstracts a read from the connection socket, so that higher level functions + * are not required to distinguish between TCP and TLS connections. + * \param conn v2g connection context + * \param buf buffer to store received message sequence. + * \param count number of read bytes. + * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and + * -2 for closed connection */ +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, std::size_t count); + +/*! + * \brief connection_write This abstracts a write to the connection socket, so that higher level functions + * are not required to distinguish between TCP and TLS connections. + * \param conn v2g connection context + * \param buf buffer to store received message sequence. + * \param count size of the buffer + * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and + * -2 for closed connection */ +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count); + +} // namespace tls + +#endif // TLS_CONNECTION_HPP_ diff --git a/modules/EvseV2G/crypto/crypto_common.hpp b/modules/EvseV2G/crypto/crypto_common.hpp new file mode 100644 index 0000000000..1e4f74567e --- /dev/null +++ b/modules/EvseV2G/crypto/crypto_common.hpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef CRTYPTO_COMMON_HPP_ +#define CRTYPTO_COMMON_HPP_ + +#include + +#include + +namespace crypto { + +using verify_result_t = openssl::verify_result_t; + +constexpr std::size_t MAX_EXI_SIZE = 8192; + +} // namespace crypto + +#endif // CRTYPTO_COMMON_HPP_ diff --git a/modules/EvseV2G/crypto/crypto_mbedtls.cpp b/modules/EvseV2G/crypto/crypto_mbedtls.cpp new file mode 100644 index 0000000000..5f1b968726 --- /dev/null +++ b/modules/EvseV2G/crypto/crypto_mbedtls.cpp @@ -0,0 +1,428 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 chargebyte GmbH +// Copyright (C) 2023 Contributors to EVerest + +#include +#include + +#include "crypto_mbedtls.hpp" +#include "iso_server.hpp" +#include "log.hpp" + +#include +#include //for V2GTP_HEADER_LENGTHs +#include +#include +#include + +#include +#include +#include // To extract the emaid +#include + +namespace { + +constexpr std::size_t DIGEST_SIZE = 32; +constexpr std::size_t MQTT_MAX_PAYLOAD_SIZE = 268435455; +constexpr std::size_t MAX_EMAID_LEN = 18; + +bool getSubjectData(const mbedtls_x509_name* ASubject, const char* AAttrName, const mbedtls_asn1_buf** AVal); +int debug_verify_cert(void* data, mbedtls_x509_crt* crt, int depth, uint32_t* flags); +void printMbedVerifyErrorCode(int AErr, uint32_t AFlags); +bool base64_decode(const char* text, std::size_t len, std::uint8_t* out_data, std::size_t& out_len); +std::string base64_encode(const std::uint8_t* data, std::size_t len, bool newLine); + +bool getSubjectData(const mbedtls_x509_name* ASubject, const char* AAttrName, const mbedtls_asn1_buf** AVal) { + const char* attrName = NULL; + + while (NULL != ASubject) { + if ((0 == mbedtls_oid_get_attr_short_name(&ASubject->oid, &attrName)) && (0 == strcmp(attrName, AAttrName))) { + + *AVal = &ASubject->val; + return true; + } else { + ASubject = ASubject->next; + } + } + *AVal = NULL; + return false; +} + +/*! + * \brief debug_verify_cert Function is from https://github.com/aws/aws-iot-device-sdk-embedded-C/blob + * /master/platform/linux/mbedtls/network_mbedtls_wrapper.c to debug certificate verification + * \param data + * \param crt + * \param depth + * \param flags + * \return + */ +int debug_verify_cert(void* data, mbedtls_x509_crt* crt, int depth, uint32_t* flags) { + char buf[1024]; + ((void)data); + + dlog(DLOG_LEVEL_INFO, "\nVerify requested for (Depth %d):\n", depth); + mbedtls_x509_crt_info(buf, sizeof(buf) - 1, "", crt); + dlog(DLOG_LEVEL_INFO, "%s", buf); + + if ((*flags) == 0) + dlog(DLOG_LEVEL_INFO, " This certificate has no flags\n"); + else { + mbedtls_x509_crt_verify_info(buf, sizeof(buf), " ! ", *flags); + dlog(DLOG_LEVEL_INFO, "%s\n", buf); + } + + return (0); +} + +/*! + * \brief printMbedVerifyErrorCode This functions prints the mbedTls specific error code. + * \param AErr is the return value of the mbed verify function + * \param AFlags includes the flags of the verification. + */ +void printMbedVerifyErrorCode(int AErr, uint32_t AFlags) { + dlog(DLOG_LEVEL_ERROR, "Failed to verify certificate (err: 0x%08x flags: 0x%08x)", AErr, AFlags); + if (AErr == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { + if (AFlags & MBEDTLS_X509_BADCERT_EXPIRED) + dlog(DLOG_LEVEL_ERROR, "CERT_EXPIRED"); + else if (AFlags & MBEDTLS_X509_BADCERT_REVOKED) + dlog(DLOG_LEVEL_ERROR, "CERT_REVOKED"); + else if (AFlags & MBEDTLS_X509_BADCERT_CN_MISMATCH) + dlog(DLOG_LEVEL_ERROR, "CN_MISMATCH"); + else if (AFlags & MBEDTLS_X509_BADCERT_NOT_TRUSTED) + dlog(DLOG_LEVEL_ERROR, "CERT_NOT_TRUSTED"); + else if (AFlags & MBEDTLS_X509_BADCRL_NOT_TRUSTED) + dlog(DLOG_LEVEL_ERROR, "CRL_NOT_TRUSTED"); + else if (AFlags & MBEDTLS_X509_BADCRL_EXPIRED) + dlog(DLOG_LEVEL_ERROR, "CRL_EXPIRED"); + else if (AFlags & MBEDTLS_X509_BADCERT_MISSING) + dlog(DLOG_LEVEL_ERROR, "CERT_MISSING"); + else if (AFlags & MBEDTLS_X509_BADCERT_SKIP_VERIFY) + dlog(DLOG_LEVEL_ERROR, "SKIP_VERIFY"); + else if (AFlags & MBEDTLS_X509_BADCERT_OTHER) + dlog(DLOG_LEVEL_ERROR, "CERT_OTHER"); + else if (AFlags & MBEDTLS_X509_BADCERT_FUTURE) + dlog(DLOG_LEVEL_ERROR, "CERT_FUTURE"); + else if (AFlags & MBEDTLS_X509_BADCRL_FUTURE) + dlog(DLOG_LEVEL_ERROR, "CRL_FUTURE"); + else if (AFlags & MBEDTLS_X509_BADCERT_KEY_USAGE) + dlog(DLOG_LEVEL_ERROR, "KEY_USAGE"); + else if (AFlags & MBEDTLS_X509_BADCERT_EXT_KEY_USAGE) + dlog(DLOG_LEVEL_ERROR, "EXT_KEY_USAGE"); + else if (AFlags & MBEDTLS_X509_BADCERT_NS_CERT_TYPE) + dlog(DLOG_LEVEL_ERROR, "NS_CERT_TYPE"); + else if (AFlags & MBEDTLS_X509_BADCERT_BAD_MD) + dlog(DLOG_LEVEL_ERROR, "BAD_MD"); + else if (AFlags & MBEDTLS_X509_BADCERT_BAD_PK) + dlog(DLOG_LEVEL_ERROR, "BAD_PK"); + else if (AFlags & MBEDTLS_X509_BADCERT_BAD_KEY) + dlog(DLOG_LEVEL_ERROR, "BAD_KEY"); + else if (AFlags & MBEDTLS_X509_BADCRL_BAD_MD) + dlog(DLOG_LEVEL_ERROR, "CRL_BAD_MD"); + else if (AFlags & MBEDTLS_X509_BADCRL_BAD_PK) + dlog(DLOG_LEVEL_ERROR, "CRL_BAD_PK"); + else if (AFlags & MBEDTLS_X509_BADCRL_BAD_KEY) + dlog(DLOG_LEVEL_ERROR, "CRL_BAD_KEY"); + } +} + +bool base64_decode(const char* text, std::size_t len, std::uint8_t* out_data, std::size_t& out_len) { + bool bResult = true; + const std::size_t dlen = out_len; + const auto rv = mbedtls_base64_decode(out_data, dlen, &out_len, reinterpret_cast(text), len); + if (rv != 0) { + std::array strerr{}; + mbedtls_strerror(rv, strerr.data(), strerr.size()); + dlog(DLOG_LEVEL_ERROR, "Failed to decode base64 stream (-0x%04x) %s", rv, strerr); + bResult = false; + } + return bResult; +} + +std::string base64_encode(const std::uint8_t* data, std::size_t len, bool /* newLine */) { + std::string result; + std::size_t olen{0}; + mbedtls_base64_encode(nullptr, 0, &olen, data, len); + + if (MQTT_MAX_PAYLOAD_SIZE < olen) { + dlog(DLOG_LEVEL_ERROR, "Mqtt payload size exceeded!"); + } else { + result = std::string(olen, '\0'); + + if ((mbedtls_base64_encode(reinterpret_cast(result.data()), result.size(), &olen, data, len) != + 0)) { + result.clear(); + dlog(DLOG_LEVEL_ERROR, "Unable to base64 encode"); + } + } + + return result; +} + +} // namespace + +namespace crypto::mbedtls { + +/*! + * \brief check_iso2_signature This function validates the ISO signature + * \param iso2_signature is the signature of the ISO EXI fragment + * \param public_key is the public key to validate the signature against the ISO EXI fragment + * \param iso2_exi_fragment iso2_exi_fragment is the ISO EXI fragment + */ +bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, mbedtls_ecdsa_context& public_key, + struct iso2_exiFragment* iso2_exi_fragment) { + /** Digest check **/ + int err = 0; + const struct iso2_SignatureType* sig = iso2_signature; + unsigned char buf[MAX_EXI_SIZE]; + const struct iso2_ReferenceType* req_ref = &sig->SignedInfo.Reference.array[0]; + exi_bitstream_t stream; + exi_bitstream_init(&stream, buf, MAX_EXI_SIZE, 0, NULL); + uint8_t digest[DIGEST_SIZE]; + err = encode_iso2_exiFragment(&stream, iso2_exi_fragment); + if (err != 0) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode fragment, error code = %d", err); + return false; + } + uint32_t frag_data_len = exi_bitstream_get_length(&stream); + mbedtls_sha256(buf, frag_data_len, digest, 0); + + if (req_ref->DigestValue.bytesLen != DIGEST_SIZE) { + dlog(DLOG_LEVEL_ERROR, "Invalid digest length %u in signature", req_ref->DigestValue.bytesLen); + return false; + } + + if (memcmp(req_ref->DigestValue.bytes, digest, DIGEST_SIZE) != 0) { + dlog(DLOG_LEVEL_ERROR, "Invalid digest in signature"); + return false; + } + + /** Validate signature **/ + struct iso2_xmldsigFragment sig_fragment; + init_iso2_xmldsigFragment(&sig_fragment); + sig_fragment.SignedInfo_isUsed = 1; + sig_fragment.SignedInfo = sig->SignedInfo; + + /** \req [V2G2-771] Don't use following fields */ + sig_fragment.SignedInfo.Id_isUsed = 0; + sig_fragment.SignedInfo.CanonicalizationMethod.ANY_isUsed = 0; + sig_fragment.SignedInfo.SignatureMethod.HMACOutputLength_isUsed = 0; + sig_fragment.SignedInfo.SignatureMethod.ANY_isUsed = 0; + for (auto* ref = sig_fragment.SignedInfo.Reference.array; + ref != (sig_fragment.SignedInfo.Reference.array + sig_fragment.SignedInfo.Reference.arrayLen); ++ref) { + ref->Type_isUsed = 0; + ref->Transforms.Transform.ANY_isUsed = 0; + ref->Transforms.Transform.XPath_isUsed = 0; + ref->DigestMethod.ANY_isUsed = 0; + } + + stream.byte_pos = 0; + stream.bit_count = 0; + err = encode_iso2_xmldsigFragment(&stream, &sig_fragment); + + if (err != 0) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode XML signature fragment, error code = %d", err); + return false; + } + uint32_t sign_info_fragmen_len = exi_bitstream_get_length(&stream); + + /* Hash the signature */ + mbedtls_sha256(buf, sign_info_fragmen_len, digest, 0); + + /* Validate the ecdsa signature using the public key */ + if (sig->SignatureValue.CONTENT.bytesLen == 0) { + dlog(DLOG_LEVEL_ERROR, "Signature len is invalid (%i)", sig->SignatureValue.CONTENT.bytesLen); + return false; + } + + /* Init mbedtls parameter */ + mbedtls_ecp_group ecp_group; + mbedtls_ecp_group_init(&ecp_group); + + mbedtls_mpi mpi_r; + mbedtls_mpi_init(&mpi_r); + mbedtls_mpi mpi_s; + mbedtls_mpi_init(&mpi_s); + + mbedtls_mpi_read_binary(&mpi_r, (const unsigned char*)&sig->SignatureValue.CONTENT.bytes[0], + sig->SignatureValue.CONTENT.bytesLen / 2); + mbedtls_mpi_read_binary( + &mpi_s, (const unsigned char*)&sig->SignatureValue.CONTENT.bytes[sig->SignatureValue.CONTENT.bytesLen / 2], + sig->SignatureValue.CONTENT.bytesLen / 2); + + err = mbedtls_ecp_group_load(&ecp_group, MBEDTLS_ECP_DP_SECP256R1); + + if (err == 0) { + err = mbedtls_ecdsa_verify(&ecp_group, static_cast(digest), 32, &public_key.Q, &mpi_r, + &mpi_s); + } + + mbedtls_ecp_group_free(&ecp_group); + mbedtls_mpi_free(&mpi_r); + mbedtls_mpi_free(&mpi_s); + + if (err != 0) { + char error_buf[100]; + mbedtls_strerror(err, error_buf, sizeof(error_buf)); + dlog(DLOG_LEVEL_ERROR, "Invalid signature, error code = -0x%08x, %s", err, error_buf); + return false; + } + + return true; +} + +bool load_contract_root_cert(Certificate_ptr& contract_root_crt, const char* V2G_file_path, const char* MO_file_path) { + int rv = 0; + + if ((rv = mbedtls_x509_crt_parse_file(contract_root_crt.get(), MO_file_path)) != 0) { + char strerr[256]; + mbedtls_strerror(rv, strerr, sizeof(strerr)); + dlog(DLOG_LEVEL_WARNING, "Unable to parse MO (%s) root certificate. (err: -0x%04x - %s)", MO_file_path, -rv, + strerr); + dlog(DLOG_LEVEL_INFO, "Attempting to parse V2G root certificate.."); + + if ((rv = mbedtls_x509_crt_parse_file(contract_root_crt.get(), V2G_file_path)) != 0) { + mbedtls_strerror(rv, strerr, sizeof(strerr)); + dlog(DLOG_LEVEL_ERROR, "Unable to parse V2G (%s) root certificate. (err: -0x%04x - %s)", V2G_file_path, -rv, + strerr); + } + } + + return (rv != 0) ? false : true; +} + +void free_connection_crypto_data(v2g_connection* conn) { + mbedtls_ecdsa_free(&conn->ctx->session.contract.pubkey); + mbedtls_ecdsa_init(&conn->ctx->session.contract.pubkey); +} + +int load_certificate(Certificate_ptr& crt, const std::uint8_t* bytes, std::uint16_t bytesLen) { + int err{-1}; + + // Parse contract leaf certificate + if (bytesLen != 0) { + err = mbedtls_x509_crt_parse(crt.get(), bytes, bytesLen); + if (err != 0) { + char strerr[256]; + mbedtls_strerror(err, strerr, std::string(strerr).size()); + dlog(DLOG_LEVEL_ERROR, "handle_payment_detail: invalid certificate received in req: %s", strerr); + } + + } else { + dlog(DLOG_LEVEL_ERROR, "No certificate received!"); + } + + return err; +} + +int parse_contract_certificate(Certificate_ptr& crt, const std::uint8_t* bytes, std::size_t bytesLen) { + auto err = mbedtls_x509_crt_parse(crt.get(), bytes, bytesLen); + + if (err != 0) { + char strerr[256]; + mbedtls_strerror(err, strerr, std::string(strerr).size()); + dlog(DLOG_LEVEL_ERROR, "handle_payment_detail: invalid certificate received in req: %s", strerr); + } + + return err; +} + +/*! + * \brief getEmaidFromContractCert This function extracts the emaid from an subject of a contract certificate. + * \param ASubject is the subject of the contract certificate. + * \return Returns the length of the extracted emaid. + */ +std::string getEmaidFromContractCert(const Certificate_ptr& crt) { + const mbedtls_x509_name* ASubject = &crt.get()->subject; + const mbedtls_asn1_buf* val = NULL; + std::size_t certEmaidLen = 0; + char certEmaid[MAX_EMAID_LEN + 1]; + + std::string result; + if (true == getSubjectData(ASubject, "CN", &val)) { + /* Check the emaid length within the certificate */ + certEmaidLen = MAX_EMAID_LEN < val->len ? 0 : val->len; + strncpy(certEmaid, reinterpret_cast(val->p), certEmaidLen); + certEmaid[certEmaidLen] = '\0'; + result = std::string{&certEmaid[0], certEmaidLen}; + } else { + dlog(DLOG_LEVEL_WARNING, "No CN found in subject of the contract certificate"); + } + + return result; +} + +std::string chain_to_pem(const Certificate_ptr& contract_crt, const void* /* chain */) { + const mbedtls_x509_crt* crt = contract_crt.get(); + std::string contract_cert_chain_pem; + + while (crt != nullptr && crt->version != 0) { + const auto pem = certificate_to_pem(crt); + if (pem.empty()) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode certificate chain"); + break; + } + contract_cert_chain_pem.append(pem); + crt = crt->next; + } + + return contract_cert_chain_pem; +} + +verify_result_t verify_certificate(Certificate_ptr& contract_crt, const void* chain, const char* v2g_root_cert_path, + const char* mo_root_cert_path, bool debugMode) { + Certificate_ptr contract_root_crt; + uint32_t flags; + verify_result_t result{verify_result_t::verified}; + + /* Load supported V2G/MO root certificates */ + if (load_contract_root_cert(contract_root_crt, v2g_root_cert_path, mo_root_cert_path)) { + // === Verify the retrieved contract ECDSA key against the root cert === + const int err = mbedtls_x509_crt_verify(contract_crt.get(), contract_root_crt.get(), NULL, NULL, &flags, + (debugMode) ? debug_verify_cert : NULL, NULL); + if (err != 0) { + printMbedVerifyErrorCode(err, flags); + dlog(DLOG_LEVEL_ERROR, "Validation of the contract certificate failed!"); + if ((err == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) && (flags & MBEDTLS_X509_BADCERT_EXPIRED)) { + result = verify_result_t::CertificateExpired; + } else if ((err == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) && (flags & MBEDTLS_X509_BADCERT_REVOKED)) { + result = verify_result_t::CertificateRevoked; + } else { + result = verify_result_t::CertChainError; + } + } + + } else { + result = verify_result_t::NoCertificateAvailable; + } + + return result; +} + +int get_public_key(mbedtls_ecdsa_context* pubkey, mbedtls_pk_context& pk) { + // Convert the public key in the certificate to a mbed TLS ECDSA public key + // This also verifies that it's an ECDSA key and not an RSA key + int err = mbedtls_ecdsa_from_keypair(pubkey, mbedtls_pk_ec(pk)); + if (err != 0) { + char strerr[256]; + mbedtls_strerror(err, strerr, std::string(strerr).size()); + dlog(DLOG_LEVEL_ERROR, "Could not retrieve ecdsa public key from certificate keypair: %s", strerr); + } + + return err; +} + +std::string certificate_to_pem(const mbedtls_x509_crt* crt) { + std::string result; + auto cert_b64 = base64_encode(crt->raw.p, crt->raw.len, true); + if (cert_b64.empty()) { + dlog(DLOG_LEVEL_ERROR, "Unable to convert certificate to PEM"); + } else { + result.append("-----BEGIN CERTIFICATE-----\n"); + result.append(cert_b64); + result.append("\n-----END CERTIFICATE-----\n"); + } + return result; +} + +} // namespace crypto::mbedtls diff --git a/modules/EvseV2G/crypto/crypto_mbedtls.hpp b/modules/EvseV2G/crypto/crypto_mbedtls.hpp new file mode 100644 index 0000000000..e96fab4b87 --- /dev/null +++ b/modules/EvseV2G/crypto/crypto_mbedtls.hpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef CRYPTO_MBEDTLS_HPP_ +#define CRYPTO_MBEDTLS_HPP_ + +#include +#include + +#include "crypto_common.hpp" +#include "v2g.hpp" + +/** + * \file Mbed TLS implementation + */ + +struct mbedtls_x509_crt; + +namespace crypto::mbedtls { + +/** + * \brief Wrapper around a mbedtls_x509_crt to ensure it is freed properly + */ +class Certificate_ptr { +private: + mbedtls_x509_crt cert = {}; + +public: + Certificate_ptr() { + mbedtls_x509_crt_init(&cert); + } + ~Certificate_ptr() { + mbedtls_x509_crt_free(&cert); + } + + Certificate_ptr(const Certificate_ptr&) = delete; + Certificate_ptr(Certificate_ptr&&) = delete; + Certificate_ptr& operator=(const Certificate_ptr&) = delete; + Certificate_ptr& operator=(Certificate_ptr&&) = delete; + + [[nodiscard]] mbedtls_x509_crt* get() { + return &cert; + } + + [[nodiscard]] const mbedtls_x509_crt* get() const { + return &cert; + } + + explicit operator mbedtls_x509_crt*() { + return &cert; + } +}; + +/** + * \brief check the signature of a signed 15118 message + * \param iso2_signature the signature to check + * \param public_key the public key from the contract certificate + * \param iso2_exi_fragment the signed data + * \return true when the signature is valid + */ +bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, mbedtls_ecdsa_context& public_key, + struct iso2_exiFragment* iso2_exi_fragment); + +/** + * \brief load the trust anchor for the contract certificate. + * Use the mobility operator certificate if it exists otherwise + * the V2G certificate + * \param contract_root_crt the retrieved trust anchor + * \param V2G_file_path the file containing the V2G trust anchor (PEM format) + * \param MO_file_path the file containing the mobility operator trust anchor (PEM format) + * \return true when a certificate was found + */ +bool load_contract_root_cert(Certificate_ptr& contract_root_crt, const char* V2G_file_path, const char* MO_file_path); + +/** + * \brief clear public key from previous connection + * \param conn the V2G connection data + */ +void free_connection_crypto_data(v2g_connection* conn); + +/** + * \brief load a contract certificate's certification path certificate from the V2G message as DER bytes + * \param crt the certificate path certificates (this certificate is added to the list) + * \param bytes the DER (ASN.1) X509v3 certificate in the V2G message + * \param bytesLen the length of the DER encoded certificate + * \return 0 when certificate successfully loaded + */ +int load_certificate(Certificate_ptr& crt, const std::uint8_t* bytes, std::uint16_t bytesLen); + +/** + * \brief load the contract certificate from the V2G message as DER bytes + * \param crt the certificate + * \param bytes the DER (ASN.1) X509v3 certificate in the V2G message + * \param bytesLen the length of the DER encoded certificate + * \return 0 when certificate successfully loaded + */ +int parse_contract_certificate(Certificate_ptr& crt, const std::uint8_t* bytes, std::size_t bytesLen); + +/** + * \brief get the EMAID from the certificate (CommonName from the SubjectName) + * \param crt the certificate + * \return the EMAD or empty on error + */ +std::string getEmaidFromContractCert(const Certificate_ptr& crt); + +/** + * \brief convert a list of certificates into a PEM string starting with the contract certificate + * \param contract_crt the contract certificate (when not the first certificate in the chain) + * \param chain the certification path chain (might include the contract certificate as the first item) + * \return PEM string or empty on error + */ +std::string chain_to_pem(const Certificate_ptr& contract_crt, const void* chain); + +/** + * \brief verify certification path of the contract certificate through to a trust anchor + * \param contract_crt (single certificate or chain with the contract certificate as the first item) + * \param chain intermediate certificates (may be nullptr) + * \param v2g_root_cert_path V2G trust anchor file name + * \param mo_root_cert_path mobility operator trust anchor file name + * \param debugMode additional information on verification failures + * \result a subset of possible verification failures where known or 'verified' on success + */ +verify_result_t verify_certificate(Certificate_ptr& contract_crt, const void* chain, const char* v2g_root_cert_path, + const char* mo_root_cert_path, bool debugMode); + +/** + * \brief convert certificate public key into a suable form + * \param pubkey the certificate public key + * \param pk the key in a form usable for checking signatures + * \return 0 when key successfully loaded + */ +int get_public_key(mbedtls_ecdsa_context* pubkey, mbedtls_pk_context& pk); + +/** + * \brief convert a certificate into a PEM string + * \param crt the certificate + * \return the PEM string or empty on error + */ +std::string certificate_to_pem(const mbedtls_x509_crt* crt); + +} // namespace crypto::mbedtls + +#endif // CRYPTO_MBEDTLS_HPP_ diff --git a/modules/EvseV2G/crypto/crypto_openssl.cpp b/modules/EvseV2G/crypto/crypto_openssl.cpp new file mode 100644 index 0000000000..a142ba0c94 --- /dev/null +++ b/modules/EvseV2G/crypto/crypto_openssl.cpp @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include +#include + +#include "crypto_openssl.hpp" +#include "iso_server.hpp" +#include "log.hpp" + +#include +#include //for V2GTP_HEADER_LENGTHs +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace crypto ::openssl { +using ::openssl::bn_const_t; +using ::openssl::bn_t; +using ::openssl::log_error; +using ::openssl::sha_256; +using ::openssl::sha_256_digest_t; +using ::openssl::verify; + +bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, EVP_PKEY* pkey, + struct iso2_exiFragment* iso2_exi_fragment) { + assert(pkey != nullptr); + assert(iso2_signature != nullptr); + assert(iso2_exi_fragment != nullptr); + + bool bRes{true}; + + // signature information + const struct iso2_ReferenceType* req_ref = &iso2_signature->SignedInfo.Reference.array[0]; + const auto signature_len = iso2_signature->SignatureValue.CONTENT.bytesLen; + const auto* signature = &iso2_signature->SignatureValue.CONTENT.bytes[0]; + + // build data to check signature against + std::array exi_buffer{}; + exi_bitstream_t stream; + exi_bitstream_init(&stream, exi_buffer.data(), MAX_EXI_SIZE, 0, NULL); + + auto err = encode_iso2_exiFragment(&stream, iso2_exi_fragment); + if (err != 0) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode fragment, error code = %d", err); + bRes = false; + } + + sha_256_digest_t digest; + + // calculate hash of data + if (bRes) { + const auto frag_data_len = exi_bitstream_get_length(&stream); + bRes = sha_256(exi_buffer.data(), frag_data_len, digest); + } + + // check hash matches the value in the message + if (bRes) { + if (req_ref->DigestValue.bytesLen != digest.size()) { + dlog(DLOG_LEVEL_ERROR, "Invalid digest length %u in signature", req_ref->DigestValue.bytesLen); + bRes = false; + } + } + if (bRes) { + if (std::memcmp(req_ref->DigestValue.bytes, digest.data(), digest.size()) != 0) { + dlog(DLOG_LEVEL_ERROR, "Invalid digest in signature"); + bRes = false; + } + } + + // verify the signature + if (bRes) { + struct iso2_xmldsigFragment sig_fragment {}; + init_iso2_xmldsigFragment(&sig_fragment); + sig_fragment.SignedInfo_isUsed = 1; + sig_fragment.SignedInfo = iso2_signature->SignedInfo; + + /** \req [V2G2-771] Don't use following fields */ + sig_fragment.SignedInfo.Id_isUsed = 0; + sig_fragment.SignedInfo.CanonicalizationMethod.ANY_isUsed = 0; + sig_fragment.SignedInfo.SignatureMethod.HMACOutputLength_isUsed = 0; + sig_fragment.SignedInfo.SignatureMethod.ANY_isUsed = 0; + for (auto* ref = sig_fragment.SignedInfo.Reference.array; + ref != (sig_fragment.SignedInfo.Reference.array + sig_fragment.SignedInfo.Reference.arrayLen); ++ref) { + ref->Type_isUsed = 0; + ref->Transforms.Transform.ANY_isUsed = 0; + ref->Transforms.Transform.XPath_isUsed = 0; + ref->DigestMethod.ANY_isUsed = 0; + } + + stream.byte_pos = 0; + stream.bit_count = 0; + err = encode_iso2_xmldsigFragment(&stream, &sig_fragment); + + if (err != 0) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode XML signature fragment, error code = %d", err); + bRes = false; + } + } + + if (bRes) { + // hash again (different data) buffer_pos has been updated ... + const auto frag_data_len = exi_bitstream_get_length(&stream); + bRes = sha_256(exi_buffer.data(), frag_data_len, digest); + } + + if (bRes) { + /* Validate the ecdsa signature using the public key */ + if (signature_len != ::openssl::signature_size) { + dlog(DLOG_LEVEL_ERROR, "Signature len is invalid (%i)", signature_len); + bRes = false; + } + } + + if (bRes) { + const std::uint8_t* r = &signature[0]; + const std::uint8_t* s = &signature[32]; + bRes = verify(pkey, r, s, digest); + } + + return bRes; +} + +bool load_contract_root_cert(::openssl::CertificateList& trust_anchors, const char* V2G_file_path, + const char* MO_file_path) { + // note the file(s) may contain more than one certificate (hence must be PEM) + // try MO_file_path first then fallback to V2G_file_path + + trust_anchors.clear(); + trust_anchors = ::openssl::load_certificates(MO_file_path); + if (trust_anchors.empty()) { + log_error("Unable to load MO root(s)"); + trust_anchors = ::openssl::load_certificates(V2G_file_path); + if (trust_anchors.empty()) { + log_error("Unable to load V2G root(s)"); + } + } + + return !trust_anchors.empty(); +} + +int load_certificate(::openssl::CertificateList* chain, const std::uint8_t* bytes, std::uint16_t bytesLen) { + assert(chain != nullptr); + int result{-1}; + + auto tmp_cert = ::openssl::der_to_certificate(bytes, bytesLen); + if (tmp_cert != nullptr) { + chain->push_back(std::move(tmp_cert)); + result = 0; + } + + return result; +} + +int parse_contract_certificate(::openssl::Certificate_ptr& crt, const std::uint8_t* buf, std::size_t buflen) { + crt = ::openssl::der_to_certificate(buf, buflen); + return (crt == nullptr) ? -1 : 0; +} + +std::string getEmaidFromContractCert(const ::openssl::Certificate_ptr& crt) { + std::string cert_emaid; + const auto subject = ::openssl::certificate_subject(crt.get()); + if (auto itt = subject.find("CN"); itt != subject.end()) { + cert_emaid = itt->second; + } + + return cert_emaid; +} + +std::string chain_to_pem(const ::openssl::Certificate_ptr& cert, const ::openssl::CertificateList* chain) { + assert(chain != nullptr); + + std::string contract_cert_chain_pem(::openssl::certificate_to_pem(cert.get())); + for (const auto& crt : *chain) { + const auto pem = ::openssl::certificate_to_pem(crt.get()); + if (pem.empty()) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode certificate chain"); + break; + } + contract_cert_chain_pem.append(pem); + } + + return contract_cert_chain_pem; +} + +verify_result_t verify_certificate(const ::openssl::Certificate_ptr& cert, const ::openssl::CertificateList* chain, + const char* v2g_root_cert_path, const char* mo_root_cert_path, + bool /* debugMode */) { + assert(chain != nullptr); + + verify_result_t result{verify_result_t::verified}; + ::openssl::CertificateList trust_anchors; + + if (!load_contract_root_cert(trust_anchors, v2g_root_cert_path, mo_root_cert_path)) { + result = verify_result_t::NoCertificateAvailable; + } else { + result = ::openssl::verify_certificate(cert.get(), trust_anchors, *chain); + } + + return result; +} + +} // namespace crypto::openssl diff --git a/modules/EvseV2G/crypto/crypto_openssl.hpp b/modules/EvseV2G/crypto/crypto_openssl.hpp new file mode 100644 index 0000000000..a65b564015 --- /dev/null +++ b/modules/EvseV2G/crypto/crypto_openssl.hpp @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef CRYPTO_OPENSSL_HPP_ +#define CRYPTO_OPENSSL_HPP_ + +#include +#include + +#include "crypto_common.hpp" +#include + +/** + * \file OpenSSL implementation + */ + +struct evp_pkey_st; +struct iso2_SignatureType; +struct iso2_exiFragment; +struct x509_st; +struct v2g_connection; + +namespace crypto::openssl { + +/** + * \brief check the signature of a signed 15118 message + * \param iso2_signature the signature to check + * \param public_key the public key from the contract certificate + * \param iso2_exi_fragment the signed data + * \return true when the signature is valid + */ +bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, evp_pkey_st* pkey, + struct iso2_exiFragment* iso2_exi_fragment); + +/** + * \brief load the trust anchor for the contract certificate. + * Use the mobility operator certificate if it exists otherwise + * the V2G certificate + * \param contract_root_crt the retrieved trust anchor + * \param V2G_file_path the file containing the V2G trust anchor (PEM format) + * \param MO_file_path the file containing the mobility operator trust anchor (PEM format) + * \return true when a certificate was found + */ +bool load_contract_root_cert(::openssl::CertificateList& trust_anchors, const char* V2G_file_path, + const char* MO_file_path); + +/** + * \brief clear certificate and public key from previous connection + * \param conn the V2G connection data + * \note not needed for the OpenSSL implementation + */ +constexpr void free_connection_crypto_data(v2g_connection* conn) { +} + +/** + * \brief load a contract certificate's certification path certificate from the V2G message as DER bytes + * \param chain the certificate path certificates (this certificate is added to the list) + * \param bytes the DER (ASN.1) X509v3 certificate in the V2G message + * \param bytesLen the length of the DER encoded certificate + * \return 0 when certificate successfully loaded + */ +int load_certificate(::openssl::CertificateList* chain, const std::uint8_t* bytes, std::uint16_t bytesLen); + +/** + * \brief load the contract certificate from the V2G message as DER bytes + * \param crt the certificate + * \param bytes the DER (ASN.1) X509v3 certificate in the V2G message + * \param bytesLen the length of the DER encoded certificate + * \return 0 when certificate successfully loaded + */ +int parse_contract_certificate(::openssl::Certificate_ptr& crt, const std::uint8_t* buf, std::size_t buflen); + +/** + * \brief get the EMAID from the certificate (CommonName from the SubjectName) + * \param crt the certificate + * \return the EMAD or empty on error + */ +std::string getEmaidFromContractCert(const ::openssl::Certificate_ptr& crt); + +/** + * \brief convert a list of certificates into a PEM string starting with the contract certificate + * \param contract_crt the contract certificate (when not the first certificate in the chain) + * \param chain the certification path chain (might include the contract certificate as the first item) + * \return PEM string or empty on error + */ +std::string chain_to_pem(const ::openssl::Certificate_ptr& cert, const ::openssl::CertificateList* chain); + +/** + * \brief verify certification path of the contract certificate through to a trust anchor + * \param contract_crt (single certificate or chain with the contract certificate as the first item) + * \param chain intermediate certificates (may be nullptr) + * \param v2g_root_cert_path V2G trust anchor file name + * \param mo_root_cert_path mobility operator trust anchor file name + * \param debugMode additional information on verification failures + * \result a subset of possible verification failures where known or 'verified' on success + */ +verify_result_t verify_certificate(const ::openssl::Certificate_ptr& cert, const ::openssl::CertificateList* chain, + const char* v2g_root_cert_path, const char* mo_root_cert_path, bool debugMode); + +} // namespace crypto::openssl + +#endif // CRYPTO_OPENSSL_HPP_ diff --git a/modules/EvseV2G/iso_server.cpp b/modules/EvseV2G/iso_server.cpp index 659c6c8a06..11c2c2c589 100644 --- a/modules/EvseV2G/iso_server.cpp +++ b/modules/EvseV2G/iso_server.cpp @@ -10,13 +10,22 @@ #include #include #include +#include +#include +#include + +#ifdef EVEREST_MBED_TLS +#include "crypto/crypto_mbedtls.hpp" +using namespace crypto::mbedtls; #include #include #include /* To extract the emaid */ #include -#include -#include -#include +#else +#include "crypto/crypto_openssl.hpp" +using namespace openssl; +using namespace crypto::openssl; +#endif // EVEREST_MBED_TLS #include "iso_server.hpp" #include "log.hpp" @@ -24,17 +33,12 @@ #include "v2g_ctx.hpp" #include "v2g_server.hpp" -#define MAX_EXI_SIZE 8192 -#define DIGEST_SIZE 32 #define MQTT_MAX_PAYLOAD_SIZE 268435455 #define V2G_SECC_MSG_CERTINSTALL_TIME 4500 -#define MAX_EXI_SIZE 8192 -#define MAX_CERT_SIZE 800 // [V2G2-010] -#define MAX_EMAID_LEN 18 #define GEN_CHALLENGE_SIZE 16 -const uint16_t SAE_V2H = 28472; -const uint16_t SAE_V2G = 28473; +constexpr uint16_t SAE_V2H = 28472; +constexpr uint16_t SAE_V2G = 28473; /*! * \brief iso_validate_state This function checks whether the received message is expected and valid at this @@ -110,213 +114,6 @@ static v2g_event iso_validate_response_code(iso2_responseCodeType* const v2g_res return next_event; } -static bool load_contract_root_cert(mbedtls_x509_crt* contract_root_crt, const char* V2G_file_path, - const char* MO_file_path) { - int rv = 0; - - if ((rv = mbedtls_x509_crt_parse_file(contract_root_crt, MO_file_path)) != 0) { - char strerr[256]; - mbedtls_strerror(rv, strerr, sizeof(strerr)); - dlog(DLOG_LEVEL_WARNING, "Unable to parse MO (%s) root certificate. (err: -0x%04x - %s)", MO_file_path, -rv, - strerr); - dlog(DLOG_LEVEL_INFO, "Attempting to parse V2G root certificate.."); - - if ((rv = mbedtls_x509_crt_parse_file(contract_root_crt, V2G_file_path)) != 0) { - mbedtls_strerror(rv, strerr, sizeof(strerr)); - dlog(DLOG_LEVEL_ERROR, "Unable to parse V2G (%s) root certificate. (err: -0x%04x - %s)", V2G_file_path, -rv, - strerr); - } - } - - return (rv != 0) ? false : true; -} - -/*! - * \brief debug_verify_cert Function is from https://github.com/aws/aws-iot-device-sdk-embedded-C/blob - * /master/platform/linux/mbedtls/network_mbedtls_wrapper.c to debug certificate verification - * \param data - * \param crt - * \param depth - * \param flags - * \return - */ -static int debug_verify_cert(void* data, mbedtls_x509_crt* crt, int depth, uint32_t* flags) { - char buf[1024]; - ((void)data); - - dlog(DLOG_LEVEL_INFO, "\nVerify requested for (Depth %d):\n", depth); - mbedtls_x509_crt_info(buf, sizeof(buf) - 1, "", crt); - dlog(DLOG_LEVEL_INFO, "%s", buf); - - if ((*flags) == 0) - dlog(DLOG_LEVEL_INFO, " This certificate has no flags\n"); - else { - mbedtls_x509_crt_verify_info(buf, sizeof(buf), " ! ", *flags); - dlog(DLOG_LEVEL_INFO, "%s\n", buf); - } - - return (0); -} - -/*! - * \brief printMbedVerifyErrorCode This functions prints the mbedTls specific error code. - * \param AErr is the return value of the mbed verify function - * \param AFlags includes the flags of the verification. - */ -static void printMbedVerifyErrorCode(int AErr, uint32_t AFlags) { - dlog(DLOG_LEVEL_ERROR, "Failed to verify certificate (err: 0x%08x flags: 0x%08x)", AErr, AFlags); - if (AErr == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { - if (AFlags & MBEDTLS_X509_BADCERT_EXPIRED) - dlog(DLOG_LEVEL_ERROR, "CERT_EXPIRED"); - else if (AFlags & MBEDTLS_X509_BADCERT_REVOKED) - dlog(DLOG_LEVEL_ERROR, "CERT_REVOKED"); - else if (AFlags & MBEDTLS_X509_BADCERT_CN_MISMATCH) - dlog(DLOG_LEVEL_ERROR, "CN_MISMATCH"); - else if (AFlags & MBEDTLS_X509_BADCERT_NOT_TRUSTED) - dlog(DLOG_LEVEL_ERROR, "CERT_NOT_TRUSTED"); - else if (AFlags & MBEDTLS_X509_BADCRL_NOT_TRUSTED) - dlog(DLOG_LEVEL_ERROR, "CRL_NOT_TRUSTED"); - else if (AFlags & MBEDTLS_X509_BADCRL_EXPIRED) - dlog(DLOG_LEVEL_ERROR, "CRL_EXPIRED"); - else if (AFlags & MBEDTLS_X509_BADCERT_MISSING) - dlog(DLOG_LEVEL_ERROR, "CERT_MISSING"); - else if (AFlags & MBEDTLS_X509_BADCERT_SKIP_VERIFY) - dlog(DLOG_LEVEL_ERROR, "SKIP_VERIFY"); - else if (AFlags & MBEDTLS_X509_BADCERT_OTHER) - dlog(DLOG_LEVEL_ERROR, "CERT_OTHER"); - else if (AFlags & MBEDTLS_X509_BADCERT_FUTURE) - dlog(DLOG_LEVEL_ERROR, "CERT_FUTURE"); - else if (AFlags & MBEDTLS_X509_BADCRL_FUTURE) - dlog(DLOG_LEVEL_ERROR, "CRL_FUTURE"); - else if (AFlags & MBEDTLS_X509_BADCERT_KEY_USAGE) - dlog(DLOG_LEVEL_ERROR, "KEY_USAGE"); - else if (AFlags & MBEDTLS_X509_BADCERT_EXT_KEY_USAGE) - dlog(DLOG_LEVEL_ERROR, "EXT_KEY_USAGE"); - else if (AFlags & MBEDTLS_X509_BADCERT_NS_CERT_TYPE) - dlog(DLOG_LEVEL_ERROR, "NS_CERT_TYPE"); - else if (AFlags & MBEDTLS_X509_BADCERT_BAD_MD) - dlog(DLOG_LEVEL_ERROR, "BAD_MD"); - else if (AFlags & MBEDTLS_X509_BADCERT_BAD_PK) - dlog(DLOG_LEVEL_ERROR, "BAD_PK"); - else if (AFlags & MBEDTLS_X509_BADCERT_BAD_KEY) - dlog(DLOG_LEVEL_ERROR, "BAD_KEY"); - else if (AFlags & MBEDTLS_X509_BADCRL_BAD_MD) - dlog(DLOG_LEVEL_ERROR, "CRL_BAD_MD"); - else if (AFlags & MBEDTLS_X509_BADCRL_BAD_PK) - dlog(DLOG_LEVEL_ERROR, "CRL_BAD_PK"); - else if (AFlags & MBEDTLS_X509_BADCRL_BAD_KEY) - dlog(DLOG_LEVEL_ERROR, "CRL_BAD_KEY"); - } -} - -/*! - * \brief check_iso2_signature This function validates the ISO signature - * \param iso2_signature is the signature of the ISO EXI fragment - * \param public_key is the public key to validate the signature against the ISO EXI fragment - * \param iso2_exi_fragment iso2_exi_fragment is the ISO EXI fragment - */ -static bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, mbedtls_ecdsa_context* public_key, - struct iso2_exiFragment* iso2_exi_fragment) { - /** Digest check **/ - int err = 0; - const struct iso2_SignatureType* sig = iso2_signature; - unsigned char buf[MAX_EXI_SIZE]; - const struct iso2_ReferenceType* req_ref = &sig->SignedInfo.Reference.array[0]; - exi_bitstream_t stream; - exi_bitstream_init(&stream, buf, MAX_EXI_SIZE, 0, NULL); - uint8_t digest[DIGEST_SIZE]; - err = encode_iso2_exiFragment(&stream, iso2_exi_fragment); - if (err != 0) { - dlog(DLOG_LEVEL_ERROR, "Unable to encode fragment, error code = %d", err); - return false; - } - uint32_t frag_data_len = exi_bitstream_get_length(&stream); - mbedtls_sha256(buf, frag_data_len, digest, 0); - - if (req_ref->DigestValue.bytesLen != DIGEST_SIZE) { - dlog(DLOG_LEVEL_ERROR, "Invalid digest length %u in signature", req_ref->DigestValue.bytesLen); - return false; - } - - if (memcmp(req_ref->DigestValue.bytes, digest, DIGEST_SIZE) != 0) { - dlog(DLOG_LEVEL_ERROR, "Invalid digest in signature"); - return false; - } - - /** Validate signature **/ - struct iso2_xmldsigFragment sig_fragment; - init_iso2_xmldsigFragment(&sig_fragment); - sig_fragment.SignedInfo_isUsed = 1; - sig_fragment.SignedInfo = sig->SignedInfo; - - /** \req [V2G2-771] Don't use following fields */ - sig_fragment.SignedInfo.Id_isUsed = 0; - sig_fragment.SignedInfo.CanonicalizationMethod.ANY_isUsed = 0; - sig_fragment.SignedInfo.SignatureMethod.HMACOutputLength_isUsed = 0; - sig_fragment.SignedInfo.SignatureMethod.ANY_isUsed = 0; - for (auto* ref = sig_fragment.SignedInfo.Reference.array; - ref != (sig_fragment.SignedInfo.Reference.array + sig_fragment.SignedInfo.Reference.arrayLen); ++ref) { - ref->Type_isUsed = 0; - ref->Transforms.Transform.ANY_isUsed = 0; - ref->Transforms.Transform.XPath_isUsed = 0; - ref->DigestMethod.ANY_isUsed = 0; - } - - stream.byte_pos = 0; - stream.bit_count = 0; - err = encode_iso2_xmldsigFragment(&stream, &sig_fragment); - - if (err != 0) { - dlog(DLOG_LEVEL_ERROR, "Unable to encode XML signature fragment, error code = %d", err); - return false; - } - uint32_t sign_info_fragmen_len = exi_bitstream_get_length(&stream); - - /* Hash the signature */ - mbedtls_sha256(buf, sign_info_fragmen_len, digest, 0); - - /* Validate the ecdsa signature using the public key */ - if (sig->SignatureValue.CONTENT.bytesLen == 0) { - dlog(DLOG_LEVEL_ERROR, "Signature len is invalid (%i)", sig->SignatureValue.CONTENT.bytesLen); - return false; - } - - /* Init mbedtls parameter */ - mbedtls_ecp_group ecp_group; - mbedtls_ecp_group_init(&ecp_group); - - mbedtls_mpi mpi_r; - mbedtls_mpi_init(&mpi_r); - mbedtls_mpi mpi_s; - mbedtls_mpi_init(&mpi_s); - - mbedtls_mpi_read_binary(&mpi_r, (const unsigned char*)&sig->SignatureValue.CONTENT.bytes[0], - sig->SignatureValue.CONTENT.bytesLen / 2); - mbedtls_mpi_read_binary( - &mpi_s, (const unsigned char*)&sig->SignatureValue.CONTENT.bytes[sig->SignatureValue.CONTENT.bytesLen / 2], - sig->SignatureValue.CONTENT.bytesLen / 2); - - err = mbedtls_ecp_group_load(&ecp_group, MBEDTLS_ECP_DP_SECP256R1); - - if (err == 0) { - err = mbedtls_ecdsa_verify(&ecp_group, static_cast(digest), 32, &public_key->Q, &mpi_r, - &mpi_s); - } - - mbedtls_ecp_group_free(&ecp_group); - mbedtls_mpi_free(&mpi_r); - mbedtls_mpi_free(&mpi_s); - - if (err != 0) { - char error_buf[100]; - mbedtls_strerror(err, error_buf, sizeof(error_buf)); - dlog(DLOG_LEVEL_ERROR, "Invalid signature, error code = -0x%08x, %s", err, error_buf); - return false; - } - - return true; -} - /*! * \brief populate_ac_evse_status This function configures the evse_status struct * \param ctx is the V2G context @@ -429,64 +226,6 @@ static void publish_DC_EVStatusType(struct v2g_context* ctx, const struct iso2_D } } -static bool getSubjectData(const mbedtls_x509_name* ASubject, const char* AAttrName, const mbedtls_asn1_buf** AVal) { - const char* attrName = NULL; - - while (NULL != ASubject) { - if ((0 == mbedtls_oid_get_attr_short_name(&ASubject->oid, &attrName)) && (0 == strcmp(attrName, AAttrName))) { - - *AVal = &ASubject->val; - return true; - } else { - ASubject = ASubject->next; - } - } - *AVal = NULL; - return false; -} - -/*! - * \brief getEmaidFromContractCert This function extracts the emaid from an subject of a contract certificate. - * \param ASubject is the subject of the contract certificate. - * \param AEmaid is the buffer for the extracted emaid. The extracted string is null terminated. - * \param AEmaidLen is length of the AEmaid buffer. - * \return Returns the length of the extracted emaid. - */ -static size_t getEmaidFromContractCert(const mbedtls_x509_name* ASubject, char* AEmaid, uint8_t AEmaidLen) { - - const mbedtls_asn1_buf* val = NULL; - size_t certEmaidLen = 0; - char certEmaid[MAX_EMAID_LEN + 1]; - - if (true == getSubjectData(ASubject, "CN", &val)) { - /* Check the emaid length within the certificate */ - certEmaidLen = MAX_EMAID_LEN < val->len ? 0 : val->len; - strncpy(certEmaid, reinterpret_cast(val->p), certEmaidLen); - certEmaid[certEmaidLen] = '\0'; - - /* Filter '-' character */ - certEmaidLen = 0; - for (uint8_t idx = 0; certEmaid[idx] != '\0'; idx++) { - if ('-' != certEmaid[idx]) { - certEmaid[certEmaidLen++] = certEmaid[idx]; - } - } - certEmaid[certEmaidLen] = '\0'; - - if (certEmaidLen > (AEmaidLen - 1)) { - dlog(DLOG_LEVEL_WARNING, "emaid buffer is too small (%i received, expected %i)", certEmaidLen, - AEmaidLen - 1); - return 0; - } else { - strcpy(AEmaid, certEmaid); - } - } else { - dlog(DLOG_LEVEL_WARNING, "No CN found in subject of the contract certificate"); - } - - return certEmaidLen; -} - static auto get_emergency_status_code(const struct v2g_context* ctx, uint8_t phase_type) { if (ctx->intl_emergency_shutdown) return iso2_DC_EVSEStatusCodeType_EVSE_EmergencyShutdown; @@ -719,6 +458,7 @@ static bool publish_iso_certificate_installation_exi_req(struct v2g_context* ctx size_t olen; types::iso15118_charger::Request_Exi_Stream_Schema certificate_request; +#ifdef EVEREST_MBED_TLS /* Parse contract leaf certificate */ mbedtls_base64_encode(NULL, 0, &olen, static_cast(AExiBuffer), AExiBufferSize); @@ -736,8 +476,19 @@ static bool publish_iso_certificate_installation_exi_req(struct v2g_context* ctx dlog(DLOG_LEVEL_ERROR, "Unable to encode contract leaf certificate"); goto exit; } - certificate_request.exiRequest = std::string(reinterpret_cast(base64Buffer), olen); +#else + certificate_request.exiRequest = openssl::base64_encode(AExiBuffer, AExiBufferSize); + if (certificate_request.exiRequest.size() > MQTT_MAX_PAYLOAD_SIZE) { + dlog(DLOG_LEVEL_ERROR, "Mqtt payload size exceeded!"); + return false; + } + if (certificate_request.exiRequest.size() == 0) { + dlog(DLOG_LEVEL_ERROR, "Unable to encode contract leaf certificate"); + return false; + } +#endif // EVEREST_MBED_TLS + certificate_request.iso15118SchemaVersion = ISO_15118_2013_MSG_DEF; certificate_request.certificateAction = types::iso15118_charger::CertificateActionEnum::Install; ctx->p_charger->publish_Certificate_Request(certificate_request); @@ -1090,46 +841,40 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { int err; // === For the contract certificate, the certificate chain should be checked === - conn->ctx->session.contract.valid_crt = false; - if (conn->ctx->session.iso_selected_payment_option == iso2_paymentOptionType_Contract) { // Free old stuff if it exists - mbedtls_x509_crt_free(&conn->ctx->session.contract.crt); - mbedtls_ecdsa_free(&conn->ctx->session.contract.pubkey); - - mbedtls_x509_crt_init(&conn->ctx->session.contract.crt); - mbedtls_ecdsa_init(&conn->ctx->session.contract.pubkey); + free_connection_crypto_data(conn); // Parse contract leaf certificate + +#ifdef EVEREST_MBED_TLS + Certificate_ptr contract_crt; + const void* chain{nullptr}; +#else + Certificate_ptr contract_crt{nullptr, nullptr}; + CertificateList chain{}; +#endif // EVEREST_MBED_TLS + if (req->ContractSignatureCertChain.Certificate.bytesLen != 0) { - err = mbedtls_x509_crt_parse(&conn->ctx->session.contract.crt, - req->ContractSignatureCertChain.Certificate.bytes, - req->ContractSignatureCertChain.Certificate.bytesLen); + err = parse_contract_certificate(contract_crt, req->ContractSignatureCertChain.Certificate.bytes, + req->ContractSignatureCertChain.Certificate.bytesLen); } else { dlog(DLOG_LEVEL_ERROR, "No certificate received!"); res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; goto error_out; } - /* Check the received emaid against the certificate emaid. */ - char cert_emaid[iso2_eMAID_CHARACTER_SIZE]; - getEmaidFromContractCert(&conn->ctx->session.contract.crt.subject, cert_emaid, sizeof(cert_emaid)); + auto cert_emaid = getEmaidFromContractCert(contract_crt); + std::string req_emaid{&req->eMAID.characters[0], req->eMAID.charactersLen}; /* Filter '-' character */ - uint8_t emaid_len = 0; - for (uint8_t idx = 0; idx < req->eMAID.charactersLen; idx++) { - if ('-' != req->eMAID.characters[idx]) { - req->eMAID.characters[emaid_len++] = req->eMAID.characters[idx]; - } - } - - req->eMAID.charactersLen = emaid_len; - req->eMAID.characters[emaid_len] = '\0'; + cert_emaid.erase(std::remove(cert_emaid.begin(), cert_emaid.end(), '-'), cert_emaid.end()); + req_emaid.erase(std::remove(req_emaid.begin(), req_emaid.end(), '-'), req_emaid.end()); - dlog(DLOG_LEVEL_TRACE, "emaid-v2g: %s emaid-cert: %s", req->eMAID.characters, cert_emaid); + dlog(DLOG_LEVEL_TRACE, "emaid-v2g: %s emaid-cert: %s", req_emaid.c_str(), cert_emaid.c_str()); - if ((std::string(cert_emaid).size() != req->eMAID.charactersLen) || - (0 != strncasecmp(req->eMAID.characters, cert_emaid, req->eMAID.charactersLen))) { + if ((req_emaid.size() != cert_emaid.size()) || + (strncasecmp(req_emaid.c_str(), cert_emaid.c_str(), req_emaid.size()) != 0)) { dlog(DLOG_LEVEL_ERROR, "emaid of the contract certificate doesn't match with the received v2g-emaid"); res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; goto error_out; @@ -1138,65 +883,49 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { if (err != 0) { memset(res, 0, sizeof(*res)); res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; - char strerr[256]; - mbedtls_strerror(err, strerr, std::string(strerr).size()); - dlog(DLOG_LEVEL_ERROR, "handle_payment_detail: invalid certificate received in req: %s", strerr); goto error_out; } // Convert the public key in the certificate to a mbed TLS ECDSA public key // This also verifies that it's an ECDSA key and not an RSA key - err = mbedtls_ecdsa_from_keypair(&conn->ctx->session.contract.pubkey, - mbedtls_pk_ec(conn->ctx->session.contract.crt.pk)); + +#ifdef EVEREST_MBED_TLS + err = get_public_key(&conn->ctx->session.contract.pubkey, contract_crt.get()->pk); +#else + assert(conn->pubkey != nullptr); + *conn->pubkey = certificate_public_key(contract_crt.get()); + err = (*conn->pubkey == nullptr) ? -1 : 0; +#endif // EVEREST_MBED_TLS + if (err != 0) { memset(res, 0, sizeof(*res)); res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; - char strerr[256]; - mbedtls_strerror(err, strerr, std::string(strerr).size()); - dlog(DLOG_LEVEL_ERROR, "Could not retrieve ecdsa public key from certificate keypair: %s", strerr); goto error_out; } // Parse contract sub certificates if (req->ContractSignatureCertChain.SubCertificates_isUsed == 1) { for (int i = 0; i < req->ContractSignatureCertChain.SubCertificates.Certificate.arrayLen; i++) { - err = mbedtls_x509_crt_parse( - &conn->ctx->session.contract.crt, - req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytes, - req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytesLen); - +#ifdef EVEREST_MBED_TLS + err = load_certificate(contract_crt, + req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytes, + req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytesLen); +#else + err = + load_certificate(&chain, req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytes, + req->ContractSignatureCertChain.SubCertificates.Certificate.array[i].bytesLen); +#endif // EVEREST_MBED_TLS if (err != 0) { - char strerr[256]; - mbedtls_strerror(err, strerr, std::string(strerr).size()); - dlog(DLOG_LEVEL_ERROR, "handle_payment_detail: invalid sub-certificate received in req: %s", - strerr); + res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; goto error_out; } } } // initialize contract cert chain to retrieve ocsp request data - std::string contract_cert_chain_pem = ""; - // Save the certificate chain in a variable in PEM format to publish it - mbedtls_x509_crt* crt = &conn->ctx->session.contract.crt; - unsigned char* base64Buffer = NULL; - size_t olen; - - while (crt != nullptr && crt->version != 0) { - mbedtls_base64_encode(NULL, 0, &olen, crt->raw.p, crt->raw.len); - base64Buffer = static_cast(malloc(olen)); - if ((base64Buffer == NULL) || - ((mbedtls_base64_encode(base64Buffer, olen, &olen, crt->raw.p, crt->raw.len)) != 0)) { - dlog(DLOG_LEVEL_ERROR, "Unable to encode certificate chain"); - break; - } - contract_cert_chain_pem.append("-----BEGIN CERTIFICATE-----\n"); - contract_cert_chain_pem.append(std::string(reinterpret_cast(base64Buffer), olen)); - contract_cert_chain_pem.append("\n-----END CERTIFICATE-----\n"); - free(base64Buffer); - crt = crt->next; - } + // Save the certificate chain in a variable in PEM format to publish it + std::string contract_cert_chain_pem = chain_to_pem(contract_crt, &chain); std::optional> iso15118_certificate_hash_data; @@ -1206,36 +935,41 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { conn->ctx->r_security->call_get_verify_file(types::evse_security::CaCertificateType::V2G); std::string mo_root_cert_path = conn->ctx->r_security->call_get_verify_file(types::evse_security::CaCertificateType::MO); - mbedtls_x509_crt contract_root_crt; - mbedtls_x509_crt_init(&contract_root_crt); - uint32_t flags; - /* Load supported V2G/MO root certificates */ - if (load_contract_root_cert(&contract_root_crt, v2g_root_cert_path.c_str(), mo_root_cert_path.c_str()) == - false) { - memset(res, 0, sizeof(*res)); + crypto::verify_result_t vRes = verify_certificate(contract_crt, &chain, v2g_root_cert_path.c_str(), + mo_root_cert_path.c_str(), conn->ctx->debugMode); + + err = -1; + switch (vRes) { + case crypto::verify_result_t::verified: + err = 0; + break; + case crypto::verify_result_t::CertificateExpired: + res->ResponseCode = iso2_responseCodeType_FAILED_CertificateExpired; + break; + case crypto::verify_result_t::CertificateRevoked: + res->ResponseCode = iso2_responseCodeType_FAILED_CertificateRevoked; + break; + case crypto::verify_result_t::NoCertificateAvailable: res->ResponseCode = iso2_responseCodeType_FAILED_NoCertificateAvailable; - goto error_out; + err = -2; + break; + case crypto::verify_result_t::CertChainError: + default: + res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; + break; } - // === Verify the retrieved contract ECDSA key against the root cert === - err = mbedtls_x509_crt_verify(&conn->ctx->session.contract.crt, &contract_root_crt, NULL, NULL, &flags, - (conn->ctx->debugMode == true) ? debug_verify_cert : NULL, NULL); - if (err != 0) { - printMbedVerifyErrorCode(err, flags); - memset(res, 0, sizeof(*res)); + if (err == -1) { dlog(DLOG_LEVEL_ERROR, "Validation of the contract certificate failed!"); - if ((err == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) && (flags & MBEDTLS_X509_BADCERT_EXPIRED)) { - res->ResponseCode = iso2_responseCodeType_FAILED_CertificateExpired; - } else if ((err == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) && (flags & MBEDTLS_X509_BADCERT_REVOKED)) { - res->ResponseCode = iso2_responseCodeType_FAILED_CertificateRevoked; - } else { - res->ResponseCode = iso2_responseCodeType_FAILED_CertChainError; - } // EVSETimeStamp and GenChallenge are mandatory, GenChallenge has fixed size res->EVSETimeStamp = time(NULL); memset(res->GenChallenge.bytes, 0, GEN_CHALLENGE_SIZE); res->GenChallenge.bytesLen = GEN_CHALLENGE_SIZE; + } + + if (err != 0) { + memset(res, 0, sizeof(*res)); goto error_out; } @@ -1249,7 +983,6 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { generate_random_data(&conn->ctx->session.gen_challenge, GEN_CHALLENGE_SIZE); memcpy(res->GenChallenge.bytes, conn->ctx->session.gen_challenge, GEN_CHALLENGE_SIZE); res->GenChallenge.bytesLen = GEN_CHALLENGE_SIZE; - conn->ctx->session.contract.valid_crt = true; // Publish the provided signature certificate chain and eMAID from EVCC // to receive PnC authorization @@ -1320,8 +1053,16 @@ static enum v2g_event handle_iso_authorization(struct v2g_connection* conn) { iso2_fragment.AuthorizationReq_isUsed = 1u; memcpy(&iso2_fragment.AuthorizationReq, req, sizeof(*req)); - if (check_iso2_signature(&conn->exi_in.iso2EXIDocument->V2G_Message.Header.Signature, - &conn->ctx->session.contract.pubkey, &iso2_fragment) == false) { +#ifdef EVEREST_MBED_TLS + const bool bSigRes = check_iso2_signature(&conn->exi_in.iso2EXIDocument->V2G_Message.Header.Signature, + conn->ctx->session.contract.pubkey, &iso2_fragment); +#else + assert(conn->pubkey != nullptr); + const bool bSigRes = check_iso2_signature(&conn->exi_in.iso2EXIDocument->V2G_Message.Header.Signature, + conn->pubkey->get(), &iso2_fragment); +#endif + + if (!bSigRes) { res->ResponseCode = iso2_responseCodeType_FAILED_SignatureError; goto error_out; } @@ -1901,6 +1642,7 @@ static enum v2g_event handle_iso_certificate_installation(struct v2g_connection* if ((conn->ctx->evse_v2g_data.cert_install_res_b64_buffer.empty() == false) && (conn->ctx->evse_v2g_data.cert_install_status == true)) { +#ifdef EVEREST_MBED_TLS size_t buffer_pos = 0; if ((rv = mbedtls_base64_decode( conn->buffer + V2GTP_HEADER_LENGTH, DEFAULT_BUFFER_SIZE, &buffer_pos, @@ -1912,6 +1654,17 @@ static enum v2g_event handle_iso_certificate_installation(struct v2g_connection* goto exit; } conn->stream.byte_pos = buffer_pos; +#else + const auto data = openssl::base64_decode(conn->ctx->evse_v2g_data.cert_install_res_b64_buffer.data(), + conn->ctx->evse_v2g_data.cert_install_res_b64_buffer.size()); + if (data.empty() || (data.size() > DEFAULT_BUFFER_SIZE)) { + dlog(DLOG_LEVEL_ERROR, "Failed to decode base64 stream"); + goto exit; + } else { + std::memcpy(conn->buffer + V2GTP_HEADER_LENGTH, data.data(), data.size()); + conn->stream.byte_pos = data.size(); + } +#endif // EVEREST_MBED_TLS nextEvent = V2G_EVENT_SEND_RECV_EXI_MSG; res->ResponseCode = iso2_responseCodeType_OK; // Is irrelevant but must be valid to serve the internal validation diff --git a/modules/EvseV2G/tests/CMakeLists.txt b/modules/EvseV2G/tests/CMakeLists.txt new file mode 100644 index 0000000000..2cc4fc01c9 --- /dev/null +++ b/modules/EvseV2G/tests/CMakeLists.txt @@ -0,0 +1,88 @@ +get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR) +find_package(libevent) + +set(TLS_GTEST_NAME v2g_openssl_test) +add_executable(${TLS_GTEST_NAME}) + +add_dependencies(${TLS_GTEST_NAME} generate_cpp_files) + +target_include_directories(${TLS_GTEST_NAME} PRIVATE + . .. ../crypto ../../../lib/staging/util + ${GENERATED_INCLUDE_DIR} + ${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME} +) + +target_compile_definitions(${TLS_GTEST_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${TLS_GTEST_NAME} PRIVATE + ../../../lib/staging/tls/tests/gtest_main.cpp + log.cpp + openssl_test.cpp + ../crypto/crypto_openssl.cpp +) + +target_link_libraries(${TLS_GTEST_NAME} PRIVATE + GTest::gtest + cbv2g::din + cbv2g::iso2 + cbv2g::tp + everest::framework + everest::evse_security + everest::tls +) + +set(V2G_MAIN_NAME v2g_server) +add_executable(${V2G_MAIN_NAME}) + +add_dependencies(${V2G_MAIN_NAME} generate_cpp_files) + +target_include_directories(${V2G_MAIN_NAME} PRIVATE + . .. ../connection ../../../tests/include ../../../lib/staging/util + ${GENERATED_INCLUDE_DIR} + ${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME} + ${CMAKE_BINARY_DIR}/generated/include +) + +target_compile_definitions(${V2G_MAIN_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${V2G_MAIN_NAME} PRIVATE + ../connection/connection.cpp + ../connection/tls_connection.cpp + ../tools.cpp + ../v2g_ctx.cpp + log.cpp + requirement.cpp + v2g_main.cpp +) + +target_link_libraries(${V2G_MAIN_NAME} PRIVATE + cbv2g::din + cbv2g::iso2 + cbv2g::tp + everest::log + everest::framework + everest::evse_security + everest::tls + -levent -lpthread -levent_pthreads +) + +install( + FILES + ../../../lib/staging/tls/tests/pki/iso_pkey.asn1 + ../../../lib/staging/tls/tests/pki/openssl-pki.conf + ../../../lib/staging/tls/tests/pki/ocsp_response.der + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" +) + +install( + PROGRAMS + ../../../lib/staging/tls/tests/pki/pki.sh + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}" +) + +# runs fine locally, fails in CI +# add_test(${TLS_GTEST_NAME} ${TLS_GTEST_NAME}) diff --git a/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp b/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp new file mode 100644 index 0000000000..2d903a3b18 --- /dev/null +++ b/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#ifndef ISO15118_CHARGERIMPLSTUB_H_ +#define ISO15118_CHARGERIMPLSTUB_H_ + +#include + +#include + +//----------------------------------------------------------------------------- +namespace module::stub { + +struct ISO15118_chargerImplStub : public ISO15118_chargerImplBase { +public: + ISO15118_chargerImplStub() : ISO15118_chargerImplBase(nullptr, "EvseV2G"){}; + + virtual void init() { + } + virtual void ready() { + } + + virtual void handle_setup(types::iso15118_charger::EVSEID& evse_id, + std::vector& supported_energy_transfer_modes, + types::iso15118_charger::SAE_J2847_Bidi_Mode& sae_j2847_mode, bool& debug_mode) { + std::cout << "ISO15118_chargerImplBase::handle_setup called" << std::endl; + } + virtual void handle_set_charging_parameters(types::iso15118_charger::SetupPhysicalValues& physical_values) { + std::cout << "ISO15118_chargerImplBase::handle_set_charging_parameters called" << std::endl; + } + virtual void handle_session_setup(std::vector& payment_options, + bool& supported_certificate_service) { + std::cout << "ISO15118_chargerImplBase::handle_session_setup called" << std::endl; + } + virtual void handle_certificate_response(types::iso15118_charger::Response_Exi_Stream_Status& exi_stream_status) { + std::cout << "ISO15118_chargerImplBase::handle_certificate_response called" << std::endl; + } + virtual void handle_authorization_response(types::authorization::AuthorizationStatus& authorization_status, + types::authorization::CertificateStatus& certificate_status) { + std::cout << "ISO15118_chargerImplBase::handle_authorization_response called" << std::endl; + } + virtual void handle_ac_contactor_closed(bool& status) { + std::cout << "ISO15118_chargerImplBase::handle_ac_contactor_closed called" << std::endl; + } + virtual void handle_dlink_ready(bool& value) { + std::cout << "ISO15118_chargerImplBase::handle_dlink_ready called" << std::endl; + } + virtual void handle_cable_check_finished(bool& status) { + std::cout << "ISO15118_chargerImplBase::handle_cable_check_finished called" << std::endl; + } + virtual void handle_receipt_is_required(bool& receipt_required) { + std::cout << "ISO15118_chargerImplBase::handle_receipt_is_required called" << std::endl; + } + virtual void handle_stop_charging(bool& stop) { + std::cout << "ISO15118_chargerImplBase::handle_stop_charging called" << std::endl; + } + virtual void handle_update_ac_max_current(double& max_current) { + std::cout << "ISO15118_chargerImplBase::handle_update_ac_max_current called" << std::endl; + } + virtual void handle_update_dc_maximum_limits(types::iso15118_charger::DC_EVSEMaximumLimits& maximum_limits) { + std::cout << "ISO15118_chargerImplBase::handle_update_dc_maximum_limits called" << std::endl; + } + virtual void handle_update_dc_minimum_limits(types::iso15118_charger::DC_EVSEMinimumLimits& minimum_limits) { + std::cout << "ISO15118_chargerImplBase::handle_update_dc_minimum_limits called" << std::endl; + } + virtual void handle_update_isolation_status(types::iso15118_charger::IsolationStatus& isolation_status) { + std::cout << "ISO15118_chargerImplBase::handle_update_isolation_status called" << std::endl; + } + virtual void + handle_update_dc_present_values(types::iso15118_charger::DC_EVSEPresentVoltage_Current& present_voltage_current) { + std::cout << "ISO15118_chargerImplBase::handle_update_dc_present_values called" << std::endl; + } + virtual void handle_update_meter_info(types::powermeter::Powermeter& powermeter) { + std::cout << "ISO15118_chargerImplBase::handle_update_meter_info called" << std::endl; + } + virtual void handle_send_error(types::iso15118_charger::EvseError& error) { + std::cout << "ISO15118_chargerImplBase::handle_send_error called" << std::endl; + } + virtual void handle_reset_error() { + std::cout << "ISO15118_chargerImplBase::handle_reset_error called" << std::endl; + } +}; + +} // namespace module::stub + +#endif // ISO15118_CHARGERIMPLSTUB_H_ diff --git a/modules/EvseV2G/tests/README.md b/modules/EvseV2G/tests/README.md new file mode 100644 index 0000000000..21b4cd129d --- /dev/null +++ b/modules/EvseV2G/tests/README.md @@ -0,0 +1,51 @@ + +# Tests + +Building tests: + +```sh +$ cd everest-core +$ mkdir build +$ cd build +$ cmake -GNinja -DEVEREST_CORE_BUILD_TESTING=ON -DUSING_MBED_TLS=OFF .. +$ ninja install +``` + +`touch release.json` may be needed if it hasn't been created +(then re-run `ninja install`). + +## Run EVerest in SIL + +1. start MQTT broker +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/) + +## Unit tests + +- `./v2g_openssl_test` +- automatically runs `pki.sh` +- run from the directory containing the executable + +### Standalone V2G TLS server + +Tests the Server class via the functions in connection.cpp and +tls_connection.cpp. + +- `./v2g_server -i ` +- connects to IPv6 only with a link local address +- requires `boost` library so LD_LIBRARY_PATH may need to be set +- displays the address it is listening on. e.g. + `[fe80::ae91:a1ff:fec9:a947%3]:64109` +- supports multiple connections +- gracefully terminates after 80 seconds +- `valgrind` can be used to check memory allocations + (has some leaks - possibly in v2g_ctx_start_events thread) +- requires client certificate +- s_client echos back what is typed with a delay since V2G has a long timeout + +The connect argument must match what was displayed by `v2g_server` + +```sh +openssl s_client -connect [fe80::ae91:a1ff:fec9:a947%3]:64109 -verify 2 -CAfile server_root_cert.pem -cert client_cert.pem -cert_chain client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname evse.pionix.de -status +``` diff --git a/modules/EvseV2G/tests/evse_securityIntfStub.hpp b/modules/EvseV2G/tests/evse_securityIntfStub.hpp new file mode 100644 index 0000000000..d2a388e0a3 --- /dev/null +++ b/modules/EvseV2G/tests/evse_securityIntfStub.hpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#ifndef EVSE_SECURITYINTFSTUB_H_ +#define EVSE_SECURITYINTFSTUB_H_ + +#include + +#include "ModuleAdapterStub.hpp" +#include "generated/types/evse_security.hpp" +#include "utils/types.hpp" +#include +#include +#include +#include + +//----------------------------------------------------------------------------- +namespace module::stub { + +class evse_securityIntfStub : public ModuleAdapterStub, public evse_securityIntf { +private: + std::map + functions; + +public: + evse_securityIntfStub() : evse_securityIntf(this, Requirement("", 0), "EvseSecurity") { + functions["get_verify_file"] = &evse_securityIntfStub::get_verify_file; + functions["get_leaf_certificate_info"] = &evse_securityIntfStub::get_leaf_certificate_info; + } + + virtual Result call_fn(const Requirement& req, const std::string& str, Parameters args) { + if (auto it = functions.find(str); it != functions.end()) { + return std::invoke(it->second, this, req, args); + } + std::printf("call_fn (%s)\n", str.c_str()); + return std::nullopt; + } + + virtual Result get_verify_file(const Requirement& req, const Parameters& args) { + std::cout << "evse_securityIntf::get_verify_file called" << std::endl; + return ""; + } + + virtual Result get_leaf_certificate_info(const Requirement& req, const Parameters& args) { + std::cout << "evse_securityIntf::get_leaf_certificate_info called" << std::endl; + return ""; + } +}; + +} // namespace module::stub + +#endif // EVSE_SECURITYINTFSTUB_H_ diff --git a/modules/EvseV2G/tests/log.cpp b/modules/EvseV2G/tests/log.cpp new file mode 100644 index 0000000000..2b06a35196 --- /dev/null +++ b/modules/EvseV2G/tests/log.cpp @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "log.hpp" + +#include +#include + +void dlog_func(const dloglevel_t loglevel, const char* filename, const int linenumber, const char* functionname, + const char* format, ...) { + va_list ap; + va_start(ap, format); + (void)std::vfprintf(stderr, format, ap); + va_end(ap); + (void)std::fprintf(stderr, "\n"); +} diff --git a/modules/EvseV2G/tests/openssl_test.cpp b/modules/EvseV2G/tests/openssl_test.cpp new file mode 100644 index 0000000000..c9e47f9346 --- /dev/null +++ b/modules/EvseV2G/tests/openssl_test.cpp @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "crypto_common.hpp" +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include + +#include +#include //for V2GTP_HEADER_LENGTHs +#include +#include +#include + +namespace { + +template constexpr void setCharacters(T& dest, const std::string& s) { + dest.charactersLen = s.size(); + std::memcpy(&dest.characters[0], s.c_str(), s.size()); +} + +template constexpr void setBytes(T& dest, const std::uint8_t* b, std::size_t len) { + dest.bytesLen = len; + std::memcpy(&dest.bytes[0], b, len); +} + +struct test_vectors_t { + const char* input; + const std::uint8_t digest[32]; +}; + +constexpr std::uint8_t sign_test[] = {0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, + 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}; + +constexpr test_vectors_t sha_256_test[] = { + {"", {0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}}, + {"abc", {0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23, + 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}}}; + +// Test vectors from ISO 15118-2 Section J.2 +// checked okay (see iso_priv.pem) +constexpr std::uint8_t iso_private_key[] = {0xb9, 0x13, 0x49, 0x63, 0xf5, 0x1c, 0x44, 0x14, 0x73, 0x84, 0x35, + 0x05, 0x7f, 0x97, 0xbb, 0xf1, 0x01, 0x0c, 0xab, 0xcb, 0x8d, 0xbd, + 0xe9, 0xc5, 0xd4, 0x81, 0x38, 0x39, 0x6a, 0xa9, 0x4b, 0x9d}; +// checked okay (see iso_priv.pem) +constexpr std::uint8_t iso_public_key[] = {0x43, 0xe4, 0xfc, 0x4c, 0xcb, 0x64, 0x39, 0x04, 0x27, 0x9c, 0x7a, 0x5e, 0x65, + 0x76, 0xb3, 0x23, 0xe5, 0x5e, 0xc7, 0x9f, 0xf0, 0xe5, 0xa4, 0x05, 0x6e, 0x33, + 0x40, 0x84, 0xcb, 0xc3, 0x36, 0xff, 0x46, 0xe4, 0x4c, 0x1a, 0xdd, 0xf6, 0x91, + 0x62, 0xe5, 0x19, 0x2c, 0x2a, 0x83, 0xfc, 0x2b, 0xca, 0x9d, 0x8f, 0x46, 0xec, + 0xf4, 0xb7, 0x80, 0x67, 0xc2, 0x47, 0x6f, 0x6b, 0x3f, 0x34, 0x60, 0x0e}; + +// EXI AuthorizationReq: checked okay (hash computes correctly) +constexpr std::uint8_t iso_exi_a[] = {0x80, 0x04, 0x01, 0x52, 0x51, 0x0c, 0x40, 0x82, 0x9b, 0x7b, 0x6b, 0x29, 0x02, + 0x93, 0x0b, 0x73, 0x23, 0x7b, 0x69, 0x02, 0x23, 0x0b, 0xa3, 0x09, 0xe8}; + +// checked okay +constexpr std::uint8_t iso_exi_a_hash[] = {0xd1, 0xb5, 0xe0, 0x3d, 0x00, 0x65, 0xbe, 0xe5, 0x6b, 0x31, 0x79, + 0x84, 0x45, 0x30, 0x51, 0xeb, 0x54, 0xca, 0x18, 0xfc, 0x0e, 0x09, + 0x16, 0x17, 0x4f, 0x8b, 0x3c, 0x77, 0xa9, 0x8f, 0x4a, 0xa9}; + +// EXI AuthorizationReq signature block: checked okay (hash computes correctly) +constexpr std::uint8_t iso_exi_b[] = { + 0x80, 0x81, 0x12, 0xb4, 0x3a, 0x3a, 0x38, 0x1d, 0x17, 0x97, 0xbb, 0xbb, 0xbb, 0x97, 0x3b, 0x99, 0x97, 0x37, 0xb9, + 0x33, 0x97, 0xaa, 0x29, 0x17, 0xb1, 0xb0, 0xb7, 0x37, 0xb7, 0x34, 0xb1, 0xb0, 0xb6, 0x16, 0xb2, 0xbc, 0x34, 0x97, + 0xa1, 0xab, 0x43, 0xa3, 0xa3, 0x81, 0xd1, 0x79, 0x7b, 0xbb, 0xbb, 0xb9, 0x73, 0xb9, 0x99, 0x73, 0x7b, 0x93, 0x39, + 0x79, 0x91, 0x81, 0x81, 0x89, 0x79, 0x81, 0xa1, 0x7b, 0xc3, 0x6b, 0x63, 0x23, 0x9b, 0x4b, 0x39, 0x6b, 0x6b, 0x7b, + 0x93, 0x29, 0x1b, 0x2b, 0x1b, 0x23, 0x9b, 0x09, 0x6b, 0x9b, 0x43, 0x09, 0x91, 0xa9, 0xb2, 0x20, 0x62, 0x34, 0x94, + 0x43, 0x10, 0x25, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72, + 0x67, 0x2f, 0x54, 0x52, 0x2f, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x2d, 0x65, 0x78, 0x69, 0x2f, + 0x48, 0x52, 0xd0, 0xe8, 0xe8, 0xe0, 0x74, 0x5e, 0x5e, 0xee, 0xee, 0xee, 0x5c, 0xee, 0x66, 0x5c, 0xde, 0xe4, 0xce, + 0x5e, 0x64, 0x60, 0x60, 0x62, 0x5e, 0x60, 0x68, 0x5e, 0xf0, 0xda, 0xd8, 0xca, 0xdc, 0xc6, 0x46, 0xe6, 0xd0, 0xc2, + 0x64, 0x6a, 0x6c, 0x84, 0x1a, 0x36, 0xbc, 0x07, 0xa0, 0x0c, 0xb7, 0xdc, 0xad, 0x66, 0x2f, 0x30, 0x88, 0xa6, 0x0a, + 0x3d, 0x6a, 0x99, 0x43, 0x1f, 0x81, 0xc1, 0x22, 0xc2, 0xe9, 0xf1, 0x67, 0x8e, 0xf5, 0x31, 0xe9, 0x55, 0x23, 0x70}; + +// checked okay +constexpr std::uint8_t iso_exi_b_hash[] = {0xa4, 0xe9, 0x03, 0xe1, 0x82, 0x43, 0x04, 0x1b, 0x55, 0x4e, 0x11, + 0x64, 0x7e, 0x10, 0x1e, 0xd2, 0x5f, 0xc9, 0xf2, 0x15, 0x2a, 0xf4, + 0x67, 0x40, 0x14, 0xfe, 0x2a, 0xde, 0xac, 0x1e, 0x1c, 0xf7}; + +// checked okay (verifies iso_exi_b_hash with iso_priv.pem) +constexpr std::uint8_t iso_exi_sig[] = {0x4c, 0x8f, 0x20, 0xc1, 0x40, 0x0b, 0xa6, 0x76, 0x06, 0xaa, 0x48, 0x11, 0x57, + 0x2a, 0x2f, 0x1a, 0xd3, 0xc1, 0x50, 0x89, 0xd9, 0x54, 0x20, 0x36, 0x34, 0x30, + 0xbb, 0x26, 0xb4, 0x9d, 0xb1, 0x04, 0xf0, 0x8d, 0xfa, 0x8b, 0xf8, 0x05, 0x5e, + 0x63, 0xa4, 0xb7, 0x5a, 0x8d, 0x31, 0x69, 0x20, 0x6f, 0xa8, 0xd5, 0x43, 0x08, + 0xba, 0x58, 0xf0, 0x56, 0x6b, 0x96, 0xba, 0xf6, 0x92, 0xce, 0x59, 0x50}; + +const char iso_exi_a_hash_b64[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk="; +const char iso_exi_a_hash_b64_nl[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=\n"; + +const char iso_exi_sig_b64[] = + "TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBvqNVDCLpY8FZrlrr2ks5ZUA=="; +const char iso_exi_sig_b64_nl[] = + "TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBv\nqNVDCLpY8FZrlrr2ks5ZUA==\n"; + +TEST(openssl, verifyIso) { + auto* bio = BIO_new_file("iso_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + BIO_free(bio); + + auto [sig, siglen] = openssl::bn_to_signature(&iso_exi_sig[0], &iso_exi_sig[32]); + EXPECT_TRUE(openssl::verify(pkey, sig.get(), siglen, &iso_exi_b_hash[0], sizeof(iso_exi_b_hash))); + EVP_PKEY_free(pkey); +} + +TEST(isoExi, signature) { + // The message is: + // header { SessionID, Signature} + // body { AuthorizationReq } + // the test vector doesn't include the entire encoded message + + auto* bio = BIO_new_file("iso_priv.pem", "r"); + ASSERT_NE(bio, nullptr); + auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + ASSERT_NE(pkey, nullptr); + BIO_free(bio); + + // decode the test vector AuthorizationReq + struct iso2_exiFragment exi_a {}; + init_iso2_exiFragment(&exi_a); + init_iso2_AuthorizationReqType(&exi_a.AuthorizationReq); + + exi_bitstream_t stream; + exi_bitstream_init(&stream, const_cast(&iso_exi_a[0]), sizeof(iso_exi_a), 0, nullptr); + EXPECT_EQ(decode_iso2_exiFragment(&stream, &exi_a), 0); + + // manually populate the Signature structure + struct iso2_SignatureType sig {}; + init_iso2_SignatureType(&sig); + + // SignedInfo + setCharacters(sig.SignedInfo.CanonicalizationMethod.Algorithm, "http://www.w3.org/TR/canonical-exi/"); + setCharacters(sig.SignedInfo.SignatureMethod.Algorithm, "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"); + sig.SignedInfo.Reference.arrayLen = 1; + sig.SignedInfo.Reference.array[0].URI_isUsed = 1; + setCharacters(sig.SignedInfo.Reference.array[0].URI, "#ID1"); + sig.SignedInfo.Reference.array[0].Transforms_isUsed = 1; + setCharacters(sig.SignedInfo.Reference.array[0].Transforms.Transform.Algorithm, + "http://www.w3.org/TR/canonical-exi/"); + setCharacters(sig.SignedInfo.Reference.array[0].DigestMethod.Algorithm, "http://www.w3.org/2001/04/xmlenc#sha256"); + setBytes(sig.SignedInfo.Reference.array[0].DigestValue, &iso_exi_a_hash[0], ::openssl::sha_256_digest_size); + // SignatureValue + setBytes(sig.SignatureValue.CONTENT, &iso_exi_sig[0], ::openssl::signature_size); + EXPECT_TRUE(crypto::openssl::check_iso2_signature(&sig, pkey, &exi_a)); + + EVP_PKEY_free(pkey); +} + +} // namespace diff --git a/modules/EvseV2G/tests/requirement.cpp b/modules/EvseV2G/tests/requirement.cpp new file mode 100644 index 0000000000..cec9c44a7e --- /dev/null +++ b/modules/EvseV2G/tests/requirement.cpp @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include + +#include "utils/types.hpp" + +Requirement::Requirement(const std::string& requirement_id_, size_t index_) { +} +bool Requirement::operator<(const Requirement& rhs) const { + return true; +} diff --git a/modules/EvseV2G/tests/v2g_main.cpp b/modules/EvseV2G/tests/v2g_main.cpp new file mode 100644 index 0000000000..a773dcad20 --- /dev/null +++ b/modules/EvseV2G/tests/v2g_main.cpp @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +/* + * testing options + * openssl s_client -connect [fe80::ae91:a1ff:fec9:a947%3]:64109 -verify 2 -CAfile server_root_cert.pem -cert + * client_cert.pem -cert_chain client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname + * evse.pionix.de -status + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ISO15118_chargerImplStub.hpp" +#include "evse_securityIntfStub.hpp" + +#include +#include +#include + +using namespace std::chrono_literals; + +// needs to be in the global namespace +int v2g_handle_connection(struct v2g_connection* conn) { + assert(conn != nullptr); + assert(conn->read != nullptr); + assert(conn->write != nullptr); + + std::array buffer{}; + bool bExit = false; + while (!bExit) { + const ssize_t readbytes = conn->read(conn, buffer.data(), buffer.size()); + if (readbytes > 0) { + const ssize_t writebytes = conn->write(conn, buffer.data(), readbytes); + if (writebytes <= 0) { + bExit = true; + } + } else if (readbytes < 0) { + bExit = true; + } + } + return 0; +} + +namespace { + +const char* interface; + +void parse_options(int argc, char** argv) { + interface = nullptr; + int c; + + while ((c = getopt(argc, argv, "hi:")) != -1) { + switch (c) { + case 'i': + interface = optarg; + break; + case 'h': + case '?': + std::cout << "Usage: " << argv[0] << " -i " << std::endl; + exit(1); + break; + default: + exit(2); + } + } + + if (interface == nullptr) { + std::cerr << "Error: " << argv[0] << " requires -i " << std::endl; + exit(3); + } +} + +// EvseSecurity "implementation" +struct EvseSecurity : public module::stub::evse_securityIntfStub { + Result get_verify_file(const Requirement& req, const Parameters& args) override { + return "client_root_cert.pem"; + } + + virtual Result get_leaf_certificate_info(const Requirement& req, const Parameters& args) { + // using types::evse_security::CertificateHashDataType; + using types::evse_security::CertificateInfo; + using types::evse_security::CertificateOCSP; + using types::evse_security::GetCertificateInfoResult; + using types::evse_security::GetCertificateInfoStatus; + using types::evse_security::HashAlgorithm; + + CertificateInfo cert_info; + cert_info.key = "server_priv.pem"; + cert_info.certificate = "server_chain.pem"; + cert_info.certificate_count = 2; + cert_info.ocsp = {{ + {HashAlgorithm::SHA256}, + {"ocsp_response.der"}, + }, + { + {HashAlgorithm::SHA256}, + {"ocsp_response.der"}, + }}; + + const GetCertificateInfoResult res = { + GetCertificateInfoStatus::Accepted, + cert_info, + }; + json jres = res; + return jres; + } +}; + +} // namespace + +int main(int argc, char** argv) { + parse_options(argc, argv); + + tls::Server tls_server; + module::stub::ISO15118_chargerImplStub charger; + EvseSecurity security; + + auto* ctx = v2g_ctx_create(&charger, &security); + if (ctx == nullptr) { + std::cerr << "failed to create context" << std::endl; + } else { +#ifndef EVEREST_MBED_TLS + ctx->tls_server = &tls_server; +#endif + ctx->if_name = interface; + ctx->tls_security = TLS_SECURITY_FORCE; + ctx->is_connection_terminated = false; + + std::thread stop([ctx]() { + // there is a 60 second read timeout in connection.cpp + std::this_thread::sleep_for(75s); + std::cout << "shutdown" << std::endl; + ctx->is_connection_terminated = true; + ctx->shutdown = true; + }); + + std::cout << "connection_init" << std::endl; + if (::connection_init(ctx) != 0) { + std::cerr << "connection_init failed" << std::endl; + } else { + std::cout << "connection_init started" << std::endl; + } + + std::cout << "connection_start_servers " << std::endl; + if (::connection_start_servers(ctx) != 0) { + std::cerr << "connection_start_servers failed" << std::endl; + } else { + std::cout << "connection_start_servers started" << std::endl; + } + + stop.join(); + tls::ServerConnection::wait_all_closed(); + + // wait for v2g_ctx_start_events thread to stop + std::this_thread::sleep_for(2s); + v2g_ctx_free(ctx); + } + + return 0; +} diff --git a/modules/EvseV2G/v2g.hpp b/modules/EvseV2G/v2g.hpp index 38d71311f6..e0de0b474b 100644 --- a/modules/EvseV2G/v2g.hpp +++ b/modules/EvseV2G/v2g.hpp @@ -6,6 +6,13 @@ #include #include + +#include +#include +#include +#include + +#ifdef EVEREST_MBED_TLS #include #include #include @@ -13,21 +20,22 @@ #include #include #include -#include -#include -#include -#include #if MBEDTLS_VERSION_MINOR == 2 #include #else #include #endif +#else +#include +#include +#endif // EVEREST_MBED_TLS + #include #include #include - #include #include + #include #include @@ -170,9 +178,13 @@ struct SAE_Bidi_Data { /** * Abstracts a charging port, i.e. a power outlet in this daemon. + * + * **** NOTE **** + * Be very careful about adding C++ objects since constructors and + * destructors are not called. (see v2g_ctx_create() and calloc) */ struct v2g_context { - volatile int shutdown; + std::atomic_bool shutdown; evse_securityIntf* r_security; ISO15118_chargerImplBase* p_charger; @@ -202,6 +214,7 @@ struct v2g_context { pthread_t tcp_thread; +#ifdef EVEREST_MBED_TLS mbedtls_ssl_config ssl_config; mbedtls_x509_crt* evseTlsCrt; uint8_t num_of_tls_crt; @@ -209,10 +222,17 @@ struct v2g_context { mbedtls_x509_crt v2g_root_crt; mbedtls_net_context tls_socket; keylogDebugCtx tls_log_ctx; - bool tls_key_logging; pthread_t tls_thread; mbedtls_x509_crt mop_root_ca_list; +#else + struct { + int fd; + } tls_socket; + tls::Server* tls_server; +#endif // EVEREST_MBED_TLS + + bool tls_key_logging; pthread_mutex_t mqtt_lock; pthread_cond_t mqtt_cond; @@ -316,11 +336,14 @@ struct v2g_context { types::authorization::CertificateStatus certificate_status; // for PnC bool authorization_rejected; // for PnC +#ifdef EVEREST_MBED_TLS + // needed by iso_server.cpp + // for OpenSSL the key is part of v2g_connection struct { - bool valid_crt; - mbedtls_x509_crt crt; mbedtls_ecdsa_context pubkey; - } contract; // for PnC + } contract; // for PnC +#endif // EVEREST_MBED_TLS + bool renegotiation_required; /* Is set to true if renegotiation is required. Only relevant for ISO */ bool is_charging; /* set to true if ChargeProgress is set to Start */ uint8_t sa_schedule_tuple_id; /* selected SA schedule tuple ID*/ @@ -363,6 +386,8 @@ struct v2g_connection { struct v2g_context* ctx; bool is_tls_connection; + +#ifdef EVEREST_MBED_TLS union { struct { mbedtls_ssl_config* ssl_config; @@ -371,6 +396,18 @@ struct v2g_connection { } ssl; int socket_fd; } conn; +#else + // used for non-TLS connections + struct { + int socket_fd; + } conn; + + tls::Connection* tls_connection; + openssl::PKey_ptr* pubkey; +#endif // EVEREST_MBED_TLS + + ssize_t (*read)(struct v2g_connection* conn, unsigned char* buf, std::size_t count); + ssize_t (*write)(struct v2g_connection* conn, unsigned char* buf, std::size_t count); /* V2GTP EXI encoding/decoding stuff */ uint8_t* buffer; diff --git a/modules/EvseV2G/v2g_ctx.cpp b/modules/EvseV2G/v2g_ctx.cpp index 0d8ec86a07..883ff9108b 100644 --- a/modules/EvseV2G/v2g_ctx.cpp +++ b/modules/EvseV2G/v2g_ctx.cpp @@ -2,11 +2,11 @@ // Copyright (C) 2022-2023 chargebyte GmbH // Copyright (C) 2022-2023 Contributors to EVerest +#include +#include #include #include #include -#include -#include #include // sleep #include "log.hpp" @@ -295,6 +295,8 @@ void v2g_ctx_init_charging_values(struct v2g_context* const ctx) { struct v2g_context* v2g_ctx_create(ISO15118_chargerImplBase* p_chargerImplBase, evse_securityIntf* r_security) { struct v2g_context* ctx; + // TODO There are c++ objects within v2g_context and calloc doesn't call initialisers. + // free() will not call destructors ctx = static_cast(calloc(1, sizeof(*ctx))); if (!ctx) return NULL; @@ -308,7 +310,7 @@ struct v2g_context* v2g_ctx_create(ISO15118_chargerImplBase* p_chargerImplBase, ctx->basic_config.evse_ac_current_limit = 0.0f; ctx->local_tcp_addr = NULL; - ctx->local_tcp_addr = NULL; + ctx->local_tls_addr = NULL; ctx->is_dc_charger = true; @@ -323,7 +325,9 @@ struct v2g_context* v2g_ctx_create(ISO15118_chargerImplBase* p_chargerImplBase, ctx->sdp_socket = -1; ctx->tcp_socket = -1; ctx->tls_socket.fd = -1; +#ifdef EVEREST_MBED_TLS memset(&ctx->tls_log_ctx, 0, sizeof(keylogDebugCtx)); +#endif // EVEREST_MBED_TLS ctx->tls_key_logging = false; ctx->debugMode = false; @@ -361,6 +365,7 @@ struct v2g_context* v2g_ctx_create(ISO15118_chargerImplBase* p_chargerImplBase, } static void v2g_ctx_free_tls(struct v2g_context* ctx) { +#ifdef EVEREST_MBED_TLS mbedtls_net_free(&ctx->tls_socket); for (uint8_t idx = 0; idx < ctx->num_of_tls_crt; idx++) { @@ -380,6 +385,7 @@ static void v2g_ctx_free_tls(struct v2g_context* ctx) { fclose(ctx->tls_log_ctx.file); memset(&ctx->tls_log_ctx, 0, sizeof(ctx->tls_log_ctx)); } +#endif // EVEREST_MBED_TLS } void v2g_ctx_free(struct v2g_context* ctx) { diff --git a/modules/EvseV2G/v2g_server.cpp b/modules/EvseV2G/v2g_server.cpp index 6f3da91fc7..16b5b83f29 100644 --- a/modules/EvseV2G/v2g_server.cpp +++ b/modules/EvseV2G/v2g_server.cpp @@ -3,14 +3,15 @@ // Copyright (C) 2023 Contributors to EVerest #include "v2g_server.hpp" +#include +#include #include -#include #include #include -#include - +#ifdef EVEREST_MBED_TLS #include +#endif // EVEREST_MBED_TLS #include #include @@ -117,6 +118,9 @@ static void publish_var_V2G_Message(v2g_connection* conn, bool is_req) { tempbuff++; } + std::string EXI_Base64; + +#ifdef EVEREST_MBED_TLS unsigned char* base64_buffer = NULL; size_t base64_buffer_len = 0; mbedtls_base64_encode(NULL, 0, &base64_buffer_len, conn->buffer, (size_t)conn->payload_len + V2GTP_HEADER_LENGTH); @@ -127,10 +131,18 @@ static void publish_var_V2G_Message(v2g_connection* conn, bool is_req) { dlog(DLOG_LEVEL_WARNING, "Unable to base64 encode EXI buffer"); } - v2gMessage.V2G_Message_EXI_Base64 = std::string(reinterpret_cast(base64_buffer), base64_buffer_len); + EXI_Base64 = std::string(reinterpret_cast(base64_buffer), base64_buffer_len); if (base64_buffer != NULL) { free(base64_buffer); } +#else + EXI_Base64 = openssl::base64_encode(conn->buffer, conn->payload_len + V2GTP_HEADER_LENGTH); + if (EXI_Base64.size() == 0) { + dlog(DLOG_LEVEL_WARNING, "Unable to base64 encode EXI buffer"); + } +#endif // EVEREST_MBED_TLS + + v2gMessage.V2G_Message_EXI_Base64 = EXI_Base64; v2gMessage.V2G_Message_ID = get_V2G_Message_ID(conn->ctx->current_v2g_msg, conn->ctx->selected_protocol, is_req); v2gMessage.V2G_Message_EXI_Hex = msg_as_hex_string; conn->ctx->p_charger->publish_V2G_Messages(v2gMessage); @@ -142,10 +154,13 @@ static void publish_var_V2G_Message(v2g_connection* conn, bool is_req) { * \return Returns 0 if the V2G-session was successfully stopped, otherwise -1. */ static int v2g_incoming_v2gtp(struct v2g_connection* conn) { + assert(conn != nullptr); + assert(conn->read != nullptr); + int rv; /* read and process header */ - rv = connection_read(conn, conn->buffer, V2GTP_HEADER_LENGTH); + rv = conn->read(conn, conn->buffer, V2GTP_HEADER_LENGTH); if (rv < 0) { dlog(DLOG_LEVEL_ERROR, "connection_read(header) failed: %s", (rv == -1) ? strerror(errno) : "connection terminated"); @@ -182,7 +197,7 @@ static int v2g_incoming_v2gtp(struct v2g_connection* conn) { return -1; } /* read request */ - rv = connection_read(conn, &conn->buffer[V2GTP_HEADER_LENGTH], conn->payload_len); + rv = conn->read(conn, &conn->buffer[V2GTP_HEADER_LENGTH], conn->payload_len); if (rv < 0) { dlog(DLOG_LEVEL_ERROR, "connection_read(payload) failed: %s", (rv == -1) ? strerror(errno) : "connection terminated"); @@ -205,12 +220,15 @@ static int v2g_incoming_v2gtp(struct v2g_connection* conn) { * \return Returns 0 if the v2g-session was successfully stopped, otherwise -1. */ int v2g_outgoing_v2gtp(struct v2g_connection* conn) { + assert(conn != nullptr); + assert(conn->write != nullptr); + /* fixup/create header */ const auto len = exi_bitstream_get_length(&conn->stream); V2GTP_WriteHeader(conn->buffer, len - V2GTP_HEADER_LENGTH); - if (connection_write(conn, conn->buffer, len) == -1) { + if (conn->write(conn, conn->buffer, len) == -1) { dlog(DLOG_LEVEL_ERROR, "connection_write(header) failed: %s", strerror(errno)); return -1; } diff --git a/modules/LemDCBM400600/CMakeLists.txt b/modules/LemDCBM400600/CMakeLists.txt index 58de474940..ebe5ebc10a 100644 --- a/modules/LemDCBM400600/CMakeLists.txt +++ b/modules/LemDCBM400600/CMakeLists.txt @@ -5,8 +5,6 @@ # module setup: # - ${MODULE_NAME}: module name -add_compile_options(-Wpedantic) - ev_setup_cpp_module() # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 diff --git a/modules/LemDCBM400600/LemDCBM400600.hpp b/modules/LemDCBM400600/LemDCBM400600.hpp index d946552657..87f3ba149e 100644 --- a/modules/LemDCBM400600/LemDCBM400600.hpp +++ b/modules/LemDCBM400600/LemDCBM400600.hpp @@ -27,12 +27,17 @@ struct Conf { int ntp_server_1_port; std::string ntp_server_2_ip_addr; int ntp_server_2_port; + std::string meter_timezone; + std::string meter_dst; int resilience_initial_connection_retries; int resilience_initial_connection_retry_delay; int resilience_transaction_request_retries; int resilience_transaction_request_retry_delay; int cable_id; int tariff_id; + int SC; + std::string UV; + std::string UD; }; class LemDCBM400600 : public Everest::ModuleBase { diff --git a/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp b/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp index 7c2c7932ea..1ad3d7bf78 100644 --- a/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp +++ b/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp @@ -7,14 +7,17 @@ namespace module::main { void LemDCBM400600Controller::init() { try { + this->time_sync_helper->set_time_config_params(config.meter_timezone, config.meter_dst); call_with_retry([this]() { this->fetch_meter_id_from_device(); }, this->config.init_number_of_http_retries, this->config.init_retry_wait_in_milliseconds); } catch (HttpClientError& http_client_error) { - EVLOG_error << "Initialization of LemDCBM400600Controller failed with http client error: " + EVLOG_error << "Initialization of LemDCBM400600Controller failed with http " + "client error: " << http_client_error.what(); throw; } catch (DCBMUnexpectedResponseException& dcbm_error) { - EVLOG_error << "Initialization of LemDCBM400600Controller failed due an unexpected device response: " + EVLOG_error << "Initialization of LemDCBM400600Controller failed due an " + "unexpected device response: " << dcbm_error.what(); throw; } @@ -22,6 +25,18 @@ void LemDCBM400600Controller::init() { this->time_sync_helper->restart_unsafe_period(); } +std::vector split(const std::string& str, char delimiter) { + std::vector tokens; + std::string token; + std::stringstream ss(str); + + while (std::getline(ss, token, delimiter)) { + tokens.push_back(token); + } + + return tokens; +} + void LemDCBM400600Controller::fetch_meter_id_from_device() { auto status_response = this->http_client->get("/v1/status"); @@ -29,7 +44,14 @@ void LemDCBM400600Controller::fetch_meter_id_from_device() { throw UnexpectedDCBMResponseCode("/v1/status", 200, status_response); } try { - this->meter_id = json::parse(status_response.body).at("meterId"); + json data = json::parse(status_response.body); + this->meter_id = data.at("meterId"); + this->public_key_ocmf = data.at("publicKeyOcmf"); + this->trasaction_is_ongoing = data.at("status").at("bits").at("transactionIsOnGoing"); + std::string version = data.at("version").at("applicationFirmwareVersion"); + auto components = split(version, '.'); + this->v2_capable = + ((components.size() == 4) && (components[1] > "1")); // the major version must be newer than 1 } catch (json::exception& json_error) { throw UnexpectedDCBMResponseBody( "/v1/status", fmt::format("Json error {} for body {}", json_error.what(), status_response.body)); @@ -62,11 +84,11 @@ LemDCBM400600Controller::start_transaction(const types::powermeter::TransactionR void LemDCBM400600Controller::request_device_to_start_transaction(const types::powermeter::TransactionReq& value) { this->time_sync_helper->sync(*this->http_client); - auto response = this->http_client->post( - "/v1/legal", module::main::LemDCBM400600Controller::transaction_start_request_to_dcbm_payload( - value, this->config.cable_id, this->config.tariff_id)); + const std::string endpoint = v2_capable ? "/v2/legal" : "/v1/legal"; + const std::string payload = this->transaction_start_request_to_dcbm_payload(value); + auto response = this->http_client->post(endpoint, payload); if (response.status_code != 201) { - throw UnexpectedDCBMResponseCode("/v1/legal", 201, response); + throw UnexpectedDCBMResponseCode(endpoint, 201, response); } try { bool running = json::parse(response.body).at("running"); @@ -75,7 +97,7 @@ void LemDCBM400600Controller::request_device_to_start_transaction(const types::p "/v1/legal", fmt::format("Created transaction {} has state running = false.", value.transaction_id)); } } catch (json::exception& json_error) { - throw UnexpectedDCBMResponseBody("/v1/legal", + throw UnexpectedDCBMResponseBody(endpoint, fmt::format("Json error {} for body '{}'", json_error.what(), response.body)); } } @@ -108,7 +130,8 @@ LemDCBM400600Controller::stop_transaction(const std::string& transaction_id) { } void LemDCBM400600Controller::request_device_to_stop_transaction(const std::string& transaction_id) { - std::string endpoint = fmt::format("/v1/legal?transactionId={}", transaction_id); + std::string endpoint = v2_capable ? fmt::format("/v2/legal?transactionId={}", transaction_id) + : fmt::format("/v1/legal?transactionId={}", transaction_id); auto legal_api_response = this->http_client->put(endpoint, R"({"running": false})"); if (legal_api_response.status_code != 200) { @@ -119,9 +142,9 @@ void LemDCBM400600Controller::request_device_to_stop_transaction(const std::stri int status = json::parse(legal_api_response.body).at("meterValue").at("transactionStatus"); bool transaction_is_ongoing = (status & 0b100) != 0; // third status bit "transactionIsOnGoing" must be false if (transaction_is_ongoing) { - throw UnexpectedDCBMResponseBody( - endpoint, fmt::format("Transaction stop request for transaction {} returned device status {}.", - transaction_id, status)); + throw UnexpectedDCBMResponseBody(endpoint, fmt::format("Transaction stop request for transaction {} " + "returned device status {}.", + transaction_id, status)); } } catch (json::exception& json_error) { throw UnexpectedDCBMResponseBody( @@ -130,7 +153,8 @@ void LemDCBM400600Controller::request_device_to_stop_transaction(const std::stri } std::string LemDCBM400600Controller::fetch_ocmf_result(const std::string& transaction_id) { - const std::string ocmf_endpoint = fmt::format("/v1/ocmf?transactionId={}", transaction_id); + const std::string ocmf_endpoint = v2_capable ? fmt::format("/v2/ocmf?transactionId={}", transaction_id) + : fmt::format("/v1/ocmf?transactionId={}", transaction_id); auto ocmf_api_response = this->http_client->get(ocmf_endpoint); if (ocmf_api_response.status_code != 200) { @@ -147,21 +171,21 @@ std::string LemDCBM400600Controller::fetch_ocmf_result(const std::string& transa types::powermeter::Powermeter LemDCBM400600Controller::get_powermeter() { this->time_sync_helper->sync_if_deadline_expired(*this->http_client); - auto response = this->http_client->get("/v1/livemeasure"); + const std::string endpoint = v2_capable ? "/v2/livemeasure" : "/v1/livemeasure"; + auto response = this->http_client->get(endpoint); if (response.status_code != 200) { - throw UnexpectedDCBMResponseCode("/v1/livemeasure", 200, response); + throw UnexpectedDCBMResponseCode(endpoint, 200, response); } - types::powermeter::Powermeter value; try { - this->convert_livemeasure_to_powermeter(response.body, value); - return value; + return this->convert_livemeasure_to_powermeter(response.body); } catch (json::exception& json_error) { - throw UnexpectedDCBMResponseBody("/v1/livemeasure", fmt::format("Json error '{}'", json_error.what())); + throw UnexpectedDCBMResponseBody(endpoint, fmt::format("Json error '{}'", json_error.what())); } } -void LemDCBM400600Controller::convert_livemeasure_to_powermeter(const std::string& livemeasure, - types::powermeter::Powermeter& powermeter) { +types::powermeter::Powermeter +LemDCBM400600Controller::convert_livemeasure_to_powermeter(const std::string& livemeasure) { + types::powermeter::Powermeter powermeter; json data = json::parse(livemeasure); powermeter.timestamp = data.at("timestamp"); powermeter.meter_id.emplace(this->meter_id); @@ -174,26 +198,44 @@ void LemDCBM400600Controller::convert_livemeasure_to_powermeter(const std::strin current.DC = data.at("current"); powermeter.current_A.emplace(current); powermeter.power_W.emplace(types::units::Power{data.at("power")}); + powermeter.temperatures.emplace({types::temperature::Temperature{data.at("temperatureH"), "temperatureH"}, + types::temperature::Temperature{data.at("temperatureL"), "temperatureL"}}); + return powermeter; } + std::string -LemDCBM400600Controller::transaction_start_request_to_dcbm_payload(const types::powermeter::TransactionReq& request, - const int cable_id, const int tariff_id) { - return nlohmann::ordered_json{{"evseId", request.evse_id}, - {"transactionId", request.transaction_id}, - {"clientId", request.transaction_id}, - {"tariffId", tariff_id}, - {"cableId", cable_id}, - {"userData", ""}} - .dump(); +LemDCBM400600Controller::transaction_start_request_to_dcbm_payload(const types::powermeter::TransactionReq& request) { + if (this->v2_capable) { + return nlohmann::ordered_json{{"evseId", request.evse_id}, + {"transactionId", request.transaction_id}, + {"clientId", request.identification_data.value_or("")}, + {"tariffId", this->config.tariff_id}, + {"TT", request.tariff_text.value_or("")}, + {"UV", this->config.UV}, + {"UD", this->config.UD}, + {"cableId", this->config.cable_id}, + {"userData", ""}, + {"SC", this->config.SC}} + .dump(); + } else { + return nlohmann::ordered_json{{"evseId", request.evse_id}, + {"transactionId", request.transaction_id}, + {"clientId", request.identification_data.value_or("")}, + {"tariffId", this->config.tariff_id}, + {"cableId", this->config.cable_id}, + {"userData", ""}} + .dump(); + } } std::pair LemDCBM400600Controller::get_transaction_stop_time_bounds() { // The LEM DCBM 400/600 Operations manual (7.2.2.) states - // "Minimum duration for transactions is 2 minutes, to prevent potential memory storage weaknesses." - // Further, the communication protocol states (4.2.9.): - // "If after a period of 48h the time was not set, time synchronization expires (preventing new transactions and - // invalidating on-going one)."" Since during an ongoing transaction, now time can synced, the max duration is set - // to 48 hours (minus a small delta). + // "Minimum duration for transactions is 2 minutes, to prevent potential + // memory storage weaknesses." Further, the communication protocol states + // (4.2.9.): "If after a period of 48h the time was not set, time + // synchronization expires (preventing new transactions and invalidating + // on-going one)."" Since during an ongoing transaction, now time can synced, + // the max duration is set to 48 hours (minus a small delta). auto now = std::chrono::time_point::clock::now(); return { Everest::Date::to_rfc3339(now + std::chrono::minutes(2)), diff --git a/modules/LemDCBM400600/main/lem_dcbm_400600_controller.hpp b/modules/LemDCBM400600/main/lem_dcbm_400600_controller.hpp index a7bbf4e8c7..f44ee65dc7 100644 --- a/modules/LemDCBM400600/main/lem_dcbm_400600_controller.hpp +++ b/modules/LemDCBM400600/main/lem_dcbm_400600_controller.hpp @@ -31,6 +31,16 @@ class LemDCBM400600Controller { const int cable_id; // Used for a unique transaction tariff designation const int tariff_id; + // meter time zone + const std::string meter_timezone; + // the meter Daylight Saving Time (DST) settings + const std::string meter_dst; + // SC + const int SC; + // UV + const std::string UV; + // UD + const std::string UD; }; class DCBMUnexpectedResponseException : public std::exception { @@ -79,6 +89,11 @@ class LemDCBM400600Controller { private: const std::unique_ptr http_client; std::string meter_id; + std::string public_key; + std::string public_key_ocmf; + std::string version; + bool v2_capable = false; + bool trasaction_is_ongoing = false; Conf config; std::unique_ptr time_sync_helper; @@ -86,9 +101,8 @@ class LemDCBM400600Controller { void request_device_to_start_transaction(const types::powermeter::TransactionReq& value); void request_device_to_stop_transaction(const std::string& transaction_id); std::string fetch_ocmf_result(const std::string& transaction_id); - void convert_livemeasure_to_powermeter(const std::string& livemeasure, types::powermeter::Powermeter& powermeter); - static std::string transaction_start_request_to_dcbm_payload(const types::powermeter::TransactionReq& request, - const int cable_id, const int tariff_id); + types::powermeter::Powermeter convert_livemeasure_to_powermeter(const std::string& livemeasure); + std::string transaction_start_request_to_dcbm_payload(const types::powermeter::TransactionReq& request); static std::pair get_transaction_stop_time_bounds(); template @@ -132,6 +146,9 @@ class LemDCBM400600Controller { types::powermeter::TransactionStartResponse start_transaction(const types::powermeter::TransactionReq& value); types::powermeter::TransactionStopResponse stop_transaction(const std::string& transaction_id); types::powermeter::Powermeter get_powermeter(); + inline std::string get_public_key_ocmf() { + return public_key_ocmf; + } }; } // namespace module::main diff --git a/modules/LemDCBM400600/main/lem_dcbm_time_sync_helper.cpp b/modules/LemDCBM400600/main/lem_dcbm_time_sync_helper.cpp index 28e77bd838..77da42e9da 100644 --- a/modules/LemDCBM400600/main/lem_dcbm_time_sync_helper.cpp +++ b/modules/LemDCBM400600/main/lem_dcbm_time_sync_helper.cpp @@ -25,6 +25,11 @@ std::string LemDCBMTimeSyncHelper::generate_dcbm_ntp_config() { return config_json.dump(); } +void LemDCBMTimeSyncHelper::set_time_config_params(const std::string& meter_timezone, const std::string& meter_dst) { + this->meter_timezone = meter_timezone; + this->meter_dst = meter_dst; +} + void LemDCBMTimeSyncHelper::sync_if_deadline_expired(const HttpClientInterface& httpClient) { const std::lock_guard lock(this->time_sync_state_lock); @@ -54,6 +59,8 @@ void LemDCBMTimeSyncHelper::sync(const HttpClientInterface& httpClient) { this->set_ntp_settings_on_device(httpClient); } else { this->sync_system_time(httpClient); + this->sync_timezone(httpClient); + this->sync_dst(httpClient); } } @@ -73,7 +80,6 @@ bool LemDCBMTimeSyncHelper::is_setting_write_safe() const { } void LemDCBMTimeSyncHelper::set_ntp_settings_on_device(const HttpClientInterface& httpClient) { - HttpResponse response = httpClient.put("/v1/settings", this->generate_dcbm_ntp_config()); if (response.status_code != 200) { throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response); @@ -93,7 +99,8 @@ void LemDCBMTimeSyncHelper::set_ntp_settings_on_device(const HttpClientInterface void LemDCBMTimeSyncHelper::sync_system_time(const HttpClientInterface& httpClient) { std::string time_update = Everest::Date::to_rfc3339(date::utc_clock::now()); - HttpResponse response = httpClient.put("/v1/settings", std::string(R"({"time":{"utc":")") + time_update + R"("}})"); + std::string payload = R"({"time":{"utc":")" + time_update + R"("}})"; + HttpResponse response = httpClient.put("/v1/settings", payload); if (response.status_code != 200) { throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response); @@ -112,6 +119,52 @@ void LemDCBMTimeSyncHelper::sync_system_time(const HttpClientInterface& httpClie std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries; } } + +void LemDCBMTimeSyncHelper::sync_timezone(const HttpClientInterface& httpClient) { + std::string payload = std::string(R"({"time": {"tz":")") + meter_timezone + R"("}})"; + HttpResponse response = httpClient.put("/v1/settings", payload); + + if (response.status_code != 200) { + throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response); + } + bool success = nlohmann::json::parse(response.body).at("result") == 1; + if (!success) { + throw LemDCBM400600Controller::UnexpectedDCBMResponseBody( + "/v1/settings", "Timezone setting was rejected by the device, e.g. because of an ongoing transaction."); + } + + if (is_setting_write_safe()) { + this->deadline_for_next_sync = + std::chrono::steady_clock::now() + this->timing_constants.deadline_increment_after_sync; + } else { + this->deadline_for_next_sync = + std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries; + } +} + +void LemDCBMTimeSyncHelper::sync_dst(const HttpClientInterface& httpClient) { + std::string payload = std::string(R"({"time": {"dst":)") + meter_dst + R"(}})"; + HttpResponse response = httpClient.put("/v1/settings", payload); + + if (response.status_code != 200) { + throw LemDCBM400600Controller::UnexpectedDCBMResponseCode("/v1/settings", 200, response); + } + bool success = nlohmann::json::parse(response.body).at("result") == 1; + if (!success) { + throw LemDCBM400600Controller::UnexpectedDCBMResponseBody( + "/v1/settings", + "Daylight saving setting was rejected by the device, e.g. because of an ongoing transaction."); + } + + if (is_setting_write_safe()) { + this->deadline_for_next_sync = + std::chrono::steady_clock::now() + this->timing_constants.deadline_increment_after_sync; + } else { + this->deadline_for_next_sync = + std::chrono::steady_clock::now() + this->timing_constants.min_time_between_sync_retries; + } +} + void LemDCBMTimeSyncHelper::restart_unsafe_period() { this->unsafe_period_start_time = std::chrono::steady_clock::now(); deadline_for_next_sync = unsafe_period_start_time.value() + timing_constants.min_time_before_setting_write_is_safe; diff --git a/modules/LemDCBM400600/main/lem_dcbm_time_sync_helper.hpp b/modules/LemDCBM400600/main/lem_dcbm_time_sync_helper.hpp index 2788877732..5505842a4f 100644 --- a/modules/LemDCBM400600/main/lem_dcbm_time_sync_helper.hpp +++ b/modules/LemDCBM400600/main/lem_dcbm_time_sync_helper.hpp @@ -43,11 +43,17 @@ class LemDCBMTimeSyncHelper { } explicit LemDCBMTimeSyncHelper(ntp_server_spec ntp_spec, timing_config tc) : - timing_constants(tc), ntp_spec(std::move(ntp_spec)), unsafe_period_start_time({}) { + timing_constants(tc), + ntp_spec(std::move(ntp_spec)), + unsafe_period_start_time({}), + meter_timezone(""), + meter_dst("") { } virtual ~LemDCBMTimeSyncHelper() = default; + void set_time_config_params(const std::string& meter_timezone, const std::string& meter_dst); + virtual void sync_if_deadline_expired(const HttpClientInterface& httpClient); virtual void sync(const HttpClientInterface& httpClient); @@ -59,6 +65,10 @@ class LemDCBMTimeSyncHelper { const ntp_server_spec ntp_spec; // Timing constants (can be overridden in a special constructor, e.g. during testing) const timing_config timing_constants; + // the meter timezone + std::string meter_timezone; + // the meter daylight saving time definition + std::string meter_dst; // RUNNING VARIABLES // The helper can be accessed by multiple threads, so we use a mutex to protect the data below @@ -74,6 +84,8 @@ class LemDCBMTimeSyncHelper { // sync_is_too_early is set if this is done earlier than MIN_TIME_BEFORE_SETTING_WRITE_IS_SAFE after init void sync_system_time(const HttpClientInterface& httpClient); + void sync_timezone(const HttpClientInterface& httpClient); + void sync_dst(const HttpClientInterface& httpClient); std::string generate_dcbm_ntp_config(); [[nodiscard]] bool is_setting_write_safe() const; diff --git a/modules/LemDCBM400600/main/powermeterImpl.cpp b/modules/LemDCBM400600/main/powermeterImpl.cpp index 8839b58d78..f335f0976e 100644 --- a/modules/LemDCBM400600/main/powermeterImpl.cpp +++ b/modules/LemDCBM400600/main/powermeterImpl.cpp @@ -22,10 +22,11 @@ void powermeterImpl::init() { this->controller = std::make_unique( std::move(http_client), std::make_unique(ntp_server_spec), - LemDCBM400600Controller::Conf{mod->config.resilience_initial_connection_retries, - mod->config.resilience_initial_connection_retry_delay, - mod->config.resilience_transaction_request_retries, - mod->config.resilience_transaction_request_retry_delay}); + LemDCBM400600Controller::Conf{ + mod->config.resilience_initial_connection_retries, mod->config.resilience_initial_connection_retry_delay, + mod->config.resilience_transaction_request_retries, mod->config.resilience_transaction_request_retry_delay, + mod->config.cable_id, mod->config.tariff_id, mod->config.meter_timezone, mod->config.meter_dst, + mod->config.SC, mod->config.UV, mod->config.UD}); this->controller->init(); } @@ -33,6 +34,7 @@ void powermeterImpl::init() { void powermeterImpl::ready() { // Start the live_measure_publisher thread, which periodically publishes the live measurements of the device this->live_measure_publisher_thread = std::thread([this] { + this->publish_public_key_ocmf(this->controller->get_public_key_ocmf()); while (true) { std::this_thread::sleep_for(std::chrono::milliseconds(1000)); try { diff --git a/modules/LemDCBM400600/manifest.yaml b/modules/LemDCBM400600/manifest.yaml index 699afbbf4c..3efafeea7e 100644 --- a/modules/LemDCBM400600/manifest.yaml +++ b/modules/LemDCBM400600/manifest.yaml @@ -27,6 +27,14 @@ config: description: The port (1-65535) fof the second NTP server. type: integer default: 123 + meter_timezone: + description: The timezone offset (ignored if NTP servers are set) - it can go from -11 to +14 for hours and 00, 15, 30, 45 for minutes + type: string + default: "+00:00" + meter_dst: + description: The Daylight Saving Time (DST) settings (ignored if NTP is set) + type: string + default: '{"activated": false, "offset": 60, "start": {"order": "last", "day": "sunday", "month": "march", "hour": "T01:00Z"}, "end": {"order": "last", "day": "sunday", "month": "october", "hour": "T01:00Z" }}' resilience_initial_connection_retries: description: For the controller resilience, the number of retries to connect to the powermeter at module initialization. type: integer @@ -51,6 +59,19 @@ config: description: Used for a unique transaction tariff designation type: integer default: 0 + SC: + description: SC (OCMF/transaction fields) + type: integer + default: 0 + UV: + description: User SW Version (OCMF/transaction fields) + type: string + default: "" + UD: + description: UD (OCMF/transaction fields) + type: string + default: "" + provides: main: description: This is the main unit of the module diff --git a/modules/LemDCBM400600/tests/test_lem_dcbm_400600_controller.cpp b/modules/LemDCBM400600/tests/test_lem_dcbm_400600_controller.cpp index c5d50c6346..6acaa901e0 100644 --- a/modules/LemDCBM400600/tests/test_lem_dcbm_400600_controller.cpp +++ b/modules/LemDCBM400600/tests/test_lem_dcbm_400600_controller.cpp @@ -58,7 +58,7 @@ class LemDCBM400600ControllerTest : public ::testing::Test { std::nullopt}; const std::string expected_start_transaction_request_body{ - R"({"evseId":"mock_evse_id","transactionId":"mock_transaction_id","clientId":"mock_transaction_id","tariffId":0,"cableId":0,"userData":""})"}; + R"({"evseId":"mock_evse_id","transactionId":"mock_transaction_id","clientId":"","tariffId":0,"cableId":0,"userData":""})"}; const std::string put_legal_response = R"({ "paginationCounter": 6, @@ -385,7 +385,7 @@ TEST_F(LemDCBM400600ControllerTest, test_init_meter_id) { .InSequence(seq) .WillRepeatedly(testing::Return(HttpResponse{ 200, - R"({ "meterId": "mock_meter_id", "some_other_field": "other_value" })", + R"({ "meterId": "mock_meter_id", "publicKeyOcmf": "KEY", "status": {"bits": {"transactionIsOnGoing": false}}, "version":{"applicationFirmwareVersion":"0.1.2.3"}, "some_other_field": "other_value" })", })); EXPECT_CALL(*this->time_sync_helper, restart_unsafe_period()).Times(1).InSequence(seq); EXPECT_CALL(*this->time_sync_helper, sync_if_deadline_expired(testing::_)).Times(1).InSequence(seq); @@ -426,7 +426,7 @@ TEST_F(LemDCBM400600ControllerTest, test_init_meter_id_retry_success) { .InSequence(seq) .WillOnce(testing::Return(HttpResponse{ 200, - R"({ "meterId": "mock_meter_id", "some_other_field": "other_value" })", + R"({ "meterId": "mock_meter_id", "publicKeyOcmf": "KEY", "status": {"bits": {"transactionIsOnGoing": false}}, "version":{"applicationFirmwareVersion":"0.1.2.3"}, "some_other_field": "other_value" })", })); EXPECT_CALL(*this->time_sync_helper, restart_unsafe_period()).Times(1).InSequence(seq); EXPECT_CALL(*this->http_client, get("/v1/livemeasure")) diff --git a/modules/LemDCBM400600/tests/test_lem_dcbm_time_sync_helper.cpp b/modules/LemDCBM400600/tests/test_lem_dcbm_time_sync_helper.cpp index 0b2a2d1c8e..b3c950b578 100644 --- a/modules/LemDCBM400600/tests/test_lem_dcbm_time_sync_helper.cpp +++ b/modules/LemDCBM400600/tests/test_lem_dcbm_time_sync_helper.cpp @@ -42,6 +42,10 @@ class LemDCBMTimeSyncHelperTest : public ::testing::Test { const std::string expected_system_sync_request_regex{ R"(\{"time":\{"utc":"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?Z"\}\})"}; + const std::string expected_tz_sync_request_regex{R"(\{"time": \{"tz":""\}\})"}; + + const std::string expected_dst_sync_request_regex{R"(\{"time": \{"dst":\}\})"}; + const std::string expected_ntp_sync_request{ R"({"ntp":{"servers":[{"ipAddress":"123.123.123.123","port":123},{"ipAddress":"213.213.213.213","port":213}],"syncPeriod":120,"ntpActivated":true}})"}; @@ -67,6 +71,12 @@ class LemDCBMTimeSyncHelperTestInvalidResponses TEST_F(LemDCBMTimeSyncHelperTest, test_sync_success_system_time) { std::string input_to_put; // Setup + EXPECT_CALL(*this->http_client, put("/v1/settings", testing::ContainsRegex(this->expected_dst_sync_request_regex))) + .Times(1) + .WillOnce(testing::Return(HttpResponse{200, this->put_settings_response_success})); + EXPECT_CALL(*this->http_client, put("/v1/settings", testing::ContainsRegex(this->expected_tz_sync_request_regex))) + .Times(1) + .WillOnce(testing::Return(HttpResponse{200, this->put_settings_response_success})); EXPECT_CALL(*this->http_client, put("/v1/settings", testing::ContainsRegex(this->expected_system_sync_request_regex))) .Times(1) @@ -148,6 +158,12 @@ TEST_F(LemDCBMTimeSyncHelperTest, test_sync_exception_if_200_but_result_is_0_ntp /// \brief sync_if_deadline_expired() called twice will not send anything the second time if the first call succeeds TEST_F(LemDCBMTimeSyncHelperTest, test_sync_if_deadline_expired_twice_when_first_succeeds) { // Setup + EXPECT_CALL(*this->http_client, put("/v1/settings", testing::ContainsRegex(this->expected_dst_sync_request_regex))) + .Times(1) + .WillOnce(testing::Return(HttpResponse{200, this->put_settings_response_success})); + EXPECT_CALL(*this->http_client, put("/v1/settings", testing::ContainsRegex(this->expected_tz_sync_request_regex))) + .Times(1) + .WillOnce(testing::Return(HttpResponse{200, this->put_settings_response_success})); EXPECT_CALL(*this->http_client, put("/v1/settings", testing::ContainsRegex(this->expected_system_sync_request_regex))) .Times(1) diff --git a/modules/OCPP/ocpp_generic/ocppImpl.cpp b/modules/OCPP/ocpp_generic/ocppImpl.cpp index a6833a089d..47198d37fc 100644 --- a/modules/OCPP/ocpp_generic/ocppImpl.cpp +++ b/modules/OCPP/ocpp_generic/ocppImpl.cpp @@ -185,7 +185,7 @@ ocppImpl::handle_get_variables(std::vector& req } std::vector -ocppImpl::handle_set_variables(std::vector& requests) { +ocppImpl::handle_set_variables(std::vector& requests, std::string& /*source*/) { std::vector results; diff --git a/modules/OCPP/ocpp_generic/ocppImpl.hpp b/modules/OCPP/ocpp_generic/ocppImpl.hpp index f9280eb81d..397f745161 100644 --- a/modules/OCPP/ocpp_generic/ocppImpl.hpp +++ b/modules/OCPP/ocpp_generic/ocppImpl.hpp @@ -39,7 +39,7 @@ class ocppImpl : public ocppImplBase { virtual std::vector handle_get_variables(std::vector& requests) override; virtual std::vector - handle_set_variables(std::vector& requests) override; + handle_set_variables(std::vector& requests, std::string& source) override; virtual types::ocpp::ChangeAvailabilityResponse handle_change_availability(types::ocpp::ChangeAvailabilityRequest& request) override; virtual void handle_monitor_variables(std::vector& component_variables) override; diff --git a/modules/OCPP201/OCPP201.cpp b/modules/OCPP201/OCPP201.cpp index da256a36fa..a836218469 100644 --- a/modules/OCPP201/OCPP201.cpp +++ b/modules/OCPP201/OCPP201.cpp @@ -11,7 +11,7 @@ #include namespace module { -const std::string SQL_CORE_MIGRTATIONS = "core_migrations"; +const std::string SQL_CORE_MIGRATIONS = "core_migrations"; const std::string CERTS_DIR = "certs"; namespace fs = std::filesystem; @@ -228,6 +228,34 @@ void OCPP201::ready() { } }(); + const auto device_model_database_migration_path = [&]() { + const auto config_device_model_database_migration_path = + fs::path(this->config.DeviceModelDatabaseMigrationPath); + if (config_device_model_database_migration_path.is_relative()) { + return this->ocpp_share_path / config_device_model_database_migration_path; + } else { + return config_device_model_database_migration_path; + } + }(); + + const auto device_model_schema_path = [&]() { + const auto config_device_model_schema_path = fs::path(this->config.DeviceModelSchemaPath); + if (config_device_model_schema_path.is_relative()) { + return this->ocpp_share_path / config_device_model_schema_path; + } else { + return config_device_model_schema_path; + } + }(); + + const auto config_file_path = [&]() { + const auto config_config_file_path = fs::path(this->config.ConfigFilePath); + if (config_config_file_path.is_relative()) { + return this->ocpp_share_path / config_config_file_path; + } else { + return config_config_file_path; + } + }(); + if (!fs::exists(this->config.MessageLogPath)) { try { fs::create_directory(this->config.MessageLogPath); @@ -436,13 +464,18 @@ void OCPP201::ready() { }; } - const auto sql_init_path = this->ocpp_share_path / SQL_CORE_MIGRTATIONS; + callbacks.connection_state_changed_callback = [this](const bool is_connected) { + this->p_ocpp_generic->publish_is_connected(is_connected); + }; + + const auto sql_init_path = this->ocpp_share_path / SQL_CORE_MIGRATIONS; std::map evse_connector_structure = this->get_connector_structure(); this->charge_point = std::make_unique( - evse_connector_structure, device_model_database_path, this->ocpp_share_path.string(), - this->config.CoreDatabasePath, sql_init_path.string(), this->config.MessageLogPath, - std::make_shared(*this->r_security), callbacks); + evse_connector_structure, device_model_database_path, true, device_model_database_migration_path, + device_model_schema_path, config_file_path, this->ocpp_share_path.string(), this->config.CoreDatabasePath, + sql_init_path.string(), this->config.MessageLogPath, std::make_shared(*this->r_security), + callbacks); const auto ev_connection_timeout_request_value_response = this->charge_point->request_value( ocpp::v201::Component{"TxCtrlr"}, ocpp::v201::Variable{"EVConnectionTimeOut"}, diff --git a/modules/OCPP201/OCPP201.hpp b/modules/OCPP201/OCPP201.hpp index 9ad79260aa..7380d6d5e3 100644 --- a/modules/OCPP201/OCPP201.hpp +++ b/modules/OCPP201/OCPP201.hpp @@ -30,7 +30,6 @@ #include #include - // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 namespace module { @@ -39,6 +38,9 @@ struct Conf { std::string MessageLogPath; std::string CoreDatabasePath; std::string DeviceModelDatabasePath; + std::string DeviceModelDatabaseMigrationPath; + std::string DeviceModelSchemaPath; + std::string ConfigFilePath; bool EnableExternalWebsocketControl; int MessageQueueResumeDelay; }; diff --git a/modules/OCPP201/manifest.yaml b/modules/OCPP201/manifest.yaml index 70a153941e..24283384b6 100644 --- a/modules/OCPP201/manifest.yaml +++ b/modules/OCPP201/manifest.yaml @@ -12,6 +12,18 @@ config: description: Path to the SQLite database for the device model type: string default: device_model_storage.db + DeviceModelDatabaseMigrationPath: + description: Path to the migration files for the device model + type: string + default: device_model_migrations + DeviceModelSchemaPath: + description: Path to the device model component schema directory + type: string + default: component_schemas + ConfigFilePath: + description: Path to the configuration file of libocpp + type: string + default: config.json EnableExternalWebsocketControl: description: If true websocket can be disconnected and connected externally type: boolean diff --git a/modules/OCPP201/ocpp_generic/ocppImpl.cpp b/modules/OCPP201/ocpp_generic/ocppImpl.cpp index 14055a5316..02b39f908d 100644 --- a/modules/OCPP201/ocpp_generic/ocppImpl.cpp +++ b/modules/OCPP201/ocpp_generic/ocppImpl.cpp @@ -35,9 +35,9 @@ ocppImpl::handle_get_variables(std::vector& req } std::vector -ocppImpl::handle_set_variables(std::vector& requests) { +ocppImpl::handle_set_variables(std::vector& requests, std::string& source) { const auto _requests = conversions::to_ocpp_set_variable_data_vector(requests); - const auto response_map = this->mod->charge_point->set_variables(_requests); + const auto response_map = this->mod->charge_point->set_variables(_requests, source); std::vector response; for (const auto& [set_variable_data, set_variable_result] : response_map) { response.push_back(set_variable_result); diff --git a/modules/OCPP201/ocpp_generic/ocppImpl.hpp b/modules/OCPP201/ocpp_generic/ocppImpl.hpp index 8e4e63b1a5..dcf089573c 100644 --- a/modules/OCPP201/ocpp_generic/ocppImpl.hpp +++ b/modules/OCPP201/ocpp_generic/ocppImpl.hpp @@ -39,7 +39,7 @@ class ocppImpl : public ocppImplBase { virtual std::vector handle_get_variables(std::vector& requests) override; virtual std::vector - handle_set_variables(std::vector& requests) override; + handle_set_variables(std::vector& requests, std::string& source) override; virtual types::ocpp::ChangeAvailabilityResponse handle_change_availability(types::ocpp::ChangeAvailabilityRequest& request) override; virtual void handle_monitor_variables(std::vector& component_variables) override; diff --git a/modules/OCPPExtensionExample/OCPPExtensionExample.cpp b/modules/OCPPExtensionExample/OCPPExtensionExample.cpp index 8c197662bd..3c12c75c6d 100644 --- a/modules/OCPPExtensionExample/OCPPExtensionExample.cpp +++ b/modules/OCPPExtensionExample/OCPPExtensionExample.cpp @@ -35,7 +35,7 @@ void OCPPExtensionExample::ready() { set_variable_requests.push_back({{{""}, {"ExampleConfigurationKey"}}, "ExampleValue"}); EVLOG_info << "Setting custom configuration key..."; - const auto set_variable_results = this->r_ocpp->call_set_variables(set_variable_requests); + const auto set_variable_results = this->r_ocpp->call_set_variables(set_variable_requests, "example"); for (const auto& set_variable_result : set_variable_results) { if (set_variable_result.status == types::ocpp::SetVariableStatusEnumType::Accepted) { diff --git a/modules/RsIskraMeter/src/main.rs b/modules/RsIskraMeter/src/main.rs index d92defc888..fa6a350751 100644 --- a/modules/RsIskraMeter/src/main.rs +++ b/modules/RsIskraMeter/src/main.rs @@ -46,7 +46,6 @@ use everestrs::serde as everest_serde; use everestrs::serde_json as everest_serde_json; use generated::types::powermeter::{ Powermeter, TransactionRequestStatus, TransactionStartResponse, TransactionStopResponse, - OCMFUserIdentificationStatus, OCMFIdentificationType, }; use generated::types::serial_comm_hub_requests::{StatusCodeEnum, VectorUint16}; use generated::types::units::{Current, Energy, Frequency, Power, ReactivePower, Voltage}; @@ -629,6 +628,7 @@ impl ReadyState { signed_meter_value: None, var_signed: None, voltage_v_signed: None, + temperatures: None, }; Ok(resp) } @@ -944,6 +944,9 @@ fn main() { mod tests { use self::generated::types::powermeter::TransactionReq; + use self::generated::types::powermeter::{ + OCMFIdentificationType, OCMFUserIdentificationStatus, + }; use super::*; use mockall::predicate::eq; @@ -1187,7 +1190,7 @@ mod tests { identification_flags: Vec::new(), identification_data: None, identification_level: None, - tariff_text: None + tariff_text: None, }); } } @@ -1287,7 +1290,7 @@ mod tests { identification_flags: Vec::new(), identification_data: None, identification_level: None, - tariff_text: None + tariff_text: None, }); } } diff --git a/modules/SerialCommHub/main/serial_communication_hubImpl.cpp b/modules/SerialCommHub/main/serial_communication_hubImpl.cpp index 5d91c95a71..af5175d094 100644 --- a/modules/SerialCommHub/main/serial_communication_hubImpl.cpp +++ b/modules/SerialCommHub/main/serial_communication_hubImpl.cpp @@ -4,9 +4,11 @@ #include "serial_communication_hubImpl.hpp" #include +#include #include #include #include +#include #include namespace module { @@ -38,6 +40,8 @@ void serial_communication_hubImpl::init() { rxtx_gpio_settings.line_number = config.rxtx_gpio_line; rxtx_gpio_settings.inverted = config.rxtx_gpio_tx_high; + system_error_logged = false; + if (!modbus.open_device(config.serial_port, config.baudrate, config.ignore_echo, rxtx_gpio_settings, static_cast(config.parity), config.rtscts, milliseconds(config.initial_timeout_ms), milliseconds(config.within_message_timeout_ms))) { @@ -48,146 +52,102 @@ void serial_communication_hubImpl::init() { void serial_communication_hubImpl::ready() { } -// Commands - types::serial_comm_hub_requests::Result -serial_communication_hubImpl::handle_modbus_read_holding_registers(int& target_device_id, int& first_register_address, - int& num_registers_to_read) { - +serial_communication_hubImpl::perform_modbus_request(uint8_t device_address, tiny_modbus::FunctionCode function, + uint16_t first_register_address, uint16_t register_quantity, + bool wait_for_reply, std::vector request) { + std::scoped_lock lock(serial_mutex); types::serial_comm_hub_requests::Result result; std::vector response; - - { - std::scoped_lock lock(serial_mutex); - - auto retry_counter = this->num_resends_on_error; - while (retry_counter > 0) { - - EVLOG_debug << fmt::format("Try {} Call modbus_client->read_holding_register(id {} addr {} len {})", - (int)retry_counter, (uint8_t)target_device_id, (uint16_t)first_register_address, - (uint16_t)num_registers_to_read); - - response = modbus.txrx(target_device_id, tiny_modbus::FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS, - first_register_address, num_registers_to_read, config.max_packet_size); - if (response.size() > 0) { - break; + auto retry_counter = config.retries + 1; + + while (retry_counter > 0) { + auto current_trial = config.retries + 1 - retry_counter + 1; + + EVLOG_debug << fmt::format("Trial {}/{}: calling {}(id {} addr {}({:#06x}) len {})", current_trial, + config.retries + 1, tiny_modbus::FunctionCode_to_string_with_hex(function), + device_address, first_register_address, first_register_address, register_quantity); + + try { + response = modbus.txrx(device_address, function, first_register_address, register_quantity, + config.max_packet_size, wait_for_reply, request); + } catch (const tiny_modbus::TinyModbusException& e) { + auto logmsg = fmt::format("Modbus call {} for device id {} addr {}({:#06x}) failed: {}", + tiny_modbus::FunctionCode_to_string_with_hex(function), device_address, + first_register_address, first_register_address, e.what()); + + if (retry_counter != 1) + EVLOG_debug << logmsg; + else + EVLOG_warning << logmsg; + } catch (const std::logic_error& e) { + EVLOG_warning << "Logic error in Modbus implementation: " << e.what(); + } catch (const std::system_error& e) { + // FIXME: report this to the infrastructure, as soon as an error interface for this is available + // Log this only once, as we are convinced this will not go away + if (not system_error_logged) { + EVLOG_error << "System error in accessing Modbus: [" << e.code() << "] " << e.what(); + system_error_logged = true; } - retry_counter--; } + + if (response.size() > 0) + break; + + retry_counter--; } - EVLOG_debug << fmt::format("Process response (size {})", response.size()); - // process response if (response.size() > 0) { + EVLOG_debug << fmt::format("Process response (size {})", response.size()); result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Success; result.value = vector_to_int(response); + system_error_logged = false; // reset after success } else { result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Error; } return result; } -types::serial_comm_hub_requests::Result -serial_communication_hubImpl::handle_modbus_read_input_registers(int& target_device_id, int& first_register_address, - int& num_registers_to_read) { - types::serial_comm_hub_requests::Result result; - std::vector response; - - { - std::scoped_lock lock(serial_mutex); +// Commands - uint8_t retry_counter{this->num_resends_on_error}; - while (retry_counter-- > 0) { +types::serial_comm_hub_requests::Result +serial_communication_hubImpl::handle_modbus_read_holding_registers(int& target_device_id, int& first_register_address, + int& num_registers_to_read) { - EVLOG_debug << fmt::format("Try {} Call modbus_client->read_input_register(id {} addr {} len {})", - (int)retry_counter, (uint8_t)target_device_id, (uint16_t)first_register_address, - (uint16_t)num_registers_to_read); + return perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS, + first_register_address, num_registers_to_read); +} - response = modbus.txrx(target_device_id, tiny_modbus::FunctionCode::READ_INPUT_REGISTERS, - first_register_address, num_registers_to_read, config.max_packet_size); - if (response.size() > 0) { - break; - } - } - } +types::serial_comm_hub_requests::Result +serial_communication_hubImpl::handle_modbus_read_input_registers(int& target_device_id, int& first_register_address, + int& num_registers_to_read) { - EVLOG_debug << fmt::format("Process response (size {})", response.size()); - // process response - if (response.size() > 0) { - result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Success; - result.value = vector_to_int(response); - } else { - result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Error; - } - return result; + return perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::READ_INPUT_REGISTERS, + first_register_address, num_registers_to_read); } types::serial_comm_hub_requests::StatusCodeEnum serial_communication_hubImpl::handle_modbus_write_multiple_registers( int& target_device_id, int& first_register_address, types::serial_comm_hub_requests::VectorUint16& data_raw) { types::serial_comm_hub_requests::Result result; - std::vector response; - std::vector data; append_array(data, data_raw.data); - { - std::scoped_lock lock(serial_mutex); - - uint8_t retry_counter{this->num_resends_on_error}; - while (retry_counter-- > 0) { - - EVLOG_debug << fmt::format("Try {} Call modbus_client->write_multiple_registers(id {} addr {} len {})", - (int)retry_counter, (uint8_t)target_device_id, (uint16_t)first_register_address, - (uint16_t)data.size()); - - response = modbus.txrx(target_device_id, tiny_modbus::FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS, - first_register_address, data.size(), config.max_packet_size, true, data); - if (response.size() > 0) { - break; - } - } - } + result = perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS, + first_register_address, data.size(), true, data); - EVLOG_debug << fmt::format("Done writing (size {})", response.size()); - // process response - if (response.size() > 0) { - return types::serial_comm_hub_requests::StatusCodeEnum::Success; - } else { - return types::serial_comm_hub_requests::StatusCodeEnum::Error; - } + return result.status_code; } types::serial_comm_hub_requests::StatusCodeEnum serial_communication_hubImpl::handle_modbus_write_single_register(int& target_device_id, int& register_address, int& data) { types::serial_comm_hub_requests::Result result; - std::vector response; - - { - std::scoped_lock lock(serial_mutex); - uint8_t retry_counter{this->num_resends_on_error}; - while (retry_counter-- > 0) { + result = perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::WRITE_SINGLE_HOLDING_REGISTER, + register_address, 1, true, {static_cast(data)}); - EVLOG_debug << fmt::format("Try {} Call modbus_client->write_single_register(id {} addr {} data 0x{:04x})", - (int)retry_counter, (uint8_t)target_device_id, (uint16_t)register_address, - (uint16_t)data); - - response = modbus.txrx(target_device_id, tiny_modbus::FunctionCode::WRITE_SINGLE_HOLDING_REGISTER, - register_address, 1, config.max_packet_size, true, {static_cast(data)}); - if (response.size() > 0) { - break; - } - } - } - EVLOG_debug << fmt::format("Done writing (size {})", response.size()); - // process response - if (response.size() > 0) { - return types::serial_comm_hub_requests::StatusCodeEnum::Success; - } else { - return types::serial_comm_hub_requests::StatusCodeEnum::Error; - } + return result.status_code; } void serial_communication_hubImpl::handle_nonstd_write(int& target_device_id, int& first_register_address, diff --git a/modules/SerialCommHub/main/serial_communication_hubImpl.hpp b/modules/SerialCommHub/main/serial_communication_hubImpl.hpp index 2a3933f7bf..41b7c98cf1 100644 --- a/modules/SerialCommHub/main/serial_communication_hubImpl.hpp +++ b/modules/SerialCommHub/main/serial_communication_hubImpl.hpp @@ -16,6 +16,7 @@ // insert your custom include headers here #include "tiny_modbus_rtu.hpp" #include +#include #include #include #include @@ -36,6 +37,7 @@ struct Conf { int max_packet_size; int initial_timeout_ms; int within_message_timeout_ms; + int retries; }; class serial_communication_hubImpl : public serial_communication_hubImplBase { @@ -80,10 +82,15 @@ class serial_communication_hubImpl : public serial_communication_hubImplBase { // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 // insert your private definitions here - static constexpr uint8_t num_resends_on_error{3}; + types::serial_comm_hub_requests::Result + perform_modbus_request(uint8_t device_address, tiny_modbus::FunctionCode function, uint16_t first_register_address, + uint16_t register_quantity, bool wait_for_reply = true, + std::vector request = std::vector()); + tiny_modbus::TinyModbusRTU modbus; std::mutex serial_mutex; + bool system_error_logged{false}; // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 }; diff --git a/modules/SerialCommHub/manifest.yaml b/modules/SerialCommHub/manifest.yaml index 5a62bc7d39..8e7bb4f4cc 100644 --- a/modules/SerialCommHub/manifest.yaml +++ b/modules/SerialCommHub/manifest.yaml @@ -58,6 +58,12 @@ provides: description: Timeout in ms for subsequent packets. type: integer default: 100 + retries: + description: Count of retries in case of error in Modbus query. + type: integer + minimum: 0 + maximum: 10 + default: 2 metadata: license: https://opensource.org/licenses/Apache-2.0 authors: diff --git a/modules/SerialCommHub/tiny_modbus_rtu.cpp b/modules/SerialCommHub/tiny_modbus_rtu.cpp index 83d8cc808f..dba1693055 100644 --- a/modules/SerialCommHub/tiny_modbus_rtu.cpp +++ b/modules/SerialCommHub/tiny_modbus_rtu.cpp @@ -7,28 +7,60 @@ // - implement GPIO to switch rx/tx #include "tiny_modbus_rtu.hpp" - +#include "crc16.hpp" +#include #include #include #include #include +#include +#include +#include #include +#include +#include +#include #include -#include -#include - #include #include #include +#include +#include +#include -#include -#include -#include -#include +namespace tiny_modbus { -#include "crc16.hpp" +std::string FunctionCode_to_string(FunctionCode fc) { + switch (fc) { + case FunctionCode::READ_COILS: + return "READ_COILS"; + case FunctionCode::READ_DISCRETE_INPUTS: + return "READ_DISCRETE_INPUTS"; + case FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS: + return "READ_MULTIPLE_HOLDING_REGISTERS"; + case FunctionCode::READ_INPUT_REGISTERS: + return "READ_INPUT_REGISTERS"; + case FunctionCode::WRITE_SINGLE_COIL: + return "WRITE_SINGLE_COIL"; + case FunctionCode::WRITE_SINGLE_HOLDING_REGISTER: + return "WRITE_SINGLE_HOLDING_REGISTER"; + case FunctionCode::WRITE_MULTIPLE_COILS: + return "WRITE_MULTIPLE_COILS"; + case FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS: + return "WRITE_MULTIPLE_HOLDING_REGISTERS"; + default: + return "unknown"; + } +} -namespace tiny_modbus { +std::string FunctionCode_to_string_with_hex(FunctionCode fc) { + return fmt::format("{}({:#04x})", FunctionCode_to_string(fc), (unsigned int)fc); +} + +std::ostream& operator<<(std::ostream& os, const FunctionCode& fc) { + os << FunctionCode_to_string_with_hex(fc); + return os; +} // This is a replacement for system library tcdrain(). // tcdrain() returns when all bytes are written to the UART, but it actually returns about 10msecs or more after the @@ -53,7 +85,7 @@ static void clear_exception_bit(uint8_t& received_function_code) { static std::string hexdump(const uint8_t* msg, int msg_len) { std::stringstream ss; for (int i = 0; i < msg_len; i++) { - ss << std::hex << (int)msg[i] << " "; + ss << "<" << std::nouppercase << std::setfill('0') << std::setw(2) << std::hex << (int)msg[i] << ">"; } return ss.str(); } @@ -79,18 +111,14 @@ static std::vector decode_reply(const uint8_t* buf, int len, uint8_t e FunctionCode function) { std::vector result; if (len == 0) { - EVLOG_error << fmt::format("Packet receive timeout (device address {}).", expected_device_address); - return result; + throw TimeoutException("Packet receive timeout"); } else if (len < MODBUS_MIN_REPLY_SIZE) { - EVLOG_error << fmt::format("Packet too small: {} bytes (device address {}).", len, expected_device_address); - return result; + throw ShortPacketException(fmt::format("Packet too small: only {} bytes", len)); } if (expected_device_address != buf[DEVICE_ADDRESS_POS]) { - EVLOG_error << fmt::format("Device address mismatch: expected: {} received: {}", expected_device_address, - buf[DEVICE_ADDRESS_POS]) - << ": " << hexdump(buf, len); - - return result; + throw AddressMismatchException(fmt::format("Device address mismatch: expected: {} received: {}", + expected_device_address, buf[DEVICE_ADDRESS_POS]) + + ": " + hexdump(buf, len)); } bool exception = false; @@ -103,92 +131,113 @@ static std::vector decode_reply(const uint8_t* buf, int len, uint8_t e } if (function != function_code_recvd) { - EVLOG_error << fmt::format("Function code mismatch: expected: {} received: {}", - static_cast>(function), function_code_recvd); - return result; + throw FunctionCodeMismatchException(fmt::format("Function code mismatch: expected: {} received: {}", + static_cast>(function), + function_code_recvd)); } if (!validate_checksum(buf, len)) { - EVLOG_error << "Checksum error"; - return result; + throw ChecksumErrorException("Retrieved Modbus checksum does not match calculated value."); } - if (!exception) { - // For a write reply we always get 4 bytes - uint8_t byte_cnt = 4; - int start_of_result = RES_TX_START_OF_PAYLOAD; - - // Was it a read reply? - if (function == FunctionCode::READ_COILS || function == FunctionCode::READ_DISCRETE_INPUTS || - function == FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS || - function == FunctionCode::READ_INPUT_REGISTERS) { - // adapt byte count and starting pos - byte_cnt = buf[RES_RX_LEN_POS]; - start_of_result = RES_RX_START_OF_PAYLOAD; - } - - // check if result is completely in received data - if (start_of_result + byte_cnt > len) { - EVLOG_error << "Result data not completely in received message."; - return result; - } - - // ready to copy actual result data to output - result.reserve(byte_cnt / 2); - - for (int i = start_of_result; i < start_of_result + byte_cnt; i += 2) { - uint16_t t; - memcpy(&t, buf + i, 2); - t = be16toh(t); - result.push_back(t); - } - return result; - } else { + if (exception) { // handle exception message uint8_t err_code = buf[RES_EXCEPTION_CODE]; switch (err_code) { case 0x01: - EVLOG_error << "Modbus exception: Illegal function"; + throw ModbusException("Modbus exception: Illegal function"); break; case 0x02: - EVLOG_error << "Modbus exception: Illegal data address"; + throw ModbusException("Modbus exception: Illegal data address"); break; case 0x03: - EVLOG_error << "Modbus exception: Illegal data value"; + throw ModbusException("Modbus exception: Illegal data value"); break; case 0x04: - EVLOG_error << "Modbus exception: Client device failure"; + throw ModbusException("Modbus exception: Client device failure"); break; case 0x05: - EVLOG_debug << "Modbus ACK"; + throw ModbusException("Modbus ACK"); break; case 0x06: - EVLOG_error << "Modbus exception: Client device busy"; + throw ModbusException("Modbus exception: Client device busy"); break; case 0x07: - EVLOG_error << "Modbus exception: NACK"; + throw ModbusException("Modbus exception: NACK"); break; case 0x08: - EVLOG_error << "Modbus exception: Memory parity error"; + throw ModbusException("Modbus exception: Memory parity error"); break; case 0x09: - EVLOG_error << "Modbus exception: Out of resources"; + throw ModbusException("Modbus exception: Out of resources"); break; case 0x0A: - EVLOG_error << "Modbus exception: Gateway path unavailable"; + throw ModbusException("Modbus exception: Gateway path unavailable"); break; case 0x0B: - EVLOG_error << "Modbus exception: Gateway target device failed to respond"; + throw ModbusException("Modbus exception: Gateway target device failed to respond"); break; default: - EVLOG_error << "Modbus exception: Unknown"; + throw ModbusException("Modbus exception: Unknown"); } - return result; } + + // For a write reply we always get 4 bytes + uint8_t byte_cnt = 4; + int start_of_result = RES_TX_START_OF_PAYLOAD; + bool even_byte_cnt_expected = false; + + // Was it a read reply? + switch (function) { + case FunctionCode::WRITE_SINGLE_COIL: + case FunctionCode::WRITE_SINGLE_HOLDING_REGISTER: + case FunctionCode::WRITE_MULTIPLE_COILS: + case FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS: + // no - nothing to do + break; + case FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS: + case FunctionCode::READ_INPUT_REGISTERS: + // yes - for 16-bit wide registers thus we can assume an even byte count + even_byte_cnt_expected = true; + [[fallthrough]]; + case FunctionCode::READ_COILS: + case FunctionCode::READ_DISCRETE_INPUTS: + // yes + // adapt byte count and starting pos + byte_cnt = buf[RES_RX_LEN_POS]; + start_of_result = RES_RX_START_OF_PAYLOAD; + break; + default: + throw std::logic_error("Missing implementation for function code " + FunctionCode_to_string_with_hex(function)); + } + + // check if result is completely in received data + if (start_of_result + byte_cnt > len) { + throw IncompletePacketException("Result data not completely in received message."); + } + + // check even number of bytes + if (even_byte_cnt_expected && byte_cnt % 2 == 1) { + throw OddByteCountException("For " + FunctionCode_to_string_with_hex(function) + + " an even byte count is expected in the response."); + } + + // ready to copy actual result data to output, so pre-allocate enough memory for the output + result.reserve((byte_cnt + 1) / 2); + + for (int i = start_of_result; i < start_of_result + byte_cnt; i += 2) { + uint16_t t = 0; + const size_t num_bytes_to_copy = (i < len - 1) ? 2 : 1; + memcpy(&t, buf + i, num_bytes_to_copy); + t = be16toh(t); + result.push_back(t); + } + + return result; } TinyModbusRTU::~TinyModbusRTU() { - if (fd) + if (fd != -1) close(fd); } @@ -279,7 +328,7 @@ bool TinyModbusRTU::open_device(const std::string& device, int _baud, bool _igno } int TinyModbusRTU::read_reply(uint8_t* rxbuf, int rxbuf_len) { - if (fd <= 0) { + if (fd == -1) { return 0; } @@ -425,7 +474,7 @@ std::vector TinyModbusRTU::txrx_impl(uint8_t device_address, FunctionC uint16_t first_register_address, uint16_t register_quantity, bool wait_for_reply, std::vector request) { { - if (fd <= 0) { + if (fd == -1) { return {}; } @@ -438,7 +487,17 @@ std::vector TinyModbusRTU::txrx_impl(uint8_t device_address, FunctionC // write to serial port rxtx_gpio.set(false); - write(fd, req.data(), req.size()); + + uint8_t* buffer = req.data(); + ssize_t written = 0; + + while (written < req.size()) { + ssize_t c = write(fd, &buffer[written], req.size() - written); + if (c == -1) + throw std::system_error(errno, std::generic_category(), "Could not send Modbus request"); + written += c; + } + if (rxtx_gpio.is_ready()) { // if we are using GPIO to switch between RX/TX, use the fast version of tcdrain with exact timing fast_tcdrain(fd); diff --git a/modules/SerialCommHub/tiny_modbus_rtu.hpp b/modules/SerialCommHub/tiny_modbus_rtu.hpp index abcd76ff75..9d7d33162b 100644 --- a/modules/SerialCommHub/tiny_modbus_rtu.hpp +++ b/modules/SerialCommHub/tiny_modbus_rtu.hpp @@ -8,6 +8,8 @@ #define TINY_MODBUS_RTU #include +#include +#include #include #include @@ -51,6 +53,38 @@ enum FunctionCode : uint8_t { WRITE_MULTIPLE_HOLDING_REGISTERS = 0x10, }; +std::string FunctionCode_to_string(FunctionCode fc); +std::string FunctionCode_to_string_with_hex(FunctionCode fc); +std::ostream& operator<<(std::ostream& os, const FunctionCode& fc); + +class TinyModbusException : public std::runtime_error { + using std::runtime_error::runtime_error; +}; +class TimeoutException : public TinyModbusException { + using TinyModbusException::TinyModbusException; +}; +class ShortPacketException : public TinyModbusException { + using TinyModbusException::TinyModbusException; +}; +class AddressMismatchException : public TinyModbusException { + using TinyModbusException::TinyModbusException; +}; +class FunctionCodeMismatchException : public TinyModbusException { + using TinyModbusException::TinyModbusException; +}; +class ChecksumErrorException : public TinyModbusException { + using TinyModbusException::TinyModbusException; +}; +class IncompletePacketException : public TinyModbusException { + using TinyModbusException::TinyModbusException; +}; +class OddByteCountException : public TinyModbusException { + using TinyModbusException::TinyModbusException; +}; +class ModbusException : public TinyModbusException { + using TinyModbusException::TinyModbusException; +}; + class TinyModbusRTU { public: @@ -66,7 +100,7 @@ class TinyModbusRTU { private: // Serial interface - int fd{0}; + int fd{-1}; bool ignore_echo{false}; std::vector txrx_impl(uint8_t device_address, FunctionCode function, uint16_t first_register_address, diff --git a/modules/YetiDriver/yeti_comms/firmware_version.hpp b/modules/YetiDriver/yeti_comms/firmware_version.hpp index c818faa1c2..8bc0f4d7be 100644 --- a/modules/YetiDriver/yeti_comms/firmware_version.hpp +++ b/modules/YetiDriver/yeti_comms/firmware_version.hpp @@ -56,15 +56,30 @@ class YetiFirmwareVersion { // parse into major, minor and revision if (tokens.size() >= 1) { - major = std::stoi(tokens[0]); + try { + major = std::stoi(tokens[0]); + } catch (...) { + // Set to 0 if we cannot parse it + major = 0; + } } if (tokens.size() >= 2) { - minor = std::stoi(tokens[1]); + try { + minor = std::stoi(tokens[1]); + } catch (...) { + // Set to 0 if we cannot parse it + minor = 0; + } } if (tokens.size() >= 3) { - revision = std::stoi(tokens[2]); + try { + revision = std::stoi(tokens[2]); + } catch (...) { + // Set to 0 if we cannot parse it + revision = 0; + } } } diff --git a/modules/module.bzl b/modules/module.bzl index 1a3aef7148..dd8c8679de 100644 --- a/modules/module.bzl +++ b/modules/module.bzl @@ -31,22 +31,29 @@ def cc_everest_module( name + ".hpp", ] + binary = name + "__binary" + manifest = native.glob(["manifest.y*ml"], allow_empty = False)[0] + native.genrule( name = "ld-ev", outs = [ "generated/modules/{}/ld-ev.hpp".format(name), "generated/modules/{}/ld-ev.cpp".format(name), ], - srcs = native.glob(["manifest.y*ml"], allow_empty = False) + [ + srcs = [ + manifest, + "@everest-core//types:types", "@everest-framework//schemas:schemas", - "//types:types", + "@everest-core//:WORKSPACE.bazel", + "@everest-core//interfaces:interfaces", ], tools = [ "@everest-utils//ev-dev-tools:ev-cli", ], cmd = """ $(location @everest-utils//ev-dev-tools:ev-cli) module generate-loader \ - --everest-dir . \ + --work-dir `dirname $(location @everest-core//:WORKSPACE.bazel)` \ + --everest-dir ~/foo \ --schemas-dir external/everest-framework/schemas \ --disable-clang-format \ --output-dir `dirname $(location generated/modules/{module_name}/ld-ev.hpp)`/.. \ @@ -54,14 +61,13 @@ def cc_everest_module( """.format(module_name = name) ) - native.cc_binary( - name = name, + name = binary, srcs = depset(srcs + impl_srcs + module_srcs + [ ":ld-ev", ]).to_list(), deps = deps + [ - "//interfaces:interfaces_lib", + "@everest-core//interfaces:interfaces_lib", "@everest-framework//:framework", ], copts = ["-std=c++17"], @@ -69,4 +75,25 @@ def cc_everest_module( ".", "generated/modules/" + name, ], + visibility = ["//visibility:public"], + ) + + native.genrule( + name = "copy_to_subdir", + srcs = [":" + binary, manifest], + outs = [ + "{}/manifest.yaml".format(name), + "{}/{}".format(name, name), + ], + cmd = "mkdir -p $(RULEDIR)/{} && ".format(name) + + "cp $(location {}) $(RULEDIR)/{}/{} && ".format(binary, name, name) + + "cp $(location {}) $(RULEDIR)/{}/".format(manifest, name), + ) + + native.filegroup( + name = name, + srcs = [ + ":copy_to_subdir", + ], + visibility = ["//visibility:public"], ) \ No newline at end of file diff --git a/modules/simulation/DCSupplySimulator/CMakeLists.txt b/modules/simulation/DCSupplySimulator/CMakeLists.txt index bac3079547..5adc6faee6 100644 --- a/modules/simulation/DCSupplySimulator/CMakeLists.txt +++ b/modules/simulation/DCSupplySimulator/CMakeLists.txt @@ -16,6 +16,7 @@ target_compile_features(${MODULE_NAME} PUBLIC cxx_std_17) target_sources(${MODULE_NAME} PRIVATE "main/power_supply_DCImpl.cpp" + "powermeter/powermeterImpl.cpp" ) # ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/simulation/DCSupplySimulator/DCSupplySimulator.cpp b/modules/simulation/DCSupplySimulator/DCSupplySimulator.cpp index 33a76effbb..8ff3f6ad46 100644 --- a/modules/simulation/DCSupplySimulator/DCSupplySimulator.cpp +++ b/modules/simulation/DCSupplySimulator/DCSupplySimulator.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2023 chargebyte GmbH -// Copyright (C) 2023 Contributors to EVerest +// Copyright chargebyte GmbH +// Copyright Pionix GmbH and Contributors to EVerest #include "DCSupplySimulator.hpp" @@ -8,10 +8,12 @@ namespace module { void DCSupplySimulator::init() { invoke_init(*p_main); + invoke_init(*p_powermeter); } void DCSupplySimulator::ready() { invoke_ready(*p_main); + invoke_ready(*p_powermeter); } } // namespace module diff --git a/modules/simulation/DCSupplySimulator/DCSupplySimulator.hpp b/modules/simulation/DCSupplySimulator/DCSupplySimulator.hpp index daf5a827d7..ed91ff9a56 100644 --- a/modules/simulation/DCSupplySimulator/DCSupplySimulator.hpp +++ b/modules/simulation/DCSupplySimulator/DCSupplySimulator.hpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright chargebyte GmbH and Contributors to EVerest +// Copyright Pionix GmbH and Contributors to EVerest #ifndef DCSUPPLY_SIMULATOR_HPP #define DCSUPPLY_SIMULATOR_HPP @@ -12,6 +13,7 @@ // headers for provided interface implementations #include +#include // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 // insert your custom include headers here @@ -24,10 +26,12 @@ struct Conf {}; class DCSupplySimulator : public Everest::ModuleBase { public: DCSupplySimulator() = delete; - DCSupplySimulator(const ModuleInfo& info, std::unique_ptr p_main, Conf& config) : - ModuleBase(info), p_main(std::move(p_main)), config(config){}; + DCSupplySimulator(const ModuleInfo& info, std::unique_ptr p_main, + std::unique_ptr p_powermeter, Conf& config) : + ModuleBase(info), p_main(std::move(p_main)), p_powermeter(std::move(p_powermeter)), config(config){}; const std::unique_ptr p_main; + const std::unique_ptr p_powermeter; const Conf& config; // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 diff --git a/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.cpp b/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.cpp index f3d1cb9b6f..90deae5e24 100644 --- a/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.cpp +++ b/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2023 chargebyte GmbH -// Copyright (C) 2023 Contributors to EVerest +// Copyright chargebyte GmbH +// Copyright Pionix GmbH and Contributors to EVerest #include #include @@ -11,10 +11,12 @@ namespace module { namespace main { void power_supply_DCImpl::init() { - this->connector_voltage = 0.0; - this->connector_current = 0.0; + connector_voltage = 0.0; + connector_current = 0.0; + energy_import_total = 0.0; + energy_export_total = 0.0; - this->power_supply_thread_handle = std::thread(&power_supply_DCImpl::power_supply_worker, this); + power_supply_thread_handle = std::thread(&power_supply_DCImpl::power_supply_worker, this); } static auto get_capabilities_from_config(const Conf& config) { @@ -40,35 +42,35 @@ static auto get_capabilities_from_config(const Conf& config) { } void power_supply_DCImpl::ready() { - publish_capabilities(get_capabilities_from_config(this->config)); + publish_capabilities(get_capabilities_from_config(config)); } void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& value) { - this->mode = value; + mode = value; - std::scoped_lock access_lock(this->power_supply_values_mutex); + std::scoped_lock access_lock(power_supply_values_mutex); if ((value == types::power_supply_DC::Mode::Off) || (value == types::power_supply_DC::Mode::Fault)) { - this->connector_voltage = 0.0; - this->connector_current = 0.0; + connector_voltage = 0.0; + connector_current = 0.0; } else if (value == types::power_supply_DC::Mode::Export) { - this->connector_voltage = this->settings_connector_export_voltage; - this->connector_current = this->settings_connector_max_export_current; + connector_voltage = settings_connector_export_voltage; + connector_current = settings_connector_max_export_current; } else if (value == types::power_supply_DC::Mode::Import) { - this->connector_voltage = this->settings_connector_import_voltage; - this->connector_current = this->settings_connector_max_import_current; + connector_voltage = settings_connector_import_voltage; + connector_current = settings_connector_max_import_current; } mod->p_main->publish_mode(value); } void power_supply_DCImpl::clampVoltageCurrent(double& voltage, double& current) { - voltage = voltage < this->config.min_voltage ? this->config.min_voltage - : voltage > this->config.max_voltage ? this->config.max_voltage - : voltage; + voltage = voltage < config.min_voltage ? config.min_voltage + : voltage > config.max_voltage ? config.max_voltage + : voltage; - current = current < this->config.min_current ? this->config.min_current - : current > this->config.max_current ? this->config.max_current - : current; + current = current < config.min_current ? config.min_current + : current > config.max_current ? config.max_current + : current; } void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) { @@ -77,13 +79,13 @@ void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double clampVoltageCurrent(temp_voltage, temp_current); - std::scoped_lock access_lock(this->power_supply_values_mutex); - this->settings_connector_export_voltage = temp_voltage; - this->settings_connector_max_export_current = temp_current; + std::scoped_lock access_lock(power_supply_values_mutex); + settings_connector_export_voltage = temp_voltage; + settings_connector_max_export_current = temp_current; - if (this->mode == types::power_supply_DC::Mode::Export) { - this->connector_voltage = this->settings_connector_export_voltage; - this->connector_current = this->settings_connector_max_export_current; + if (mode == types::power_supply_DC::Mode::Export) { + connector_voltage = settings_connector_export_voltage; + connector_current = settings_connector_max_export_current; } } @@ -93,32 +95,56 @@ void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double clampVoltageCurrent(temp_voltage, temp_current); - std::scoped_lock access_lock(this->power_supply_values_mutex); - this->settings_connector_import_voltage = temp_voltage; - this->settings_connector_max_import_current = temp_current; + std::scoped_lock access_lock(power_supply_values_mutex); + settings_connector_import_voltage = temp_voltage; + settings_connector_max_import_current = temp_current; - if (this->mode == types::power_supply_DC::Mode::Import) { - this->connector_voltage = this->settings_connector_import_voltage; - this->connector_current = -this->settings_connector_max_import_current; + if (mode == types::power_supply_DC::Mode::Import) { + connector_voltage = settings_connector_import_voltage; + connector_current = -settings_connector_max_import_current; } } +types::powermeter::Powermeter power_supply_DCImpl::power_meter_external() { + types::powermeter::Powermeter powermeter; + + powermeter.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now()); + powermeter.meter_id = "DC_POWERMETER"; + + if (connector_current > 0) { + energy_import_total += (connector_voltage * connector_current * LOOP_SLEEP_MS / 1000) / 3600; + } + if (connector_current < 0) { + energy_export_total += (connector_voltage * -connector_current * LOOP_SLEEP_MS / 1000) / 3600; + } + + powermeter.energy_Wh_import = {static_cast(energy_import_total)}; + powermeter.energy_Wh_export = {static_cast(energy_export_total)}; + + powermeter.power_W = {static_cast(connector_current * connector_voltage)}; + powermeter.current_A = {static_cast(connector_current)}; + powermeter.voltage_V = {static_cast(connector_voltage)}; + + return powermeter; +} + void power_supply_DCImpl::power_supply_worker(void) { types::power_supply_DC::VoltageCurrent voltage_current; while (true) { - if (this->power_supply_thread_handle.shouldExit()) { + if (power_supply_thread_handle.shouldExit()) { break; } // set interval for publishing std::this_thread::sleep_for(std::chrono::milliseconds(LOOP_SLEEP_MS)); - std::scoped_lock access_lock(this->power_supply_values_mutex); - voltage_current.voltage_V = static_cast(this->connector_voltage); - voltage_current.current_A = static_cast(this->connector_current); + std::scoped_lock access_lock(power_supply_values_mutex); + voltage_current.voltage_V = static_cast(connector_voltage); + voltage_current.current_A = static_cast(connector_current); - this->mod->p_main->publish_voltage_current(voltage_current); + mod->p_main->publish_voltage_current(voltage_current); + mod->p_powermeter->publish_powermeter(power_meter_external()); } } } // namespace main diff --git a/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.hpp b/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.hpp index 8a4305a002..e966d63630 100644 --- a/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.hpp +++ b/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.hpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright chargebyte GmbH and Contributors to EVerest +// Copyright Pionix GmbH and Contributors to EVerest #ifndef MAIN_POWER_SUPPLY_DC_IMPL_HPP #define MAIN_POWER_SUPPLY_DC_IMPL_HPP @@ -66,8 +67,11 @@ class power_supply_DCImpl : public power_supply_DCImplBase { types::power_supply_DC::Mode mode; double connector_voltage; double connector_current; + double energy_import_total; + double energy_export_total; std::mutex power_supply_values_mutex; Everest::Thread power_supply_thread_handle; + types::powermeter::Powermeter power_meter_external(); void power_supply_worker(void); static constexpr int LOOP_SLEEP_MS{500}; diff --git a/modules/simulation/DCSupplySimulator/manifest.yaml b/modules/simulation/DCSupplySimulator/manifest.yaml index 7e0634a44f..6aabc8a0e3 100644 --- a/modules/simulation/DCSupplySimulator/manifest.yaml +++ b/modules/simulation/DCSupplySimulator/manifest.yaml @@ -28,9 +28,13 @@ provides: description: Max supported current type: number default: 200.0 + powermeter: + interface: powermeter + description: Power meter interface for simulation metadata: license: https://opensource.org/licenses/Apache-2.0 authors: - Cornelius Claussen (Pionix GmbH) - Fabian Hartung (chargebyte GmbH) - Mohannad Oraby (chargebyte GmbH) + - Sebastian Lukas (Pionix GmbH) diff --git a/modules/simulation/DCSupplySimulator/powermeter/powermeterImpl.cpp b/modules/simulation/DCSupplySimulator/powermeter/powermeterImpl.cpp new file mode 100644 index 0000000000..8bbe9de66d --- /dev/null +++ b/modules/simulation/DCSupplySimulator/powermeter/powermeterImpl.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "powermeterImpl.hpp" + +namespace module { +namespace powermeter { + +void powermeterImpl::init() { +} + +void powermeterImpl::ready() { +} + +types::powermeter::TransactionStartResponse +powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& value) { + return {types::powermeter::TransactionRequestStatus::NOT_SUPPORTED, + "DcSupplySimulator does not support start transaction request."}; +} + +types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transaction(std::string& transaction_id) { + return {types::powermeter::TransactionRequestStatus::NOT_SUPPORTED, + {}, + {}, + "DcSupplySimulator does not support stop transaction request."}; +} + +} // namespace powermeter +} // namespace module diff --git a/modules/simulation/DCSupplySimulator/powermeter/powermeterImpl.hpp b/modules/simulation/DCSupplySimulator/powermeter/powermeterImpl.hpp new file mode 100644 index 0000000000..d1bb6126df --- /dev/null +++ b/modules/simulation/DCSupplySimulator/powermeter/powermeterImpl.hpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef POWERMETER_POWERMETER_IMPL_HPP +#define POWERMETER_POWERMETER_IMPL_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 3 +// + +#include + +#include "../DCSupplySimulator.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +// insert your custom include headers here +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 + +namespace module { +namespace powermeter { + +struct Conf {}; + +class powermeterImpl : public powermeterImplBase { +public: + powermeterImpl() = delete; + powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : + powermeterImplBase(ev, "powermeter"), mod(mod), config(config){}; + + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + // insert your public definitions here + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + +protected: + // command handler functions (virtual) + virtual types::powermeter::TransactionStartResponse + handle_start_transaction(types::powermeter::TransactionReq& value) override; + virtual types::powermeter::TransactionStopResponse handle_stop_transaction(std::string& transaction_id) override; + + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + // insert your protected definitions here + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + +private: + const Everest::PtrContainer& mod; + const Conf& config; + + virtual void init() override; + virtual void ready() override; + + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 + // insert your private definitions here + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 +}; + +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 +// insert other definitions here +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 + +} // namespace powermeter +} // namespace module + +#endif // POWERMETER_POWERMETER_IMPL_HPP diff --git a/modules/simulation/JsEvManager/index.js b/modules/simulation/JsEvManager/index.js index a3fff15c95..5af87f92be 100644 --- a/modules/simulation/JsEvManager/index.js +++ b/modules/simulation/JsEvManager/index.js @@ -511,7 +511,7 @@ boot_module(async ({ // register commands setup.provides.main.register.enable(enable); - setup.provides.main.register.executeChargingSession(execute_charging_session); + setup.provides.main.register.execute_charging_session(execute_charging_session); // subscribe vars of used modules setup.uses.ev_board_support.subscribe.bsp_event((mod, str) => { diff --git a/modules/simulation/JsYetiSimulator/index.js b/modules/simulation/JsYetiSimulator/index.js index 190a1e87ae..c4f670d89f 100644 --- a/modules/simulation/JsYetiSimulator/index.js +++ b/modules/simulation/JsYetiSimulator/index.js @@ -133,10 +133,19 @@ function publish_event(mod, event) { function check_error_rcd(mod) { if (mod.simulation_data.rcd_current > 5.0) { if (!mod.rcd_error_reported) { - mod.provides.rcd.raise.ac_rcd_DC('Simulated fault event', 'High'); + let error = mod.provides.board_support.error_factory.create_error( + 'ac_rcd/DC', + '', + 'Simulated fault event', + 'High' + ); + mod.provides.board_support.raise_error(error); mod.rcd_error_reported = true; } } else { + if (mod.rcd_error_reported) { + mod.provides.board_support.clear_error('ac_rcd/DC'); + } mod.rcd_error_reported = false; } mod.provides.rcd.publish.rcd_current_mA = mod.simulation_data.rcd_current; @@ -1019,8 +1028,11 @@ function read_from_car(mod) { let amps2 = 0.0; let amps3 = 0.0; + let hlc_active = false; + if (mod.pwm_duty_cycle >= 0.03 && mod.pwm_duty_cycle <= 0.07) hlc_active = true; + let amps = dutyCycleToAmps(mod.pwm_duty_cycle); - if (amps > mod.ev_max_current) amps = mod.ev_max_current; + if (amps > mod.ev_max_current || hlc_active === true) amps = mod.ev_max_current; if (mod.relais_on === true && mod.ev_three_phases > 0) amps1 = amps; else amps1 = 0; diff --git a/tests/core_tests/startup_tests.py b/tests/core_tests/startup_tests.py index de36f823ec..3c655a36ba 100644 --- a/tests/core_tests/startup_tests.py +++ b/tests/core_tests/startup_tests.py @@ -58,7 +58,7 @@ def test(self, timeout: float) -> bool: self._mod.call_command(car_sim_ff, 'enable', {'value': True}) # start charging simulation - self._mod.call_command(car_sim_ff, 'executeChargingSession', { + self._mod.call_command(car_sim_ff, 'execute_charging_session', { 'value': 'sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 5;unplug'}) expected_events = ['TransactionStarted', 'TransactionFinished'] diff --git a/third-party/bazel/edm-wrapper.sh b/third-party/bazel/edm-wrapper.sh new file mode 100755 index 0000000000..06f26b1ba5 --- /dev/null +++ b/third-party/bazel/edm-wrapper.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +EDM_LINK=git+https://github.com/Everest/everest-dev-environment.git#subdirectory=dependency_manager +mkdir -p venv > /dev/null +TARGET_PATH=$(realpath venv) +pip3 install --upgrade -t $TARGET_PATH $EDM_LINK > /dev/null +export PYTHONPATH=$TARGET_PATH +export PATH=$TARGET_PATH/bin:$PATH + +edm "$@" \ No newline at end of file diff --git a/third-party/bazel/edm.bzl b/third-party/bazel/edm.bzl index 8cd08674ab..1acc872a26 100644 --- a/third-party/bazel/edm.bzl +++ b/third-party/bazel/edm.bzl @@ -5,8 +5,9 @@ def _edm_repositories_impl(rctx): build_files_args.append("--build-file") build_files_args.append(str(build_file)) print("build_files_args: ", build_files_args) + edm_tool = rctx.attr._edm_tool exec_result = rctx.execute( - ["edm", "bazel", rctx.attr.dependencies_yaml] + + [edm_tool, "bazel", rctx.attr.dependencies_yaml] + build_files_args, ) if exec_result.return_code != 0: @@ -27,5 +28,10 @@ edm_repositories = repository_rule( allow_files=True, doc="List of build files for external repositories", ), + "_edm_tool": attr.label( + default=Label("@everest-core//third-party/bazel:edm-wrapper.sh"), + allow_single_file=True, + doc="Path to the edm script", + ), }, ) \ No newline at end of file diff --git a/third-party/bazel/repos.bzl b/third-party/bazel/repos.bzl index 8f3a54578d..42bc4cfcd7 100644 --- a/third-party/bazel/repos.bzl +++ b/third-party/bazel/repos.bzl @@ -3,7 +3,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") load("@rules_rust//crate_universe:defs.bzl", "crates_repository", "crate") load("@rules_rust//crate_universe:repositories.bzl", "crate_universe_dependencies") -load("//third-party/bazel:edm.bzl", "edm_repositories") +load("@everest-core//third-party/bazel:edm.bzl", "edm_repositories") def everest_core_repos(): @@ -22,7 +22,6 @@ def everest_core_repos(): strip_prefix = "rules_boost-f02f84fac7673c56bbcfe69dea68044e6e40f92b", ) - crates_repository( name = "everest_core_crate_index", cargo_lockfile = "@everest-core//modules:Cargo.lock", @@ -52,7 +51,7 @@ def everest_core_repos(): edm_repositories( name = "edm_deps", - dependencies_yaml = "//:dependencies.yaml", + dependencies_yaml = "@everest-core//:dependencies.yaml", build_files = [ "@everest-core//third-party/bazel:BUILD.libmodbus.bazel", "@everest-core//third-party/bazel:BUILD.libtimer.bazel", diff --git a/types/BUILD.bazel b/types/BUILD.bazel index 7d3c7e3071..8c03e87fdb 100644 --- a/types/BUILD.bazel +++ b/types/BUILD.bazel @@ -19,13 +19,15 @@ genrule( outs = cpp_headers, srcs = yaml_srcs + [ "@everest-framework//schemas:schemas", + "@everest-core//:WORKSPACE.bazel", ], tools = [ "@everest-utils//ev-dev-tools:ev-cli", ], cmd = """ $(location @everest-utils//ev-dev-tools:ev-cli) types generate-headers \ - --everest-dir . \ + --work-dir `dirname $(location @everest-core//:WORKSPACE.bazel)` \ + --everest-dir `dirname $(location @everest-core//:WORKSPACE.bazel)` \ --schemas-dir external/everest-framework/schemas \ --disable-clang-format \ --output-dir `dirname $(location {some_header})` diff --git a/types/powermeter.yaml b/types/powermeter.yaml index 5bafcc9b65..ab0460be16 100644 --- a/types/powermeter.yaml +++ b/types/powermeter.yaml @@ -299,3 +299,12 @@ types: This is intended for meters that only support signing a collection of meter values. type: object $ref: /units_signed#/SignedMeterValue + temperatures: + description: >- + An array of temperature sensors expressed in C, their definition and meaning is product specific + type: array + items: + type: object + $ref: /temperature#/Temperature + + diff --git a/types/temperature.yaml b/types/temperature.yaml new file mode 100644 index 0000000000..9c7cc06d92 --- /dev/null +++ b/types/temperature.yaml @@ -0,0 +1,17 @@ +description: Temperature type +types: + Temperature: + description: Temperature sensor expressed in C and a description (vendor specific) allowing to identify its purpose or meaning + type: object + additionalProperties: false + required: + - temperature + properties: + temperature: + description: The temperature of the sensor measured in C + type: number + identification: + description: A (vendor specific) ID if required + type: string + +