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 <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-25 08:18:01 +08:00
parent 0467179f70
commit 2b779d5532
22 changed files with 2665 additions and 77 deletions
@@ -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<double>(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<GatewayBridgeService&>(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<std::string> 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") {
@@ -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<uint8_t>(
(gateway::knx_internal::kReg1DaliManufacturerId >> 8) & 0xff);
serial[1] = static_cast<uint8_t>(
gateway::knx_internal::kReg1DaliManufacturerId & 0xff);
std::copy(mac.begin() + 2, mac.end(), serial + 2);
uint32_t suffix = (static_cast<uint32_t>(mac[2]) << 24) |
(static_cast<uint32_t>(mac[3]) << 16) |
(static_cast<uint32_t>(mac[4]) << 8) |
static_cast<uint32_t>(mac[5]);
suffix += context.serialMacIncrement;
serial[0] = static_cast<uint8_t>((context.manufacturerId >> 8) & 0xff);
serial[1] = static_cast<uint8_t>(context.manufacturerId & 0xff);
serial[2] = static_cast<uint8_t>((suffix >> 24) & 0xff);
serial[3] = static_cast<uint8_t>((suffix >> 16) & 0xff);
serial[4] = static_cast<uint8_t>((suffix >> 8) & 0xff);
serial[5] = static_cast<uint8_t>(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<uint8_t, sizeof(kFdskDerivationLabel) - 1 + kSerialSize> 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<uint8_t> 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<uint8_t, 32> 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<uint8_t, kSerialSize> serial{};
std::array<uint8_t, kFdskSize> 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<uint8_t, kFdskSize> key{};
std::array<uint8_t, kSerialSize> 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<uint8_t, kFdskSize> 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<uint8_t, kFdskSize> key{};
if (!parseHexKey(hex_key, key.data())) {
return false;
}
std::array<uint8_t, kSerialSize> serial{};
std::array<uint8_t, kFdskSize> 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<char, 18> buffer{};
std::snprintf(buffer.data(), buffer.size(), "ipsec_tunnel_%02u",
static_cast<unsigned>(index));
return buffer.data();
}
bool BlobKeyAvailable(nvs_handle_t handle, const char* key) {
std::array<uint8_t, kFdskSize> 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<uint8_t, kFdskSize> 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<uint8_t, kFdskSize>* out) {
if (out == nullptr) {
return false;
}
std::array<uint8_t, kFdskSize> 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<uint8_t>(tunnel_count, 16);
material.tunnel_user_keys.reserve(tunnel_count);
for (uint8_t index = 0; index < tunnel_count; ++index) {
std::array<uint8_t, kFdskSize> 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<std::string>& 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<uint8_t>(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<uint8_t>(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) {