feat: 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 10:26:58 +08:00
parent 626f86ec4e
commit f3fdd5c4e9
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 Starts the KNXnet/IP tunneling/multicast listener at boot. Disabled by
default so UDP port 3671 is opened only after provisioning or explicit start. 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 config GATEWAY_KNX_MAIN_GROUP
int "KNX DALI main group" int "KNX DALI main group"
depends on GATEWAY_KNX_BRIDGE_SUPPORTED 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_START_BACNET_BRIDGE_ENABLED is not set
CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED=y CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED=y
CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=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_MAIN_GROUP=0
CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y
CONFIG_GATEWAY_KNX_MULTICAST_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_ETH_SPI_ETHERNET_W5500=y
CONFIG_GATEWAY_ETHERNET_SUPPORTED=y CONFIG_GATEWAY_ETHERNET_SUPPORTED=y
CONFIG_GATEWAY_START_ETHERNET_ENABLED=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_modbus.hpp"
#include "gateway_provisioning.hpp" #include "gateway_provisioning.hpp"
#include "openknx_idf/ets_memory_loader.h" #include "openknx_idf/ets_memory_loader.h"
#include "openknx_idf/security_storage.h"
#include "cJSON.h" #include "cJSON.h"
#include "driver/uart.h" #include "driver/uart.h"
@@ -121,6 +122,25 @@ GatewayBridgeHttpResponse ErrorResponse(esp_err_t err, const char* message) {
return GatewayBridgeHttpResponse{err, body}; 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 char* JsonString(const cJSON* parent, const char* name) {
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name); const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
return cJSON_IsString(item) && item->valuestring != nullptr ? item->valuestring : nullptr; return cJSON_IsString(item) && item->valuestring != nullptr ? item->valuestring : nullptr;
@@ -2041,6 +2061,40 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddStringToObject(knx_json, "lastError", cJSON_AddStringToObject(knx_json, "lastError",
knx_last_error.empty() ? router_error.c_str() knx_last_error.empty() ? router_error.c_str()
: knx_last_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()) { if (effective_knx.has_value()) {
cJSON_AddBoolToObject(knx_json, "daliRouterEnabled", effective_knx->dali_router_enabled); cJSON_AddBoolToObject(knx_json, "daliRouterEnabled", effective_knx->dali_router_enabled);
cJSON_AddBoolToObject(knx_json, "ipRouterEnabled", effective_knx->ip_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()); 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") { if (action == "bacnet_start") {
const esp_err_t err = runtime->startBacnet(); const esp_err_t err = runtime->startBacnet();
if (err != ESP_OK) { if (err != ESP_OK) {
+13
View File
@@ -13,6 +13,12 @@ file(GLOB OPENKNX_SRCS
"${OPENKNX_ROOT}/src/knx/*.cpp" "${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 set(TPUART_SRCS
"${TPUART_ROOT}/src/TPUart/DataLinkLayer.cpp" "${TPUART_ROOT}/src/TPUart/DataLinkLayer.cpp"
"${TPUART_ROOT}/src/TPUart/Receiver.cpp" "${TPUART_ROOT}/src/TPUart/Receiver.cpp"
@@ -31,6 +37,7 @@ idf_component_register(
"src/esp_idf_platform.cpp" "src/esp_idf_platform.cpp"
"src/ets_device_runtime.cpp" "src/ets_device_runtime.cpp"
"src/ets_memory_loader.cpp" "src/ets_memory_loader.cpp"
"src/security_storage.cpp"
"src/tpuart_uart_interface.cpp" "src/tpuart_uart_interface.cpp"
${OPENKNX_SRCS} ${OPENKNX_SRCS}
${TPUART_SRCS} ${TPUART_SRCS}
@@ -42,11 +49,13 @@ idf_component_register(
esp_driver_gpio esp_driver_gpio
esp_driver_uart esp_driver_uart
esp_netif esp_netif
esp_system
esp_timer esp_timer
esp_wifi esp_wifi
freertos freertos
log log
lwip lwip
mbedtls
nvs_flash nvs_flash
) )
@@ -58,6 +67,10 @@ target_compile_definitions(${COMPONENT_LIB} PUBLIC
USE_CEMI_SERVER 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 target_compile_options(${COMPONENT_LIB} PRIVATE
-Wno-unused-parameter -Wno-unused-parameter
) )
@@ -3,6 +3,7 @@
#include "openknx_idf/ets_memory_loader.h" #include "openknx_idf/ets_memory_loader.h"
#include "openknx_idf/ets_device_runtime.h" #include "openknx_idf/ets_device_runtime.h"
#include "openknx_idf/esp_idf_platform.h" #include "openknx_idf/esp_idf_platform.h"
#include "openknx_idf/security_storage.h"
#include "openknx_idf/tpuart_uart_interface.h" #include "openknx_idf/tpuart_uart_interface.h"
#include "knx/bau07B0.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