feat(gateway): add KNX Data Secure support and related configurations

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-12 12:48:18 +08:00
parent 626f86ec4e
commit e58115d303
9 changed files with 340 additions and 2 deletions
+35
View File
@@ -621,6 +621,41 @@ config GATEWAY_START_KNX_BRIDGE_ENABLED
Starts the KNXnet/IP tunneling/multicast listener at boot. Disabled by
default so UDP port 3671 is opened only after provisioning or explicit start.
config GATEWAY_KNX_DATA_SECURE_SUPPORTED
bool "Enable KNX Data Secure support"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
default n
help
Compiles the OpenKNX SecurityInterfaceObject and SecureApplicationLayer
into the ETS runtime. This is the application-layer security path used
for secure KNX group-object and ETS tool traffic.
config GATEWAY_KNX_IP_SECURE_SUPPORTED
bool "Enable KNXnet/IP Secure support"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
default n
help
Builds gateway support for KNXnet/IP Secure tunneling and routing. The
secure session transport is implemented by the gateway-owned KNX/IP
router and is separate from KNX Data Secure APDU handling.
config GATEWAY_KNX_SECURITY_DEV_ENDPOINTS
bool "Enable KNX security development HTTP endpoints"
depends on GATEWAY_KNX_DATA_SECURE_SUPPORTED || GATEWAY_KNX_IP_SECURE_SUPPORTED
default n
help
Exposes development-only HTTP actions for reading, writing, generating,
and resetting KNX security material. Disable this for production builds.
config GATEWAY_KNX_SECURITY_PLAIN_NVS
bool "Store KNX security material in plain NVS"
depends on GATEWAY_KNX_DATA_SECURE_SUPPORTED || GATEWAY_KNX_IP_SECURE_SUPPORTED
default y
help
Stores development KNX security material in normal NVS. This is useful
during bring-up, but production builds should replace it with encrypted
NVS, flash encryption, and secure boot before exposing real keys.
config GATEWAY_KNX_MAIN_GROUP
int "KNX DALI main group"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED
+4
View File
@@ -688,6 +688,10 @@ CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y
# CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set
CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED=y
CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=y
CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y
# CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED is not set
# CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS is not set
CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS=y
CONFIG_GATEWAY_KNX_MAIN_GROUP=0
CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y
CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y
+2 -1
View File
@@ -15,4 +15,5 @@ CONFIG_ETH_USE_SPI_ETHERNET=y
CONFIG_ETH_SPI_ETHERNET_W5500=y
CONFIG_GATEWAY_ETHERNET_SUPPORTED=y
CONFIG_GATEWAY_START_ETHERNET_ENABLED=y
CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE=y
CONFIG_GATEWAY_ETHERNET_IGNORE_INIT_FAILURE=y
CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED=y
@@ -16,6 +16,7 @@
#include "gateway_modbus.hpp"
#include "gateway_provisioning.hpp"
#include "openknx_idf/ets_memory_loader.h"
#include "openknx_idf/security_storage.h"
#include "cJSON.h"
#include "driver/uart.h"
@@ -121,6 +122,25 @@ GatewayBridgeHttpResponse ErrorResponse(esp_err_t err, const char* message) {
return GatewayBridgeHttpResponse{err, body};
}
cJSON* FactoryFdskInfoToCjson(const openknx::FactoryFdskInfo& fdsk_info,
bool include_secret_strings) {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return nullptr;
}
cJSON_AddBoolToObject(root, "available", fdsk_info.available);
if (fdsk_info.available) {
cJSON_AddStringToObject(root, "serialNumber", fdsk_info.serialNumber.c_str());
cJSON_AddNumberToObject(root, "labelLength", static_cast<double>(fdsk_info.label.size()));
cJSON_AddNumberToObject(root, "qrCodeLength", static_cast<double>(fdsk_info.qrCode.size()));
if (include_secret_strings) {
cJSON_AddStringToObject(root, "label", fdsk_info.label.c_str());
cJSON_AddStringToObject(root, "qrCode", fdsk_info.qrCode.c_str());
}
}
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;
@@ -2041,6 +2061,40 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddStringToObject(knx_json, "lastError",
knx_last_error.empty() ? router_error.c_str()
: knx_last_error.c_str());
cJSON* security_json = cJSON_CreateObject();
if (security_json != nullptr) {
#if defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
cJSON_AddBoolToObject(security_json, "dataSecureCompiled", true);
#else
cJSON_AddBoolToObject(security_json, "dataSecureCompiled", false);
#endif
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
cJSON_AddBoolToObject(security_json, "knxnetIpSecureCompiled", true);
#else
cJSON_AddBoolToObject(security_json, "knxnetIpSecureCompiled", false);
#endif
cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", false);
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", true);
#else
cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", false);
#endif
#if defined(CONFIG_GATEWAY_KNX_SECURITY_PLAIN_NVS)
cJSON_AddBoolToObject(security_json, "plainNvsStorage", true);
cJSON_AddStringToObject(security_json, "storage", "plain_nvs_development");
#else
cJSON_AddBoolToObject(security_json, "plainNvsStorage", false);
cJSON_AddStringToObject(security_json, "storage", "none");
#endif
#if defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
const auto fdsk_info = openknx::LoadFactoryFdskInfo();
cJSON* fdsk_json = FactoryFdskInfoToCjson(fdsk_info, false);
if (fdsk_json != nullptr) {
cJSON_AddItemToObject(security_json, "factorySetupKey", fdsk_json);
}
#endif
cJSON_AddItemToObject(knx_json, "security", security_json);
}
if (effective_knx.has_value()) {
cJSON_AddBoolToObject(knx_json, "daliRouterEnabled", effective_knx->dali_router_enabled);
cJSON_AddBoolToObject(knx_json, "ipRouterEnabled", effective_knx->ip_router_enabled);
@@ -4008,6 +4062,36 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
}
return handleGet("knx", 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)
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-factory-setup-key";
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "factory setup key read confirmation is required");
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate security response");
}
cJSON* fdsk_json = FactoryFdskInfoToCjson(openknx::LoadFactoryFdskInfo(), true);
if (fdsk_json == nullptr) {
cJSON_Delete(response);
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate factory setup key response");
}
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 == "bacnet_start") {
const esp_err_t err = runtime->startBacnet();
if (err != ESP_OK) {
+13
View File
@@ -13,6 +13,12 @@ file(GLOB OPENKNX_SRCS
"${OPENKNX_ROOT}/src/knx/*.cpp"
)
if(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
list(APPEND OPENKNX_SRCS
"${OPENKNX_ROOT}/src/knx/aes.c"
)
endif()
set(TPUART_SRCS
"${TPUART_ROOT}/src/TPUart/DataLinkLayer.cpp"
"${TPUART_ROOT}/src/TPUart/Receiver.cpp"
@@ -31,6 +37,7 @@ idf_component_register(
"src/esp_idf_platform.cpp"
"src/ets_device_runtime.cpp"
"src/ets_memory_loader.cpp"
"src/security_storage.cpp"
"src/tpuart_uart_interface.cpp"
${OPENKNX_SRCS}
${TPUART_SRCS}
@@ -42,11 +49,13 @@ idf_component_register(
esp_driver_gpio
esp_driver_uart
esp_netif
esp_system
esp_timer
esp_wifi
freertos
log
lwip
mbedtls
nvs_flash
)
@@ -58,6 +67,10 @@ target_compile_definitions(${COMPONENT_LIB} PUBLIC
USE_CEMI_SERVER
)
if(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
target_compile_definitions(${COMPONENT_LIB} PUBLIC USE_DATASECURE)
endif()
target_compile_options(${COMPONENT_LIB} PRIVATE
-Wno-unused-parameter
)
@@ -3,6 +3,7 @@
#include "openknx_idf/ets_memory_loader.h"
#include "openknx_idf/ets_device_runtime.h"
#include "openknx_idf/esp_idf_platform.h"
#include "openknx_idf/security_storage.h"
#include "openknx_idf/tpuart_uart_interface.h"
#include "knx/bau07B0.h"
@@ -0,0 +1,19 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
namespace gateway::openknx {
struct FactoryFdskInfo {
bool available{false};
std::string serialNumber;
std::string label;
std::string qrCode;
};
bool LoadFactoryFdsk(uint8_t* data, size_t len);
FactoryFdskInfo LoadFactoryFdskInfo();
} // namespace gateway::openknx
@@ -0,0 +1,181 @@
#include "openknx_idf/security_storage.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_random.h"
#include "nvs.h"
#include "nvs_flash.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
namespace {
constexpr const char* kTag = "openknx_sec";
constexpr const char* kNamespace = "knx_sec";
constexpr const char* kFactoryFdskKey = "factory_fdsk";
constexpr size_t kFdskSize = 16;
constexpr size_t kSerialSize = 6;
constexpr size_t kFdskQrSize = 36;
constexpr uint8_t kCrc4Tab[16] = {
0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9,
0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2,
};
constexpr char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
constexpr char kHexAlphabet[] = "0123456789ABCDEF";
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) {
if (nvs_flash_erase() != ESP_OK) {
return false;
}
return nvs_flash_init() == ESP_OK;
}
return err == ESP_OK || err == ESP_ERR_INVALID_STATE;
}
bool plausibleKey(const uint8_t* data) {
const bool all_zero = std::all_of(data, data + kFdskSize, [](uint8_t value) {
return value == 0x00;
});
const bool all_ff = std::all_of(data, data + kFdskSize, [](uint8_t value) {
return value == 0xff;
});
return !all_zero && !all_ff;
}
void generateKey(uint8_t* data) {
do {
esp_fill_random(data, kFdskSize);
} while (!plausibleKey(data));
}
uint8_t crc4Array(const uint8_t* data, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len; ++i) {
crc = kCrc4Tab[crc ^ (data[i] >> 4)];
crc = kCrc4Tab[crc ^ (data[i] & 0x0f)];
}
return crc;
}
std::string toBase32NoPadding(const uint8_t* data, size_t len) {
std::string result;
result.reserve(((len * 8) + 4) / 5);
uint32_t buffer = 0;
int bits_left = 0;
for (size_t i = 0; i < len; ++i) {
buffer = (buffer << 8) | data[i];
bits_left += 8;
while (bits_left >= 5) {
const uint8_t index = static_cast<uint8_t>((buffer >> (bits_left - 5)) & 0x1f);
result.push_back(kBase32Alphabet[index]);
bits_left -= 5;
}
}
if (bits_left > 0) {
const uint8_t index = static_cast<uint8_t>((buffer << (5 - bits_left)) & 0x1f);
result.push_back(kBase32Alphabet[index]);
}
return result;
}
std::string toHex(const uint8_t* data, size_t len) {
std::string result;
result.reserve(len * 2);
for (size_t i = 0; i < len; ++i) {
result.push_back(kHexAlphabet[(data[i] >> 4) & 0x0f]);
result.push_back(kHexAlphabet[data[i] & 0x0f]);
}
return result;
}
std::string generateFdskQrCode(const uint8_t* serial, const uint8_t* key) {
std::array<uint8_t, kSerialSize + kFdskSize + 1> buffer{};
std::copy(serial, serial + kSerialSize, buffer.begin());
std::copy(key, key + kFdskSize, buffer.begin() + kSerialSize);
buffer[kSerialSize + kFdskSize] = static_cast<uint8_t>((crc4Array(buffer.data(), buffer.size() - 1) << 4) & 0xff);
std::string encoded = toBase32NoPadding(buffer.data(), buffer.size());
if (encoded.size() > kFdskQrSize) {
encoded.resize(kFdskQrSize);
}
return encoded;
}
std::string formatFdskLabel(const std::string& qr_code) {
std::string label;
label.reserve(qr_code.size() + (qr_code.size() / 6));
for (size_t i = 0; i < qr_code.size(); ++i) {
if (i != 0 && (i % 6) == 0) {
label.push_back('-');
}
label.push_back(qr_code[i]);
}
return label;
}
} // namespace
namespace gateway::openknx {
bool LoadFactoryFdsk(uint8_t* data, size_t len) {
if (data == nullptr || len < kFdskSize || !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;
}
size_t stored_size = kFdskSize;
err = nvs_get_blob(handle, kFactoryFdskKey, data, &stored_size);
if (err == ESP_OK && stored_size == kFdskSize && plausibleKey(data)) {
nvs_close(handle);
return true;
}
generateKey(data);
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 generated KNX factory FDSK: %s", esp_err_to_name(err));
return false;
}
return true;
}
FactoryFdskInfo LoadFactoryFdskInfo() {
FactoryFdskInfo info;
std::array<uint8_t, kFdskSize> key{};
std::array<uint8_t, kSerialSize> serial{};
if (!LoadFactoryFdsk(key.data(), key.size()) ||
esp_read_mac(serial.data(), ESP_MAC_WIFI_STA) != ESP_OK) {
return info;
}
info.available = true;
info.serialNumber = toHex(serial.data(), serial.size());
info.qrCode = generateFdskQrCode(serial.data(), key.data());
info.label = formatFdskLabel(info.qrCode);
return info;
}
} // namespace gateway::openknx
extern "C" bool knx_platform_get_fdsk(uint8_t* data, size_t len) {
return gateway::openknx::LoadFactoryFdsk(data, len);
}
+1 -1
Submodule knx updated: 339d8472e7...5da3b1f30e