feat(gateway): Add Matter factory data handling and restart functionality

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-08-08 09:23:06 +08:00
parent 8505958b65
commit ca9e95e985
6 changed files with 336 additions and 5 deletions
+10 -1
View File
@@ -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.
+11
View File
@@ -13,6 +13,7 @@
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_system.h"
#include "sdkconfig.h"
#include <algorithm>
@@ -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\":\"") +
@@ -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");
}
+1 -1
View File
@@ -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)
@@ -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<uint8_t> 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
@@ -1,6 +1,7 @@
#include "gateway_matter.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <cstring>
@@ -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<uint32_t>(item->valuedouble);
return true;
}
bool DecodeBase64Value(cJSON* root, const char* key, size_t minimum,
size_t maximum, std::vector<uint8_t>* value,
std::string* encoded = nullptr) {
std::string input;
if (!JsonStringValue(root, key, 1, ((maximum + 2) / 3) * 4 + 4,
&input)) {
return false;
}
std::vector<uint8_t> decoded(maximum);
size_t decoded_length = 0;
const int result = mbedtls_base64_decode(
decoded.data(), decoded.size(), &decoded_length,
reinterpret_cast<const unsigned char*>(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<uint8_t>& data) {
std::array<unsigned char, 32> 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<uint8_t>& dac_der,
const std::vector<uint8_t>& 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<unsigned char, MBEDTLS_MD_MAX_SIZE> 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<uint8_t> salt;
std::vector<uint8_t> verifier;
std::vector<uint8_t> dac_certificate;
std::vector<uint8_t> dac_private_key;
std::vector<uint8_t> dac_public_key;
std::vector<uint8_t> pai_certificate;
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> 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);