From ca9e95e9856af4cb5fb7863d74b52451f4bbdc9a Mon Sep 17 00:00:00 2001 From: Tony Date: Sat, 8 Aug 2026 09:23:06 +0800 Subject: [PATCH] feat(gateway): Add Matter factory data handling and restart functionality Signed-off-by: Tony --- README.md | 11 +- apps/gateway/main/app_main.cpp | 11 + .../gateway_bridge/src/gateway_bridge.cpp | 3 +- components/gateway_matter/CMakeLists.txt | 2 +- .../gateway_matter/include/gateway_matter.hpp | 2 + .../gateway_matter/src/gateway_matter.cpp | 312 +++++++++++++++++- 6 files changed, 336 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 42674a4..de0245e 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,16 @@ ESP-Matter factory providers; controllers must treat this response as sensitive and DaliMaster keeps it only in page memory. `matter_config` applies a selected channel patch, while `matter_config_reset` restores automatic selection. `matter_open_commissioning`, `matter_close_commissioning`, `matter_rescan`, -`matter_config`, and `matter_config_reset` are POST actions. They are available through the active +`matter_config`, and `matter_config_reset` are POST actions. The development-only +`matter_factory_data` action validates and writes a compact test-factory payload +into `chip-factory` without erasing unrelated NVS state. It accepts only the +fixed `FFF2/8001` Matter Test PAA profile, verifies the embedded PAI and +Certification Declaration fingerprints, and verifies the DAC signature and +certificate SHA-256 before committing the DAC/private/public keys, PAI, +Certification Declaration, SPAKE2+ data, and device identity. Its response reports +`factoryData.restartRequired`; `matter_restart` schedules a delayed reboot so +the transport can deliver its response before ESP-Matter reloads the providers. +These actions are available through the active BLE/USB/IP/KNX/cloud bridge transport as well as the local HTTP `/bridge` endpoint, so Matter setup does not require a working IP profile. diff --git a/apps/gateway/main/app_main.cpp b/apps/gateway/main/app_main.cpp index e7230aa..8bc8007 100644 --- a/apps/gateway/main/app_main.cpp +++ b/apps/gateway/main/app_main.cpp @@ -13,6 +13,7 @@ #include "esp_heap_caps.h" #include "esp_log.h" +#include "esp_system.h" #include "sdkconfig.h" #include @@ -1390,6 +1391,16 @@ extern "C" void app_main(void) { err = s_matter_bridge->applyConfiguration(*gateway_id, body); } else if (action == "matter_config_reset" && gateway_id.has_value()) { err = s_matter_bridge->resetConfiguration(*gateway_id); + } else if (action == "matter_factory_data") { + err = s_matter_bridge->installFactoryData(body); + } else if (action == "matter_restart") { + const BaseType_t created = xTaskCreate( + [](void*) { + vTaskDelay(pdMS_TO_TICKS(750)); + esp_restart(); + }, + "matter_restart", 2048, nullptr, 3, nullptr); + err = created == pdPASS ? ESP_OK : ESP_ERR_NO_MEM; } if (err != ESP_OK) { const std::string message = std::string("{\"status\":\"error\",\"error\":\"") + diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index 2fe1521..d749a37 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -4829,7 +4829,8 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost( } if (action == "matter_open_commissioning" || action == "matter_close_commissioning" || action == "matter_rescan" || - action == "matter_config" || action == "matter_config_reset") { + action == "matter_config" || action == "matter_config_reset" || + action == "matter_factory_data" || action == "matter_restart") { if (!config_.matter_action_handler) { return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "Matter control is not available"); } diff --git a/components/gateway_matter/CMakeLists.txt b/components/gateway_matter/CMakeLists.txt index 420f85c..b6231cc 100644 --- a/components/gateway_matter/CMakeLists.txt +++ b/components/gateway_matter/CMakeLists.txt @@ -1,7 +1,7 @@ idf_component_register( SRCS "src/gateway_matter.cpp" "src/gateway_matter_plan.cpp" INCLUDE_DIRS "include" - REQUIRES dali_domain esp_driver_gpio esp_matter freertos json log nvs_flash + REQUIRES dali_domain esp_driver_gpio esp_matter freertos json log mbedtls nvs_flash ) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) diff --git a/components/gateway_matter/include/gateway_matter.hpp b/components/gateway_matter/include/gateway_matter.hpp index f148a07..aec0bac 100644 --- a/components/gateway_matter/include/gateway_matter.hpp +++ b/components/gateway_matter/include/gateway_matter.hpp @@ -53,6 +53,7 @@ class GatewayMatterBridge { esp_err_t rescanDaliDevices(); esp_err_t applyConfiguration(uint8_t gateway_id, std::string_view patch); esp_err_t resetConfiguration(uint8_t gateway_id); + esp_err_t installFactoryData(std::string_view payload); std::string statusJson(std::optional gateway_id = std::nullopt) const; std::string onboardingJson() const; @@ -133,6 +134,7 @@ class GatewayMatterBridge { std::atomic_bool ble_advertising_{false}; std::atomic_bool commissioning_window_open_{false}; std::atomic_uint8_t fabric_count_{0}; + std::atomic_bool factory_restart_required_{false}; }; } // namespace gateway diff --git a/components/gateway_matter/src/gateway_matter.cpp b/components/gateway_matter/src/gateway_matter.cpp index 6b00fb1..b4c0aff 100644 --- a/components/gateway_matter/src/gateway_matter.cpp +++ b/components/gateway_matter/src/gateway_matter.cpp @@ -1,6 +1,7 @@ #include "gateway_matter.hpp" #include +#include #include #include #include @@ -19,9 +20,14 @@ #include "esp_heap_caps.h" #include "esp_log.h" #include "freertos/task.h" +#include "mbedtls/base64.h" +#include "mbedtls/md.h" +#include "mbedtls/sha256.h" +#include "mbedtls/x509_crt.h" #include "nvs.h" #include "nvs_flash.h" #include "platform/CHIPDeviceLayer.h" +#include "sdkconfig.h" namespace gateway { namespace { @@ -29,6 +35,19 @@ namespace { constexpr char kTag[] = "gateway_matter"; constexpr char kBindingNamespace[] = "dali_matter"; constexpr char kConfigurationNamespace[] = "matter_cfg"; +constexpr char kFactoryNamespace[] = "chip-factory"; +constexpr char kFactoryProfile[] = "matter-test-paa"; +constexpr char kFactoryInstallConfirmation[] = + "install-matter-test-factory-data"; +constexpr char kFactoryPaiSha256[] = + "27cf4e8cbf73f8fac2b9ab78b7d0fe59a10100b8e9a2df136a85b41a5d271bae"; +constexpr char kFactoryCertificationDeclarationSha256[] = + "867e7710cf20307e4b4ae08a8a1b98fac93a7f716071dd8ae10dc1575366a1bf"; +constexpr uint32_t kFactoryVendorId = 0xFFF2; +constexpr uint32_t kFactoryProductId = 0x8001; +constexpr uint32_t kFactoryDiscriminator = 3840; +constexpr uint32_t kFactoryIterationCount = 10000; +constexpr uint32_t kFactorySetupPasscode = 20202021; constexpr uint8_t kBindingVersion = 2; constexpr uint8_t kConfigurationVersion = 1; constexpr uint16_t kAllDaliDeviceTypesMask = 0x01FF; @@ -73,6 +92,108 @@ struct PersistedChannelConfiguration { uint8_t group_color_methods[16]; }; +bool JsonStringValue(cJSON* root, const char* key, size_t min_length, + size_t max_length, std::string* value) { + const cJSON* item = cJSON_GetObjectItemCaseSensitive(root, key); + if (!cJSON_IsString(item) || item->valuestring == nullptr) return false; + const size_t length = std::strlen(item->valuestring); + if (length < min_length || length > max_length) return false; + *value = item->valuestring; + return true; +} + +bool JsonUint32Value(cJSON* root, const char* key, uint32_t minimum, + uint32_t maximum, uint32_t* value) { + const cJSON* item = cJSON_GetObjectItemCaseSensitive(root, key); + if (!cJSON_IsNumber(item) || !std::isfinite(item->valuedouble) || + std::floor(item->valuedouble) != item->valuedouble || + item->valuedouble < minimum || item->valuedouble > maximum) { + return false; + } + *value = static_cast(item->valuedouble); + return true; +} + +bool DecodeBase64Value(cJSON* root, const char* key, size_t minimum, + size_t maximum, std::vector* value, + std::string* encoded = nullptr) { + std::string input; + if (!JsonStringValue(root, key, 1, ((maximum + 2) / 3) * 4 + 4, + &input)) { + return false; + } + std::vector decoded(maximum); + size_t decoded_length = 0; + const int result = mbedtls_base64_decode( + decoded.data(), decoded.size(), &decoded_length, + reinterpret_cast(input.data()), input.size()); + if (result != 0 || decoded_length < minimum || decoded_length > maximum) { + return false; + } + decoded.resize(decoded_length); + *value = std::move(decoded); + if (encoded != nullptr) *encoded = std::move(input); + return true; +} + +std::string Sha256Hex(const std::vector& data) { + std::array digest{}; + if (mbedtls_sha256(data.data(), data.size(), digest.data(), 0) != 0) { + return {}; + } + constexpr char kHex[] = "0123456789abcdef"; + std::string result(64, '0'); + for (size_t index = 0; index < digest.size(); ++index) { + result[index * 2] = kHex[digest[index] >> 4]; + result[index * 2 + 1] = kHex[digest[index] & 0x0F]; + } + return result; +} + +bool VerifyDacSignature(const std::vector& dac_der, + const std::vector& pai_der) { + mbedtls_x509_crt dac; + mbedtls_x509_crt pai; + mbedtls_x509_crt_init(&dac); + mbedtls_x509_crt_init(&pai); + bool verified = + mbedtls_x509_crt_parse_der(&dac, dac_der.data(), dac_der.size()) == 0 && + mbedtls_x509_crt_parse_der(&pai, pai_der.data(), pai_der.size()) == 0 && + dac.issuer_raw.len == pai.subject_raw.len && + std::memcmp(dac.issuer_raw.p, pai.subject_raw.p, dac.issuer_raw.len) == 0; + if (verified) { + const auto md_type = dac.MBEDTLS_PRIVATE(sig_md); + const mbedtls_md_info_t* md_info = mbedtls_md_info_from_type(md_type); + std::array digest{}; + const size_t digest_length = + md_info == nullptr ? 0 : mbedtls_md_get_size(md_info); + verified = md_info != nullptr && digest_length > 0 && + mbedtls_md(md_info, dac.tbs.p, dac.tbs.len, digest.data()) == 0 && + mbedtls_pk_verify(&pai.pk, md_type, digest.data(), + digest_length, dac.MBEDTLS_PRIVATE(sig).p, + dac.MBEDTLS_PRIVATE(sig).len) == 0; + } + mbedtls_x509_crt_free(&dac); + mbedtls_x509_crt_free(&pai); + return verified; +} + +bool HasNvsBlob(nvs_handle_t handle, const char* key) { + size_t length = 0; + return nvs_get_blob(handle, key, nullptr, &length) == ESP_OK && length > 0; +} + +std::string ReadNvsString(nvs_handle_t handle, const char* key) { + size_t length = 0; + if (nvs_get_str(handle, key, nullptr, &length) != ESP_OK || length <= 1) { + return {}; + } + std::string result(length, '\0'); + if (nvs_get_str(handle, key, result.data(), &length) != ESP_OK) return {}; + result.resize(length > 0 ? length - 1 : 0); + return result; +} + std::string BindingKey(uint16_t endpoint_id) { char key[16] = {}; std::snprintf(key, sizeof(key), "b%04x", endpoint_id); @@ -854,6 +975,133 @@ esp_err_t GatewayMatterBridge::resetConfiguration(uint8_t gateway_id) { return err; } +esp_err_t GatewayMatterBridge::installFactoryData(std::string_view payload) { + cJSON* root = cJSON_ParseWithLength(payload.data(), payload.size()); + if (root == nullptr || !cJSON_IsObject(root)) { + cJSON_Delete(root); + return ESP_ERR_INVALID_ARG; + } + + uint32_t schema_version = 0; + uint32_t discriminator = 0; + uint32_t iteration_count = 0; + uint32_t vendor_id = 0; + uint32_t product_id = 0; + uint32_t hardware_version = 0; + std::string profile; + std::string confirmation; + std::string serial_number; + std::string vendor_name; + std::string product_name; + std::string hardware_version_string; + std::string salt_base64; + std::string verifier_base64; + std::string requested_dac_sha256; + std::vector salt; + std::vector verifier; + std::vector dac_certificate; + std::vector dac_private_key; + std::vector dac_public_key; + std::vector pai_certificate; + std::vector certification_declaration; + + const bool valid = + JsonUint32Value(root, "schemaVersion", 1, 1, &schema_version) && + JsonStringValue(root, "profile", 1, 32, &profile) && + profile == kFactoryProfile && + JsonStringValue(root, "confirm", 1, 64, &confirmation) && + confirmation == kFactoryInstallConfirmation && + JsonStringValue(root, "serialNumber", 1, 32, &serial_number) && + JsonUint32Value(root, "discriminator", kFactoryDiscriminator, + kFactoryDiscriminator, &discriminator) && + JsonUint32Value(root, "iterationCount", kFactoryIterationCount, + kFactoryIterationCount, &iteration_count) && + JsonUint32Value(root, "vendorId", kFactoryVendorId, kFactoryVendorId, + &vendor_id) && + JsonStringValue(root, "vendorName", 1, 32, &vendor_name) && + JsonUint32Value(root, "productId", kFactoryProductId, + kFactoryProductId, &product_id) && + JsonStringValue(root, "productName", 1, 32, &product_name) && + JsonUint32Value(root, "hardwareVersion", 1, 65535, + &hardware_version) && + JsonStringValue(root, "hardwareVersionString", 1, 64, + &hardware_version_string) && + DecodeBase64Value(root, "salt", 32, 32, &salt, &salt_base64) && + DecodeBase64Value(root, "verifier", 97, 97, &verifier, + &verifier_base64) && + DecodeBase64Value(root, "dacCertificate", 128, 2048, + &dac_certificate) && + DecodeBase64Value(root, "dacPrivateKey", 32, 32, &dac_private_key) && + DecodeBase64Value(root, "dacPublicKey", 65, 65, &dac_public_key) && + dac_public_key.front() == 0x04 && + DecodeBase64Value(root, "paiCertificate", 128, 2048, + &pai_certificate) && + DecodeBase64Value(root, "certificationDeclaration", 64, 2048, + &certification_declaration) && + Sha256Hex(pai_certificate) == kFactoryPaiSha256 && + Sha256Hex(certification_declaration) == + kFactoryCertificationDeclarationSha256 && + VerifyDacSignature(dac_certificate, pai_certificate) && + JsonStringValue(root, "dacCertificateSha256", 64, 64, + &requested_dac_sha256) && + requested_dac_sha256 == Sha256Hex(dac_certificate); + cJSON_Delete(root); + if (!valid) return ESP_ERR_INVALID_ARG; + + nvs_handle_t handle = 0; + esp_err_t err = nvs_open_from_partition( + CONFIG_CHIP_FACTORY_NAMESPACE_PARTITION_LABEL, kFactoryNamespace, + NVS_READWRITE, &handle); + if (err != ESP_OK) return err; + + err = nvs_set_u32(handle, "discriminator", discriminator); + if (err == ESP_OK) err = nvs_set_u32(handle, "iteration-count", iteration_count); + if (err == ESP_OK) err = nvs_set_str(handle, "salt", salt_base64.c_str()); + if (err == ESP_OK) err = nvs_set_str(handle, "verifier", verifier_base64.c_str()); + if (err == ESP_OK) err = nvs_set_u32(handle, "vendor-id", vendor_id); + if (err == ESP_OK) err = nvs_set_str(handle, "vendor-name", vendor_name.c_str()); + if (err == ESP_OK) err = nvs_set_u32(handle, "product-id", product_id); + if (err == ESP_OK) err = nvs_set_str(handle, "product-name", product_name.c_str()); + if (err == ESP_OK) err = nvs_set_u32(handle, "hardware-ver", hardware_version); + if (err == ESP_OK) { + err = nvs_set_str(handle, "hw-ver-str", hardware_version_string.c_str()); + } + if (err == ESP_OK) err = nvs_set_str(handle, "serial-num", serial_number.c_str()); + if (err == ESP_OK) err = nvs_set_str(handle, "dm-profile", profile.c_str()); + if (err == ESP_OK) { + err = nvs_set_str(handle, "dm-dac-sha", requested_dac_sha256.c_str()); + } + if (err == ESP_OK) { + err = nvs_set_blob(handle, "dac-cert", dac_certificate.data(), + dac_certificate.size()); + } + if (err == ESP_OK) { + err = nvs_set_blob(handle, "dac-key", dac_private_key.data(), + dac_private_key.size()); + } + if (err == ESP_OK) { + err = nvs_set_blob(handle, "dac-pub-key", dac_public_key.data(), + dac_public_key.size()); + } + if (err == ESP_OK) { + err = nvs_set_blob(handle, "pai-cert", pai_certificate.data(), + pai_certificate.size()); + } + if (err == ESP_OK) { + err = nvs_set_blob(handle, "cert-dclrn", certification_declaration.data(), + certification_declaration.size()); + } + if (err == ESP_OK) err = nvs_commit(handle); + nvs_close(handle); + if (err == ESP_OK) { + factory_restart_required_.store(true, std::memory_order_release); + ESP_LOGW(kTag, + "installed development Matter factory data serial=%s; restart required", + serial_number.c_str()); + } + return err; +} + std::string GatewayMatterBridge::statusJson(std::optional gateway_id) const { bool commissioning_window_open = commissioning_window_open_.load(std::memory_order_acquire); @@ -899,6 +1147,43 @@ std::string GatewayMatterBridge::statusJson(std::optional gateway_id) c config_.wifi_provision_long_press_ms); cJSON_AddStringToObject(matter, "storage", "plainNvs"); cJSON_AddStringToObject(matter, "networkPreference", "ethernet"); + cJSON* factory_data = cJSON_CreateObject(); + if (factory_data != nullptr) { + bool present = false; + std::string profile; + std::string serial_number; + std::string dac_sha256; + uint32_t vendor_id = 0; + uint32_t product_id = 0; + nvs_handle_t handle = 0; + if (nvs_open_from_partition(CONFIG_CHIP_FACTORY_NAMESPACE_PARTITION_LABEL, + kFactoryNamespace, NVS_READONLY, + &handle) == ESP_OK) { + profile = ReadNvsString(handle, "dm-profile"); + serial_number = ReadNvsString(handle, "serial-num"); + dac_sha256 = ReadNvsString(handle, "dm-dac-sha"); + present = !serial_number.empty() && HasNvsBlob(handle, "dac-cert") && + HasNvsBlob(handle, "dac-key") && + HasNvsBlob(handle, "dac-pub-key") && + HasNvsBlob(handle, "pai-cert") && + HasNvsBlob(handle, "cert-dclrn") && + nvs_get_u32(handle, "vendor-id", &vendor_id) == ESP_OK && + nvs_get_u32(handle, "product-id", &product_id) == ESP_OK; + nvs_close(handle); + } + cJSON_AddBoolToObject(factory_data, "present", present); + cJSON_AddStringToObject(factory_data, "profile", profile.c_str()); + cJSON_AddStringToObject(factory_data, "serialNumber", + serial_number.c_str()); + cJSON_AddNumberToObject(factory_data, "vendorId", vendor_id); + cJSON_AddNumberToObject(factory_data, "productId", product_id); + cJSON_AddStringToObject(factory_data, "dacCertificateSha256", + dac_sha256.c_str()); + cJSON_AddBoolToObject( + factory_data, "restartRequired", + factory_restart_required_.load(std::memory_order_acquire)); + cJSON_AddItemToObject(matter, "factoryData", factory_data); + } if (!last_apply_error_.empty()) { cJSON_AddStringToObject(matter, "lastApplyError", last_apply_error_.c_str()); } else { @@ -1061,8 +1346,8 @@ std::string GatewayMatterBridge::onboardingJson() const { chip::MutableCharSpan manual_span(manual_buffer, sizeof(manual_buffer)); const auto flags = chip::RendezvousInformationFlags( chip::RendezvousInformationFlag::kBLE); - const CHIP_ERROR qr_err = GetQRCode(qr_span, flags); - const CHIP_ERROR manual_err = GetManualPairingCode(manual_span, flags); + CHIP_ERROR qr_err = CHIP_ERROR_INCORRECT_STATE; + CHIP_ERROR manual_err = CHIP_ERROR_INCORRECT_STATE; uint16_t vendor_id = 0; uint16_t product_id = 0; auto* provider = chip::DeviceLayer::GetDeviceInstanceInfoProvider(); @@ -1074,6 +1359,29 @@ std::string GatewayMatterBridge::onboardingJson() const { provider->GetVendorId(vendor_id); provider->GetProductId(product_id); } + std::string factory_profile; + nvs_handle_t factory_handle = 0; + if (nvs_open_from_partition(CONFIG_CHIP_FACTORY_NAMESPACE_PARTITION_LABEL, + kFactoryNamespace, NVS_READONLY, + &factory_handle) == ESP_OK) { + factory_profile = ReadNvsString(factory_handle, "dm-profile"); + nvs_close(factory_handle); + } + if (factory_profile == kFactoryProfile && vendor_id == kFactoryVendorId && + product_id == kFactoryProductId) { + chip::PayloadContents payload; + payload.version = 0; + payload.rendezvousInformation.SetValue(flags); + payload.setUpPINCode = kFactorySetupPasscode; + payload.discriminator.SetLongValue(kFactoryDiscriminator); + payload.vendorID = vendor_id; + payload.productID = product_id; + qr_err = GetQRCode(qr_span, payload); + manual_err = GetManualPairingCode(manual_span, payload); + } else { + qr_err = GetQRCode(qr_span, flags); + manual_err = GetManualPairingCode(manual_span, flags); + } cJSON* root = cJSON_CreateObject(); cJSON* onboarding = cJSON_CreateObject(); cJSON_AddItemToObject(root, "matterOnboarding", onboarding);