From 2b779d5532e95a8613bd69338daa02d9fd565ce1 Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 25 May 2026 08:18:01 +0800 Subject: [PATCH] Add secure transport and OAM router runtime implementations - Implement secure transport mechanisms in `gateway_knx_secure_transport.cpp` for handling secure sessions, including AES encryption, session key generation, and secure packet wrapping and unwrapping. - Introduce `OamRouterRuntime` in `oam_router_runtime.cpp` to manage OAM router identity, individual addresses, and tunnel frame handling. - Enhance secure session management with functions for session allocation, authentication, and secure packet processing. - Ensure compatibility with existing KNXnet/IP protocols while adding support for secure communications. Signed-off-by: Tony --- README.md | 10 +- apps/gateway/main/Kconfig.projbuild | 95 +++ apps/gateway/main/app_main.cpp | 47 ++ apps/gateway/sdkconfig | 16 +- apps/gateway/sdkconfig.old | 8 +- components/gateway_ble/src/gateway_ble.cpp | 25 +- .../gateway_bridge/include/security_storage.h | 28 + .../gateway_bridge/src/gateway_bridge.cpp | 366 ++++++++ .../gateway_bridge/src/security_storage.cpp | 369 ++++++++- components/gateway_knx/CMakeLists.txt | 4 +- .../gateway_knx/include/gateway_knx.hpp | 105 ++- .../include/gateway_knx_internal.h | 43 + .../gateway_knx/include/oam_router_runtime.h | 63 ++ components/gateway_knx/src/gateway_knx.cpp | 119 +++ .../gateway_knx/src/gateway_knx_private.hpp | 14 + .../src/gateway_knx_router_lifecycle.cpp | 185 ++++- .../src/gateway_knx_router_openknx.cpp | 93 +++ .../src/gateway_knx_router_packets.cpp | 59 +- .../src/gateway_knx_router_services.cpp | 33 +- .../src/gateway_knx_secure_transport.cpp | 780 ++++++++++++++++++ .../gateway_knx/src/oam_router_runtime.cpp | 278 +++++++ knx | 2 +- 22 files changed, 2665 insertions(+), 77 deletions(-) create mode 100644 components/gateway_knx/include/oam_router_runtime.h create mode 100644 components/gateway_knx/src/gateway_knx_secure_transport.cpp create mode 100644 components/gateway_knx/src/oam_router_runtime.cpp diff --git a/README.md b/README.md index 817a00a..59beba7 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,13 @@ The native rewrite now wires a shared `gateway_core` bootstrap component, a mult ## KNX Security -KNX Data Secure and KNXnet/IP Secure support are controlled by `GATEWAY_KNX_DATA_SECURE_SUPPORTED` and `GATEWAY_KNX_IP_SECURE_SUPPORTED`. The current KNXnet/IP Secure flag reserves and reports secure service capability, while runtime secure-session transport is still reported as not implemented until that path is wired. The gateway derives its KNX serial identity from the ESP base MAC, and the development factory setup key is deterministically derived from that KNX serial so the same board keeps the same FDSK across NVS erases. +KNX Data Secure and KNXnet/IP Secure support are controlled by `GATEWAY_KNX_DATA_SECURE_SUPPORTED` and `GATEWAY_KNX_IP_SECURE_SUPPORTED`. KNXnet/IP Secure now recognizes the secure service family, performs secure session setup/authentication with provisioned tunnel user keys, wraps secure tunnel responses, handles secure group-sync frames, and wraps/unpacks secure multicast routing frames when an OAM backbone key is active. The gateway derives its KNX serial identity from the ESP base MAC, and the development factory setup key is deterministically derived from that KNX serial so the same board keeps the same FDSK across NVS erases. + +The shared KNXnet/IP endpoint can also be provisioned with an OAM-compatible IP-Router persona by enabling `GATEWAY_KNX_OAM_ROUTER_SUPPORTED` and the nested `knx.oamRouter` config. This second logical application is part of the same KNX/IP router endpoint: it does not open a second UDP/TCP listener and it does not own a second TP-UART driver. The gateway hosts a BAU091A/OAM router runtime beside the REG1-Dali BAU07B0 runtime, with separate individual/tunnel addresses, separate programming button/LED GPIOs, separate ETS/security storage, and a KNX serial derived from the ESP base MAC plus one. Secure tunnels are assigned to the OAM persona and OAM-addressed management frames are dispatched to the BAU091A runtime while DALI group/function-property traffic stays on the REG1-Dali application. The default OAM identity follows the OAM-IP-Router release database (`0x00FA` manufacturer, `0xA11F` application number, version `0x07`) unless overridden in Kconfig. + +OAM IP Secure keyring preparation uses the `knx_oam_sec` NVS namespace. Development HTTP actions can read/generate/reset/export the OAM factory setup key and store already-extracted IP Secure keyring material (`backboneKeyHex`, tunnel user keys, and an optional device-authentication key). Stored OAM credentials are reported in `knx.security.oamRouter.ipSecureCredentials`; the tunnel user keys authenticate secure sessions and the backbone key protects secure routing/group-sync traffic. The routing sequence counter is persisted back to NVS after secure routing sends or authenticated sync updates. + +Cloud KNX remote-access preparation is part of the `knx.oamRouter.cloudRemote` config. The status JSON reports the selected mode (`mqtt`, relay, or UDP punch-through-oriented deployments), whether secure tunnels are required, and whether relay endpoint, MQTT topic prefix, and token-reference fields are configured. The firmware does not start an external relay client yet; this config is the stable handoff surface for a future UDP relay/MQTT tunnel transport that will reuse the secure OAM tunnel path. The KNXnet/IP tunnel can start from the built-in default configuration before any ETS download. KNX TP-UART is enabled only when `GATEWAY_KNX_TP_UART_PORT` is `0`, `1`, or `2`; set that UART port to `-1` for IP-only operation. UART TX/RX GPIO values of `-1` mean use the ESP-IDF target default pins for that UART, not disabled. `GATEWAY_KNX_TP_UART_9BIT_MODE` enables the NCN5120/OpenKNX-style 9-bit host frame on the wire, represented on ESP-IDF as 8 data bits plus even parity. Enable `GATEWAY_KNX_TP_FULL_IP_FORWARD` when the gateway must mirror all physical TP telegrams back out to KNXnet/IP tunnelling and multicast so ETS can monitor or download other TP devices through the gateway's IP endpoint. Non-UART GPIO options use `-1` as disabled, including the KNX programming button, KNX programming LED, setup AP button, Wi-Fi reset button, and status LED. @@ -68,6 +74,8 @@ The bridge service exposes one shared KNXnet/IP endpoint per physical gateway on KNX programming mode can be controlled locally with `GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO`, and `GATEWAY_KNX_PROGRAMMING_LED_GPIO` mirrors the current programming-mode state. The setup AP entry button is configured separately with `GATEWAY_SETUP_AP_BUTTON_GPIO`; Wi-Fi credential reset remains a separate long-press function on `GATEWAY_BOOT_BUTTON_GPIO` when enabled. +When the OAM router persona is enabled, use `GATEWAY_KNX_OAM_PROGRAMMING_BUTTON_GPIO` and `GATEWAY_KNX_OAM_PROGRAMMING_LED_GPIO` for its separate programming controls. The bridge config validator rejects duplicate REG1-Dali and OAM programming button or LED GPIO assignments so ETS programming-mode selection remains unambiguous. + When `GATEWAY_KNX_SECURITY_DEV_ENDPOINTS` is enabled, the bridge HTTP action surface exposes development-only operations for reading, writing, generating, and resetting the factory setup key, exporting the factory certificate payload, and clearing local KNX security failure diagnostics. These endpoints require explicit confirmation fields in the JSON body and should stay disabled in production builds. The default development storage mode is plain NVS via `GATEWAY_KNX_SECURITY_PLAIN_NVS`; production builds should replace that with encrypted NVS, flash encryption, and secure boot before handling real commissioning keys. The normal bridge status response includes a `knx.security` object with compile-time capability flags, storage mode, factory setup key metadata, factory certificate metadata, and security failure counters/log entries. Secret FDSK strings are returned only by the explicit development actions, not by passive status polling. diff --git a/apps/gateway/main/Kconfig.projbuild b/apps/gateway/main/Kconfig.projbuild index a1d463a..66a11e0 100644 --- a/apps/gateway/main/Kconfig.projbuild +++ b/apps/gateway/main/Kconfig.projbuild @@ -779,6 +779,101 @@ config GATEWAY_KNX_INDIVIDUAL_ADDRESS Raw 16-bit individual address used by the ETS-programmable KNX-DALI gateway device. The default 65534 is 15.15.254, used as the unprogrammed logical device address. +config GATEWAY_KNX_OAM_ROUTER_SUPPORTED + bool "OAM-compatible KNX/IP router persona is supported" + depends on GATEWAY_KNX_BRIDGE_SUPPORTED + default n + help + Compiles support for a second OAM-compatible BAU091A KNX/IP router + application behind the same KNXnet/IP endpoint and TP interface as the + KNX-DALI gateway application. + +config GATEWAY_KNX_OAM_ROUTER_ENABLED + bool "Enable OAM-compatible KNX/IP router persona by default" + depends on GATEWAY_KNX_OAM_ROUTER_SUPPORTED + default n + help + Enables the second router application in the default KNX bridge config. + The physical UDP/TCP endpoint and TP-UART remain shared with the main + KNX/IP router settings. + +config GATEWAY_KNX_OAM_ROUTER_OEM_MANUFACTURER_ID + hex "OAM router OEM manufacturer ID" + depends on GATEWAY_KNX_OAM_ROUTER_SUPPORTED + range 0x0000 0xffff + default 0x00FA + help + Manufacturer ID advertised by the OAM-compatible IP router application. + The default follows the OpenKNX OAM-IP-Router reference database. + +config GATEWAY_KNX_OAM_ROUTER_HARDWARE_ID + hex "OAM router hardware ID" + depends on GATEWAY_KNX_OAM_ROUTER_SUPPORTED + range 0x0000 0xffff + default 0x0001 + +config GATEWAY_KNX_OAM_ROUTER_APPLICATION_NUMBER + hex "OAM router application number" + depends on GATEWAY_KNX_OAM_ROUTER_SUPPORTED + range 0x0000 0xffff + default 0xA11F + help + Application number for the OAM IP-Router release database. + +config GATEWAY_KNX_OAM_ROUTER_APPLICATION_VERSION + hex "OAM router application version" + depends on GATEWAY_KNX_OAM_ROUTER_SUPPORTED + range 0x00 0xff + default 0x07 + help + Application version for the OAM IP-Router release database. + +config GATEWAY_KNX_OAM_ROUTER_INDIVIDUAL_ADDRESS + int "OAM router individual address raw value" + depends on GATEWAY_KNX_OAM_ROUTER_SUPPORTED + range 0 65535 + default 65282 + help + Raw 16-bit individual address for the second BAU091A router application. + The default 65282 is 15.15.2. + +config GATEWAY_KNX_OAM_ROUTER_TUNNEL_ADDRESS_BASE + int "OAM router tunnel address base raw value" + depends on GATEWAY_KNX_OAM_ROUTER_SUPPORTED + range 0 65520 + default 65296 + help + First raw 16-bit individual address reserved for the OAM router tunnel + users. The default 65296 is 15.15.16 and leaves room for 16 tunnels. + +config GATEWAY_KNX_OAM_PROGRAMMING_BUTTON_GPIO + int "OAM router programming button GPIO" + depends on GATEWAY_KNX_OAM_ROUTER_SUPPORTED + range -1 48 + default -1 + help + GPIO used to toggle programming mode for the second OAM router + application. Set to -1 to disable the local OAM programming button. + +config GATEWAY_KNX_OAM_PROGRAMMING_BUTTON_ACTIVE_LOW + bool "OAM router programming button is active low" + depends on GATEWAY_KNX_OAM_PROGRAMMING_BUTTON_GPIO >= 0 + default y + +config GATEWAY_KNX_OAM_PROGRAMMING_LED_GPIO + int "OAM router programming LED GPIO" + depends on GATEWAY_KNX_OAM_ROUTER_SUPPORTED + range -1 48 + default -1 + help + GPIO used to show programming mode for the second OAM router + application. Set to -1 to disable the local OAM programming LED. + +config GATEWAY_KNX_OAM_PROGRAMMING_LED_ACTIVE_HIGH + bool "OAM router programming LED is active high" + depends on GATEWAY_KNX_OAM_PROGRAMMING_LED_GPIO >= 0 + default y + config GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO int "KNX programming button GPIO" depends on GATEWAY_KNX_BRIDGE_SUPPORTED diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index 437b4c2..bf31ad4 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -243,6 +243,22 @@ #define CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS 65281 #endif +#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_INDIVIDUAL_ADDRESS +#define CONFIG_GATEWAY_KNX_OAM_ROUTER_INDIVIDUAL_ADDRESS 65282 +#endif + +#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_TUNNEL_ADDRESS_BASE +#define CONFIG_GATEWAY_KNX_OAM_ROUTER_TUNNEL_ADDRESS_BASE 65296 +#endif + +#ifndef CONFIG_GATEWAY_KNX_OAM_PROGRAMMING_BUTTON_GPIO +#define CONFIG_GATEWAY_KNX_OAM_PROGRAMMING_BUTTON_GPIO -1 +#endif + +#ifndef CONFIG_GATEWAY_KNX_OAM_PROGRAMMING_LED_GPIO +#define CONFIG_GATEWAY_KNX_OAM_PROGRAMMING_LED_GPIO -1 +#endif + #ifndef CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO #define CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO -1 #endif @@ -400,6 +416,18 @@ constexpr bool kKnxMulticastEnabled = true; constexpr bool kKnxMulticastEnabled = false; #endif +#ifdef CONFIG_GATEWAY_KNX_OAM_ROUTER_SUPPORTED +constexpr bool kKnxOamRouterSupported = true; +#else +constexpr bool kKnxOamRouterSupported = false; +#endif + +#ifdef CONFIG_GATEWAY_KNX_OAM_ROUTER_ENABLED +constexpr bool kKnxOamRouterEnabled = true; +#else +constexpr bool kKnxOamRouterEnabled = false; +#endif + #ifdef CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED constexpr bool kCloudBridgeSupported = true; #else @@ -904,6 +932,25 @@ extern "C" void app_main(void) { static_cast(CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS); default_knx.individual_address = static_cast(CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS); + default_knx.oam_router.enabled = kKnxOamRouterSupported && kKnxOamRouterEnabled; + default_knx.oam_router.individual_address = + static_cast(CONFIG_GATEWAY_KNX_OAM_ROUTER_INDIVIDUAL_ADDRESS); + default_knx.oam_router.tunnel_address_base = + static_cast(CONFIG_GATEWAY_KNX_OAM_ROUTER_TUNNEL_ADDRESS_BASE); + default_knx.oam_router.programming_button_gpio = + CONFIG_GATEWAY_KNX_OAM_PROGRAMMING_BUTTON_GPIO; + default_knx.oam_router.programming_led_gpio = + CONFIG_GATEWAY_KNX_OAM_PROGRAMMING_LED_GPIO; + #ifdef CONFIG_GATEWAY_KNX_OAM_PROGRAMMING_BUTTON_ACTIVE_LOW + default_knx.oam_router.programming_button_active_low = true; + #else + default_knx.oam_router.programming_button_active_low = false; + #endif + #ifdef CONFIG_GATEWAY_KNX_OAM_PROGRAMMING_LED_ACTIVE_HIGH + default_knx.oam_router.programming_led_active_high = true; + #else + default_knx.oam_router.programming_led_active_high = false; + #endif default_knx.programming_button_gpio = CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO; default_knx.programming_led_gpio = CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO; #ifdef CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW diff --git a/apps/gateway/sdkconfig b/apps/gateway/sdkconfig index 000d0ae..bd4e6b3 100644 --- a/apps/gateway/sdkconfig +++ b/apps/gateway/sdkconfig @@ -596,7 +596,7 @@ CONFIG_PARTITION_TABLE_MD5=y # # Gateway App # -CONFIG_GATEWAY_CHANNEL_COUNT=1 +CONFIG_GATEWAY_CHANNEL_COUNT=2 # # Gateway Channel 1 @@ -615,6 +615,15 @@ CONFIG_GATEWAY_CHANNEL1_NATIVE_BAUDRATE=1200 # # Gateway Channel 2 # +CONFIG_GATEWAY_CHANNEL2_GW_ID=4 +# CONFIG_GATEWAY_CHANNEL2_PHY_DISABLED is not set +CONFIG_GATEWAY_CHANNEL2_PHY_NATIVE=y +# CONFIG_GATEWAY_CHANNEL2_PHY_UART1 is not set +# CONFIG_GATEWAY_CHANNEL2_PHY_UART2 is not set +CONFIG_GATEWAY_CHANNEL2_NATIVE_BUS_ID=1 +CONFIG_GATEWAY_CHANNEL2_NATIVE_TX_PIN=4 +CONFIG_GATEWAY_CHANNEL2_NATIVE_RX_PIN=3 +CONFIG_GATEWAY_CHANNEL2_NATIVE_BAUDRATE=1200 # end of Gateway Channel 2 # @@ -624,7 +633,7 @@ CONFIG_GATEWAY_CACHE_SUPPORTED=y CONFIG_GATEWAY_CACHE_START_ENABLED=y CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED=y CONFIG_GATEWAY_CACHE_FULL_STATE_MIRROR=y -CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS=10000 +CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS=600000 CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS=120000 CONFIG_GATEWAY_CACHE_OUTSIDE_BUS_FIRST=y # CONFIG_GATEWAY_CACHE_LOCAL_GATEWAY_FIRST is not set @@ -658,7 +667,7 @@ CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO=33 CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO=34 CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO=36 CONFIG_GATEWAY_ETHERNET_W5500_POLL_PERIOD_MS=0 -CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=40 +CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=20 CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO=-1 CONFIG_GATEWAY_ETHERNET_PHY_ADDR=1 CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=4096 @@ -693,6 +702,7 @@ CONFIG_GATEWAY_KNX_UDP_PORT=3671 CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS="224.0.23.12" CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS=65281 CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=65534 +# CONFIG_GATEWAY_KNX_OAM_ROUTER_SUPPORTED is not set CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO=0 CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW=y CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO=10 diff --git a/apps/gateway/sdkconfig.old b/apps/gateway/sdkconfig.old index 2b1e99a..a3c7b1e 100644 --- a/apps/gateway/sdkconfig.old +++ b/apps/gateway/sdkconfig.old @@ -622,9 +622,12 @@ CONFIG_GATEWAY_CHANNEL1_NATIVE_BAUDRATE=1200 # CONFIG_GATEWAY_CACHE_SUPPORTED=y CONFIG_GATEWAY_CACHE_START_ENABLED=y -# CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED is not set +CONFIG_GATEWAY_CACHE_RECONCILIATION_ENABLED=y +CONFIG_GATEWAY_CACHE_FULL_STATE_MIRROR=y CONFIG_GATEWAY_CACHE_FLUSH_INTERVAL_MS=10000 CONFIG_GATEWAY_CACHE_REFRESH_INTERVAL_MS=120000 +CONFIG_GATEWAY_CACHE_OUTSIDE_BUS_FIRST=y +# CONFIG_GATEWAY_CACHE_LOCAL_GATEWAY_FIRST is not set # end of Gateway Cache # CONFIG_GATEWAY_ENABLE_DALI_BUS is not set @@ -655,7 +658,7 @@ CONFIG_GATEWAY_ETHERNET_W5500_MISO_GPIO=33 CONFIG_GATEWAY_ETHERNET_W5500_CS_GPIO=34 CONFIG_GATEWAY_ETHERNET_W5500_INT_GPIO=36 CONFIG_GATEWAY_ETHERNET_W5500_POLL_PERIOD_MS=0 -CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=40 +CONFIG_GATEWAY_ETHERNET_W5500_CLOCK_MHZ=20 CONFIG_GATEWAY_ETHERNET_PHY_RESET_GPIO=-1 CONFIG_GATEWAY_ETHERNET_PHY_ADDR=1 CONFIG_GATEWAY_ETHERNET_RX_TASK_STACK_SIZE=4096 @@ -690,6 +693,7 @@ CONFIG_GATEWAY_KNX_UDP_PORT=3671 CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS="224.0.23.12" CONFIG_GATEWAY_KNX_IP_INTERFACE_INDIVIDUAL_ADDRESS=65281 CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=65534 +# CONFIG_GATEWAY_KNX_OAM_ROUTER_SUPPORTED is not set CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_GPIO=0 CONFIG_GATEWAY_KNX_PROGRAMMING_BUTTON_ACTIVE_LOW=y CONFIG_GATEWAY_KNX_PROGRAMMING_LED_GPIO=10 diff --git a/components/gateway_ble/src/gateway_ble.cpp b/components/gateway_ble/src/gateway_ble.cpp index ed5db12..7021461 100644 --- a/components/gateway_ble/src/gateway_ble.cpp +++ b/components/gateway_ble/src/gateway_ble.cpp @@ -4,6 +4,9 @@ #include "gateway_controller.hpp" #include "gateway_runtime.hpp" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + #include "esp_log.h" #include "esp_timer.h" #include "host/ble_gap.h" @@ -29,6 +32,8 @@ constexpr uint16_t kChannel2Uuid = 0xFFF2; constexpr uint16_t kGatewayUuid = 0xFFF3; constexpr int64_t kGenericDedupeWindowUs = 120000; constexpr size_t kGatewayCharacteristicIndex = 2; +constexpr int kGatewayNotifyAllocationAttempts = 6; +constexpr TickType_t kGatewayNotifyRetryDelayTicks = pdMS_TO_TICKS(20); gateway::GatewayBleBridge* s_active_bridge = nullptr; uint16_t s_value_handles[3] = {0, 0, 0}; @@ -348,9 +353,25 @@ void GatewayBleBridge::notifyCharacteristic(size_t index, const std::vector(index)); + ESP_LOGW(kTag, "failed to allocate notify mbuf idx=%u attempts=%d len=%u", + static_cast(index), allocation_attempts, + static_cast(payload.size())); return; } const int rc = ble_gatts_notify_custom(conn_handle_, s_value_handles[index], buffer); diff --git a/components/gateway_bridge/include/security_storage.h b/components/gateway_bridge/include/security_storage.h index 5faea8f..c1bff20 100644 --- a/components/gateway_bridge/include/security_storage.h +++ b/components/gateway_bridge/include/security_storage.h @@ -3,6 +3,9 @@ #include #include #include +#include + +#include "gateway_knx.hpp" namespace gateway::openknx { @@ -27,6 +30,14 @@ struct FactoryCertificatePayload { std::string checksum; }; +struct IpSecureCredentialStatus { + bool activated{false}; + bool backboneKeyAvailable{false}; + bool deviceAuthenticationKeyAvailable{false}; + uint8_t tunnelUserCount{0}; + uint64_t routingSequence{0}; +}; + bool LoadFactoryFdsk(uint8_t* data, size_t len); FactoryFdskInfo LoadFactoryFdskInfo(); bool GenerateFactoryFdsk(FactoryFdskInfo* info = nullptr); @@ -34,4 +45,21 @@ bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info = nul bool ResetFactoryFdskCache(FactoryFdskInfo* info = nullptr); FactoryCertificatePayload BuildFactoryCertificatePayload(); +bool LoadOamFactoryFdsk(uint8_t* data, size_t len); +FactoryFdskInfo LoadOamFactoryFdskInfo(); +bool GenerateOamFactoryFdsk(FactoryFdskInfo* info = nullptr); +bool WriteOamFactoryFdskHex(const std::string& hex_key, + FactoryFdskInfo* info = nullptr); +bool ResetOamFactoryFdskCache(FactoryFdskInfo* info = nullptr); +FactoryCertificatePayload BuildOamFactoryCertificatePayload(); + +IpSecureCredentialStatus LoadOamIpSecureCredentialStatus(); +::gateway::GatewayKnxIpSecureCredentialMaterial LoadOamIpSecureCredentialMaterial(); +bool WriteOamIpSecureKeyringHex(const std::string& backbone_key_hex, + const std::vector& tunnel_user_key_hex, + const std::string& device_auth_key_hex, + bool activated); +bool StoreOamIpSecureRoutingSequence(uint64_t sequence); +bool ClearOamIpSecureKeyring(); + } // namespace gateway::openknx diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index d9899c7..8d9f788 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -199,6 +199,22 @@ cJSON* FactoryCertificateToCjson(const openknx::FactoryCertificatePayload& certi return root; } +cJSON* IpSecureCredentialStatusToCjson( + const openknx::IpSecureCredentialStatus& status) { + cJSON* root = cJSON_CreateObject(); + if (root == nullptr) { + return nullptr; + } + cJSON_AddBoolToObject(root, "activated", status.activated); + cJSON_AddBoolToObject(root, "backboneKeyAvailable", status.backboneKeyAvailable); + cJSON_AddBoolToObject(root, "deviceAuthenticationKeyAvailable", + status.deviceAuthenticationKeyAvailable); + cJSON_AddNumberToObject(root, "tunnelUserCount", status.tunnelUserCount); + cJSON_AddNumberToObject(root, "routingSequence", + static_cast(status.routingSequence)); + return root; +} + cJSON* SecurityFailuresToCjson() { cJSON* root = cJSON_CreateObject(); if (root == nullptr) { @@ -1522,6 +1538,10 @@ struct GatewayBridgeService::ChannelRuntime { [this](uint16_t group_object_number, const uint8_t* data, size_t len) { return service.routeKnxGroupObjectWrite(group_object_number, data, len); }); + knx_router->setOamIpSecureRoutingSequenceStoreHandler([](uint64_t sequence) { + openknx::StoreOamIpSecureRoutingSequence(sequence); + }); + knx_router->setOamIpSecureCredentials(openknx::LoadOamIpSecureCredentialMaterial()); if (const auto active_knx = activeKnxConfigLocked(); active_knx.has_value()) { knx->setConfig(active_knx.value()); knx_router->setConfig(active_knx.value()); @@ -2154,6 +2174,7 @@ struct GatewayBridgeService::ChannelRuntime { runtime_config.individual_address); knx->setConfig(runtime_config); knx_router->setConfig(runtime_config); + knx_router->setOamIpSecureCredentials(openknx::LoadOamIpSecureCredentialMaterial()); knx_router->setCommissioningOnly(commissioning_only); if (!runtime_config.ip_router_enabled) { knx_started = false; @@ -2218,6 +2239,7 @@ struct GatewayBridgeService::ChannelRuntime { endpoint_runtime = const_cast(service).selectKnxEndpointRuntime(); } bool programming_mode = false; + bool oam_programming_mode = false; bool programming_control_available = false; int endpoint_owner_gateway_id = -1; if (endpoint_runtime != nullptr) { @@ -2227,6 +2249,7 @@ struct GatewayBridgeService::ChannelRuntime { endpoint_runtime->knx_router->started(); if (programming_control_available) { programming_mode = endpoint_runtime->knx_router->programmingMode(); + oam_programming_mode = endpoint_runtime->knx_router->oamProgrammingMode(); } } const auto effective_knx = @@ -2236,6 +2259,7 @@ struct GatewayBridgeService::ChannelRuntime { cJSON_AddBoolToObject(knx_json, "started", knx_started); cJSON_AddBoolToObject(knx_json, "routerReady", knx_router != nullptr && knx_router->started()); cJSON_AddBoolToObject(knx_json, "programmingMode", programming_mode); + cJSON_AddBoolToObject(knx_json, "oamProgrammingMode", oam_programming_mode); cJSON_AddBoolToObject(knx_json, "programmingControlAvailable", programming_control_available); cJSON_AddBoolToObject(knx_json, "endpointOwner", @@ -2265,7 +2289,11 @@ struct GatewayBridgeService::ChannelRuntime { #else cJSON_AddBoolToObject(security_json, "knxnetIpSecureServicesRecognized", false); #endif + #if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", true); + #else cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", false); + #endif #if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", true); #else @@ -2294,6 +2322,27 @@ struct GatewayBridgeService::ChannelRuntime { cJSON_AddItemToObject(security_json, "failures", failures_json); } #endif + cJSON* oam_security_json = cJSON_CreateObject(); + if (oam_security_json != nullptr) { + cJSON* oam_fdsk_json = FactoryFdskInfoToCjson( + openknx::LoadOamFactoryFdskInfo(), false); + if (oam_fdsk_json != nullptr) { + cJSON_AddItemToObject(oam_security_json, "factorySetupKey", oam_fdsk_json); + } + cJSON* oam_certificate_json = FactoryCertificateToCjson( + openknx::BuildOamFactoryCertificatePayload(), false); + if (oam_certificate_json != nullptr) { + cJSON_AddItemToObject(oam_security_json, "factoryCertificate", + oam_certificate_json); + } + cJSON* oam_credentials_json = IpSecureCredentialStatusToCjson( + openknx::LoadOamIpSecureCredentialStatus()); + if (oam_credentials_json != nullptr) { + cJSON_AddItemToObject(oam_security_json, "ipSecureCredentials", + oam_credentials_json); + } + cJSON_AddItemToObject(security_json, "oamRouter", oam_security_json); + } cJSON_AddItemToObject(knx_json, "security", security_json); } if (effective_knx.has_value()) { @@ -2314,6 +2363,29 @@ struct GatewayBridgeService::ChannelRuntime { effective_knx->ip_interface_individual_address); cJSON_AddNumberToObject(knx_json, "individualAddress", effective_knx->individual_address); + cJSON* oam_json = ToCjson( + GatewayKnxOamRouterConfigToValue(effective_knx->oam_router)); + if (oam_json != nullptr) { + cJSON_AddItemToObject(knx_json, "oamRouter", oam_json); + } + cJSON* cloud_remote_json = cJSON_CreateObject(); + if (cloud_remote_json != nullptr) { + const auto& cloud_remote = effective_knx->oam_router.cloud_remote; + cJSON_AddBoolToObject(cloud_remote_json, "prepared", true); + cJSON_AddBoolToObject(cloud_remote_json, "enabled", cloud_remote.enabled); + cJSON_AddStringToObject(cloud_remote_json, "mode", cloud_remote.mode.c_str()); + cJSON_AddBoolToObject(cloud_remote_json, "requireSecureTunnel", + cloud_remote.require_secure_tunnel); + cJSON_AddBoolToObject(cloud_remote_json, "udpPunchEnabled", + cloud_remote.udp_punch_enabled); + cJSON_AddBoolToObject(cloud_remote_json, "relayEndpointConfigured", + !cloud_remote.relay_endpoint.empty()); + cJSON_AddBoolToObject(cloud_remote_json, "mqttTopicPrefixConfigured", + !cloud_remote.mqtt_topic_prefix.empty()); + cJSON_AddBoolToObject(cloud_remote_json, "authTokenRefConfigured", + !cloud_remote.auth_token_ref.empty()); + cJSON_AddItemToObject(knx_json, "cloudKnxRemoteAccess", cloud_remote_json); + } cJSON* serial_json = cJSON_CreateObject(); if (serial_json != nullptr) { cJSON_AddNumberToObject(serial_json, "uartPort", effective_knx->tp_uart.uart_port); @@ -3220,6 +3292,46 @@ struct GatewayBridgeService::ChannelRuntime { } return ESP_ERR_INVALID_ARG; } + if (config.oam_router.enabled) { + if (config.oam_router.individual_address == 0 || + config.oam_router.individual_address == 0xffff) { + if (error_message != nullptr) { + *error_message = "OAM KNX/IP router individual address must be a configured address"; + } + return ESP_ERR_INVALID_ARG; + } + if (config.oam_router.tunnel_address_base == 0 || + config.oam_router.tunnel_address_base == 0xffff || + config.oam_router.tunnel_address_base > 0xffef) { + if (error_message != nullptr) { + *error_message = "OAM KNX/IP router tunnel address base must leave room for 16 tunnels"; + } + return ESP_ERR_INVALID_ARG; + } + if (config.oam_router.individual_address == config.individual_address || + config.oam_router.individual_address == config.ip_interface_individual_address || + config.oam_router.tunnel_address_base == config.individual_address || + config.oam_router.tunnel_address_base == config.ip_interface_individual_address) { + if (error_message != nullptr) { + *error_message = "OAM KNX/IP router addresses must differ from the shared IP interface and KNX-DALI gateway addresses"; + } + return ESP_ERR_INVALID_ARG; + } + if (config.programming_button_gpio >= 0 && + config.programming_button_gpio == config.oam_router.programming_button_gpio) { + if (error_message != nullptr) { + *error_message = "OAM and KNX-DALI programming buttons must use separate GPIOs"; + } + return ESP_ERR_INVALID_ARG; + } + if (config.programming_led_gpio >= 0 && + config.programming_led_gpio == config.oam_router.programming_led_gpio) { + if (error_message != nullptr) { + *error_message = "OAM and KNX-DALI programming LEDs must use separate GPIOs"; + } + return ESP_ERR_INVALID_ARG; + } + } if (!config.ip_router_enabled || !GatewayKnxConfigUsesTpUart(config)) { return ESP_OK; } @@ -3360,6 +3472,7 @@ struct GatewayBridgeService::ChannelRuntime { } if (knx_router != nullptr) { knx_router->setConfig(merged_config); + knx_router->setOamIpSecureCredentials(openknx::LoadOamIpSecureCredentialMaterial()); knx_router->setCommissioningOnly(false); } if (restart_router) { @@ -4615,6 +4728,40 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost( } return handleGet("status", gateway_id.value()); } + if (action == "knx_oam_programming_mode") { + cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size()); + if (body_root == nullptr) { + return ErrorResponse(ESP_ERR_INVALID_ARG, "OAM KNX programming mode JSON is required"); + } + const cJSON* enabled_item = cJSON_GetObjectItemCaseSensitive(body_root, "enabled"); + if (!cJSON_IsBool(enabled_item)) { + cJSON_Delete(body_root); + return ErrorResponse(ESP_ERR_INVALID_ARG, "boolean enabled field is required"); + } + const bool enabled = cJSON_IsTrue(enabled_item); + cJSON_Delete(body_root); + + ChannelRuntime* owner = selectKnxEndpointRuntime(); + if (owner == nullptr) { + return ErrorResponse(ESP_ERR_NOT_FOUND, "no KNX/IP endpoint owner is configured"); + } + + esp_err_t err = ESP_ERR_INVALID_STATE; + std::string detail = "KNX/IP router is unavailable"; + { + LockGuard guard(owner->lock); + if (owner->knx_router != nullptr) { + err = owner->knx_router->setOamProgrammingMode(enabled); + detail = owner->knx_router->lastError(); + } + owner->knx_last_error = err == ESP_OK ? std::string() : detail; + } + if (err != ESP_OK) { + return ErrorResponse(err, detail.empty() ? "failed to change OAM KNX programming mode" + : detail.c_str()); + } + return handleGet("status", gateway_id.value()); + } if (action == "knx_security_read_factory_key") { #if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \ defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED) @@ -4807,6 +4954,225 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost( #else return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "KNX security development endpoints are disabled"); +#endif + } + if (action == "knx_oam_security_read_factory_key") { +#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) + cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size()); + if (body_root == nullptr) { + return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required"); + } + const char* confirm = JsonString(body_root, "confirm"); + const bool confirmed = confirm != nullptr && + std::string_view(confirm) == "read-oam-factory-setup-key"; + cJSON_Delete(body_root); + if (!confirmed) { + return ErrorResponse(ESP_ERR_INVALID_ARG, + "OAM factory setup key read confirmation is required"); + } + cJSON* response = cJSON_CreateObject(); + if (response == nullptr) { + return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response"); + } + cJSON* fdsk_json = FactoryFdskInfoToCjson(openknx::LoadOamFactoryFdskInfo(), true); + if (fdsk_json != nullptr) { + cJSON_AddItemToObject(response, "factorySetupKey", fdsk_json); + } + return JsonOk(response); +#else + return ErrorResponse(ESP_ERR_NOT_SUPPORTED, + "KNX security development endpoints are disabled"); +#endif + } + if (action == "knx_oam_security_generate_factory_key") { +#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) + cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size()); + if (body_root == nullptr) { + return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required"); + } + const char* confirm = JsonString(body_root, "confirm"); + const bool include_secret = JsonBool(body_root, "includeSecret", false); + const bool confirmed = confirm != nullptr && + std::string_view(confirm) == "generate-oam-factory-setup-key"; + cJSON_Delete(body_root); + if (!confirmed) { + return ErrorResponse(ESP_ERR_INVALID_ARG, + "OAM factory setup key generation confirmation is required"); + } + openknx::FactoryFdskInfo info; + if (!openknx::GenerateOamFactoryFdsk(&info)) { + return ErrorResponse(ESP_FAIL, "failed to generate OAM factory setup key"); + } + cJSON* response = cJSON_CreateObject(); + if (response == nullptr) { + return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response"); + } + cJSON* fdsk_json = FactoryFdskInfoToCjson(info, include_secret); + if (fdsk_json != nullptr) { + cJSON_AddItemToObject(response, "factorySetupKey", fdsk_json); + } + return JsonOk(response); +#else + return ErrorResponse(ESP_ERR_NOT_SUPPORTED, + "KNX security development endpoints are disabled"); +#endif + } + if (action == "knx_oam_security_reset_factory_key") { +#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) + cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size()); + if (body_root == nullptr) { + return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required"); + } + const char* confirm = JsonString(body_root, "confirm"); + const bool confirmed = confirm != nullptr && + std::string_view(confirm) == "reset-oam-factory-setup-key"; + cJSON_Delete(body_root); + if (!confirmed) { + return ErrorResponse(ESP_ERR_INVALID_ARG, + "OAM factory setup key reset confirmation is required"); + } + openknx::FactoryFdskInfo info; + if (!openknx::ResetOamFactoryFdskCache(&info)) { + return ErrorResponse(ESP_FAIL, "failed to reload OAM factory setup key"); + } + cJSON* response = cJSON_CreateObject(); + if (response == nullptr) { + return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response"); + } + cJSON* fdsk_json = FactoryFdskInfoToCjson(info, false); + if (fdsk_json != nullptr) { + cJSON_AddItemToObject(response, "factorySetupKey", fdsk_json); + } + return JsonOk(response); +#else + return ErrorResponse(ESP_ERR_NOT_SUPPORTED, + "KNX security development endpoints are disabled"); +#endif + } + if (action == "knx_oam_security_export_factory_certificate") { +#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) + cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size()); + if (body_root == nullptr) { + return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required"); + } + const char* confirm = JsonString(body_root, "confirm"); + const bool confirmed = confirm != nullptr && + std::string_view(confirm) == "export-oam-factory-certificate"; + cJSON_Delete(body_root); + if (!confirmed) { + return ErrorResponse(ESP_ERR_INVALID_ARG, + "OAM factory certificate export confirmation is required"); + } + cJSON* response = cJSON_CreateObject(); + if (response == nullptr) { + return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response"); + } + cJSON* certificate_json = FactoryCertificateToCjson( + openknx::BuildOamFactoryCertificatePayload(), true); + if (certificate_json != nullptr) { + cJSON_AddItemToObject(response, "factoryCertificate", certificate_json); + } + return JsonOk(response); +#else + return ErrorResponse(ESP_ERR_NOT_SUPPORTED, + "KNX security development endpoints are disabled"); +#endif + } + if (action == "knx_oam_security_upload_keyring") { +#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \ + defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size()); + if (body_root == nullptr) { + return ErrorResponse(ESP_ERR_INVALID_ARG, "OAM keyring JSON is required"); + } + const char* confirm = JsonString(body_root, "confirm"); + const char* backbone_key = JsonString(body_root, "backboneKeyHex"); + const char* device_auth_key = JsonString(body_root, "deviceAuthenticationKeyHex"); + const bool activated = JsonBool(body_root, "activated", true); + const bool confirmed = confirm != nullptr && + std::string_view(confirm) == "upload-oam-ip-secure-keyring"; + std::vector tunnel_keys; + const cJSON* tunnel_array = cJSON_GetObjectItemCaseSensitive(body_root, "tunnelUserKeysHex"); + if (tunnel_array == nullptr) { + tunnel_array = cJSON_GetObjectItemCaseSensitive(body_root, "tunnelUserKeyHex"); + } + if (cJSON_IsArray(tunnel_array)) { + const cJSON* item = nullptr; + cJSON_ArrayForEach(item, tunnel_array) { + if (cJSON_IsString(item) && item->valuestring != nullptr) { + tunnel_keys.emplace_back(item->valuestring); + } + } + } else if (cJSON_IsString(tunnel_array) && tunnel_array->valuestring != nullptr) { + tunnel_keys.emplace_back(tunnel_array->valuestring); + } + if (!confirmed || backbone_key == nullptr) { + cJSON_Delete(body_root); + return ErrorResponse(ESP_ERR_INVALID_ARG, + "OAM keyring upload confirmation and backboneKeyHex are required"); + } + const std::string backbone_key_value(backbone_key); + const std::string device_auth_key_value(device_auth_key == nullptr ? "" : device_auth_key); + cJSON_Delete(body_root); + if (!openknx::WriteOamIpSecureKeyringHex(backbone_key_value, tunnel_keys, + device_auth_key_value, activated)) { + return ErrorResponse(ESP_ERR_INVALID_ARG, + "invalid OAM IP Secure keyring material"); + } + if (runtime->knx_router != nullptr) { + runtime->knx_router->setOamIpSecureCredentials( + openknx::LoadOamIpSecureCredentialMaterial()); + } + cJSON* response = cJSON_CreateObject(); + if (response == nullptr) { + return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM keyring response"); + } + cJSON* credentials_json = IpSecureCredentialStatusToCjson( + openknx::LoadOamIpSecureCredentialStatus()); + if (credentials_json != nullptr) { + cJSON_AddItemToObject(response, "ipSecureCredentials", credentials_json); + } + return JsonOk(response); +#else + return ErrorResponse(ESP_ERR_NOT_SUPPORTED, + "KNXnet/IP Secure development endpoints are disabled"); +#endif + } + if (action == "knx_oam_security_clear_keyring") { +#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \ + defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size()); + if (body_root == nullptr) { + return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required"); + } + const char* confirm = JsonString(body_root, "confirm"); + const bool confirmed = confirm != nullptr && + std::string_view(confirm) == "clear-oam-ip-secure-keyring"; + cJSON_Delete(body_root); + if (!confirmed) { + return ErrorResponse(ESP_ERR_INVALID_ARG, + "OAM keyring clear confirmation is required"); + } + if (!openknx::ClearOamIpSecureKeyring()) { + return ErrorResponse(ESP_FAIL, "failed to clear OAM IP Secure keyring"); + } + if (runtime->knx_router != nullptr) { + runtime->knx_router->setOamIpSecureCredentials( + openknx::LoadOamIpSecureCredentialMaterial()); + } + cJSON* response = cJSON_CreateObject(); + if (response == nullptr) { + return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM keyring response"); + } + cJSON* credentials_json = IpSecureCredentialStatusToCjson( + openknx::LoadOamIpSecureCredentialStatus()); + if (credentials_json != nullptr) { + cJSON_AddItemToObject(response, "ipSecureCredentials", credentials_json); + } + return JsonOk(response); +#else + return ErrorResponse(ESP_ERR_NOT_SUPPORTED, + "KNXnet/IP Secure development endpoints are disabled"); #endif } if (action == "bacnet_start") { diff --git a/components/gateway_bridge/src/security_storage.cpp b/components/gateway_bridge/src/security_storage.cpp index 90e18d2..44b1370 100644 --- a/components/gateway_bridge/src/security_storage.cpp +++ b/components/gateway_bridge/src/security_storage.cpp @@ -22,13 +22,21 @@ namespace { constexpr const char* kTag = "openknx_sec"; constexpr const char* kNamespace = "knx_sec"; +constexpr const char* kOamNamespace = "knx_oam_sec"; constexpr const char* kFactoryFdskKey = "factory_fdsk"; +constexpr const char* kIpSecureBackboneKey = "ipsec_backbone"; +constexpr const char* kIpSecureDeviceAuthKey = "ipsec_dev_auth"; +constexpr const char* kIpSecureTunnelCountKey = "ipsec_tunnels"; +constexpr const char* kIpSecureActivatedKey = "ipsec_active"; +constexpr const char* kIpSecureRoutingSeqKey = "ipsec_route_seq"; constexpr size_t kFdskSize = 16; constexpr size_t kSerialSize = 6; constexpr size_t kFdskQrSize = 36; constexpr const char* kProductIdentity = "REG1-Dali"; +constexpr const char* kOamProductIdentity = "OpenKNX-IP-Router"; constexpr const char* kDevelopmentStorage = "base_mac_derived_plain_nvs_development"; constexpr char kFdskDerivationLabel[] = "DaliMaster REG1-Dali deterministic FDSK v1"; +constexpr char kOamFdskDerivationLabel[] = "DaliMaster OAM-IP-Router deterministic FDSK v1"; constexpr uint8_t kCrc4Tab[16] = { 0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9, 0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2, @@ -36,6 +44,37 @@ constexpr uint8_t kCrc4Tab[16] = { constexpr char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; constexpr char kHexAlphabet[] = "0123456789ABCDEF"; +struct FactoryFdskContext { + const char* nvsNamespace; + const char* productIdentity; + const char* derivationLabel; + uint16_t manufacturerId; + uint16_t applicationNumber; + uint8_t applicationVersion; + uint32_t serialMacIncrement; + bool clearOpenKnxCache; +}; + +constexpr FactoryFdskContext kReg1Context{ + kNamespace, + kProductIdentity, + kFdskDerivationLabel, + gateway::knx_internal::kReg1DaliManufacturerId, + gateway::knx_internal::kReg1DaliApplicationNumber, + gateway::knx_internal::kReg1DaliApplicationVersion, + gateway::knx_internal::kReg1DaliSerialMacIncrement, + true}; + +constexpr FactoryFdskContext kOamContext{ + kOamNamespace, + kOamProductIdentity, + kOamFdskDerivationLabel, + gateway::knx_internal::kOamRouterManufacturerId, + gateway::knx_internal::kOamRouterApplicationNumber, + gateway::knx_internal::kOamRouterApplicationVersion, + gateway::knx_internal::kOamRouterSerialMacIncrement, + false}; + extern "C" void knx_platform_clear_cached_fdsk() __attribute__((weak)); std::string hexValue(uint32_t value, int width) { @@ -120,7 +159,7 @@ bool parseHexKey(const std::string& value, uint8_t* out) { return plausibleKey(out); } -bool loadKnxSerialNumber(uint8_t* serial) { +bool loadKnxSerialNumber(const FactoryFdskContext& context, uint8_t* serial) { if (serial == nullptr) { return false; } @@ -129,22 +168,30 @@ bool loadKnxSerialNumber(uint8_t* serial) { return false; } - serial[0] = static_cast( - (gateway::knx_internal::kReg1DaliManufacturerId >> 8) & 0xff); - serial[1] = static_cast( - gateway::knx_internal::kReg1DaliManufacturerId & 0xff); - std::copy(mac.begin() + 2, mac.end(), serial + 2); + uint32_t suffix = (static_cast(mac[2]) << 24) | + (static_cast(mac[3]) << 16) | + (static_cast(mac[4]) << 8) | + static_cast(mac[5]); + suffix += context.serialMacIncrement; + + serial[0] = static_cast((context.manufacturerId >> 8) & 0xff); + serial[1] = static_cast(context.manufacturerId & 0xff); + serial[2] = static_cast((suffix >> 24) & 0xff); + serial[3] = static_cast((suffix >> 16) & 0xff); + serial[4] = static_cast((suffix >> 8) & 0xff); + serial[5] = static_cast(suffix & 0xff); return true; } -bool deriveFactoryFdskFromSerial(const uint8_t* serial, uint8_t* key) { +bool deriveFactoryFdskFromSerial(const FactoryFdskContext& context, + const uint8_t* serial, uint8_t* key) { if (serial == nullptr || key == nullptr) { return false; } - std::array material{}; - std::copy(kFdskDerivationLabel, kFdskDerivationLabel + sizeof(kFdskDerivationLabel) - 1, - material.begin()); - std::copy(serial, serial + kSerialSize, material.begin() + sizeof(kFdskDerivationLabel) - 1); + const size_t label_len = std::strlen(context.derivationLabel); + std::vector material(label_len + kSerialSize); + std::copy(context.derivationLabel, context.derivationLabel + label_len, material.begin()); + std::copy(serial, serial + kSerialSize, material.begin() + label_len); std::array digest{}; if (mbedtls_sha256(material.data(), material.size(), digest.data(), 0) != 0) { @@ -157,7 +204,7 @@ bool deriveFactoryFdskFromSerial(const uint8_t* serial, uint8_t* key) { return plausibleKey(key); } -void syncFactoryFdskToNvs(const uint8_t* data) { +void syncFactoryFdskToNvs(const FactoryFdskContext& context, const uint8_t* data) { if (data == nullptr || !plausibleKey(data) || !ensureNvsReady()) { return; } @@ -166,7 +213,7 @@ void syncFactoryFdskToNvs(const uint8_t* data) { size_t stored_size = stored.size(); nvs_handle_t handle = 0; - esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle); + esp_err_t err = nvs_open(context.nvsNamespace, NVS_READWRITE, &handle); if (err != ESP_OK) { ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err)); return; @@ -186,7 +233,9 @@ void syncFactoryFdskToNvs(const uint8_t* data) { ESP_LOGW(kTag, "failed to mirror deterministic KNX factory FDSK: %s", esp_err_to_name(err)); return; } - clearOpenKnxFdskCache(); + if (context.clearOpenKnxCache) { + clearOpenKnxFdskCache(); + } } uint8_t crc4Array(const uint8_t* data, size_t len) { @@ -275,27 +324,28 @@ std::string fnv1aHex(const std::string& value) { namespace gateway::openknx { -bool LoadFactoryFdsk(uint8_t* data, size_t len) { +bool LoadFactoryFdskForContext(const FactoryFdskContext& context, uint8_t* data, size_t len) { if (data == nullptr || len < kFdskSize) { return false; } std::array serial{}; std::array key{}; - if (!loadKnxSerialNumber(serial.data()) || - !deriveFactoryFdskFromSerial(serial.data(), key.data())) { + if (!loadKnxSerialNumber(context, serial.data()) || + !deriveFactoryFdskFromSerial(context, serial.data(), key.data())) { return false; } std::memcpy(data, key.data(), kFdskSize); - syncFactoryFdskToNvs(key.data()); + syncFactoryFdskToNvs(context, key.data()); return true; } -FactoryFdskInfo LoadFactoryFdskInfo() { +FactoryFdskInfo LoadFactoryFdskInfoForContext(const FactoryFdskContext& context) { FactoryFdskInfo info; std::array key{}; std::array serial{}; - if (!loadKnxSerialNumber(serial.data()) || !LoadFactoryFdsk(key.data(), key.size())) { + if (!loadKnxSerialNumber(context, serial.data()) || + !LoadFactoryFdskForContext(context, key.data(), key.size())) { return info; } @@ -306,31 +356,34 @@ FactoryFdskInfo LoadFactoryFdskInfo() { return info; } -bool GenerateFactoryFdsk(FactoryFdskInfo* info) { +bool GenerateFactoryFdskForContext(const FactoryFdskContext& context, + FactoryFdskInfo* info) { std::array key{}; - const bool stored = LoadFactoryFdsk(key.data(), key.size()); + const bool stored = LoadFactoryFdskForContext(context, key.data(), key.size()); std::fill(key.begin(), key.end(), 0); if (!stored) { return false; } if (info != nullptr) { - *info = LoadFactoryFdskInfo(); + *info = LoadFactoryFdskInfoForContext(context); } return true; } -bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) { +bool WriteFactoryFdskHexForContext(const FactoryFdskContext& context, + const std::string& hex_key, + FactoryFdskInfo* info) { std::array key{}; if (!parseHexKey(hex_key, key.data())) { return false; } std::array serial{}; std::array derived{}; - const bool stored = loadKnxSerialNumber(serial.data()) && - deriveFactoryFdskFromSerial(serial.data(), derived.data()) && + const bool stored = loadKnxSerialNumber(context, serial.data()) && + deriveFactoryFdskFromSerial(context, serial.data(), derived.data()) && std::equal(key.begin(), key.end(), derived.begin()); if (stored) { - syncFactoryFdskToNvs(derived.data()); + syncFactoryFdskToNvs(context, derived.data()); } std::fill(key.begin(), key.end(), 0); std::fill(derived.begin(), derived.end(), 0); @@ -338,33 +391,36 @@ bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) { return false; } if (info != nullptr) { - *info = LoadFactoryFdskInfo(); + *info = LoadFactoryFdskInfoForContext(context); } return true; } -bool ResetFactoryFdskCache(FactoryFdskInfo* info) { - clearOpenKnxFdskCache(); - const auto loaded = LoadFactoryFdskInfo(); +bool ResetFactoryFdskCacheForContext(const FactoryFdskContext& context, + FactoryFdskInfo* info) { + if (context.clearOpenKnxCache) { + clearOpenKnxFdskCache(); + } + const auto loaded = LoadFactoryFdskInfoForContext(context); if (info != nullptr) { *info = loaded; } return loaded.available; } -FactoryCertificatePayload BuildFactoryCertificatePayload() { +FactoryCertificatePayload BuildFactoryCertificatePayloadForContext( + const FactoryFdskContext& context) { FactoryCertificatePayload payload; - const auto info = LoadFactoryFdskInfo(); + const auto info = LoadFactoryFdskInfoForContext(context); if (!info.available) { return payload; } payload.available = true; - payload.productIdentity = kProductIdentity; - payload.manufacturerId = hexValue(gateway::knx_internal::kReg1DaliManufacturerId, 4); - payload.applicationNumber = hexValue( - gateway::knx_internal::kReg1DaliApplicationNumber, 2); - payload.applicationVersion = hexValue( - gateway::knx_internal::kReg1DaliApplicationVersion, 2); + payload.productIdentity = context.productIdentity; + payload.manufacturerId = hexValue(context.manufacturerId, 4); + payload.applicationNumber = hexValue(context.applicationNumber, + context.applicationNumber <= 0xff ? 2 : 4); + payload.applicationVersion = hexValue(context.applicationVersion, 2); payload.serialNumber = info.serialNumber; payload.fdskLabel = info.label; payload.fdskQrCode = info.qrCode; @@ -377,6 +433,241 @@ FactoryCertificatePayload BuildFactoryCertificatePayload() { return payload; } +std::string TunnelUserKeyName(uint8_t index) { + std::array buffer{}; + std::snprintf(buffer.data(), buffer.size(), "ipsec_tunnel_%02u", + static_cast(index)); + return buffer.data(); +} + +bool BlobKeyAvailable(nvs_handle_t handle, const char* key) { + std::array data{}; + size_t len = data.size(); + return nvs_get_blob(handle, key, data.data(), &len) == ESP_OK && len == data.size() && + plausibleKey(data.data()); +} + +bool SetOptionalHexBlob(nvs_handle_t handle, const char* key, const std::string& hex) { + if (hex.empty()) { + nvs_erase_key(handle, key); + return true; + } + std::array data{}; + if (!parseHexKey(hex, data.data())) { + return false; + } + const esp_err_t err = nvs_set_blob(handle, key, data.data(), data.size()); + std::fill(data.begin(), data.end(), 0); + return err == ESP_OK; +} + +bool LoadOptionalKey(nvs_handle_t handle, const char* key, std::array* out) { + if (out == nullptr) { + return false; + } + std::array data{}; + size_t len = data.size(); + if (nvs_get_blob(handle, key, data.data(), &len) != ESP_OK || len != data.size() || + !plausibleKey(data.data())) { + return false; + } + *out = data; + return true; +} + +bool LoadFactoryFdsk(uint8_t* data, size_t len) { + return LoadFactoryFdskForContext(kReg1Context, data, len); +} + +FactoryFdskInfo LoadFactoryFdskInfo() { + return LoadFactoryFdskInfoForContext(kReg1Context); +} + +bool GenerateFactoryFdsk(FactoryFdskInfo* info) { + return GenerateFactoryFdskForContext(kReg1Context, info); +} + +bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) { + return WriteFactoryFdskHexForContext(kReg1Context, hex_key, info); +} + +bool ResetFactoryFdskCache(FactoryFdskInfo* info) { + return ResetFactoryFdskCacheForContext(kReg1Context, info); +} + +FactoryCertificatePayload BuildFactoryCertificatePayload() { + return BuildFactoryCertificatePayloadForContext(kReg1Context); +} + +bool LoadOamFactoryFdsk(uint8_t* data, size_t len) { + return LoadFactoryFdskForContext(kOamContext, data, len); +} + +FactoryFdskInfo LoadOamFactoryFdskInfo() { + return LoadFactoryFdskInfoForContext(kOamContext); +} + +bool GenerateOamFactoryFdsk(FactoryFdskInfo* info) { + return GenerateFactoryFdskForContext(kOamContext, info); +} + +bool WriteOamFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) { + return WriteFactoryFdskHexForContext(kOamContext, hex_key, info); +} + +bool ResetOamFactoryFdskCache(FactoryFdskInfo* info) { + return ResetFactoryFdskCacheForContext(kOamContext, info); +} + +FactoryCertificatePayload BuildOamFactoryCertificatePayload() { + return BuildFactoryCertificatePayloadForContext(kOamContext); +} + +IpSecureCredentialStatus LoadOamIpSecureCredentialStatus() { + IpSecureCredentialStatus status; + if (!ensureNvsReady()) { + return status; + } + nvs_handle_t handle = 0; + if (nvs_open(kOamNamespace, NVS_READONLY, &handle) != ESP_OK) { + return status; + } + uint8_t activated = 0; + if (nvs_get_u8(handle, kIpSecureActivatedKey, &activated) == ESP_OK) { + status.activated = activated != 0; + } + uint8_t tunnel_count = 0; + if (nvs_get_u8(handle, kIpSecureTunnelCountKey, &tunnel_count) == ESP_OK) { + status.tunnelUserCount = tunnel_count; + } + uint64_t routing_sequence = 0; + if (nvs_get_u64(handle, kIpSecureRoutingSeqKey, &routing_sequence) == ESP_OK) { + status.routingSequence = routing_sequence; + } + status.backboneKeyAvailable = BlobKeyAvailable(handle, kIpSecureBackboneKey); + status.deviceAuthenticationKeyAvailable = BlobKeyAvailable(handle, kIpSecureDeviceAuthKey); + nvs_close(handle); + return status; +} + +::gateway::GatewayKnxIpSecureCredentialMaterial LoadOamIpSecureCredentialMaterial() { + ::gateway::GatewayKnxIpSecureCredentialMaterial material; + if (!ensureNvsReady()) { + return material; + } + nvs_handle_t handle = 0; + if (nvs_open(kOamNamespace, NVS_READONLY, &handle) != ESP_OK) { + return material; + } + uint8_t activated = 0; + if (nvs_get_u8(handle, kIpSecureActivatedKey, &activated) == ESP_OK) { + material.activated = activated != 0; + } + material.backbone_key_available = LoadOptionalKey(handle, kIpSecureBackboneKey, + &material.backbone_key); + material.device_authentication_key_available = + LoadOptionalKey(handle, kIpSecureDeviceAuthKey, + &material.device_authentication_key); + uint8_t tunnel_count = 0; + if (nvs_get_u8(handle, kIpSecureTunnelCountKey, &tunnel_count) == ESP_OK) { + tunnel_count = std::min(tunnel_count, 16); + material.tunnel_user_keys.reserve(tunnel_count); + for (uint8_t index = 0; index < tunnel_count; ++index) { + std::array key{}; + const std::string key_name = TunnelUserKeyName(index); + if (LoadOptionalKey(handle, key_name.c_str(), &key)) { + material.tunnel_user_keys.push_back(key); + } + } + } + uint64_t routing_sequence = 0; + if (nvs_get_u64(handle, kIpSecureRoutingSeqKey, &routing_sequence) == ESP_OK) { + material.routing_sequence = routing_sequence; + } + nvs_close(handle); + return material; +} + +bool WriteOamIpSecureKeyringHex(const std::string& backbone_key_hex, + const std::vector& tunnel_user_key_hex, + const std::string& device_auth_key_hex, + bool activated) { + if (tunnel_user_key_hex.size() > 16 || !ensureNvsReady()) { + return false; + } + nvs_handle_t handle = 0; + if (nvs_open(kOamNamespace, NVS_READWRITE, &handle) != ESP_OK) { + return false; + } + bool ok = SetOptionalHexBlob(handle, kIpSecureBackboneKey, backbone_key_hex) && + SetOptionalHexBlob(handle, kIpSecureDeviceAuthKey, device_auth_key_hex); + for (uint8_t index = 0; index < 16; ++index) { + const std::string key = TunnelUserKeyName(index); + nvs_erase_key(handle, key.c_str()); + } + if (ok) { + for (size_t index = 0; index < tunnel_user_key_hex.size(); ++index) { + const std::string key = TunnelUserKeyName(static_cast(index)); + ok = SetOptionalHexBlob(handle, key.c_str(), tunnel_user_key_hex[index]); + if (!ok) { + break; + } + } + } + if (ok) { + ok = nvs_set_u8(handle, kIpSecureTunnelCountKey, + static_cast(tunnel_user_key_hex.size())) == ESP_OK && + nvs_set_u8(handle, kIpSecureActivatedKey, activated ? 1 : 0) == ESP_OK; + } + if (ok) { + uint64_t existing_sequence = 0; + if (nvs_get_u64(handle, kIpSecureRoutingSeqKey, &existing_sequence) != ESP_OK) { + ok = nvs_set_u64(handle, kIpSecureRoutingSeqKey, 0) == ESP_OK; + } + } + if (ok) { + ok = nvs_commit(handle) == ESP_OK; + } + nvs_close(handle); + return ok; +} + +bool StoreOamIpSecureRoutingSequence(uint64_t sequence) { + if (!ensureNvsReady()) { + return false; + } + nvs_handle_t handle = 0; + if (nvs_open(kOamNamespace, NVS_READWRITE, &handle) != ESP_OK) { + return false; + } + const bool ok = nvs_set_u64(handle, kIpSecureRoutingSeqKey, sequence) == ESP_OK && + nvs_commit(handle) == ESP_OK; + nvs_close(handle); + return ok; +} + +bool ClearOamIpSecureKeyring() { + if (!ensureNvsReady()) { + return false; + } + nvs_handle_t handle = 0; + if (nvs_open(kOamNamespace, NVS_READWRITE, &handle) != ESP_OK) { + return false; + } + nvs_erase_key(handle, kIpSecureBackboneKey); + nvs_erase_key(handle, kIpSecureDeviceAuthKey); + nvs_erase_key(handle, kIpSecureTunnelCountKey); + nvs_erase_key(handle, kIpSecureActivatedKey); + nvs_erase_key(handle, kIpSecureRoutingSeqKey); + for (uint8_t index = 0; index < 16; ++index) { + const std::string key = TunnelUserKeyName(index); + nvs_erase_key(handle, key.c_str()); + } + const bool ok = nvs_commit(handle) == ESP_OK; + nvs_close(handle); + return ok; +} + } // namespace gateway::openknx extern "C" bool knx_platform_get_fdsk(uint8_t* data, size_t len) { diff --git a/components/gateway_knx/CMakeLists.txt b/components/gateway_knx/CMakeLists.txt index 9ef49b6..a22eda7 100644 --- a/components/gateway_knx/CMakeLists.txt +++ b/components/gateway_knx/CMakeLists.txt @@ -6,10 +6,12 @@ idf_component_register( "src/gateway_knx_router_openknx.cpp" "src/gateway_knx_router_packets.cpp" "src/gateway_knx_router_services.cpp" + "src/gateway_knx_secure_transport.cpp" + "src/oam_router_runtime.cpp" "src/ets_device_runtime.cpp" "src/ets_memory_loader.cpp" INCLUDE_DIRS "include" - REQUIRES dali_cpp esp_driver_gpio esp_driver_uart esp_hw_support esp_netif freertos log lwip knx + REQUIRES dali_cpp esp_driver_gpio esp_driver_uart esp_hw_support esp_netif freertos log lwip knx mbedtls ) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index 7c5d502..0d8a86f 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -27,6 +27,7 @@ namespace gateway { namespace openknx { class EtsDeviceRuntime; +class OamRouterRuntime; class TpuartUartInterface; } @@ -59,6 +60,40 @@ struct GatewayKnxEtsAssociation { uint16_t group_object_number{0}; }; +struct GatewayKnxCloudRemoteConfig { + bool enabled{false}; + std::string mode{"mqtt"}; + std::string relay_endpoint; + std::string mqtt_topic_prefix; + std::string auth_token_ref; + bool require_secure_tunnel{true}; + bool udp_punch_enabled{false}; +}; + +struct GatewayKnxOamRouterConfig { + bool enabled{false}; + bool ets_database_enabled{true}; + bool secure_tunnel_enabled{true}; + bool secure_routing_enabled{true}; + uint16_t individual_address{0xff02}; + uint16_t tunnel_address_base{0xff10}; + int programming_button_gpio{-1}; + bool programming_button_active_low{true}; + int programming_led_gpio{-1}; + bool programming_led_active_high{true}; + GatewayKnxCloudRemoteConfig cloud_remote; +}; + +struct GatewayKnxIpSecureCredentialMaterial { + bool activated{false}; + bool backbone_key_available{false}; + std::array backbone_key{}; + bool device_authentication_key_available{false}; + std::array device_authentication_key{}; + std::vector> tunnel_user_keys; + uint64_t routing_sequence{0}; +}; + struct GatewayKnxConfig { bool dali_router_enabled{true}; bool ip_router_enabled{false}; @@ -76,6 +111,7 @@ struct GatewayKnxConfig { bool programming_button_active_low{true}; int programming_led_gpio{-1}; bool programming_led_active_high{true}; + GatewayKnxOamRouterConfig oam_router; std::vector ets_associations; GatewayKnxTpUartConfig tp_uart; }; @@ -134,6 +170,9 @@ struct GatewayKnxReg1ScanOptions { std::optional GatewayKnxConfigFromValue(const DaliValue* value); DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config); +std::optional GatewayKnxOamRouterConfigFromValue( + const DaliValue* value); +DaliValue GatewayKnxOamRouterConfigToValue(const GatewayKnxOamRouterConfig& config); bool GatewayKnxConfigUsesTpUart(const GatewayKnxConfig& config); const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode); @@ -228,6 +267,7 @@ class GatewayKnxTpIpRouter { using GroupObjectWriteHandler = std::function; + using RoutingSequenceStoreHandler = std::function; GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, std::string openknx_namespace = "openknx"); @@ -237,11 +277,16 @@ class GatewayKnxTpIpRouter { void setCommissioningOnly(bool enabled); void setGroupWriteHandler(GroupWriteHandler handler); void setGroupObjectWriteHandler(GroupObjectWriteHandler handler); + void setOamIpSecureCredentials(const GatewayKnxIpSecureCredentialMaterial& credentials); + void setOamIpSecureRoutingSequenceStoreHandler(RoutingSequenceStoreHandler handler); const GatewayKnxConfig& config() const; bool tpUartOnline() const; bool programmingMode(); esp_err_t setProgrammingMode(bool enabled); esp_err_t toggleProgrammingMode(); + bool oamProgrammingMode(); + esp_err_t setOamProgrammingMode(bool enabled); + esp_err_t toggleOamProgrammingMode(); esp_err_t start(uint32_t task_stack_size, UBaseType_t task_priority); esp_err_t stop(); @@ -285,6 +330,24 @@ class GatewayKnxTpIpRouter { ::sockaddr_in data_remote{}; std::vector last_received_cemi; std::vector last_tunnel_confirmation_packet; + uint16_t secure_session_id{0}; + bool oam_router_persona{false}; + }; + + struct SecureSession { + bool active{false}; + bool authenticated{false}; + uint16_t session_id{0}; + int tcp_sock{-1}; + ::sockaddr_in remote{}; + std::array client_public_key{}; + std::array server_public_key{}; + std::array shared_secret{}; + std::array session_key{}; + uint64_t send_sequence{0}; + uint64_t receive_sequence{0}; + uint8_t user_id{0}; + TickType_t last_activity_tick{0}; }; static void TaskEntry(void* arg); @@ -297,6 +360,7 @@ class GatewayKnxTpIpRouter { void handleTcpAccept(); void handleTcpClient(TcpClient& client); void closeTcpClient(TcpClient& client); + void closeSecureSessionsForTcp(int sock); std::unique_ptr createOpenKnxTpUartInterface(); bool configureTpUart(); bool configureProgrammingGpio(); @@ -316,6 +380,12 @@ class GatewayKnxTpIpRouter { void handleDisconnectRequest(const uint8_t* packet_data, size_t len, const ::sockaddr_in& remote); void handleSecureService(uint16_t service, const uint8_t* body, size_t len, const ::sockaddr_in& remote); + void handleSecureSessionRequest(const uint8_t* body, size_t len, + const ::sockaddr_in& remote); + void handleSecureWrapper(const uint8_t* body, size_t len, + const ::sockaddr_in& remote); + void handleSecureGroupSync(const uint8_t* body, size_t len, + const ::sockaddr_in& remote); void sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status, const ::sockaddr_in& remote); void sendDeviceConfigurationAck(uint8_t channel_id, uint8_t sequence, uint8_t status, @@ -335,9 +405,9 @@ class GatewayKnxTpIpRouter { const ::sockaddr_in& remote, uint8_t connection_type, uint16_t tunnel_address); void sendRoutingIndication(const uint8_t* data, size_t len); - bool sendPacket(const std::vector& packet, const ::sockaddr_in& remote) const; + bool sendPacket(const std::vector& packet, const ::sockaddr_in& remote); bool sendPacketToTunnelClient(const TunnelClient& client, - const std::vector& packet) const; + const std::vector& packet); bool currentTransportAllowsTcpHpai() const; std::optional> localHpaiForRemote(const ::sockaddr_in& remote, bool tcp = false) const; @@ -359,6 +429,22 @@ class GatewayKnxTpIpRouter { const ::sockaddr_in& data_remote, uint8_t connection_type); void resetTunnelClient(TunnelClient& client); + SecureSession* findSecureSession(uint16_t session_id, const ::sockaddr_in& remote); + const SecureSession* findSecureSession(uint16_t session_id, + const ::sockaddr_in& remote) const; + SecureSession* allocateSecureSession(const ::sockaddr_in& remote); + SecureSession* activeSecureSession(); + bool wrapSecurePacket(SecureSession& session, const std::vector& inner, + std::vector* wrapped); + bool wrapSecureRoutingPacket(const std::vector& inner, + std::vector* wrapped); + bool decryptSecureWrapper(SecureSession& session, const uint8_t* body, size_t len, + std::vector* inner); + bool decryptSecureRoutingWrapper(const uint8_t* body, size_t len, + std::vector* inner); + bool verifySecureSessionAuth(SecureSession& session, const uint8_t* packet, + size_t len, uint8_t* status); + bool secureCredentialsReady() const; uint8_t nextTunnelChannelId() const; uint16_t effectiveTunnelAddressForSlot(size_t slot) const; void pruneStaleTunnelClients(); @@ -366,6 +452,10 @@ class GatewayKnxTpIpRouter { TunnelClient* response_client, uint16_t response_service, const uint8_t* suppress_routing_echo = nullptr, size_t suppress_routing_echo_len = 0); + bool handleOamRouterTunnelFrame(const uint8_t* data, size_t len, + TunnelClient* response_client, uint16_t response_service, + const uint8_t* suppress_routing_echo = nullptr, + size_t suppress_routing_echo_len = 0); bool handleOpenKnxBusFrame(const uint8_t* data, size_t len); bool transmitOpenKnxTpFrame(const uint8_t* data, size_t len); void selectOpenKnxNetworkInterface(const ::sockaddr_in& remote); @@ -380,13 +470,16 @@ class GatewayKnxTpIpRouter { void pollProgrammingButton(); void updateProgrammingLed(); void setProgrammingLed(bool on); + void setOamProgrammingLed(bool on); GatewayKnxBridge& bridge_; GroupWriteHandler group_write_handler_; GroupObjectWriteHandler group_object_write_handler_; + RoutingSequenceStoreHandler routing_sequence_store_handler_; std::string openknx_namespace_; GatewayKnxConfig config_; std::unique_ptr ets_device_; + std::unique_ptr oam_router_; TaskHandle_t task_handle_{nullptr}; SemaphoreHandle_t openknx_lock_{nullptr}; SemaphoreHandle_t startup_semaphore_{nullptr}; @@ -403,14 +496,22 @@ class GatewayKnxTpIpRouter { TickType_t network_refresh_tick_{0}; std::array tcp_clients_{}; std::array tunnel_clients_{}; + std::array secure_sessions_{}; std::unique_ptr knx_ip_parameters_; uint8_t last_tunnel_channel_id_{0}; + uint16_t last_secure_session_id_{0}; + uint16_t active_secure_session_id_{0}; + GatewayKnxIpSecureCredentialMaterial oam_ip_secure_credentials_{}; bool tp_uart_online_{false}; bool commissioning_only_{false}; std::atomic_bool openknx_configured_{false}; bool programming_button_last_pressed_{false}; bool programming_led_state_{false}; TickType_t programming_button_last_toggle_tick_{0}; + bool oam_programming_mode_{false}; + bool oam_programming_button_last_pressed_{false}; + bool oam_programming_led_state_{false}; + TickType_t oam_programming_button_last_toggle_tick_{0}; std::string last_error_; }; diff --git a/components/gateway_knx/include/gateway_knx_internal.h b/components/gateway_knx/include/gateway_knx_internal.h index 2469f19..a63278e 100644 --- a/components/gateway_knx/include/gateway_knx_internal.h +++ b/components/gateway_knx/include/gateway_knx_internal.h @@ -32,6 +32,22 @@ constexpr const char* kTag = "gateway_knx"; #define CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID 0xA401 #endif +#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_OEM_MANUFACTURER_ID +#define CONFIG_GATEWAY_KNX_OAM_ROUTER_OEM_MANUFACTURER_ID 0x00FA +#endif + +#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_NUMBER +#define CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_NUMBER 0xA11F +#endif + +#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_VERSION +#define CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_VERSION 0x07 +#endif + +#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_HARDWARE_ID +#define CONFIG_GATEWAY_KNX_OAM_ROUTER_HARDWARE_ID 0x0001 +#endif + inline constexpr uint16_t kReg1DaliManufacturerId = static_cast(CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID); inline constexpr uint16_t kReg1DaliHardwareId = @@ -56,6 +72,33 @@ inline constexpr uint8_t kReg1DaliProgramVersion[5] = { static_cast(kReg1DaliApplicationNumber & 0xff), kReg1DaliApplicationVersion}; +inline constexpr uint32_t kReg1DaliSerialMacIncrement = 0; +inline constexpr uint32_t kOamRouterSerialMacIncrement = 1; +inline constexpr uint16_t kOamRouterDeviceDescriptor = 0x091A; +inline constexpr uint16_t kOamRouterManufacturerId = + static_cast(CONFIG_GATEWAY_KNX_OAM_ROUTER_OEM_MANUFACTURER_ID); +inline constexpr uint16_t kOamRouterHardwareId = + static_cast(CONFIG_GATEWAY_KNX_OAM_ROUTER_HARDWARE_ID); +inline constexpr uint16_t kOamRouterApplicationNumber = + static_cast(CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_NUMBER); +inline constexpr uint8_t kOamRouterApplicationVersion = + static_cast(CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_VERSION); +inline constexpr uint8_t kOamRouterHardwareType[6] = { + 0x00, + 0x00, + static_cast((kOamRouterHardwareId >> 8) & 0xff), + static_cast(kOamRouterHardwareId & 0xff), + kOamRouterApplicationVersion, + 0x00}; +inline constexpr uint8_t kOamRouterOrderNumber[10] = { + 'I', 'P', '-', 'R', 'o', 'u', 't', 'e', 'r', 0}; +inline constexpr uint8_t kOamRouterProgramVersion[5] = { + static_cast((kOamRouterManufacturerId >> 8) & 0xff), + static_cast(kOamRouterManufacturerId & 0xff), + static_cast((kOamRouterApplicationNumber >> 8) & 0xff), + static_cast(kOamRouterApplicationNumber & 0xff), + kOamRouterApplicationVersion}; + // RAII semaphore guard. class SemaphoreGuard { public: diff --git a/components/gateway_knx/include/oam_router_runtime.h b/components/gateway_knx/include/oam_router_runtime.h new file mode 100644 index 0000000..f3a71cd --- /dev/null +++ b/components/gateway_knx/include/oam_router_runtime.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esp_idf_platform.h" +#include "ets_memory_loader.h" + +#include "esp_netif.h" +#include "freertos/FreeRTOS.h" +#include "knx/cemi_frame.h" +#include "knx/device_object.h" +#include "knx/platform.h" + +#include +#include +#include +#include +#include +#include + +#if defined(ENABLE_BAU091A_PERSONA) +#include "knx/bau091A.h" +#endif + +namespace gateway::openknx { + +class OamRouterRuntime { + public: + using CemiFrameSender = std::function; + + OamRouterRuntime(std::string nvs_namespace, + uint16_t fallback_individual_address, + uint16_t tunnel_client_address = 0); + ~OamRouterRuntime(); + + bool available() const; + uint16_t individualAddress() const; + uint16_t tunnelClientAddress() const; + bool configured() const; + bool programmingMode() const; + void setProgrammingMode(bool enabled); + void toggleProgrammingMode(); + EtsMemorySnapshot snapshot() const; + + DeviceObject* deviceObject(); + Platform* platform(); + void setNetworkInterface(esp_netif_t* netif); + bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender); + void loop(); + + private: + static bool HandleOutboundCemiFrame(CemiFrame& frame, void* context); + static void EmitTunnelFrame(CemiFrame& frame, void* context); + static uint16_t DefaultTunnelClientAddress(uint16_t individual_address); + bool shouldConsumeTunnelFrame(CemiFrame& frame) const; + + std::string nvs_namespace_; + CemiFrameSender sender_; +#if defined(ENABLE_BAU091A_PERSONA) + EspIdfPlatform platform_; + Bau091A device_; +#endif +}; + +} // namespace gateway::openknx diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index bba7b6b..93cf9b3 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -2,6 +2,115 @@ namespace gateway { +namespace { + +GatewayKnxCloudRemoteConfig GatewayKnxCloudRemoteConfigFromValue(const DaliValue* value) { + GatewayKnxCloudRemoteConfig config; + if (value == nullptr || value->asObject() == nullptr) { + return config; + } + const auto& object = *value->asObject(); + config.enabled = ObjectBoolAny(object, {"enabled", "cloudRemoteEnabled", + "cloud_remote_enabled"}) + .value_or(config.enabled); + config.mode = ObjectStringAny(object, {"mode", "remoteMode", "remote_mode"}) + .value_or(config.mode); + config.relay_endpoint = ObjectStringAny(object, {"relayEndpoint", "relay_endpoint", + "endpoint"}) + .value_or(config.relay_endpoint); + config.mqtt_topic_prefix = ObjectStringAny(object, {"mqttTopicPrefix", "mqtt_topic_prefix", + "topicPrefix", "topic_prefix"}) + .value_or(config.mqtt_topic_prefix); + config.auth_token_ref = ObjectStringAny(object, {"authTokenRef", "auth_token_ref", + "tokenRef", "token_ref"}) + .value_or(config.auth_token_ref); + config.require_secure_tunnel = ObjectBoolAny(object, {"requireSecureTunnel", + "require_secure_tunnel"}) + .value_or(config.require_secure_tunnel); + config.udp_punch_enabled = ObjectBoolAny(object, {"udpPunchEnabled", "udp_punch_enabled"}) + .value_or(config.udp_punch_enabled); + return config; +} + +DaliValue GatewayKnxCloudRemoteConfigToValue(const GatewayKnxCloudRemoteConfig& config) { + DaliValue::Object out; + out["enabled"] = config.enabled; + out["mode"] = config.mode; + out["relayEndpoint"] = config.relay_endpoint; + out["mqttTopicPrefix"] = config.mqtt_topic_prefix; + out["authTokenRef"] = config.auth_token_ref; + out["requireSecureTunnel"] = config.require_secure_tunnel; + out["udpPunchEnabled"] = config.udp_punch_enabled; + return DaliValue(std::move(out)); +} + +} // namespace + +std::optional GatewayKnxOamRouterConfigFromValue( + const DaliValue* value) { + if (value == nullptr || value->asObject() == nullptr) { + return std::nullopt; + } + const auto& object = *value->asObject(); + GatewayKnxOamRouterConfig config; + config.enabled = ObjectBoolAny(object, {"enabled", "oamRouterEnabled", + "oam_router_enabled"}) + .value_or(config.enabled); + config.ets_database_enabled = ObjectBoolAny(object, {"etsDatabaseEnabled", + "ets_database_enabled"}) + .value_or(config.ets_database_enabled); + config.secure_tunnel_enabled = ObjectBoolAny(object, {"secureTunnelEnabled", + "secure_tunnel_enabled"}) + .value_or(config.secure_tunnel_enabled); + config.secure_routing_enabled = ObjectBoolAny(object, {"secureRoutingEnabled", + "secure_routing_enabled"}) + .value_or(config.secure_routing_enabled); + config.individual_address = static_cast(std::clamp( + ObjectIntAny(object, {"individualAddress", "individual_address", "routerAddress", + "router_address"}) + .value_or(config.individual_address), + 0, 0xffff)); + config.tunnel_address_base = static_cast(std::clamp( + ObjectIntAny(object, {"tunnelAddressBase", "tunnel_address_base", + "tunnelBaseAddress", "tunnel_base_address"}) + .value_or(config.tunnel_address_base), + 0, 0xffff)); + config.programming_button_gpio = std::clamp( + ObjectIntAny(object, {"programmingButtonGpio", "programming_button_gpio"}) + .value_or(config.programming_button_gpio), + -1, 48); + config.programming_button_active_low = + ObjectBoolAny(object, {"programmingButtonActiveLow", "programming_button_active_low"}) + .value_or(config.programming_button_active_low); + config.programming_led_gpio = std::clamp( + ObjectIntAny(object, {"programmingLedGpio", "programming_led_gpio"}) + .value_or(config.programming_led_gpio), + -1, 48); + config.programming_led_active_high = + ObjectBoolAny(object, {"programmingLedActiveHigh", "programming_led_active_high"}) + .value_or(config.programming_led_active_high); + config.cloud_remote = GatewayKnxCloudRemoteConfigFromValue( + ObjectValueAny(object, {"cloudRemote", "cloud_remote", "remoteAccess", + "remote_access"})); + return config; +} + +DaliValue GatewayKnxOamRouterConfigToValue(const GatewayKnxOamRouterConfig& config) { + DaliValue::Object out; + out["enabled"] = config.enabled; + out["etsDatabaseEnabled"] = config.ets_database_enabled; + out["secureTunnelEnabled"] = config.secure_tunnel_enabled; + out["secureRoutingEnabled"] = config.secure_routing_enabled; + out["individualAddress"] = static_cast(config.individual_address); + out["tunnelAddressBase"] = static_cast(config.tunnel_address_base); + out["programmingButtonGpio"] = config.programming_button_gpio; + out["programmingButtonActiveLow"] = config.programming_button_active_low; + out["programmingLedGpio"] = config.programming_led_gpio; + out["programmingLedActiveHigh"] = config.programming_led_active_high; + out["cloudRemote"] = GatewayKnxCloudRemoteConfigToValue(config.cloud_remote); + return DaliValue(std::move(out)); +} + std::optional GatewayKnxConfigFromValue(const DaliValue* value) { if (value == nullptr || value->asObject() == nullptr) { return std::nullopt; @@ -65,6 +174,15 @@ std::optional GatewayKnxConfigFromValue(const DaliValue* value config.programming_led_active_high = ObjectBoolAny(object, {"programmingLedActiveHigh", "programming_led_active_high"}) .value_or(config.programming_led_active_high); + if (const auto oam_router = GatewayKnxOamRouterConfigFromValue( + ObjectValueAny(object, {"oamRouter", "oam_router", "routerApplication", + "router_application"}))) { + config.oam_router = oam_router.value(); + } else { + config.oam_router.enabled = ObjectBoolAny(object, {"oamRouterEnabled", + "oam_router_enabled"}) + .value_or(config.oam_router.enabled); + } const auto* tp_uart = getObjectValue(object, "tpUart"); if (tp_uart == nullptr) { @@ -117,6 +235,7 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) { out["programmingButtonActiveLow"] = config.programming_button_active_low; out["programmingLedGpio"] = config.programming_led_gpio; out["programmingLedActiveHigh"] = config.programming_led_active_high; + out["oamRouter"] = GatewayKnxOamRouterConfigToValue(config.oam_router); DaliValue::Object serial; serial["uartPort"] = config.tp_uart.uart_port; serial["txPin"] = config.tp_uart.tx_pin; diff --git a/components/gateway_knx/src/gateway_knx_private.hpp b/components/gateway_knx/src/gateway_knx_private.hpp index 012938e..35dfdaa 100644 --- a/components/gateway_knx/src/gateway_knx_private.hpp +++ b/components/gateway_knx/src/gateway_knx_private.hpp @@ -12,6 +12,7 @@ #include "lwip/sockets.h" #include "ets_device_runtime.h" #include "gateway_knx_internal.h" +#include "oam_router_runtime.h" #include "soc/uart_periph.h" #include "tpuart_uart_interface.h" @@ -105,6 +106,7 @@ constexpr uint8_t kKnxServiceFamilyCore = 0x02; constexpr uint8_t kKnxServiceFamilyDeviceManagement = 0x03; constexpr uint8_t kKnxServiceFamilyTunnelling = 0x04; constexpr uint8_t kKnxServiceFamilyRouting = 0x05; +constexpr uint8_t kKnxServiceFamilySecurity = 0x06; constexpr uint16_t kKnxIpOnlyDeviceDescriptor = 0x57b0; constexpr uint16_t kKnxTpIpInterfaceDeviceDescriptor = 0x091a; constexpr uint8_t kKnxIpAssignmentManual = 0x01; @@ -848,6 +850,18 @@ bool MatchesOpenKnxLocalIndividualAddress(const CemiFrame& frame, return dest == own_address || dest == client_address || (commissioning && dest == 0xffff); } +bool MatchesOamRouterLocalIndividualAddress(const CemiFrame& frame, + const openknx::OamRouterRuntime& oam_router) { + if (frame.addressType() != IndividualAddress) { + return false; + } + const uint16_t dest = frame.destinationAddress(); + const uint16_t own_address = oam_router.individualAddress(); + const uint16_t client_address = oam_router.tunnelClientAddress(); + const bool commissioning = !oam_router.configured() || oam_router.programmingMode(); + return dest == own_address || dest == client_address || (commissioning && dest == 0xffff); +} + bool BuildLocalRoutingTunnelFrame(const uint8_t* data, size_t len, std::vector* local_frame) { if (data == nullptr || local_frame == nullptr || len < 2) { diff --git a/components/gateway_knx/src/gateway_knx_router_lifecycle.cpp b/components/gateway_knx/src/gateway_knx_router_lifecycle.cpp index ca28629..bc15c68 100644 --- a/components/gateway_knx/src/gateway_knx_router_lifecycle.cpp +++ b/components/gateway_knx/src/gateway_knx_router_lifecycle.cpp @@ -36,6 +36,16 @@ void GatewayKnxTpIpRouter::setGroupObjectWriteHandler(GroupObjectWriteHandler ha group_object_write_handler_ = std::move(handler); } +void GatewayKnxTpIpRouter::setOamIpSecureCredentials( + const GatewayKnxIpSecureCredentialMaterial& credentials) { + oam_ip_secure_credentials_ = credentials; +} + +void GatewayKnxTpIpRouter::setOamIpSecureRoutingSequenceStoreHandler( + RoutingSequenceStoreHandler handler) { + routing_sequence_store_handler_ = std::move(handler); +} + const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; } bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; } @@ -69,6 +79,38 @@ esp_err_t GatewayKnxTpIpRouter::toggleProgrammingMode() { return setProgrammingMode(!programmingMode()); } +bool GatewayKnxTpIpRouter::oamProgrammingMode() { + if (openknx_lock_ == nullptr) { + return false; + } + SemaphoreGuard guard(openknx_lock_); + return oam_router_ != nullptr ? oam_router_->programmingMode() : oam_programming_mode_; +} + +esp_err_t GatewayKnxTpIpRouter::setOamProgrammingMode(bool enabled) { + if (openknx_lock_ == nullptr) { + last_error_ = "KNX runtime lock is unavailable"; + return ESP_ERR_INVALID_STATE; + } + if (!config_.oam_router.enabled) { + last_error_ = "OAM KNX/IP router persona is disabled"; + return ESP_ERR_NOT_SUPPORTED; + } + SemaphoreGuard guard(openknx_lock_); + oam_programming_mode_ = enabled; + if (oam_router_ != nullptr) { + oam_router_->setProgrammingMode(enabled); + } + setOamProgrammingLed(enabled); + ESP_LOGI(kTag, "OAM KNX/IP router programming mode %s namespace=%s", + enabled ? "enabled" : "disabled", openknx_namespace_.c_str()); + return ESP_OK; +} + +esp_err_t GatewayKnxTpIpRouter::toggleOamProgrammingMode() { + return setOamProgrammingMode(!oamProgrammingMode()); +} + esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) { if (started_ || task_handle_ != nullptr) { return ESP_OK; @@ -193,6 +235,19 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { bridge_.setRuntimeContext(ets_device_.get()); knx_ip_parameters_ = std::make_unique( ets_device_->deviceObject(), ets_device_->platform()); + if (config_.oam_router.enabled) { + oam_router_ = std::make_unique( + openknx_namespace_ + "_oam", config_.oam_router.individual_address, + config_.oam_router.tunnel_address_base); + if (oam_router_->available()) { + oam_router_->setProgrammingMode(oam_programming_mode_); + } else { + ESP_LOGW(kTag, "OAM router persona requested but BAU091A support is not compiled in"); + oam_router_.reset(); + } + } else { + oam_router_.reset(); + } openknx_configured_.store(ets_device_->configured()); ESP_LOGI(kTag, "OpenKNX runtime namespace=%s configured=%d ipInterface=0x%04x " @@ -200,6 +255,14 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { openknx_namespace_.c_str(), ets_device_->configured(), effectiveIpInterfaceIndividualAddress(), ets_device_->individualAddress(), ets_device_->tunnelClientAddress(), commissioning_only_); + if (oam_router_ != nullptr) { + ESP_LOGI(kTag, + "OAM router persona namespace=%s_oam configured=%d device=0x%04x tunnelClient=0x%04x secureTunnel=%d secureRouting=%d", + openknx_namespace_.c_str(), oam_router_->configured(), + oam_router_->individualAddress(), oam_router_->tunnelClientAddress(), + config_.oam_router.secure_tunnel_enabled, + config_.oam_router.secure_routing_enabled); + } ets_device_->setFunctionPropertyHandlers( [this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { @@ -300,6 +363,10 @@ void GatewayKnxTpIpRouter::taskLoop() { if (ets_device_ != nullptr) { pollProgrammingButton(); ets_device_->loop(); + if (oam_router_ != nullptr) { + oam_router_->loop(); + oam_programming_mode_ = oam_router_->programmingMode(); + } tp_uart_online_ = ets_device_->tpUartOnline(); updateProgrammingLed(); } @@ -376,8 +443,11 @@ void GatewayKnxTpIpRouter::finishTask() { { SemaphoreGuard guard(openknx_lock_); setProgrammingLed(false); + setOamProgrammingLed(false); + oam_programming_mode_ = false; knx_ip_parameters_.reset(); bridge_.setRuntimeContext(nullptr); + oam_router_.reset(); ets_device_.reset(); openknx_configured_.store(false); } @@ -387,32 +457,57 @@ void GatewayKnxTpIpRouter::finishTask() { } void GatewayKnxTpIpRouter::pollProgrammingButton() { - if (config_.programming_button_gpio < 0 || ets_device_ == nullptr) { + const TickType_t now = xTaskGetTickCount(); + + if (config_.programming_button_gpio >= 0 && ets_device_ != nullptr) { + const int level = gpio_get_level(static_cast(config_.programming_button_gpio)); + const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0; + if (pressed && !programming_button_last_pressed_ && + now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) { + ets_device_->toggleProgrammingMode(); + ESP_LOGI(kTag, "KNX programming mode %s namespace=%s", + ets_device_->programmingMode() ? "enabled" : "disabled", + openknx_namespace_.c_str()); + programming_button_last_toggle_tick_ = now; + } + programming_button_last_pressed_ = pressed; + } + + if (!config_.oam_router.enabled || config_.oam_router.programming_button_gpio < 0) { return; } - const int level = gpio_get_level(static_cast(config_.programming_button_gpio)); - const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0; - const TickType_t now = xTaskGetTickCount(); - if (pressed && !programming_button_last_pressed_ && - now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) { - ets_device_->toggleProgrammingMode(); - ESP_LOGI(kTag, "KNX programming mode %s namespace=%s", - ets_device_->programmingMode() ? "enabled" : "disabled", + const int oam_level = gpio_get_level( + static_cast(config_.oam_router.programming_button_gpio)); + const bool oam_pressed = config_.oam_router.programming_button_active_low + ? oam_level == 0 + : oam_level != 0; + if (oam_pressed && !oam_programming_button_last_pressed_ && + now - oam_programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) { + oam_programming_mode_ = !oam_programming_mode_; + if (oam_router_ != nullptr) { + oam_router_->setProgrammingMode(oam_programming_mode_); + } + setOamProgrammingLed(oam_programming_mode_); + ESP_LOGI(kTag, "OAM KNX/IP router programming mode %s namespace=%s", + oam_programming_mode_ ? "enabled" : "disabled", openknx_namespace_.c_str()); - programming_button_last_toggle_tick_ = now; + oam_programming_button_last_toggle_tick_ = now; } - programming_button_last_pressed_ = pressed; + oam_programming_button_last_pressed_ = oam_pressed; } void GatewayKnxTpIpRouter::updateProgrammingLed() { - if (config_.programming_led_gpio < 0 || ets_device_ == nullptr) { - return; + if (config_.programming_led_gpio >= 0 && ets_device_ != nullptr) { + const bool programming_mode = ets_device_->programmingMode(); + if (programming_mode != programming_led_state_) { + setProgrammingLed(programming_mode); + } } - const bool programming_mode = ets_device_->programmingMode(); - if (programming_mode == programming_led_state_) { - return; + + if (config_.oam_router.enabled && config_.oam_router.programming_led_gpio >= 0 && + oam_programming_mode_ != oam_programming_led_state_) { + setOamProgrammingLed(oam_programming_mode_); } - setProgrammingLed(programming_mode); } void GatewayKnxTpIpRouter::setProgrammingLed(bool on) { @@ -425,6 +520,17 @@ void GatewayKnxTpIpRouter::setProgrammingLed(bool on) { programming_led_state_ = on; } +void GatewayKnxTpIpRouter::setOamProgrammingLed(bool on) { + if (config_.oam_router.programming_led_gpio < 0) { + oam_programming_led_state_ = on; + return; + } + const bool level = config_.oam_router.programming_led_active_high ? on : !on; + gpio_set_level(static_cast(config_.oam_router.programming_led_gpio), + level ? 1 : 0); + oam_programming_led_state_ = on; +} + void GatewayKnxTpIpRouter::closeSockets() { if (udp_sock_ >= 0) { shutdown(udp_sock_, SHUT_RDWR); @@ -609,6 +715,7 @@ void GatewayKnxTpIpRouter::closeTcpClient(TcpClient& client) { resetTunnelClient(tunnel); } } + closeSecureSessionsForTcp(sock); if (active_tcp_sock_ == sock) { active_tcp_sock_ = -1; } @@ -754,6 +861,9 @@ bool GatewayKnxTpIpRouter::configureProgrammingGpio() { programming_button_last_pressed_ = false; programming_button_last_toggle_tick_ = 0; programming_led_state_ = false; + oam_programming_button_last_pressed_ = false; + oam_programming_button_last_toggle_tick_ = 0; + oam_programming_led_state_ = false; if (config_.programming_button_gpio >= 0) { gpio_config_t button_config{}; @@ -792,6 +902,47 @@ bool GatewayKnxTpIpRouter::configureProgrammingGpio() { setProgrammingLed(false); } + if (config_.oam_router.enabled && config_.oam_router.programming_button_gpio >= 0) { + gpio_config_t button_config{}; + button_config.pin_bit_mask = + 1ULL << static_cast(config_.oam_router.programming_button_gpio); + button_config.mode = GPIO_MODE_INPUT; + button_config.pull_up_en = config_.oam_router.programming_button_active_low + ? GPIO_PULLUP_ENABLE + : GPIO_PULLUP_DISABLE; + button_config.pull_down_en = config_.oam_router.programming_button_active_low + ? GPIO_PULLDOWN_DISABLE + : GPIO_PULLDOWN_ENABLE; + button_config.intr_type = GPIO_INTR_DISABLE; + const esp_err_t err = gpio_config(&button_config); + if (err != ESP_OK) { + last_error_ = EspErrDetail("failed to configure OAM KNX programming button GPIO" + + std::to_string(config_.oam_router.programming_button_gpio), + err); + ESP_LOGE(kTag, "%s", last_error_.c_str()); + return false; + } + } + + if (config_.oam_router.enabled && config_.oam_router.programming_led_gpio >= 0) { + gpio_config_t led_config{}; + led_config.pin_bit_mask = + 1ULL << static_cast(config_.oam_router.programming_led_gpio); + led_config.mode = GPIO_MODE_OUTPUT; + led_config.pull_up_en = GPIO_PULLUP_DISABLE; + led_config.pull_down_en = GPIO_PULLDOWN_DISABLE; + led_config.intr_type = GPIO_INTR_DISABLE; + const esp_err_t err = gpio_config(&led_config); + if (err != ESP_OK) { + last_error_ = EspErrDetail("failed to configure OAM KNX programming LED GPIO" + + std::to_string(config_.oam_router.programming_led_gpio), + err); + ESP_LOGE(kTag, "%s", last_error_.c_str()); + return false; + } + setOamProgrammingLed(false); + } + return true; } diff --git a/components/gateway_knx/src/gateway_knx_router_openknx.cpp b/components/gateway_knx/src/gateway_knx_router_openknx.cpp index d96ade2..e1af22f 100644 --- a/components/gateway_knx/src/gateway_knx_router_openknx.cpp +++ b/components/gateway_knx/src/gateway_knx_router_openknx.cpp @@ -8,6 +8,9 @@ void GatewayKnxTpIpRouter::selectOpenKnxNetworkInterface(const sockaddr_in& remo if (ets_device_ != nullptr) { ets_device_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr); } + if (oam_router_ != nullptr) { + oam_router_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr); + } } bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len, @@ -15,6 +18,20 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t uint16_t response_service, const uint8_t* suppress_routing_echo, size_t suppress_routing_echo_len) { + bool route_to_oam = response_client != nullptr && response_client->oam_router_persona; + if (!route_to_oam && data != nullptr && len >= 2) { + std::vector frame_data(data, data + len); + CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); + if (frame.valid() && oam_router_ != nullptr && + MatchesOamRouterLocalIndividualAddress(frame, *oam_router_)) { + route_to_oam = true; + } + } + if (route_to_oam) { + return handleOamRouterTunnelFrame(data, len, response_client, response_service, + suppress_routing_echo, suppress_routing_echo_len); + } + SemaphoreGuard guard(openknx_lock_); if (ets_device_ == nullptr) { return false; @@ -87,6 +104,82 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t return consumed; } +bool GatewayKnxTpIpRouter::handleOamRouterTunnelFrame(const uint8_t* data, size_t len, + TunnelClient* response_client, + uint16_t response_service, + const uint8_t* suppress_routing_echo, + size_t suppress_routing_echo_len) { + SemaphoreGuard guard(openknx_lock_); + if (oam_router_ == nullptr) { + return false; + } + std::vector tunnel_confirmation; + const bool needs_tunnel_confirmation = + response_client != nullptr && response_client->connected && + response_service == kServiceTunnellingRequest && + BuildTunnelConfirmationFrame(data, len, &tunnel_confirmation); + bool sent_tunnel_confirmation = false; + const bool consumed = oam_router_->handleTunnelFrame( + data, len, + [this, response_client, response_service, needs_tunnel_confirmation, + &tunnel_confirmation, &sent_tunnel_confirmation, + suppress_routing_echo, suppress_routing_echo_len](const uint8_t* response, + size_t response_len) { + if (response == nullptr || response_len == 0) { + return; + } + const bool routing_context = + response_client == nullptr && response_service == kServiceRoutingIndication; + const auto message_code = CemiMessageCode(response, response_len); + if (routing_context && suppress_routing_echo != nullptr && + IsLocalRoutingEchoIndication(response, response_len, suppress_routing_echo, + suppress_routing_echo_len)) { + return; + } + if (needs_tunnel_confirmation && !sent_tunnel_confirmation && + message_code.has_value() && message_code.value() != L_data_con) { + sent_tunnel_confirmation = sendCemiFrameToClient( + *response_client, kServiceTunnellingRequest, + tunnel_confirmation.data(), tunnel_confirmation.size()); + } + + const uint16_t service = KnxIpServiceForCemi(response, response_len, response_service); + if (service == kServiceDeviceConfigurationRequest) { + if (response_client != nullptr && response_client->connected) { + sendCemiFrameToClient(*response_client, service, response, response_len); + } else if (routing_context) { + sendRoutingIndication(response, response_len); + } + return; + } + if (message_code.has_value() && message_code.value() == L_data_con) { + if (routing_context) { + return; + } + if (response_client != nullptr && response_client->connected) { + sent_tunnel_confirmation = + sendCemiFrameToClient(*response_client, service, response, response_len) || + sent_tunnel_confirmation; + } + return; + } + if (routing_context) { + sendRoutingIndication(response, response_len); + return; + } + if (response_client != nullptr && response_client->connected) { + sendCemiFrameToClient(*response_client, service, response, response_len); + return; + } + sendTunnelIndication(response, response_len); + }); + if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) { + sendCemiFrameToClient(*response_client, kServiceTunnellingRequest, + tunnel_confirmation.data(), tunnel_confirmation.size()); + } + return consumed; +} + bool GatewayKnxTpIpRouter::transmitOpenKnxTpFrame(const uint8_t* data, size_t len) { SemaphoreGuard guard(openknx_lock_); if (ets_device_ == nullptr) { diff --git a/components/gateway_knx/src/gateway_knx_router_packets.cpp b/components/gateway_knx/src/gateway_knx_router_packets.cpp index 56ce69e..a4ff57a 100644 --- a/components/gateway_knx/src/gateway_knx_router_packets.cpp +++ b/components/gateway_knx/src/gateway_knx_router_packets.cpp @@ -33,10 +33,20 @@ void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockadd } bool GatewayKnxTpIpRouter::sendPacket(const std::vector& packet, - const sockaddr_in& remote) const { + const sockaddr_in& remote) { if (packet.empty()) { return false; } + if (SecureSession* session = activeSecureSession(); session != nullptr) { + std::vector wrapped; + if (wrapSecurePacket(*session, packet, &wrapped)) { + if (active_tcp_sock_ >= 0) { + return SendStream(active_tcp_sock_, wrapped.data(), wrapped.size()); + } + return udp_sock_ >= 0 && SendAll(udp_sock_, wrapped.data(), wrapped.size(), remote); + } + return false; + } if (active_tcp_sock_ >= 0) { return SendStream(active_tcp_sock_, packet.data(), packet.size()); } @@ -44,10 +54,32 @@ bool GatewayKnxTpIpRouter::sendPacket(const std::vector& packet, } bool GatewayKnxTpIpRouter::sendPacketToTunnelClient( - const TunnelClient& client, const std::vector& packet) const { + const TunnelClient& client, const std::vector& packet) { if (packet.empty()) { return false; } + if (client.secure_session_id != 0) { + SecureSession* session = nullptr; + for (auto& candidate : secure_sessions_) { + if (candidate.active && candidate.authenticated && + candidate.session_id == client.secure_session_id) { + session = &candidate; + break; + } + } + if (session != nullptr) { + std::vector wrapped; + if (wrapSecurePacket(*session, packet, &wrapped)) { + if (client.tcp_sock >= 0) { + return SendStream(client.tcp_sock, wrapped.data(), wrapped.size()); + } + return udp_sock_ >= 0 && SendAll(udp_sock_, wrapped.data(), wrapped.size(), + client.data_remote); + } + return false; + } + return false; + } if (client.tcp_sock >= 0) { return SendStream(client.tcp_sock, packet.data(), packet.size()); } @@ -211,6 +243,15 @@ std::vector GatewayKnxTpIpRouter::buildSupportedServiceDib() const { if (config_.multicast_enabled) { services.emplace_back(kKnxServiceFamilyRouting, 1); } +#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + const bool secure_routing_ready = + config_.oam_router.enabled && config_.oam_router.secure_routing_enabled && + oam_ip_secure_credentials_.activated && + oam_ip_secure_credentials_.backbone_key_available; + if (secureCredentialsReady() || secure_routing_ready) { + services.emplace_back(kKnxServiceFamilySecurity, 1); + } +#endif std::vector dib(2 + services.size() * 2U, 0); dib[0] = static_cast(dib.size()); dib[1] = kKnxDibSupportedServices; @@ -399,7 +440,19 @@ void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len return; } KnxIpRoutingIndication routing(frame); - const std::vector packet(routing.data(), routing.data() + routing.totalLength()); + std::vector packet(routing.data(), routing.data() + routing.totalLength()); +#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + if (config_.oam_router.enabled && config_.oam_router.secure_routing_enabled) { + std::vector secure_packet; + if (wrapSecureRoutingPacket(packet, &secure_packet)) { + packet = std::move(secure_packet); + } else if (oam_ip_secure_credentials_.activated && + oam_ip_secure_credentials_.backbone_key_available) { + ESP_LOGW(kTag, "failed to wrap KNXnet/IP Secure routing indication"); + return; + } + } +#endif const auto netifs = ActiveKnxNetifs(); if (netifs.empty()) { SendAll(udp_sock_, packet.data(), packet.size(), remote); diff --git a/components/gateway_knx/src/gateway_knx_router_services.cpp b/components/gateway_knx/src/gateway_knx_router_services.cpp index a400c00..b330792 100644 --- a/components/gateway_knx/src/gateway_knx_router_services.cpp +++ b/components/gateway_knx/src/gateway_knx_router_services.cpp @@ -184,7 +184,11 @@ void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* packet_data, s const uint8_t* cemi = frame.data(); const size_t cemi_len = frame.dataLength(); bool consumed_by_local_application = false; - if (ets_device_ != nullptr && MatchesOpenKnxLocalIndividualAddress(frame, *ets_device_)) { + const bool addressed_to_oam = + oam_router_ != nullptr && MatchesOamRouterLocalIndividualAddress(frame, *oam_router_); + const bool addressed_to_reg1 = + ets_device_ != nullptr && MatchesOpenKnxLocalIndividualAddress(frame, *ets_device_); + if (addressed_to_oam || addressed_to_reg1) { std::vector local_tunnel_frame; if (BuildLocalRoutingTunnelFrame(cemi, cemi_len, &local_tunnel_frame)) { consumed_by_local_application = handleOpenKnxTunnelFrame( @@ -300,11 +304,26 @@ GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::allocateTunnelClient( free_client->connection_type = connection_type; free_client->received_sequence = 255; free_client->send_sequence = 0; - free_client->individual_address = effectiveTunnelAddressForSlot(free_index); + const bool oam_router_persona = + config_.oam_router.enabled && config_.oam_router.secure_tunnel_enabled && + active_secure_session_id_ != 0; + if (oam_router_persona) { + const uint16_t first = config_.oam_router.tunnel_address_base; + const uint16_t line = first & 0xff00; + uint16_t device = static_cast((first & 0x00ff) + free_index); + if (device == 0 || device > 0xff) { + device = static_cast(1 + free_index); + } + free_client->individual_address = static_cast(line | (device & 0x00ff)); + } else { + free_client->individual_address = effectiveTunnelAddressForSlot(free_index); + } free_client->last_activity_tick = xTaskGetTickCount(); free_client->control_remote = control_remote; free_client->data_remote = data_remote; free_client->tcp_sock = active_tcp_sock_; + free_client->secure_session_id = active_secure_session_id_; + free_client->oam_router_persona = oam_router_persona; last_tunnel_channel_id_ = channel_id; return free_client; } @@ -636,16 +655,18 @@ void GatewayKnxTpIpRouter::handleSecureService(uint16_t service, const uint8_t* #if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) switch (service) { case kServiceSecureSessionRequest: + handleSecureSessionRequest(body, len, remote); + break; case kServiceSecureSessionAuth: - ESP_LOGW(kTag, "KNXnet/IP Secure service 0x%04x rejected: secure sessions are not provisioned", service); + ESP_LOGW(kTag, "KNXnet/IP Secure auth from %s rejected outside a secure wrapper", + EndpointString(remote).c_str()); sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote); break; case kServiceSecureWrapper: - ESP_LOGW(kTag, "KNXnet/IP Secure wrapper rejected: no authenticated secure session"); - sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote); + handleSecureWrapper(body, len, remote); break; case kServiceSecureGroupSync: - ESP_LOGD(kTag, "KNXnet/IP Secure group sync ignored until secure routing is provisioned"); + handleSecureGroupSync(body, len, remote); break; default: ESP_LOGD(kTag, "KNXnet/IP Secure service 0x%04x ignored", service); diff --git a/components/gateway_knx/src/gateway_knx_secure_transport.cpp b/components/gateway_knx/src/gateway_knx_secure_transport.cpp new file mode 100644 index 0000000..e0138f2 --- /dev/null +++ b/components/gateway_knx/src/gateway_knx_secure_transport.cpp @@ -0,0 +1,780 @@ +#include "gateway_knx_private.hpp" + +#include "esp_random.h" +#include "mbedtls/aes.h" +#include "mbedtls/ecp.h" +#include "mbedtls/sha256.h" + +namespace gateway { +namespace { + +constexpr size_t kSecurePublicKeyLen = 32; +constexpr size_t kSecureKeyLen = 16; +constexpr size_t kSecureSeqLen = 6; +constexpr size_t kSecureSerialLen = 6; +constexpr size_t kSecureMacLen = 16; +constexpr size_t kSecureWrapperBodyOverhead = 2 + kSecureSeqLen + kSecureSerialLen + 2 + kSecureMacLen; +constexpr std::array kAuthCtrIv{0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0x00}; + +int SecureRandom(void*, unsigned char* output, size_t len) { + esp_fill_random(output, len); + return 0; +} + +void WriteSeq48(uint8_t* data, uint64_t value) { + data[0] = static_cast((value >> 40) & 0xff); + data[1] = static_cast((value >> 32) & 0xff); + data[2] = static_cast((value >> 24) & 0xff); + data[3] = static_cast((value >> 16) & 0xff); + data[4] = static_cast((value >> 8) & 0xff); + data[5] = static_cast(value & 0xff); +} + +uint64_t ReadSeq48(const uint8_t* data) { + uint64_t value = 0; + for (size_t index = 0; index < kSecureSeqLen; ++index) { + value = (value << 8) | data[index]; + } + return value; +} + +std::array OamRouterSerial() { + std::array serial{}; + uint8_t mac[6]{}; + if (!ReadBaseMac(mac)) { + return serial; + } + uint32_t suffix = (static_cast(mac[2]) << 24) | + (static_cast(mac[3]) << 16) | + (static_cast(mac[4]) << 8) | + static_cast(mac[5]); + suffix += knx_internal::kOamRouterSerialMacIncrement; + serial[0] = static_cast((knx_internal::kOamRouterManufacturerId >> 8) & 0xff); + serial[1] = static_cast(knx_internal::kOamRouterManufacturerId & 0xff); + serial[2] = static_cast((suffix >> 24) & 0xff); + serial[3] = static_cast((suffix >> 16) & 0xff); + serial[4] = static_cast((suffix >> 8) & 0xff); + serial[5] = static_cast(suffix & 0xff); + return serial; +} + +bool AesCbcMac(const std::array& key, + const std::vector& additional_data, + const std::vector& payload, + const std::array& block0, + std::array* mac) { + if (mac == nullptr || additional_data.size() > 0xffff) { + return false; + } + std::vector blocks; + blocks.reserve(block0.size() + 2 + additional_data.size() + payload.size() + kSecureMacLen); + blocks.insert(blocks.end(), block0.begin(), block0.end()); + blocks.push_back(static_cast((additional_data.size() >> 8) & 0xff)); + blocks.push_back(static_cast(additional_data.size() & 0xff)); + blocks.insert(blocks.end(), additional_data.begin(), additional_data.end()); + blocks.insert(blocks.end(), payload.begin(), payload.end()); + const size_t remainder = blocks.size() % kSecureMacLen; + if (remainder != 0) { + blocks.insert(blocks.end(), kSecureMacLen - remainder, 0x00); + } + + std::array iv{}; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + const int set_key = mbedtls_aes_setkey_enc(&aes, key.data(), 128); + int crypt = 0; + if (set_key == 0) { + crypt = mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, blocks.size(), + iv.data(), blocks.data(), blocks.data()); + } + mbedtls_aes_free(&aes); + if (set_key != 0 || crypt != 0) { + return false; + } + std::copy(blocks.end() - kSecureMacLen, blocks.end(), mac->begin()); + return true; +} + +bool AesCtrTransform(const std::array& key, + const std::array& counter, + const uint8_t* input, size_t len, + std::vector* output) { + if (output == nullptr || (input == nullptr && len != 0)) { + return false; + } + output->assign(len, 0); + std::array nonce_counter = counter; + std::array stream_block{}; + size_t nc_off = 0; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + const int set_key = mbedtls_aes_setkey_enc(&aes, key.data(), 128); + int crypt = 0; + if (set_key == 0 && len > 0) { + crypt = mbedtls_aes_crypt_ctr(&aes, len, &nc_off, nonce_counter.data(), + stream_block.data(), input, output->data()); + } + mbedtls_aes_free(&aes); + return set_key == 0 && crypt == 0; +} + +bool GenerateSessionKey(const std::array& client_public, + std::array* server_public, + std::array* shared_secret, + std::array* session_key) { + if (server_public == nullptr || shared_secret == nullptr || session_key == nullptr) { + return false; + } + mbedtls_ecp_group group; + mbedtls_mpi private_key; + mbedtls_ecp_point public_key; + mbedtls_ecp_point peer_public_key; + mbedtls_ecp_point shared_point; + mbedtls_ecp_group_init(&group); + mbedtls_mpi_init(&private_key); + mbedtls_ecp_point_init(&public_key); + mbedtls_ecp_point_init(&peer_public_key); + mbedtls_ecp_point_init(&shared_point); + + int ret = mbedtls_ecp_group_load(&group, MBEDTLS_ECP_DP_CURVE25519); + if (ret == 0) { + ret = mbedtls_ecp_gen_privkey(&group, &private_key, SecureRandom, nullptr); + } + if (ret == 0) { + ret = mbedtls_ecp_mul(&group, &public_key, &private_key, &group.G, SecureRandom, nullptr); + } + if (ret == 0) { + ret = mbedtls_ecp_point_read_binary(&group, &peer_public_key, client_public.data(), + client_public.size()); + } + if (ret == 0) { + ret = mbedtls_ecp_mul(&group, &shared_point, &private_key, &peer_public_key, + SecureRandom, nullptr); + } + size_t public_len = 0; + if (ret == 0) { + ret = mbedtls_ecp_point_write_binary(&group, &public_key, MBEDTLS_ECP_PF_UNCOMPRESSED, + &public_len, server_public->data(), + server_public->size()); + } + size_t secret_len = 0; + if (ret == 0) { + ret = mbedtls_ecp_point_write_binary(&group, &shared_point, MBEDTLS_ECP_PF_UNCOMPRESSED, + &secret_len, shared_secret->data(), + shared_secret->size()); + } + mbedtls_ecp_point_free(&shared_point); + mbedtls_ecp_point_free(&peer_public_key); + mbedtls_ecp_point_free(&public_key); + mbedtls_mpi_free(&private_key); + mbedtls_ecp_group_free(&group); + if (ret != 0 || public_len != server_public->size() || secret_len != shared_secret->size()) { + return false; + } + std::array digest{}; + if (mbedtls_sha256(shared_secret->data(), shared_secret->size(), digest.data(), 0) != 0) { + return false; + } + std::copy(digest.begin(), digest.begin() + session_key->size(), session_key->begin()); + return true; +} + +} // namespace + +bool GatewayKnxTpIpRouter::secureCredentialsReady() const { +#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + return config_.oam_router.enabled && config_.oam_router.secure_tunnel_enabled && + oam_ip_secure_credentials_.activated && + !oam_ip_secure_credentials_.tunnel_user_keys.empty(); +#else + return false; +#endif +} + +GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::findSecureSession( + uint16_t session_id, const sockaddr_in& remote) { + if (session_id == 0) { + return nullptr; + } + for (auto& session : secure_sessions_) { + if (session.active && session.session_id == session_id && + EndpointEquals(session.remote, remote)) { + return &session; + } + } + return nullptr; +} + +const GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::findSecureSession( + uint16_t session_id, const sockaddr_in& remote) const { + if (session_id == 0) { + return nullptr; + } + for (const auto& session : secure_sessions_) { + if (session.active && session.session_id == session_id && + EndpointEquals(session.remote, remote)) { + return &session; + } + } + return nullptr; +} + +GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::allocateSecureSession( + const sockaddr_in& remote) { + SecureSession* slot = nullptr; + for (auto& session : secure_sessions_) { + if (session.active && EndpointEquals(session.remote, remote)) { + slot = &session; + break; + } + if (!session.active && slot == nullptr) { + slot = &session; + } + } + if (slot == nullptr) { + return nullptr; + } + *slot = SecureSession{}; + slot->active = true; + slot->remote = remote; + slot->tcp_sock = active_tcp_sock_; + for (uint32_t attempt = 0; attempt < 0xffff; ++attempt) { + last_secure_session_id_ = static_cast(last_secure_session_id_ + 1); + if (last_secure_session_id_ == 0) { + last_secure_session_id_ = 1; + } + if (findSecureSession(last_secure_session_id_, remote) == nullptr) { + slot->session_id = last_secure_session_id_; + break; + } + } + slot->last_activity_tick = xTaskGetTickCount(); + return slot->session_id == 0 ? nullptr : slot; +} + +GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::activeSecureSession() { + if (active_secure_session_id_ == 0) { + return nullptr; + } + for (auto& session : secure_sessions_) { + if (session.active && session.session_id == active_secure_session_id_) { + return &session; + } + } + return nullptr; +} + +void GatewayKnxTpIpRouter::closeSecureSessionsForTcp(int sock) { + if (sock < 0) { + return; + } + for (auto& session : secure_sessions_) { + if (session.active && session.tcp_sock == sock) { + session = SecureSession{}; + } + } +} + +bool GatewayKnxTpIpRouter::wrapSecurePacket(SecureSession& session, + const std::vector& inner, + std::vector* wrapped) { + if (wrapped == nullptr || inner.empty() || inner.size() > 0xffff) { + return false; + } + const size_t total_len = kKnxNetIpHeaderSize + kSecureWrapperBodyOverhead + inner.size(); + if (total_len > 0xffff) { + return false; + } + const auto serial = OamRouterSerial(); + std::array sequence{}; + WriteSeq48(sequence.data(), session.send_sequence++ & 0xffffffffffffULL); + std::array tag{0x00, 0x00}; + + std::vector header(kKnxNetIpHeaderSize, 0); + header[0] = kKnxNetIpHeaderSize; + header[1] = kKnxNetIpVersion10; + WriteBe16(header.data() + 2, kServiceSecureWrapper); + WriteBe16(header.data() + 4, static_cast(total_len)); + + std::vector additional = header; + additional.push_back(static_cast((session.session_id >> 8) & 0xff)); + additional.push_back(static_cast(session.session_id & 0xff)); + + std::array block0{}; + std::copy(sequence.begin(), sequence.end(), block0.begin()); + std::copy(serial.begin(), serial.end(), block0.begin() + kSecureSeqLen); + block0[12] = tag[0]; + block0[13] = tag[1]; + WriteBe16(block0.data() + 14, static_cast(inner.size())); + + std::array mac_cbc{}; + if (!AesCbcMac(session.session_key, additional, inner, block0, &mac_cbc)) { + return false; + } + + std::array counter{}; + std::copy(sequence.begin(), sequence.end(), counter.begin()); + std::copy(serial.begin(), serial.end(), counter.begin() + kSecureSeqLen); + counter[12] = tag[0]; + counter[13] = tag[1]; + counter[14] = 0xff; + counter[15] = 0x00; + + std::vector enc_mac; + if (!AesCtrTransform(session.session_key, counter, mac_cbc.data(), mac_cbc.size(), + &enc_mac)) { + return false; + } + std::vector enc_payload; + if (!AesCtrTransform(session.session_key, counter, inner.data(), inner.size(), + &enc_payload)) { + return false; + } + + wrapped->clear(); + wrapped->reserve(total_len); + wrapped->insert(wrapped->end(), header.begin(), header.end()); + wrapped->push_back(static_cast((session.session_id >> 8) & 0xff)); + wrapped->push_back(static_cast(session.session_id & 0xff)); + wrapped->insert(wrapped->end(), sequence.begin(), sequence.end()); + wrapped->insert(wrapped->end(), serial.begin(), serial.end()); + wrapped->insert(wrapped->end(), tag.begin(), tag.end()); + wrapped->insert(wrapped->end(), enc_payload.begin(), enc_payload.end()); + wrapped->insert(wrapped->end(), enc_mac.begin(), enc_mac.end()); + session.last_activity_tick = xTaskGetTickCount(); + return true; +} + +bool GatewayKnxTpIpRouter::wrapSecureRoutingPacket(const std::vector& inner, + std::vector* wrapped) { +#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + if (wrapped == nullptr || inner.empty() || inner.size() > 0xffff || + !config_.oam_router.enabled || !config_.oam_router.secure_routing_enabled || + !oam_ip_secure_credentials_.activated || + !oam_ip_secure_credentials_.backbone_key_available) { + return false; + } + const size_t total_len = kKnxNetIpHeaderSize + kSecureWrapperBodyOverhead + inner.size(); + if (total_len > 0xffff) { + return false; + } + const uint16_t routing_session_id = 0; + const auto serial = OamRouterSerial(); + const uint64_t sequence_value = oam_ip_secure_credentials_.routing_sequence; + std::array sequence{}; + WriteSeq48(sequence.data(), sequence_value & 0xffffffffffffULL); + std::array tag{0x00, 0x00}; + + std::vector header(kKnxNetIpHeaderSize, 0); + header[0] = kKnxNetIpHeaderSize; + header[1] = kKnxNetIpVersion10; + WriteBe16(header.data() + 2, kServiceSecureWrapper); + WriteBe16(header.data() + 4, static_cast(total_len)); + + std::vector additional = header; + additional.push_back(0x00); + additional.push_back(0x00); + + std::array block0{}; + std::copy(sequence.begin(), sequence.end(), block0.begin()); + std::copy(serial.begin(), serial.end(), block0.begin() + kSecureSeqLen); + block0[12] = tag[0]; + block0[13] = tag[1]; + WriteBe16(block0.data() + 14, static_cast(inner.size())); + + std::array mac_cbc{}; + if (!AesCbcMac(oam_ip_secure_credentials_.backbone_key, additional, inner, block0, + &mac_cbc)) { + return false; + } + + std::array counter{}; + std::copy(sequence.begin(), sequence.end(), counter.begin()); + std::copy(serial.begin(), serial.end(), counter.begin() + kSecureSeqLen); + counter[12] = tag[0]; + counter[13] = tag[1]; + counter[14] = 0xff; + counter[15] = 0x00; + + std::vector enc_mac; + if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter, + mac_cbc.data(), mac_cbc.size(), &enc_mac)) { + return false; + } + std::vector enc_payload; + if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter, + inner.data(), inner.size(), &enc_payload)) { + return false; + } + + wrapped->clear(); + wrapped->reserve(total_len); + wrapped->insert(wrapped->end(), header.begin(), header.end()); + wrapped->push_back(static_cast((routing_session_id >> 8) & 0xff)); + wrapped->push_back(static_cast(routing_session_id & 0xff)); + wrapped->insert(wrapped->end(), sequence.begin(), sequence.end()); + wrapped->insert(wrapped->end(), serial.begin(), serial.end()); + wrapped->insert(wrapped->end(), tag.begin(), tag.end()); + wrapped->insert(wrapped->end(), enc_payload.begin(), enc_payload.end()); + wrapped->insert(wrapped->end(), enc_mac.begin(), enc_mac.end()); + oam_ip_secure_credentials_.routing_sequence = sequence_value + 1; + if (routing_sequence_store_handler_) { + routing_sequence_store_handler_(oam_ip_secure_credentials_.routing_sequence); + } + return true; +#else + (void)inner; + (void)wrapped; + return false; +#endif +} + +bool GatewayKnxTpIpRouter::decryptSecureWrapper(SecureSession& session, + const uint8_t* body, size_t len, + std::vector* inner) { + if (body == nullptr || inner == nullptr || len < kSecureWrapperBodyOverhead) { + return false; + } + const uint16_t session_id = ReadBe16(body); + if (session_id != session.session_id) { + return false; + } + const uint8_t* sequence = body + 2; + const uint8_t* serial = body + 8; + const uint8_t* tag = body + 14; + const uint8_t* encrypted_payload = body + 16; + const size_t encrypted_payload_len = len - kSecureWrapperBodyOverhead; + const uint8_t* encrypted_mac = body + len - kSecureMacLen; + + const uint64_t incoming_sequence = ReadSeq48(sequence); + if (incoming_sequence < session.receive_sequence) { + return false; + } + + std::array counter{}; + std::copy(sequence, sequence + kSecureSeqLen, counter.begin()); + std::copy(serial, serial + kSecureSerialLen, counter.begin() + kSecureSeqLen); + counter[12] = tag[0]; + counter[13] = tag[1]; + counter[14] = 0xff; + counter[15] = 0x00; + + std::vector mac_tr; + if (!AesCtrTransform(session.session_key, counter, encrypted_mac, kSecureMacLen, &mac_tr)) { + return false; + } + if (!AesCtrTransform(session.session_key, counter, encrypted_payload, + encrypted_payload_len, inner)) { + return false; + } + + std::vector header(kKnxNetIpHeaderSize, 0); + header[0] = kKnxNetIpHeaderSize; + header[1] = kKnxNetIpVersion10; + WriteBe16(header.data() + 2, kServiceSecureWrapper); + WriteBe16(header.data() + 4, static_cast(kKnxNetIpHeaderSize + len)); + std::vector additional = header; + additional.push_back(static_cast((session_id >> 8) & 0xff)); + additional.push_back(static_cast(session_id & 0xff)); + + std::array block0{}; + std::copy(sequence, sequence + kSecureSeqLen, block0.begin()); + std::copy(serial, serial + kSecureSerialLen, block0.begin() + kSecureSeqLen); + block0[12] = tag[0]; + block0[13] = tag[1]; + WriteBe16(block0.data() + 14, static_cast(inner->size())); + + std::array mac_cbc{}; + if (!AesCbcMac(session.session_key, additional, *inner, block0, &mac_cbc) || + mac_tr.size() != mac_cbc.size() || + !std::equal(mac_cbc.begin(), mac_cbc.end(), mac_tr.begin())) { + return false; + } + session.receive_sequence = incoming_sequence + 1; + session.last_activity_tick = xTaskGetTickCount(); + return true; +} + +bool GatewayKnxTpIpRouter::decryptSecureRoutingWrapper(const uint8_t* body, + size_t len, + std::vector* inner) { +#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + if (body == nullptr || inner == nullptr || len < kSecureWrapperBodyOverhead || + !config_.oam_router.enabled || !config_.oam_router.secure_routing_enabled || + !oam_ip_secure_credentials_.activated || + !oam_ip_secure_credentials_.backbone_key_available) { + return false; + } + const uint16_t session_id = ReadBe16(body); + if (session_id != 0) { + return false; + } + const uint8_t* sequence = body + 2; + const uint8_t* serial = body + 8; + const uint8_t* tag = body + 14; + const uint8_t* encrypted_payload = body + 16; + const size_t encrypted_payload_len = len - kSecureWrapperBodyOverhead; + const uint8_t* encrypted_mac = body + len - kSecureMacLen; + const uint64_t incoming_sequence = ReadSeq48(sequence); + + std::array counter{}; + std::copy(sequence, sequence + kSecureSeqLen, counter.begin()); + std::copy(serial, serial + kSecureSerialLen, counter.begin() + kSecureSeqLen); + counter[12] = tag[0]; + counter[13] = tag[1]; + counter[14] = 0xff; + counter[15] = 0x00; + + std::vector mac_tr; + if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter, + encrypted_mac, kSecureMacLen, &mac_tr)) { + return false; + } + if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter, + encrypted_payload, encrypted_payload_len, inner)) { + return false; + } + + std::vector header(kKnxNetIpHeaderSize, 0); + header[0] = kKnxNetIpHeaderSize; + header[1] = kKnxNetIpVersion10; + WriteBe16(header.data() + 2, kServiceSecureWrapper); + WriteBe16(header.data() + 4, static_cast(kKnxNetIpHeaderSize + len)); + std::vector additional = header; + additional.push_back(0x00); + additional.push_back(0x00); + + std::array block0{}; + std::copy(sequence, sequence + kSecureSeqLen, block0.begin()); + std::copy(serial, serial + kSecureSerialLen, block0.begin() + kSecureSeqLen); + block0[12] = tag[0]; + block0[13] = tag[1]; + WriteBe16(block0.data() + 14, static_cast(inner->size())); + + std::array mac_cbc{}; + if (!AesCbcMac(oam_ip_secure_credentials_.backbone_key, additional, *inner, block0, + &mac_cbc) || + mac_tr.size() != mac_cbc.size() || + !std::equal(mac_cbc.begin(), mac_cbc.end(), mac_tr.begin())) { + return false; + } + oam_ip_secure_credentials_.routing_sequence = + std::max(oam_ip_secure_credentials_.routing_sequence, incoming_sequence + 1); + if (routing_sequence_store_handler_) { + routing_sequence_store_handler_(oam_ip_secure_credentials_.routing_sequence); + } + return true; +#else + (void)body; + (void)len; + (void)inner; + return false; +#endif +} + +bool GatewayKnxTpIpRouter::verifySecureSessionAuth(SecureSession& session, + const uint8_t* packet, + size_t len, uint8_t* status) { + if (status != nullptr) { + *status = kKnxSecureStatusAuthFailed; + } + uint16_t service = 0; + uint16_t total_len = 0; + if (!ParseKnxNetIpHeader(packet, len, &service, &total_len) || + service != kServiceSecureSessionAuth || total_len < kKnxNetIpHeaderSize + 18) { + return false; + } + const uint8_t user_id = packet[7]; + const uint8_t* received_mac = packet + 8; + const std::array* user_key = nullptr; + if (user_id > 0 && user_id - 1 < oam_ip_secure_credentials_.tunnel_user_keys.size()) { + user_key = &oam_ip_secure_credentials_.tunnel_user_keys[user_id - 1]; + } else if (oam_ip_secure_credentials_.tunnel_user_keys.size() == 1) { + user_key = &oam_ip_secure_credentials_.tunnel_user_keys.front(); + } + if (user_key == nullptr) { + return false; + } + + std::vector additional; + additional.reserve(6 + 2 + kSecurePublicKeyLen); + additional.insert(additional.end(), packet, packet + 6); + additional.push_back(packet[6]); + additional.push_back(user_id); + for (size_t index = 0; index < kSecurePublicKeyLen; ++index) { + additional.push_back(session.client_public_key[index] ^ session.server_public_key[index]); + } + std::array block0{}; + std::array mac_cbc{}; + if (!AesCbcMac(*user_key, additional, {}, block0, &mac_cbc)) { + return false; + } + std::vector transformed_mac; + if (!AesCtrTransform(*user_key, kAuthCtrIv, mac_cbc.data(), mac_cbc.size(), + &transformed_mac) || + transformed_mac.size() != kSecureMacLen || + !std::equal(transformed_mac.begin(), transformed_mac.end(), received_mac)) { + return false; + } + session.authenticated = true; + session.user_id = user_id; + if (status != nullptr) { + *status = kKnxNoError; + } + return true; +} + +void GatewayKnxTpIpRouter::handleSecureSessionRequest(const uint8_t* body, + size_t len, + const sockaddr_in& remote) { +#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + if (!secureCredentialsReady()) { + ESP_LOGW(kTag, "reject KNXnet/IP Secure session from %s: OAM IP Secure credentials are not active", + EndpointString(remote).c_str()); + sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote); + return; + } + if (body == nullptr || len < 8 + kSecurePublicKeyLen) { + sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote); + return; + } + SecureSession* session = allocateSecureSession(remote); + if (session == nullptr) { + sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote); + return; + } + std::copy(body + 8, body + 8 + kSecurePublicKeyLen, session->client_public_key.begin()); + if (!GenerateSessionKey(session->client_public_key, &session->server_public_key, + &session->shared_secret, &session->session_key)) { + *session = SecureSession{}; + sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote); + return; + } + std::vector response_body; + response_body.reserve(2 + kSecurePublicKeyLen); + response_body.push_back(static_cast((session->session_id >> 8) & 0xff)); + response_body.push_back(static_cast(session->session_id & 0xff)); + response_body.insert(response_body.end(), session->server_public_key.begin(), + session->server_public_key.end()); + sendPacket(OpenKnxIpPacket(kServiceSecureSessionResponse, response_body), remote); + ESP_LOGI(kTag, "accepted KNXnet/IP Secure session request sid=%u from %s", + static_cast(session->session_id), EndpointString(remote).c_str()); +#else + (void)body; + (void)len; + (void)remote; +#endif +} + +void GatewayKnxTpIpRouter::handleSecureWrapper(const uint8_t* body, size_t len, + const sockaddr_in& remote) { +#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + if (body == nullptr || len < kSecureWrapperBodyOverhead) { + sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote); + return; + } + const uint16_t session_id = ReadBe16(body); + if (session_id == 0) { + std::vector inner; + if (!decryptSecureRoutingWrapper(body, len, &inner)) { + return; + } + handleUdpDatagram(inner.data(), inner.size(), remote); + return; + } + SecureSession* session = findSecureSession(session_id, remote); + if (session == nullptr) { + sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote); + return; + } + std::vector inner; + if (!decryptSecureWrapper(*session, body, len, &inner)) { + sendSecureSessionStatus(0x04, remote); + return; + } + uint16_t service = 0; + uint16_t total_len = 0; + if (!ParseKnxNetIpHeader(inner.data(), inner.size(), &service, &total_len)) { + sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote); + return; + } + const uint16_t previous_session = active_secure_session_id_; + active_secure_session_id_ = session->session_id; + if (service == kServiceSecureSessionAuth) { + uint8_t status = kKnxSecureStatusAuthFailed; + verifySecureSessionAuth(*session, inner.data(), inner.size(), &status); + sendSecureSessionStatus(status, remote); + active_secure_session_id_ = previous_session; + return; + } + if (!session->authenticated) { + sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote); + active_secure_session_id_ = previous_session; + return; + } + handleUdpDatagram(inner.data(), inner.size(), remote); + active_secure_session_id_ = previous_session; +#else + (void)body; + (void)len; + (void)remote; +#endif +} + +void GatewayKnxTpIpRouter::handleSecureGroupSync(const uint8_t* body, size_t len, + const sockaddr_in& remote) { +#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED) + if (body == nullptr || len < kSecureSeqLen + kSecureSerialLen + 2 + kSecureMacLen || + !oam_ip_secure_credentials_.activated || + !oam_ip_secure_credentials_.backbone_key_available) { + return; + } + const uint8_t* sequence = body; + const uint8_t* serial = body + kSecureSeqLen; + const uint8_t* tag = body + kSecureSeqLen + kSecureSerialLen; + const uint8_t* encrypted_mac = tag + 2; + std::array counter{}; + std::copy(sequence, sequence + kSecureSeqLen, counter.begin()); + std::copy(serial, serial + kSecureSerialLen, counter.begin() + kSecureSeqLen); + counter[12] = tag[0]; + counter[13] = tag[1]; + counter[14] = 0xff; + counter[15] = 0x00; + std::vector mac_tr; + if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter, encrypted_mac, + kSecureMacLen, &mac_tr)) { + return; + } + std::vector header(kKnxNetIpHeaderSize, 0); + header[0] = kKnxNetIpHeaderSize; + header[1] = kKnxNetIpVersion10; + WriteBe16(header.data() + 2, kServiceSecureGroupSync); + WriteBe16(header.data() + 4, static_cast(kKnxNetIpHeaderSize + len)); + std::array block0{}; + std::copy(sequence, sequence + kSecureSeqLen, block0.begin()); + std::copy(serial, serial + kSecureSerialLen, block0.begin() + kSecureSeqLen); + block0[12] = tag[0]; + block0[13] = tag[1]; + std::array mac_cbc{}; + if (AesCbcMac(oam_ip_secure_credentials_.backbone_key, header, {}, block0, &mac_cbc) && + mac_tr.size() == mac_cbc.size() && + std::equal(mac_cbc.begin(), mac_cbc.end(), mac_tr.begin())) { + oam_ip_secure_credentials_.routing_sequence = + std::max(oam_ip_secure_credentials_.routing_sequence, ReadSeq48(sequence) + 1); + if (routing_sequence_store_handler_) { + routing_sequence_store_handler_(oam_ip_secure_credentials_.routing_sequence); + } + ESP_LOGD(kTag, "authenticated KNXnet/IP Secure group sync from %s", + EndpointString(remote).c_str()); + } +#else + (void)body; + (void)len; + (void)remote; +#endif +} + +} // namespace gateway diff --git a/components/gateway_knx/src/oam_router_runtime.cpp b/components/gateway_knx/src/oam_router_runtime.cpp new file mode 100644 index 0000000..83ab2aa --- /dev/null +++ b/components/gateway_knx/src/oam_router_runtime.cpp @@ -0,0 +1,278 @@ +#include "oam_router_runtime.h" + +#include "gateway_knx_internal.h" + +#include "esp_log.h" +#include "esp_mac.h" +#include "knx/cemi_server.h" +#include "knx/property.h" + +#include +#include +#include +#include + +namespace gateway::openknx { +namespace { + +constexpr uint16_t kInvalidIndividualAddress = 0xffff; +constexpr uint16_t kKnxUnconfiguredBroadcastAddress = 0xffff; + +bool IsUsableIndividualAddress(uint16_t address) { + return address != 0 && address != kInvalidIndividualAddress; +} + +uint32_t OamBauNumberFromBaseMac() { + uint8_t mac[6]{}; + if (esp_efuse_mac_get_default(mac) != ESP_OK && esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) { + return 0; + } + uint32_t suffix = (static_cast(mac[2]) << 24) | + (static_cast(mac[3]) << 16) | + (static_cast(mac[4]) << 8) | + static_cast(mac[5]); + return suffix + knx_internal::kOamRouterSerialMacIncrement; +} + +#if defined(ENABLE_BAU091A_PERSONA) +void ApplyOamRouterIdentity(Bau091A& device) { + device.deviceObject().manufacturerId(knx_internal::kOamRouterManufacturerId); + device.deviceObject().bauNumber(OamBauNumberFromBaseMac()); + device.deviceObject().hardwareType(knx_internal::kOamRouterHardwareType); + device.deviceObject().orderNumber(knx_internal::kOamRouterOrderNumber); + if (auto* property = device.parameters().property(PID_PROG_VERSION); property != nullptr) { + property->write(knx_internal::kOamRouterProgramVersion); + } +} +#endif + +} // namespace + +OamRouterRuntime::OamRouterRuntime(std::string nvs_namespace, + uint16_t fallback_individual_address, + uint16_t tunnel_client_address) + : nvs_namespace_(std::move(nvs_namespace)) +#if defined(ENABLE_BAU091A_PERSONA) + , + platform_(nullptr, nvs_namespace_.c_str()), + device_(platform_) +#endif +{ +#if defined(ENABLE_BAU091A_PERSONA) + platform_.outboundCemiFrameCallback(&OamRouterRuntime::HandleOutboundCemiFrame, this); + ApplyOamRouterIdentity(device_); + if (IsUsableIndividualAddress(fallback_individual_address)) { + device_.deviceObject().individualAddress(fallback_individual_address); + } + ESP_LOGI("gateway_knx", "OAM OpenKNX loading memory namespace=%s", nvs_namespace_.c_str()); + device_.readMemory(); + ApplyOamRouterIdentity(device_); + if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) && + IsUsableIndividualAddress(fallback_individual_address)) { + device_.deviceObject().individualAddress(fallback_individual_address); + } +#ifdef USE_CEMI_SERVER + if (auto* server = device_.getCemiServer()) { + server->clientAddress(IsUsableIndividualAddress(tunnel_client_address) + ? tunnel_client_address + : DefaultTunnelClientAddress( + device_.deviceObject().individualAddress())); + server->deviceAddressPropertiesTargetClient(false); + server->tunnelFrameCallback(&OamRouterRuntime::EmitTunnelFrame, this); + } +#endif + uint8_t program_version[5]{}; + if (auto* property = device_.parameters().property(PID_PROG_VERSION); property != nullptr) { + property->read(program_version); + } + ESP_LOGI("gateway_knx", + "OAM router runtime namespace=%s configured=%d manufacturer=0x%04x mask=0x%04x device=0x%04x tunnelClient=0x%04x progVersion=%02X %02X %02X %02X %02X", + nvs_namespace_.c_str(), device_.configured(), device_.deviceObject().manufacturerId(), + device_.deviceObject().maskVersion(), device_.deviceObject().individualAddress(), + tunnelClientAddress(), program_version[0], program_version[1], program_version[2], + program_version[3], program_version[4]); +#else + (void)fallback_individual_address; + (void)tunnel_client_address; +#endif +} + +OamRouterRuntime::~OamRouterRuntime() { +#if defined(ENABLE_BAU091A_PERSONA) + platform_.outboundCemiFrameCallback(nullptr, nullptr); +#ifdef USE_CEMI_SERVER + if (auto* server = device_.getCemiServer()) { + server->tunnelFrameCallback(nullptr, nullptr); + } +#endif +#endif +} + +bool OamRouterRuntime::available() const { +#if defined(ENABLE_BAU091A_PERSONA) + return true; +#else + return false; +#endif +} + +uint16_t OamRouterRuntime::individualAddress() const { +#if defined(ENABLE_BAU091A_PERSONA) + return const_cast(device_).deviceObject().individualAddress(); +#else + return 0xffff; +#endif +} + +uint16_t OamRouterRuntime::tunnelClientAddress() const { +#if defined(ENABLE_BAU091A_PERSONA) && defined(USE_CEMI_SERVER) + if (auto* server = const_cast(device_).getCemiServer()) { + return server->clientAddress(); + } +#endif + return DefaultTunnelClientAddress(individualAddress()); +} + +bool OamRouterRuntime::configured() const { +#if defined(ENABLE_BAU091A_PERSONA) + return const_cast(device_).configured(); +#else + return false; +#endif +} + +bool OamRouterRuntime::programmingMode() const { +#if defined(ENABLE_BAU091A_PERSONA) + return const_cast(device_).deviceObject().progMode(); +#else + return false; +#endif +} + +void OamRouterRuntime::setProgrammingMode(bool enabled) { +#if defined(ENABLE_BAU091A_PERSONA) + device_.deviceObject().progMode(enabled); +#else + (void)enabled; +#endif +} + +void OamRouterRuntime::toggleProgrammingMode() { setProgrammingMode(!programmingMode()); } + +EtsMemorySnapshot OamRouterRuntime::snapshot() const { + EtsMemorySnapshot out; +#if defined(ENABLE_BAU091A_PERSONA) + auto& device = const_cast(device_); + out.configured = device.configured(); + out.individual_address = device.deviceObject().individualAddress(); +#endif + return out; +} + +DeviceObject* OamRouterRuntime::deviceObject() { +#if defined(ENABLE_BAU091A_PERSONA) + return &device_.deviceObject(); +#else + return nullptr; +#endif +} + +Platform* OamRouterRuntime::platform() { +#if defined(ENABLE_BAU091A_PERSONA) + return &platform_; +#else + return nullptr; +#endif +} + +void OamRouterRuntime::setNetworkInterface(esp_netif_t* netif) { +#if defined(ENABLE_BAU091A_PERSONA) + platform_.networkInterface(netif); +#else + (void)netif; +#endif +} + +bool OamRouterRuntime::handleTunnelFrame(const uint8_t* data, size_t len, + CemiFrameSender sender) { +#if defined(ENABLE_BAU091A_PERSONA) && defined(USE_CEMI_SERVER) + auto* server = device_.getCemiServer(); + if (server == nullptr || data == nullptr || len < 2) { + return false; + } + std::vector frame_data(data, data + len); + CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); + if (!shouldConsumeTunnelFrame(frame)) { + return false; + } + sender_ = std::move(sender); + server->frameReceived(frame); + loop(); + sender_ = nullptr; + return true; +#else + (void)data; + (void)len; + (void)sender; + return false; +#endif +} + +void OamRouterRuntime::loop() { +#if defined(ENABLE_BAU091A_PERSONA) + device_.loop(); +#endif +} + +bool OamRouterRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context) { + auto* self = static_cast(context); + if (self == nullptr || !self->sender_) { + return false; + } + self->sender_(frame.data(), frame.dataLength()); + return true; +} + +void OamRouterRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) { + auto* self = static_cast(context); + if (self == nullptr || !self->sender_) { + return; + } + self->sender_(frame.data(), frame.dataLength()); +} + +uint16_t OamRouterRuntime::DefaultTunnelClientAddress(uint16_t individual_address) { + if (!IsUsableIndividualAddress(individual_address)) { + return 0x1102; + } + const uint16_t line_base = individual_address & 0xff00; + uint16_t device = static_cast((individual_address & 0x00ff) + 1); + if (device == 0 || device > 0xff) { + device = 2; + } + return static_cast(line_base | device); +} + +bool OamRouterRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const { + switch (frame.messageCode()) { + case M_PropRead_req: + case M_PropWrite_req: + case M_Reset_req: + case M_FuncPropCommand_req: + case M_FuncPropStateRead_req: + return true; + case L_data_req: { + const uint16_t dest = frame.destinationAddress(); + const bool commissioning = !configured() || programmingMode(); + if (frame.addressType() == IndividualAddress) { + return dest == individualAddress() || dest == tunnelClientAddress() || + (commissioning && dest == kKnxUnconfiguredBroadcastAddress); + } + return false; + } + default: + return false; + } +} + +} // namespace gateway::openknx diff --git a/knx b/knx index af8e8a6..d2bdeb1 160000 --- a/knx +++ b/knx @@ -1 +1 @@ -Subproject commit af8e8a6204f243b3f70e222fdc50eea68153cd30 +Subproject commit d2bdeb14b655cb659b78e798464e3e59e8da2e22