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:
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user