feat(gateway): implement KNX security features including secure session handling and factory certificate management
Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
@@ -12,7 +12,7 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
|
||||
- `dali_domain/`: native DALI domain facade over `dali_cpp` and raw frame sinks.
|
||||
- `gateway_cache/`: DALI scene/group/settings/runtime cache used by controller reconciliation and protocol bridges.
|
||||
- `gateway_bridge/`: per-channel bridge provisioning, command execution, protocol startup, and HTTP bridge actions.
|
||||
- `openknx_idf/`: ESP-IDF port layer for the OpenKNX `gateway/knx` and `gateway/tpuart` submodules, including NVS-backed OpenKNX memory, ETS cEMI programming support, UDP multicast/unicast plumbing, and a native TP-UART interface without the Arduino framework.
|
||||
- `openknx_idf/`: ESP-IDF port layer for the OpenKNX `gateway/knx` and `gateway/tpuart` submodules, including NVS-backed OpenKNX memory, development KNX security storage, ETS cEMI programming support, UDP multicast/unicast plumbing, and a native TP-UART interface without the Arduino framework.
|
||||
- `gateway_modbus/`: gateway-owned Modbus TCP/RTU/ASCII config, generated DALI point tables, and provisioned Modbus model override dispatch.
|
||||
- `gateway_bacnet/`: BACnet/IP server adapter backed by bacnet-stack, including the gateway-owned BACnet bridge model adapter.
|
||||
- `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`, including raw DALI notifications.
|
||||
@@ -26,6 +26,14 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
|
||||
|
||||
The native rewrite now wires a shared `gateway_core` bootstrap component, a multi-channel `dali_domain` wrapper over `dali_cpp`, a local vendored `dali` hardware backend from the LuatOS ESP-IDF port with raw receive fan-out, an initial `gateway_runtime` service that provides persistent settings, device info, Lua-compatible command framing helpers, and Lua-style query command deduplication, plus a `gateway_controller` service that starts the gateway command task, dispatches core Lua gateway opcodes, and owns internal scene/group state. The gateway app also includes a `gateway_ble` NimBLE bridge that advertises a Lua-compatible GATT service and forwards `FFF3` framed notifications, incoming `FFF1`/`FFF2`/`FFF3` writes, and native raw DALI frame notifications into the matching raw channel, a `gateway_network` service that provides the native HTTP `/info`, `GET`/`POST /dali/cmd`, `/led/1`, `/led/0`, `/jq.js`, UDP control-plane router on port `2020`, W5500 SPI Ethernet with DHCP, Wi-Fi STA lifecycle, ESP-Touch smartconfig credential provisioning, the Lua-style `LAMMIN_Gateway` setup AP on `192.168.3.1`, ESP-NOW setup ingress for Lua-compatible `connReq`/`connAck`/`echo`/`cmd`/`data`/`uart` packets, native raw DALI frame forwarding back to connected setup peers, and BOOT-button Wi-Fi credential clearing, and an optional `gateway_485_control` bridge that claims UART0 for Lua-compatible framed command ingress plus `0x22` notification egress when the console is moved off UART0. Startup behavior is configured in `main/Kconfig.projbuild`: BLE and wired Ethernet are enabled by default, W5500 initialization and startup probe failures are ignored by default for boards without populated Ethernet hardware by fully disabling Ethernet for that boot, Wi-Fi STA, smartconfig, and ESP-NOW setup mode are disabled by default, the built-in USB Serial/JTAG interface stays in debug mode unless the optional USB setup bridge mode is selected, and the UART0 control bridge stays disabled unless the deployment explicitly repurposes UART0 away from the ESP-IDF console. Runtime settings and internal scene/group data are cached in RAM after load, skip unchanged flash writes, and batch Wi-Fi credential commits to reduce flash stalls on ESP32-S3 boards where flash and PSRAM share the SPI bus. The gateway app exposes per-channel PHY selection through `main/Kconfig.projbuild`; each channel can be disabled, bound to the native DALI GPIO HAL, or bound to a UART1/UART2 serial PHY. The checked-in `sdkconfig` is aligned with the app's custom 16 MB partition table so the Wi-Fi/BLE/network-enabled image fits the OTA app slots.
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
## Modbus
|
||||
|
||||
Modbus TCP, RTU, and ASCII are owned by `gateway/components/gateway_modbus` and started through the per-channel bridge service. The gateway keeps the existing bridge config JSON shape with a top-level `modbus` object containing `transport`, `host`, `port`, and `unitID`, and now adds nested serial UART settings for RTU/ASCII. Parsing and runtime behavior live in the gateway project rather than in `dali_cpp`.
|
||||
|
||||
@@ -82,6 +82,11 @@ struct DaliKnxStatusUpdate {
|
||||
|
||||
using BridgeDiscoveryInventory = std::map<int, BridgeDiscoveryEntry>;
|
||||
|
||||
extern "C" uint8_t knx_platform_copy_security_failures(uint8_t* counters, size_t countersLen,
|
||||
uint8_t* records, size_t recordSize,
|
||||
size_t maxRecords) __attribute__((weak));
|
||||
extern "C" void knx_platform_clear_security_failures() __attribute__((weak));
|
||||
|
||||
class LockGuard {
|
||||
public:
|
||||
explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) {
|
||||
@@ -150,11 +155,90 @@ cJSON* FactoryFdskInfoToCjson(const openknx::FactoryFdskInfo& fdsk_info,
|
||||
return root;
|
||||
}
|
||||
|
||||
cJSON* FactoryCertificateToCjson(const openknx::FactoryCertificatePayload& certificate,
|
||||
bool include_secret_strings) {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
if (root == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
cJSON_AddBoolToObject(root, "available", certificate.available);
|
||||
if (!certificate.available) {
|
||||
return root;
|
||||
}
|
||||
cJSON_AddStringToObject(root, "productIdentity", certificate.productIdentity.c_str());
|
||||
cJSON_AddStringToObject(root, "manufacturerId", certificate.manufacturerId.c_str());
|
||||
cJSON_AddStringToObject(root, "applicationNumber", certificate.applicationNumber.c_str());
|
||||
cJSON_AddStringToObject(root, "applicationVersion", certificate.applicationVersion.c_str());
|
||||
cJSON_AddStringToObject(root, "serialNumber", certificate.serialNumber.c_str());
|
||||
cJSON_AddStringToObject(root, "storage", certificate.storage.c_str());
|
||||
cJSON_AddStringToObject(root, "createdAt", certificate.createdAt.c_str());
|
||||
cJSON_AddStringToObject(root, "checksum", certificate.checksum.c_str());
|
||||
cJSON_AddNumberToObject(root, "payloadLength",
|
||||
static_cast<double>(certificate.fdskLabel.size() +
|
||||
certificate.fdskQrCode.size()));
|
||||
if (include_secret_strings) {
|
||||
cJSON_AddStringToObject(root, "fdskLabel", certificate.fdskLabel.c_str());
|
||||
cJSON_AddStringToObject(root, "fdskQrCode", certificate.fdskQrCode.c_str());
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
cJSON* SecurityFailuresToCjson() {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
if (root == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
cJSON* counters_json = cJSON_CreateArray();
|
||||
cJSON* log_json = cJSON_CreateArray();
|
||||
std::array<uint8_t, 8> counters{};
|
||||
std::array<uint8_t, 8 * 8> records{};
|
||||
uint8_t count = 0;
|
||||
#if defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
|
||||
if (knx_platform_copy_security_failures != nullptr) {
|
||||
count = knx_platform_copy_security_failures(counters.data(), counters.size(), records.data(), 8, 8);
|
||||
}
|
||||
#endif
|
||||
if (counters_json != nullptr) {
|
||||
for (uint8_t value : counters) {
|
||||
cJSON_AddItemToArray(counters_json, cJSON_CreateNumber(static_cast<double>(value)));
|
||||
}
|
||||
cJSON_AddItemToObject(root, "counters", counters_json);
|
||||
}
|
||||
if (log_json != nullptr) {
|
||||
for (uint8_t index = 0; index < count && index < 8; ++index) {
|
||||
const size_t offset = static_cast<size_t>(index) * 8U;
|
||||
cJSON* entry = cJSON_CreateObject();
|
||||
if (entry == nullptr) {
|
||||
continue;
|
||||
}
|
||||
cJSON_AddNumberToObject(entry, "class", records[offset]);
|
||||
cJSON_AddNumberToObject(entry, "detail", records[offset + 1]);
|
||||
cJSON_AddNumberToObject(entry, "source",
|
||||
static_cast<double>((records[offset + 2] << 8) | records[offset + 3]));
|
||||
cJSON_AddNumberToObject(entry, "destination",
|
||||
static_cast<double>((records[offset + 4] << 8) | records[offset + 5]));
|
||||
cJSON_AddNumberToObject(entry, "count", records[offset + 6]);
|
||||
cJSON_AddNumberToObject(entry, "loadState", records[offset + 7]);
|
||||
cJSON_AddItemToArray(log_json, entry);
|
||||
}
|
||||
cJSON_AddItemToObject(root, "log", log_json);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
const char* JsonString(const cJSON* parent, const char* name) {
|
||||
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
|
||||
return cJSON_IsString(item) && item->valuestring != nullptr ? item->valuestring : nullptr;
|
||||
}
|
||||
|
||||
[[maybe_unused]] bool JsonBool(const cJSON* parent, const char* name, bool fallback = false) {
|
||||
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
|
||||
if (cJSON_IsBool(item)) {
|
||||
return cJSON_IsTrue(item);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
std::optional<int> JsonInt(const cJSON* parent, const char* name) {
|
||||
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
|
||||
if (!cJSON_IsNumber(item)) {
|
||||
@@ -2127,7 +2211,12 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
#else
|
||||
cJSON_AddBoolToObject(security_json, "knxnetIpSecureCompiled", false);
|
||||
#endif
|
||||
cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", false);
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
cJSON_AddBoolToObject(security_json, "knxnetIpSecureServicesRecognized", true);
|
||||
#else
|
||||
cJSON_AddBoolToObject(security_json, "knxnetIpSecureServicesRecognized", false);
|
||||
#endif
|
||||
cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", false);
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
|
||||
cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", true);
|
||||
#else
|
||||
@@ -2146,6 +2235,15 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
if (fdsk_json != nullptr) {
|
||||
cJSON_AddItemToObject(security_json, "factorySetupKey", fdsk_json);
|
||||
}
|
||||
cJSON* certificate_json = FactoryCertificateToCjson(
|
||||
openknx::BuildFactoryCertificatePayload(), false);
|
||||
if (certificate_json != nullptr) {
|
||||
cJSON_AddItemToObject(security_json, "factoryCertificate", certificate_json);
|
||||
}
|
||||
cJSON* failures_json = SecurityFailuresToCjson();
|
||||
if (failures_json != nullptr) {
|
||||
cJSON_AddItemToObject(security_json, "failures", failures_json);
|
||||
}
|
||||
#endif
|
||||
cJSON_AddItemToObject(knx_json, "security", security_json);
|
||||
}
|
||||
@@ -4163,6 +4261,170 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
|
||||
#else
|
||||
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
|
||||
"KNX security development endpoints are disabled");
|
||||
#endif
|
||||
}
|
||||
if (action == "knx_security_generate_factory_key") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
|
||||
defined(CONFIG_GATEWAY_KNX_DATA_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 include_secret = JsonBool(body_root, "includeSecret", false);
|
||||
const bool confirmed = confirm != nullptr &&
|
||||
std::string_view(confirm) == "generate-factory-setup-key";
|
||||
cJSON_Delete(body_root);
|
||||
if (!confirmed) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "factory setup key generation confirmation is required");
|
||||
}
|
||||
openknx::FactoryFdskInfo info;
|
||||
if (!openknx::GenerateFactoryFdsk(&info)) {
|
||||
return ErrorResponse(ESP_FAIL, "failed to generate factory setup key");
|
||||
}
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
if (response == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate 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_security_write_factory_key") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
|
||||
defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
|
||||
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
|
||||
if (body_root == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "factory setup key JSON is required");
|
||||
}
|
||||
const char* confirm = JsonString(body_root, "confirm");
|
||||
const char* key_hex = JsonString(body_root, "keyHex");
|
||||
const bool include_secret = JsonBool(body_root, "includeSecret", false);
|
||||
const bool confirmed = confirm != nullptr &&
|
||||
std::string_view(confirm) == "write-factory-setup-key";
|
||||
if (!confirmed || key_hex == nullptr) {
|
||||
cJSON_Delete(body_root);
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG,
|
||||
"factory setup key write confirmation and keyHex are required");
|
||||
}
|
||||
const std::string key_value(key_hex);
|
||||
cJSON_Delete(body_root);
|
||||
openknx::FactoryFdskInfo info;
|
||||
if (!openknx::WriteFactoryFdskHex(key_value, &info)) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid factory setup key hex value");
|
||||
}
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
if (response == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate 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_security_reset_factory_key") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
|
||||
defined(CONFIG_GATEWAY_KNX_DATA_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) == "reset-factory-setup-key";
|
||||
cJSON_Delete(body_root);
|
||||
if (!confirmed) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "factory setup key reset confirmation is required");
|
||||
}
|
||||
openknx::FactoryFdskInfo info;
|
||||
if (!openknx::ResetFactoryFdskCache(&info)) {
|
||||
return ErrorResponse(ESP_FAIL, "failed to reload factory setup key");
|
||||
}
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
if (response == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate 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_security_export_factory_certificate") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
|
||||
defined(CONFIG_GATEWAY_KNX_DATA_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) == "export-factory-certificate";
|
||||
cJSON_Delete(body_root);
|
||||
if (!confirmed) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "factory certificate export confirmation is required");
|
||||
}
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
if (response == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate security response");
|
||||
}
|
||||
cJSON* certificate_json = FactoryCertificateToCjson(
|
||||
openknx::BuildFactoryCertificatePayload(), true);
|
||||
if (certificate_json == nullptr) {
|
||||
cJSON_Delete(response);
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate factory certificate response");
|
||||
}
|
||||
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_security_clear_failures") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
|
||||
defined(CONFIG_GATEWAY_KNX_DATA_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-security-failures";
|
||||
cJSON_Delete(body_root);
|
||||
if (!confirmed) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "security failure clear confirmation is required");
|
||||
}
|
||||
if (knx_platform_clear_security_failures != nullptr) {
|
||||
knx_platform_clear_security_failures();
|
||||
}
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
if (response == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate security response");
|
||||
}
|
||||
cJSON* failures_json = SecurityFailuresToCjson();
|
||||
if (failures_json != nullptr) {
|
||||
cJSON_AddItemToObject(response, "failures", failures_json);
|
||||
}
|
||||
return JsonOk(response);
|
||||
#else
|
||||
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
|
||||
"KNX security development endpoints are disabled");
|
||||
#endif
|
||||
}
|
||||
if (action == "bacnet_start") {
|
||||
|
||||
@@ -216,8 +216,11 @@ class GatewayKnxTpIpRouter {
|
||||
void handleConnectionStateRequest(const uint8_t* body, size_t len,
|
||||
const ::sockaddr_in& remote);
|
||||
void handleDisconnectRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote);
|
||||
void handleSecureService(uint16_t service, 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 sendSecureSessionStatus(uint8_t status, const ::sockaddr_in& remote);
|
||||
void sendTunnelIndication(const uint8_t* data, size_t len);
|
||||
void sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
|
||||
const ::sockaddr_in& remote);
|
||||
|
||||
@@ -35,6 +35,12 @@ constexpr uint16_t kServiceDisconnectResponse = 0x020a;
|
||||
constexpr uint16_t kServiceTunnellingRequest = 0x0420;
|
||||
constexpr uint16_t kServiceTunnellingAck = 0x0421;
|
||||
constexpr uint16_t kServiceRoutingIndication = 0x0530;
|
||||
constexpr uint16_t kServiceSecureWrapper = 0x0950;
|
||||
constexpr uint16_t kServiceSecureSessionRequest = 0x0951;
|
||||
constexpr uint16_t kServiceSecureSessionResponse = 0x0952;
|
||||
constexpr uint16_t kServiceSecureSessionAuth = 0x0953;
|
||||
constexpr uint16_t kServiceSecureSessionStatus = 0x0954;
|
||||
constexpr uint16_t kServiceSecureGroupSync = 0x0955;
|
||||
constexpr uint8_t kKnxNetIpHeaderSize = 0x06;
|
||||
constexpr uint8_t kKnxNetIpVersion10 = 0x10;
|
||||
constexpr uint8_t kKnxNoError = 0x00;
|
||||
@@ -42,6 +48,8 @@ constexpr uint8_t kKnxErrorConnectionId = 0x21;
|
||||
constexpr uint8_t kKnxErrorConnectionType = 0x22;
|
||||
constexpr uint8_t kKnxErrorNoMoreConnections = 0x24;
|
||||
constexpr uint8_t kKnxErrorSequenceNumber = 0x04;
|
||||
constexpr uint8_t kKnxSecureStatusAuthFailed = 0x01;
|
||||
constexpr uint8_t kKnxSecureStatusUnauthenticated = 0x02;
|
||||
constexpr uint8_t kKnxConnectionTypeTunnel = 0x04;
|
||||
constexpr uint8_t kKnxTunnelLayerLink = 0x02;
|
||||
constexpr uint8_t kTpUartResetRequest = 0x01;
|
||||
@@ -507,6 +515,20 @@ bool ParseKnxNetIpHeader(const uint8_t* data, size_t len, uint16_t* service,
|
||||
return *total_len >= 6 && *total_len <= len;
|
||||
}
|
||||
|
||||
bool IsKnxNetIpSecureService(uint16_t service) {
|
||||
switch (service) {
|
||||
case kServiceSecureWrapper:
|
||||
case kServiceSecureSessionRequest:
|
||||
case kServiceSecureSessionResponse:
|
||||
case kServiceSecureSessionAuth:
|
||||
case kServiceSecureSessionStatus:
|
||||
case kServiceSecureGroupSync:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsExtendedTpFrame(const uint8_t* data, size_t len) {
|
||||
return len > 0 && (data[0] & 0xD3) == 0x10;
|
||||
}
|
||||
@@ -1875,6 +1897,10 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len,
|
||||
}
|
||||
const uint8_t* body = data + 6;
|
||||
const size_t body_len = total_len - 6;
|
||||
if (IsKnxNetIpSecureService(service)) {
|
||||
handleSecureService(service, body, body_len, remote);
|
||||
return;
|
||||
}
|
||||
switch (service) {
|
||||
case kServiceRoutingIndication:
|
||||
if (config_.multicast_enabled) {
|
||||
@@ -2001,6 +2027,34 @@ void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* body, size_t l
|
||||
sendDisconnectResponse(channel_id, status, remote);
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::handleSecureService(uint16_t service, const uint8_t* body,
|
||||
size_t len, const sockaddr_in& remote) {
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
switch (service) {
|
||||
case kServiceSecureSessionRequest:
|
||||
case kServiceSecureSessionAuth:
|
||||
ESP_LOGW(kTag, "KNXnet/IP Secure service 0x%04x rejected: secure sessions are not provisioned", service);
|
||||
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
|
||||
break;
|
||||
case kServiceSecureWrapper:
|
||||
ESP_LOGW(kTag, "KNXnet/IP Secure wrapper rejected: no authenticated secure session");
|
||||
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
|
||||
break;
|
||||
case kServiceSecureGroupSync:
|
||||
ESP_LOGD(kTag, "KNXnet/IP Secure group sync ignored until secure routing is provisioned");
|
||||
break;
|
||||
default:
|
||||
ESP_LOGD(kTag, "KNXnet/IP Secure service 0x%04x ignored", service);
|
||||
break;
|
||||
}
|
||||
#else
|
||||
(void)service;
|
||||
(void)body;
|
||||
(void)len;
|
||||
(void)remote;
|
||||
#endif
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence,
|
||||
uint8_t status, const sockaddr_in& remote) {
|
||||
const std::vector<uint8_t> body{0x04, channel_id, sequence, status};
|
||||
@@ -2008,6 +2062,12 @@ void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequenc
|
||||
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockaddr_in& remote) {
|
||||
const std::vector<uint8_t> body{status, 0x00};
|
||||
const auto packet = KnxNetIpPacket(kServiceSecureSessionStatus, body);
|
||||
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) {
|
||||
if (!tunnel_connected_ || udp_sock_ < 0 || data == nullptr || len == 0) {
|
||||
return;
|
||||
|
||||
@@ -13,7 +13,25 @@ struct FactoryFdskInfo {
|
||||
std::string qrCode;
|
||||
};
|
||||
|
||||
struct FactoryCertificatePayload {
|
||||
bool available{false};
|
||||
std::string productIdentity;
|
||||
std::string manufacturerId;
|
||||
std::string applicationNumber;
|
||||
std::string applicationVersion;
|
||||
std::string serialNumber;
|
||||
std::string fdskLabel;
|
||||
std::string fdskQrCode;
|
||||
std::string storage;
|
||||
std::string createdAt;
|
||||
std::string checksum;
|
||||
};
|
||||
|
||||
bool LoadFactoryFdsk(uint8_t* data, size_t len);
|
||||
FactoryFdskInfo LoadFactoryFdskInfo();
|
||||
bool GenerateFactoryFdsk(FactoryFdskInfo* info = nullptr);
|
||||
bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info = nullptr);
|
||||
bool ResetFactoryFdskCache(FactoryFdskInfo* info = nullptr);
|
||||
FactoryCertificatePayload BuildFactoryCertificatePayload();
|
||||
|
||||
} // namespace gateway::openknx
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esp_log.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_timer.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
@@ -10,6 +11,7 @@
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
@@ -20,6 +22,11 @@ constexpr const char* kFactoryFdskKey = "factory_fdsk";
|
||||
constexpr size_t kFdskSize = 16;
|
||||
constexpr size_t kSerialSize = 6;
|
||||
constexpr size_t kFdskQrSize = 36;
|
||||
constexpr const char* kProductIdentity = "REG1-Dali";
|
||||
constexpr const char* kManufacturerId = "00A4";
|
||||
constexpr const char* kApplicationNumber = "01";
|
||||
constexpr const char* kApplicationVersion = "05";
|
||||
constexpr const char* kDevelopmentStorage = "plain_nvs_development";
|
||||
constexpr uint8_t kCrc4Tab[16] = {
|
||||
0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9,
|
||||
0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2,
|
||||
@@ -27,6 +34,8 @@ constexpr uint8_t kCrc4Tab[16] = {
|
||||
constexpr char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
constexpr char kHexAlphabet[] = "0123456789ABCDEF";
|
||||
|
||||
extern "C" void knx_platform_clear_cached_fdsk() __attribute__((weak));
|
||||
|
||||
bool ensureNvsReady() {
|
||||
const esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
@@ -54,6 +63,75 @@ void generateKey(uint8_t* data) {
|
||||
} while (!plausibleKey(data));
|
||||
}
|
||||
|
||||
void clearOpenKnxFdskCache() {
|
||||
if (knx_platform_clear_cached_fdsk != nullptr) {
|
||||
knx_platform_clear_cached_fdsk();
|
||||
}
|
||||
}
|
||||
|
||||
int fromHexDigit(char value) {
|
||||
if (value >= '0' && value <= '9') {
|
||||
return value - '0';
|
||||
}
|
||||
if (value >= 'a' && value <= 'f') {
|
||||
return value - 'a' + 10;
|
||||
}
|
||||
if (value >= 'A' && value <= 'F') {
|
||||
return value - 'A' + 10;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool parseHexKey(const std::string& value, uint8_t* out) {
|
||||
std::string digits;
|
||||
digits.reserve(value.size());
|
||||
for (char ch : value) {
|
||||
if (ch == ':' || ch == '-' || ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') {
|
||||
continue;
|
||||
}
|
||||
if (fromHexDigit(ch) < 0) {
|
||||
return false;
|
||||
}
|
||||
digits.push_back(ch);
|
||||
}
|
||||
if (digits.size() != kFdskSize * 2U) {
|
||||
return false;
|
||||
}
|
||||
for (size_t index = 0; index < kFdskSize; ++index) {
|
||||
const int hi = fromHexDigit(digits[index * 2U]);
|
||||
const int lo = fromHexDigit(digits[index * 2U + 1U]);
|
||||
if (hi < 0 || lo < 0) {
|
||||
return false;
|
||||
}
|
||||
out[index] = static_cast<uint8_t>((hi << 4) | lo);
|
||||
}
|
||||
return plausibleKey(out);
|
||||
}
|
||||
|
||||
bool storeFactoryFdsk(const uint8_t* data) {
|
||||
if (data == nullptr || !plausibleKey(data) || !ensureNvsReady()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nvs_handle_t handle = 0;
|
||||
esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
err = nvs_set_blob(handle, kFactoryFdskKey, data, kFdskSize);
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_commit(handle);
|
||||
}
|
||||
nvs_close(handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "failed to store KNX factory FDSK: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
clearOpenKnxFdskCache();
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t crc4Array(const uint8_t* data, size_t len) {
|
||||
uint8_t crc = 0;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
@@ -121,6 +199,21 @@ std::string formatFdskLabel(const std::string& qr_code) {
|
||||
return label;
|
||||
}
|
||||
|
||||
std::string fnv1aHex(const std::string& value) {
|
||||
uint32_t hash = 2166136261u;
|
||||
for (unsigned char ch : value) {
|
||||
hash ^= ch;
|
||||
hash *= 16777619u;
|
||||
}
|
||||
std::array<uint8_t, 4> bytes{
|
||||
static_cast<uint8_t>((hash >> 24) & 0xff),
|
||||
static_cast<uint8_t>((hash >> 16) & 0xff),
|
||||
static_cast<uint8_t>((hash >> 8) & 0xff),
|
||||
static_cast<uint8_t>(hash & 0xff),
|
||||
};
|
||||
return toHex(bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace gateway::openknx {
|
||||
@@ -174,6 +267,68 @@ FactoryFdskInfo LoadFactoryFdskInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
bool GenerateFactoryFdsk(FactoryFdskInfo* info) {
|
||||
std::array<uint8_t, kFdskSize> key{};
|
||||
generateKey(key.data());
|
||||
const bool stored = storeFactoryFdsk(key.data());
|
||||
std::fill(key.begin(), key.end(), 0);
|
||||
if (!stored) {
|
||||
return false;
|
||||
}
|
||||
if (info != nullptr) {
|
||||
*info = LoadFactoryFdskInfo();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
|
||||
std::array<uint8_t, kFdskSize> key{};
|
||||
if (!parseHexKey(hex_key, key.data())) {
|
||||
return false;
|
||||
}
|
||||
const bool stored = storeFactoryFdsk(key.data());
|
||||
std::fill(key.begin(), key.end(), 0);
|
||||
if (!stored) {
|
||||
return false;
|
||||
}
|
||||
if (info != nullptr) {
|
||||
*info = LoadFactoryFdskInfo();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResetFactoryFdskCache(FactoryFdskInfo* info) {
|
||||
clearOpenKnxFdskCache();
|
||||
const auto loaded = LoadFactoryFdskInfo();
|
||||
if (info != nullptr) {
|
||||
*info = loaded;
|
||||
}
|
||||
return loaded.available;
|
||||
}
|
||||
|
||||
FactoryCertificatePayload BuildFactoryCertificatePayload() {
|
||||
FactoryCertificatePayload payload;
|
||||
const auto info = LoadFactoryFdskInfo();
|
||||
if (!info.available) {
|
||||
return payload;
|
||||
}
|
||||
payload.available = true;
|
||||
payload.productIdentity = kProductIdentity;
|
||||
payload.manufacturerId = kManufacturerId;
|
||||
payload.applicationNumber = kApplicationNumber;
|
||||
payload.applicationVersion = kApplicationVersion;
|
||||
payload.serialNumber = info.serialNumber;
|
||||
payload.fdskLabel = info.label;
|
||||
payload.fdskQrCode = info.qrCode;
|
||||
payload.storage = kDevelopmentStorage;
|
||||
payload.createdAt = "uptime_us:" + std::to_string(esp_timer_get_time());
|
||||
payload.checksum = fnv1aHex(payload.productIdentity + "|" + payload.manufacturerId + "|" +
|
||||
payload.applicationNumber + "|" + payload.applicationVersion + "|" +
|
||||
payload.serialNumber + "|" + payload.fdskLabel + "|" +
|
||||
payload.fdskQrCode + "|" + payload.createdAt);
|
||||
return payload;
|
||||
}
|
||||
|
||||
} // namespace gateway::openknx
|
||||
|
||||
extern "C" bool knx_platform_get_fdsk(uint8_t* data, size_t len) {
|
||||
|
||||
+1
-1
Submodule knx updated: 1549366447...5d7d6e573b
Reference in New Issue
Block a user