From a3135d346cea79d5daa18a7111f48d2532205dcb Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Wed, 16 Mar 2022 17:50:01 +0530 Subject: [PATCH] Mass manufacturing tool for Matter Utility generates manufacturing NVS partition image and Onboarding QR codes --- .gitignore | 2 + components/esp_matter/CMakeLists.txt | 2 +- components/esp_matter/Kconfig | 12 - components/esp_matter/esp_matter_core.cpp | 41 +- components/esp_matter/esp_matter_dac.cpp | 111 --- components/esp_matter/esp_matter_dac.h | 33 - components/esp_matter_factory/CMakeLists.txt | 3 - components/esp_matter_factory/Kconfig | 13 - .../esp_matter_factory/esp_matter_factory.c | 123 ---- .../esp_matter_factory/esp_matter_factory.h | 92 --- tools/certs/cert_dclrn.bin | 1 - tools/certs/generate_matter_factory.sh | 171 ----- tools/mfg_tool/README.md | 162 +++++ tools/mfg_tool/chip_nvs_keys.py | 71 ++ tools/mfg_tool/mfg_tool.py | 657 ++++++++++++++++++ tools/mfg_tool/requirements.txt | 6 + 16 files changed, 920 insertions(+), 580 deletions(-) delete mode 100644 components/esp_matter/Kconfig delete mode 100644 components/esp_matter/esp_matter_dac.cpp delete mode 100644 components/esp_matter/esp_matter_dac.h delete mode 100644 components/esp_matter_factory/CMakeLists.txt delete mode 100644 components/esp_matter_factory/Kconfig delete mode 100644 components/esp_matter_factory/esp_matter_factory.c delete mode 100644 components/esp_matter_factory/esp_matter_factory.h delete mode 100644 tools/certs/cert_dclrn.bin delete mode 100755 tools/certs/generate_matter_factory.sh create mode 100644 tools/mfg_tool/README.md create mode 100644 tools/mfg_tool/chip_nvs_keys.py create mode 100755 tools/mfg_tool/mfg_tool.py create mode 100644 tools/mfg_tool/requirements.txt diff --git a/.gitignore b/.gitignore index db1a50cec..1249d34cf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ sdkconfig sdkconfig.old dependencies.lock managed_components/ +__pycache__/ +out/ diff --git a/components/esp_matter/CMakeLists.txt b/components/esp_matter/CMakeLists.txt index 63ecd56c1..efc720c42 100644 --- a/components/esp_matter/CMakeLists.txt +++ b/components/esp_matter/CMakeLists.txt @@ -72,7 +72,7 @@ set(INCLUDE_DIRS_LIST "." "${MATTER_SDK_PATH}/src" "${ZAP_GENERATED_PATH}/../") -set(REQUIRES_LIST chip bt esp_matter_console esp_matter_factory) +set(REQUIRES_LIST chip bt esp_matter_console) if ("${IDF_TARGET}" STREQUAL "esp32h2") list(APPEND REQUIRES_LIST openthread esp_matter_openthread) diff --git a/components/esp_matter/Kconfig b/components/esp_matter/Kconfig deleted file mode 100644 index 400c65e73..000000000 --- a/components/esp_matter/Kconfig +++ /dev/null @@ -1,12 +0,0 @@ -menu "ESP Matter" - - config ESP_MATTER_USE_ESP_DAC_PROVIDER - bool "Use ESP Matter DAC Provider" - default n - help - Use the ESP Matter DAC provider instead of the example DAC provider. - ESP Matter DAC provider reads the attestation certificates and keys - from the factory partition. Before enabling please flash the factory - partition with the attestation certificates and keys. - -endmenu diff --git a/components/esp_matter/esp_matter_core.cpp b/components/esp_matter/esp_matter_core.cpp index a4c333111..b05ba1d7d 100644 --- a/components/esp_matter/esp_matter_core.cpp +++ b/components/esp_matter/esp_matter_core.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -23,28 +22,20 @@ #include #include #include +#include #include +#include #include #if CHIP_DEVICE_CONFIG_ENABLE_THREAD #include #endif -/* TODO: Remove the examples DAC provider once we have a concrete - * way to generate attestation credentials. - */ -#if CONFIG_ESP_MATTER_USE_ESP_DAC_PROVIDER -#include -using chip::Credentials::esp::esp_matter_dac_provider_get; -#else -#include -using chip::Credentials::Examples::GetExampleDACProvider; -#endif - using chip::CommandId; using chip::DataVersion; using chip::kInvalidCommandId; using chip::kInvalidEndpointId; using chip::Credentials::SetDeviceAttestationCredentialsProvider; +using chip::Credentials::Examples::GetExampleDACProvider; using chip::DeviceLayer::ChipDeviceEvent; using chip::DeviceLayer::ConfigurationMgr; using chip::DeviceLayer::ConnectivityManager; @@ -59,6 +50,13 @@ using chip::DeviceLayer::ThreadStackMgr; static const char *TAG = "esp_matter_core"; namespace esp_matter { + +namespace { +#if CONFIG_ENABLE_ESP32_FACTORY_DATA_PROVIDER +chip::DeviceLayer::ESP32FactoryDataProvider factory_data_provider; +#endif // CONFIG_ENABLE_ESP32_FACTORY_DATA_PROVIDER +} // namespace + typedef struct _attribute { int attribute_id; int cluster_id; @@ -644,11 +642,15 @@ static void esp_matter_chip_init_task(intptr_t context) // initParams.appDelegate = &sCallbacks; chip::Server::GetInstance().Init(initParams); -#if CONFIG_ESP_MATTER_USE_ESP_DAC_PROVIDER - SetDeviceAttestationCredentialsProvider(esp_matter_dac_provider_get()); -#else +/* TODO: Remove the examples DAC provider once we have a concrete + * way to generate attestation credentials. + */ +#if CONFIG_ENABLE_ESP32_FACTORY_DATA_PROVIDER + SetDeviceAttestationCredentialsProvider(&factory_data_provider); +#else // CONFIG_ENABLE_ESP32_FACTORY_DATA_PROVIDER SetDeviceAttestationCredentialsProvider(GetExampleDACProvider()); #endif + #if CHIP_DEVICE_CONFIG_ENABLE_THREAD // If Thread is Provisioned, publish the dns service if (chip::DeviceLayer::ConnectivityMgr().IsThreadProvisioned() && @@ -673,6 +675,10 @@ static void esp_matter_chip_init_task(intptr_t context) static esp_err_t chip_init(event_callback_t callback) { +#if CONFIG_ENABLE_ESP32_FACTORY_DATA_PROVIDER + SetCommissionableDataProvider(&factory_data_provider); +#endif // CONFIG_ENABLE_ESP32_FACTORY_DATA_PROVIDER + if (chip::Platform::MemoryInit() != CHIP_NO_ERROR) { ESP_LOGE(TAG, "Failed to initialize CHIP memory pool"); return ESP_ERR_NO_MEM; @@ -711,11 +717,6 @@ esp_err_t start(event_callback_t callback) esp_err_t err = chip_init(callback); if (err != ESP_OK) { ESP_LOGE(TAG, "Error initializing matter"); - return err; - } - err = esp_matter_factory_init(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Error initializing factory"); } return err; } diff --git a/components/esp_matter/esp_matter_dac.cpp b/components/esp_matter/esp_matter_dac.cpp deleted file mode 100644 index a243eb68f..000000000 --- a/components/esp_matter/esp_matter_dac.cpp +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2022 Espressif Systems (Shanghai) PTE LTD -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include -#include - -#include -#include -#include - -namespace chip { -namespace Credentials { -namespace esp { - -namespace { - -static constexpr uint32_t kDACPrivateKeySize = 32; -static constexpr uint32_t kDACPublicKeySize = 65; - -// TODO: This should be moved to a method of P256Keypair -CHIP_ERROR LoadKeypairFromRaw(ByteSpan privateKey, ByteSpan publicKey, Crypto::P256Keypair & keypair) -{ - Crypto::P256SerializedKeypair serializedKeypair; - ReturnErrorOnFailure(serializedKeypair.SetLength(privateKey.size() + publicKey.size())); - memcpy(serializedKeypair.Bytes(), publicKey.data(), publicKey.size()); - memcpy(serializedKeypair.Bytes() + publicKey.size(), privateKey.data(), privateKey.size()); - return keypair.Deserialize(serializedKeypair); -} - -class ESPMatterDAC : public DeviceAttestationCredentialsProvider -{ -public: - CHIP_ERROR GetCertificationDeclaration(MutableByteSpan & outBuffer) override - { - size_t certSize = outBuffer.size(); - VerifyOrReturnError(esp_matter_factory_get_cert_declrn(outBuffer.data(), &certSize) == ESP_OK, CHIP_ERROR_READ_FAILED); - outBuffer.reduce_size(certSize); - return CHIP_NO_ERROR; - } - - CHIP_ERROR GetFirmwareInformation(MutableByteSpan & out_firmware_info_buffer) override - { - // TODO: We need a real example FirmwareInformation to be populated. - out_firmware_info_buffer.reduce_size(0); - return CHIP_NO_ERROR; - } - - CHIP_ERROR GetDeviceAttestationCert(MutableByteSpan & outBuffer) override - { - size_t dacCertSz = outBuffer.size(); - VerifyOrReturnError(esp_matter_factory_get_dac_cert(outBuffer.data(), &dacCertSz) == ESP_OK, CHIP_ERROR_READ_FAILED); - outBuffer.reduce_size(dacCertSz); - return CHIP_NO_ERROR; - } - - CHIP_ERROR GetProductAttestationIntermediateCert(MutableByteSpan & outBuffer) override - { - size_t paiCertSz = outBuffer.size(); - VerifyOrReturnError(esp_matter_factory_get_pai_cert(outBuffer.data(), &paiCertSz) == ESP_OK, CHIP_ERROR_READ_FAILED); - outBuffer.reduce_size(paiCertSz); - return CHIP_NO_ERROR; - } - - CHIP_ERROR SignWithDeviceAttestationKey(const ByteSpan & digestToSign, MutableByteSpan & outSignBuffer) override - { - Crypto::P256ECDSASignature signature; - Crypto::P256Keypair keypair; - - VerifyOrReturnError(IsSpanUsable(outSignBuffer), CHIP_ERROR_INVALID_ARGUMENT); - VerifyOrReturnError(IsSpanUsable(digestToSign), CHIP_ERROR_INVALID_ARGUMENT); - VerifyOrReturnError(outSignBuffer.size() >= signature.Capacity(), CHIP_ERROR_BUFFER_TOO_SMALL); - - uint8_t privKeyBuf[kDACPrivateKeySize]; - uint8_t pubKeyBuf[kDACPublicKeySize]; - size_t privKeyLen = sizeof(privKeyBuf); - size_t pubKeyLen = sizeof(pubKeyBuf); - - VerifyOrReturnError(esp_matter_factory_get_dac_private_key(privKeyBuf, &privKeyLen) == ESP_OK, CHIP_ERROR_READ_FAILED); - VerifyOrReturnError(esp_matter_factory_get_dac_public_key(pubKeyBuf, &pubKeyLen) == ESP_OK, CHIP_ERROR_READ_FAILED); - - // In a non-exemplary implementation, the public key is not needed here. It is used here merely because - // Crypto::P256Keypair is only (currently) constructable from raw keys if both private/public keys are present. - ReturnErrorOnFailure(LoadKeypairFromRaw(ByteSpan(privKeyBuf, privKeyLen), ByteSpan(pubKeyBuf, pubKeyLen), keypair)); - ReturnErrorOnFailure(keypair.ECDSA_sign_hash(digestToSign.data(), digestToSign.size(), signature)); - - return CopySpanToMutableSpan(ByteSpan{ signature.ConstBytes(), signature.Length() }, outSignBuffer); - } -}; - -} // namespace - -DeviceAttestationCredentialsProvider * esp_matter_dac_provider_get(void) -{ - static ESPMatterDAC dacProvider; - return &dacProvider; -} - -} // namespace esp -} // namespace Credentials -} // namespace chip diff --git a/components/esp_matter/esp_matter_dac.h b/components/esp_matter/esp_matter_dac.h deleted file mode 100644 index f898757f6..000000000 --- a/components/esp_matter/esp_matter_dac.h +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2022 Espressif Systems (Shanghai) PTE LTD -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#pragma once - -#include - -namespace chip { -namespace Credentials { -namespace esp { - -/** - * @brief Get implementation of a ESP DAC provider to validate device - * attestation procedure. - * - * @returns a singleton DeviceAttestationCredentialsProvider - */ -DeviceAttestationCredentialsProvider * esp_matter_dac_provider_get(void); - -} // namespace esp -} // namespace Credentials -} // namespace chip diff --git a/components/esp_matter_factory/CMakeLists.txt b/components/esp_matter_factory/CMakeLists.txt deleted file mode 100644 index 0e6efedc7..000000000 --- a/components/esp_matter_factory/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -idf_component_register(SRCS "esp_matter_factory.c" - INCLUDE_DIRS . - REQUIRES chip nvs_flash esp32_mbedtls) diff --git a/components/esp_matter_factory/Kconfig b/components/esp_matter_factory/Kconfig deleted file mode 100644 index 8fc27c2a8..000000000 --- a/components/esp_matter_factory/Kconfig +++ /dev/null @@ -1,13 +0,0 @@ -menu "ESP Matter Factory" - - config ESP_MATTER_FACTORY_PARTITION - string "Matter Factory Partition Name" - default "fctry" - help - Partition name where the matter factory is located. - - config ESP_MATTER_FACTORY_NAMESPACE - string "Matter Factory Namespace Name" - default "chip_creds" - -endmenu diff --git a/components/esp_matter_factory/esp_matter_factory.c b/components/esp_matter_factory/esp_matter_factory.c deleted file mode 100644 index 40f90b9ae..000000000 --- a/components/esp_matter_factory/esp_matter_factory.c +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2022 Espressif Systems (Shanghai) PTE LTD -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include -#include -#include - -static const char *TAG = "esp_matter_factory"; - -static const char *NVS_KEY_DAC_CERT = "dac_cert"; -static const char *NVS_KEY_DAC_PUBLIC_KEY = "dac_pubkey"; -static const char *NVS_KEY_DAC_PRIVATE_KEY = "dac_privkey"; -static const char *NVS_KEY_PAI_CERT = "pai_cert"; -static const char *NVS_KEY_CERT_DCLRN = "cert_dclrn"; - -static bool initialized = false; -static nvs_handle_t factory_nvs_handle; - -esp_err_t esp_matter_factory_init(void) -{ - if (initialized) { - ESP_LOGW(TAG, "Matter Factory already initialized"); - return ESP_OK; - } - - esp_err_t err = nvs_flash_init_partition(CONFIG_ESP_MATTER_FACTORY_PARTITION); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to initialized partition:%s err:0x%x", CONFIG_ESP_MATTER_FACTORY_PARTITION, err); - return err; - } - - err = nvs_open_from_partition(CONFIG_ESP_MATTER_FACTORY_PARTITION, CONFIG_ESP_MATTER_FACTORY_NAMESPACE, - NVS_READONLY, &factory_nvs_handle); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to open partition:%s namespace:%s mode:NVS_READONLY err:%x", - CONFIG_ESP_MATTER_FACTORY_PARTITION, CONFIG_ESP_MATTER_FACTORY_NAMESPACE, err); - nvs_flash_deinit_partition(CONFIG_ESP_MATTER_FACTORY_PARTITION); - return err; - } - - initialized = true; - return ESP_OK; -} - -void esp_matter_factory_deinit(void) -{ - if (initialized) { - nvs_close(factory_nvs_handle); - nvs_flash_deinit_partition(CONFIG_ESP_MATTER_FACTORY_PARTITION); - initialized = false; - } -} - -/* - * Read value from the factory partition. - * @param [in] key Key to read - * @param [out] buf Buffer to store the value - * @param [in, out] len Length of the buf, if read is successful, it will be updated to the actual length of the value - * - * @return ESP_OK on success, error code otherwise - * @return ESP_ERR_INVALID_ARG if key is NULL or len is NULL - * @return ESP_ERR_INVALID_STATE if factory is not initialized - */ -static esp_err_t matter_factory_read_value(const char *key, uint8_t *buf, size_t *len) -{ - assert(key); - - if (buf == NULL || len == NULL) - { - return ESP_ERR_INVALID_ARG; - } - - if (initialized == false) - { - ESP_LOGE(TAG, "Matter Factory not initialized"); - return ESP_ERR_INVALID_STATE; - } - - esp_err_t err = nvs_get_blob(factory_nvs_handle, key, buf, len); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to read value key:%s err:%x", key, err); - } - return err; -} - -esp_err_t esp_matter_factory_get_dac_cert(uint8_t *buf, size_t *len) -{ - return matter_factory_read_value(NVS_KEY_DAC_CERT, buf, len); -} - -esp_err_t esp_matter_factory_get_pai_cert(uint8_t *buf, size_t *len) -{ - return matter_factory_read_value(NVS_KEY_PAI_CERT, buf, len); -} - -esp_err_t esp_matter_factory_get_cert_declrn(uint8_t *buf, size_t *len) -{ - return matter_factory_read_value(NVS_KEY_CERT_DCLRN, buf, len); -} - -esp_err_t esp_matter_factory_get_dac_public_key(uint8_t *buf, size_t *len) -{ - return matter_factory_read_value(NVS_KEY_DAC_PUBLIC_KEY, buf, len); -} - -esp_err_t esp_matter_factory_get_dac_private_key(uint8_t *buf, size_t *len) -{ - return matter_factory_read_value(NVS_KEY_DAC_PRIVATE_KEY, buf, len); -} diff --git a/components/esp_matter_factory/esp_matter_factory.h b/components/esp_matter_factory/esp_matter_factory.h deleted file mode 100644 index c40fd4d34..000000000 --- a/components/esp_matter_factory/esp_matter_factory.h +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2022 Espressif Systems (Shanghai) PTE LTD -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#pragma once - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Initialize and open the matter factory partition. - * - * @return ESP_OK on success or if already initialized, - * appropriate error code otherwise. - */ -esp_err_t esp_matter_factory_init(void); - -/** - * @brief Deinitialize and close the matter factory partition. - */ -void esp_matter_factory_deinit(void); - -/** - * @brief Get the device attestation certificate (DAC) from matter factory partition. - * - * @param[out] out_buf Pointer to the buffer to store the certificate. - * @param[in, out] buf_len The length of the buffer, and the length of the - * certificate after the function returns. - * - * @return ESP_OK on success, appropriate error code otherwise. - */ -esp_err_t esp_matter_factory_get_dac_cert(uint8_t *out_buf, size_t *len); - -/** - * @brief Get the Device Attestation Public Key from matter factory partition. - * - * @param[out] out_buf Pointer to the buffer to store the public key. - * @param[in, out] buf_len The length of the buffer, and the length of the - * public key after the function returns. - * - * @return ESP_OK on success, appropriate error code otherwise. - */ -esp_err_t esp_matter_factory_get_dac_public_key(uint8_t *out_buf, size_t *len); - -/** - * @brief Get the Device Attestation Private Key from matter factory partition. - * - * @param[out] out_buf Pointer to the buffer to store the private key. - * @param[in, out] buf_len The length of the buffer, and the length of the - * - * @return ESP_OK on success, appropriate error code otherwise. - */ -esp_err_t esp_matter_factory_get_dac_private_key(uint8_t *out_buf, size_t *len); - -/** - * @brief Get the Product Attestation Intermediate Certificate (PAI) from matter factory partition. - * - * @param[out] out_buf Pointer to the buffer to store the certificate. - * @param[in, out] buf_len The length of the buffer, and the length of the - * certificate after the function returns. - * - * @return ESP_OK on success, appropriate error code otherwise. - */ -esp_err_t esp_matter_factory_get_pai_cert(uint8_t *out_buf, size_t *len); - -/** - * @brief Get the Certificate Declaration from matter factory partition. - * - * @param[out] out_buf Pointer to the buffer to store the certificate. - * @param[in, out] buf_len The length of the buffer, and the length of the - * certificate after the function returns. - * - * @return ESP_OK on success, appropriate error code otherwise. - */ -esp_err_t esp_matter_factory_get_cert_declrn(uint8_t *out_buf, size_t *len); - -#ifdef __cplusplus -} -#endif diff --git a/tools/certs/cert_dclrn.bin b/tools/certs/cert_dclrn.bin deleted file mode 100644 index ca810eaa4..000000000 --- a/tools/certs/cert_dclrn.bin +++ /dev/null @@ -1 +0,0 @@ -3081e806092a864886f70d010702a081da3081d7020103310d300b0609608648016503040201304506092a864886f70d010701a0380436152400012501f1ff360205008018250334122c04135a494732303134315a423333303030312d32342405002406002507942624080018317c307a020103801462fa823359acfaa9963e1cfa140addf504f37160300b0609608648016503040201300a06082a8648ce3d04030204463044022043a63f2b943df33c38b3e02fcaa75fe3532aebbf5e63f5bbdbc0b1f01d3c4f6002204c1abf5f1807b81894b1576c47e4724e4d966c612ed3fa25c118c3f2b3f90369 \ No newline at end of file diff --git a/tools/certs/generate_matter_factory.sh b/tools/certs/generate_matter_factory.sh deleted file mode 100755 index 1a84120ff..000000000 --- a/tools/certs/generate_matter_factory.sh +++ /dev/null @@ -1,171 +0,0 @@ -#!/bin/sh -# Script to generate the DACs and matter factory partition - -# TODO: Lot of stuff here is hardcoded, and could be done better -# by using the arguments to the script or even better, switch to python - -CHIP_CERT_TOOL="../../connectedhomeip/connectedhomeip/out/host/chip-cert gen-att-cert" -NVS_GENERATOR="$IDF_PATH/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py" - -PAA_CERT_SUBJECT="ESP Matter PAA 01" -PAI_CERT_SUBJECT="ESP Matter PAI 01" -DAC_CERT_SUBJECT="ESP Matter DAC 01" - -CERT_VALID_FROM="2022-01-01 14:23:43" -CERT_LIFETIME=4294967295 - -VENDOR_ID=FFF1 -PRODUCT_ID=8000 - -OUT_DIR=out/$VENDOR_ID-$PRODUCT_ID -NAME_PREFIX="$OUT_DIR/ESP-Matter" -mkdir -p $OUT_DIR - -# PAA_CERT_NAME="$NAME_PREFIX-PAA-Cert.pem" -PAA_CERT_NAME="../../connectedhomeip/connectedhomeip/credentials/test/attestation/Chip-Test-PAA-FFF1-Cert.pem" -PAI_CERT_NAME="$NAME_PREFIX-PAI-Cert.pem" -DAC_CERT_NAME="$NAME_PREFIX-DAC-Cert.pem" - -PAI_CERT_NAME_DER="$NAME_PREFIX-PAI-Cert.der" -DAC_CERT_NAME_DER="$NAME_PREFIX-DAC-Cert.der" - -# PAA_PRIVATE_KEY_NAME="$NAME_PREFIX-PAA-Private-Key.pem" -PAA_PRIVATE_KEY_NAME="../../connectedhomeip/connectedhomeip/credentials/test/attestation/Chip-Test-PAA-FFF1-Key.pem" -PAI_PRIVATE_KEY_NAME="$NAME_PREFIX-PAI-Private-Key.pem" -DAC_PRIVATE_KEY_NAME="$NAME_PREFIX-DAC-Private-Key.pem" - -DAC_PRIVATE_KEY_BIN="$NAME_PREFIX-DAC-Private-Key.bin" -DAC_PUBLIC_KEY_BIN="$NAME_PREFIX-DAC-Public-Key.bin" - -# @brief Convert certificates in PEM format to DER format -# -# @param $1 - PEM Certificate File Name -# @param $2 - DER Certificate File Name -convert_pem_to_der() -{ - openssl x509 -inform pem -in $1 -out $2 -outform DER > /dev/null 2>&1 -} - -generate_paa() -{ - $CHIP_CERT_TOOL --type a \ - --subject-cn "$PAA_CERT_SUBJECT" \ - --valid-from "$CERT_VALID_FROM" \ - --lifetime "$CERT_LIFETIME" \ - --out-key "$PAA_PRIVATE_KEY_NAME" \ - --out "$PAA_CERT_NAME" - - if [ $? -ne 0 ]; then - echo "Failed to generate PAA certificate" - exit 1 - fi - - echo "Generated PAA certificate" -} - -generate_pai() -{ - $CHIP_CERT_TOOL --type i \ - --subject-cn "$PAI_CERT_SUBJECT" \ - --subject-vid "$VENDOR_ID" \ - --valid-from "$CERT_VALID_FROM" \ - --lifetime "$CERT_LIFETIME" \ - --ca-key "$PAA_PRIVATE_KEY_NAME" \ - --ca-cert "$PAA_CERT_NAME" \ - --out-key "$PAI_PRIVATE_KEY_NAME" \ - --out "$PAI_CERT_NAME" - - if [ $? -ne 0 ]; then - echo "Failed to generate PAA certificate" - exit 1 - fi - - convert_pem_to_der $PAI_CERT_NAME $PAI_CERT_NAME_DER - echo "Generated PAI certificate" -} - -generate_keypair_bin() -{ - openssl ec -in $DAC_PRIVATE_KEY_NAME | openssl asn1parse | awk -F ":" '/OCTET STRING/ { print $4 }' \ - | xxd -r -p > $DAC_PRIVATE_KEY_BIN - - openssl ec -inform PEM -in $DAC_PRIVATE_KEY_NAME -text | grep -A 5 'pub:' | sed "1 d" \ - | tr -d ':\n ' | xxd -r -p > $DAC_PUBLIC_KEY_BIN -} - -generate_dac() -{ - $CHIP_CERT_TOOL --type d \ - --subject-cn "$DAC_CERT_SUBJECT" \ - --subject-vid "$VENDOR_ID" \ - --subject-pid "$PRODUCT_ID" \ - --valid-from "$CERT_VALID_FROM" \ - --lifetime "$CERT_LIFETIME" \ - --ca-key "$PAI_PRIVATE_KEY_NAME" \ - --ca-cert "$PAI_CERT_NAME" \ - --out-key "$DAC_PRIVATE_KEY_NAME" \ - --out "$DAC_CERT_NAME" - - if [ $? -ne 0 ]; then - echo "Failed to generate PAA certificate" - exit 1 - fi - - generate_keypair_bin $DAC_PRIVATE_KEY_NAME $DAC_PRIVATE_KEY_BIN $DAC_PUBLIC_KEY_BIN - convert_pem_to_der $DAC_CERT_NAME $DAC_CERT_NAME_DER - echo "Generated DAC certificate, key pair" -} - -populate_csv() -{ - -cat << EOF >> $OUT_DIR/matter_factory.csv -key,type,encoding,value -chip_creds,namespace,, -dac_cert,file,binary,$DAC_CERT_NAME_DER -pai_cert,file,binary,$PAI_CERT_NAME_DER -dac_pubkey,file,binary,$DAC_PUBLIC_KEY_BIN -dac_privkey,file,binary,$DAC_PRIVATE_KEY_BIN -cert_dclrn,file,hex2bin,cert_dclrn.bin -EOF - -} - -print_flash_help() -{ - echo "" - echo "==============================================================" - echo "Generated matter factory partition:$1" - echo "Please flash the partition to the device using esptool.py" - echo "esptool.py -p write_flash $2 $1" - echo "==============================================================" -} - -generate_matter_factory_partition() -{ - populate_csv - - if [ $? -ne 0 ]; then - echo "Failed to generate CSV file" - exit 1 - fi - - $NVS_GENERATOR generate $OUT_DIR/matter_factory.csv $OUT_DIR/matter_factory.bin 0x6000 - - if [ $? -ne 0 ]; then - echo "Failed to generate matter factory partition" - exit 1 - fi - - print_flash_help $OUT_DIR/matter_factory.bin 0x6000 -} - -main() -{ - generate_pai - generate_dac - generate_matter_factory_partition -} - -# Execution starts here -main diff --git a/tools/mfg_tool/README.md b/tools/mfg_tool/README.md new file mode 100644 index 000000000..3a459b0a1 --- /dev/null +++ b/tools/mfg_tool/README.md @@ -0,0 +1,162 @@ +# Manufacturing Partition Generator Utility + +## Dependencies +* [CHIP Certificate Tool](https://github.com/project-chip/connectedhomeip/tree/master/src/tools/chip-cert), +chip-cert provides command line interface (CLI) utility used for generating and manipulating CHIP certificates and CHIP private keys. + +* [SPAKE2P Parameters Tool](https://github.com/project-chip/connectedhomeip/tree/master/src/tools/spake2p), +spake2p tool provides command line interface (CLI) utility used for generating spake parameters (PIN code and verifier) for device manufacturing provisioning. + +* [QR Code Tool](https://github.com/project-chip/connectedhomeip/tree/master/src/qrcodetool), +qrcodetool generates onboarding QR code payload and manual pairing code. + +* [Mass Manufacturing Utility](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/mass_mfg.html#manufacturing-utility), +mfg_gen.py to create factory NVS partition images. + +### Install python dependencies +``` +cd path/to/esp-matter/tools/mfg_tool +python3 -m pip install -r requirements.txt +``` + +### [Build and setup tools in Matter SDK](https://github.com/project-chip/connectedhomeip/blob/master/docs/guides/BUILDING.md#build-for-the-host-os-linux-or-macos) + +#### Build chip-cert and spake2p +``` +cd path/to/esp-matter/connectedhomeip/connectedhomeip +source scripts/activate.sh +gn gen out/host +ninja -C out/host +``` +Above commands will generate spake2p and chip-cert at `esp-matter/connectedhomeip/connectedhomeip/out/host`. + +#### Build qrcodetool +``` +cd path/to/esp-matter/connectedhomeip/connectedhomeip +ninja -C out/host src/qrcodetool + +# This builds the qrcodetool at path out/host/obj/src/qrcodetool/bin +# Move this to out/host to keep all the tools at the same path +cp out/host/obj/src/qrcodetool/bin/qrcodetool out/host +``` + +#### Add the tools path to $PATH +``` +export PATH="$PATH:path/to/esp-matter/connectedhomeip/connectedhomeip/out/host" +``` + +### mfg_gen.py +`mfg_gen.py` is present at path `$IDF_PATH/tools/mass_mfg/mfg_gen.py` + +## Output files and directory structure +``` +out +└── fff1_8000 + ├── 11fe2c53-9a38-445c-b58f-2ff0554cd981 + │   ├── 11fe2c53-9a38-445c-b58f-2ff0554cd981-onb_codes.csv + │   ├── 11fe2c53-9a38-445c-b58f-2ff0554cd981-partition.bin + │   ├── 11fe2c53-9a38-445c-b58f-2ff0554cd981-qrcode.png + │   └── internal + │   ├── DAC_cert.der + │   ├── DAC_cert.pem + │   ├── DAC_key.pem + │   ├── DAC_private_key.bin + │   ├── DAC_public_key.bin + │   ├── PAI_cert.der + │   └── partition.csv + ├── 14874525-30b5-4c66-a00e-30e4af5dfb20 + │   ├── 14874525-30b5-4c66-a00e-30e4af5dfb20-onb_codes.csv + │   ├── 14874525-30b5-4c66-a00e-30e4af5dfb20-partition.bin + │   ├── 14874525-30b5-4c66-a00e-30e4af5dfb20-qrcode.png + │   └── internal + │   ├── DAC_cert.der + │   ├── DAC_cert.pem + │   ├── DAC_key.pem + │   ├── DAC_private_key.bin + │   ├── DAC_public_key.bin + │   ├── PAI_cert.der + │   └── partition.csv + └── staging + ├── config.csv + ├── master.csv + ├── pai_cert.der + └── pin_disc.csv +``` + +Tool generates following output files: +- Partition Binary : `-partition.bin` +- Onboarding codes : `-onb_codes.csv` +- QR Code image : `-qrcode.png` + +Other intermediate files are stored in `internal/` directory: +- Partition CSV : `partition.csv` +- PAI Certificate : `PAI_cert.der` +- DAC Certificates : `DAC_cert.der`, `DAC_cert.pem` +- DAC Private Key : `DAC_private_key.bin` +- DAC Public Key : `DAC_public_key.bin` + +Above files are stored at `out//`. Each device is identified with an unique UUID. + +Common intermediate files are stored at `out//staging`. + +## Usage examples +`mfg_tool.py -h` lists the mandatory as well as optional arguments. + +Below commands uses the test PAI signing certificate and key, test certificate declaration present in Matter SDK, Vendor ID: 0xFFF2, and Product ID: 0x8001. + +### Generate a factory partition +``` +./mfg_tool.py -cn "My bulb" -v 0xFFF2 -p 0x8001 --pai \ + -k path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Key.pem \ + -c path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Cert.pem \ + -cd path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/certification-declaration/Chip-Test-CD-FFF2-8001.der +``` + +### Generate 5 factory partitions [Optional argument : `-n`] +``` +./mfg_tool.py -n 5 -cn "My bulb" -v 0xFFF2 -p 0x8001 --pai \ + -k path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Key.pem \ + -c path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Cert.pem \ + -cd path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/certification-declaration/Chip-Test-CD-FFF2-8001.der +``` + +### Generate factory partition using existing DAC certificate and private key [Optional arguments : `--dac-cert` and `--dac-key`] +``` +./mfg_tool.py -cn "My Bulb" -v 0xFFF2 -p 0x8001 --pai \ + -c path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Cert.pem \ + -cd path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/certification-declaration/Chip-Test-CD-FFF2-8001.der \ + --dac-key DAC_key.pem --dac-cert DAC_cert.pem +``` + +### Generate factory partitions using existing Passcode, Discriminator, and unique ID [Optional arguments : `--passcode`, `--discriminator`, and `--unique-id`] +``` +./mfg_tool.py -cn "My bulb" -v 0xFFF2 -p 0x8001 --pai \ + -k path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Key.pem \ + -c path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Cert.pem \ + -cd path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/certification-declaration/Chip-Test-CD-FFF2-8001.der \ + --passcode 20202021 --discriminator 3840 --unique-id d2f351f57bb9387445a5f92a601d1c14 +``` + +* NOTE: Script generates only one factory partition if **DAC or Discriminator or Passcode or Unique-ID** is specified. + +### Generate factory partitions with extra NVS key-values specified using csv and mcsv file [Optional arguments : `--csv` and `--mcsv`] +``` +./mfg_tool.py -cn "My bulb" -v 0xFFF2 -p 0x8001 --pai \ + -k path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Key.pem \ + -c path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Cert.pem \ + -cd path/to/esp-matter/connectedhomeip/connectedhomeip/credentials/test/certification-declaration/Chip-Test-CD-FFF2-8001.der \ + --csv extra_nvs_key_config.csv --mcsv extra_nvs_key_value.csv +``` +Above command will generate `n` number of partitions. Where `n` is the rows in the mcsv file. +Output binary contains all the chip specific key/value and key/values specified using `--csv` and `--mcsv` option. + + +## Flashing the manufacturing binary +Please note that `mfg_tool.py` only generates manufacturing binary images which need to be flashed onto device using `esptool.py`. + +`esptool.py` is present at path `$IDF_PATH/components/esptool_py/esptool/esptool.py` + +* Flashing a binary image to the device +``` +esptool.py -p write_flash
path/to/-partition.bin +``` diff --git a/tools/mfg_tool/chip_nvs_keys.py b/tools/mfg_tool/chip_nvs_keys.py new file mode 100644 index 000000000..b6df07068 --- /dev/null +++ b/tools/mfg_tool/chip_nvs_keys.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +# Copyright 2022 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This file contains the CHIP specific key along with the data type and encoding format +""" + +CHIP_KEY_MAP = { + 'chip-factory': { + 'discriminator': { + 'type': 'data', + 'encoding': 'u32', + }, + 'pin-code': { + 'type': 'data', + 'encoding': 'u32', + }, + 'iteration-count': { + 'type': 'data', + 'encoding': 'u32', + }, + 'salt': { + 'type': 'data', + 'encoding': 'string', + }, + 'verifier': { + 'type': 'data', + 'encoding': 'string', + }, + 'dac-cert': { + 'type': 'file', + 'encoding': 'binary', + }, + 'dac-key': { + 'type': 'file', + 'encoding': 'binary', + }, + 'dac-pub-key': { + 'type': 'file', + 'encoding': 'binary', + }, + 'pai-cert': { + 'type': 'file', + 'encoding': 'binary', + }, + 'cert-dclrn': { + 'type': 'file', + 'encoding': 'binary', + }, + }, + 'chip-config': { + 'unique-id': { + 'type': 'data', + 'encoding': 'string', + }, + }, +} + diff --git a/tools/mfg_tool/mfg_tool.py b/tools/mfg_tool/mfg_tool.py new file mode 100755 index 000000000..4754d1a05 --- /dev/null +++ b/tools/mfg_tool/mfg_tool.py @@ -0,0 +1,657 @@ +#!/usr/bin/env python3 + +# Copyright 2022 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Script to generate Matter factory NVS partition image, Onboarding codes, and QR codes. +""" + +import os +import sys +import csv +import uuid +import shutil +import random +import logging +import binascii +import argparse +import pyqrcode +import subprocess +import cryptography.hazmat.backends +import cryptography.x509 + +from chip_nvs_keys import CHIP_KEY_MAP + +if not os.getenv('IDF_PATH'): + logging.error("IDF_PATH environment variable is not set") + sys.exit(1) + +# TODO: Handle count > 100 case. spake2p gen-verifier doesn't support count > 100 + +TOOLS = { + 'spake2p' : None, + 'chip-cert' : None, + 'qrcodetool': None, + 'mfg_gen' : None, +} + +PAI = { + 'cert_pem': None, + 'cert_der': None, + 'key_pem' : None, + 'key_der' : None, +} + +OUT_DIR = { + 'top' : None, + 'chip': None, +} + +OUT_FILE = { + 'config_csv' : None, + 'chip_mcsv' : None, + 'mcsv' : None, + 'pin_csv': None, + 'pin_disc_csv': None, +} + +UNIQUE_ID_LEN = 16 # 16 bytes (128-bits) + +INVALID_PASSCODES = [ 00000000, 11111111, 22222222, 33333333, 44444444, 55555555, + 66666666, 77777777, 88888888, 99999999, 12345678, 87654321 ] + +UUIDs = list() + +def vid_pid_str(vid, pid): + return '_'.join([hex(vid)[2:], hex(pid)[2:]]) + +def disc_pin_str(discriminator, passcode): + return '_'.join([hex(discriminator)[2:], hex(passcode)[2:]]) + +def check_tools_exists(): + TOOLS['spake2p'] = shutil.which('spake2p') + if TOOLS['spake2p'] is None: + logging.error('spake2p not found, please add spake2p path to PATH environment variable') + sys.exit(1) + + TOOLS['chip-cert'] = shutil.which('chip-cert') + if TOOLS['chip-cert'] is None: + logging.error('chip-cert not found, please add chip-cert path to PATH environment variable') + sys.exit(1) + + TOOLS['qrcodetool'] = shutil.which('qrcodetool') + if TOOLS['qrcodetool'] is None: + logging.error('qrcodetool not found, please add qrcodetool path to PATH environment variable') + sys.exit(1) + + TOOLS['mfg_gen'] = os.sep.join([os.getenv('IDF_PATH'), 'tools', 'mass_mfg', 'mfg_gen.py']) + if not os.path.exists(TOOLS['mfg_gen']): + logging.error('mfg_gen.py not found, please make sure IDF_PATH environment variable is set correctly') + sys.exit(1) + + logging.debug('Using following tools:') + logging.debug('spake2p: {}'.format(TOOLS['spake2p'])) + logging.debug('chip-cert: {}'.format(TOOLS['chip-cert'])) + logging.debug('qrcodetool: {}'.format(TOOLS['qrcodetool'])) + logging.debug('mfg_gen: {}'.format(TOOLS['mfg_gen'])) + +def execute_cmd(cmd): + logging.debug('Executing Command: {}'.format(cmd)) + status = subprocess.run(cmd, capture_output=True) + + try: + status.check_returncode() + except subprocess.CalledProcessError as e: + if status.stderr: + logging.error('[stderr]: {}'.format(status.stderr.decode('utf-8').strip())) + logging.error('Command failed with error: {}'.format(e)) + sys.exit(1) + +def generate_passcodes(args): + iter_count_max = 10000 + salt_len_max = 32 + + cmd = [ + TOOLS['spake2p'], 'gen-verifier', + '--count', str(args.count), + '--iteration-count', str(iter_count_max), + '--salt-len', str(salt_len_max), + '--out', OUT_FILE['pin_csv'], + ] + + # If passcode is provided, use it + if (args.passcode): + cmd.extend(['--pin-code', str(args.passcode)]) + + execute_cmd(cmd) + +def generate_discriminators(args): + discriminators = list() + + # If discriminator is provided, use it + if args.discriminator: + discriminators.append(args.discriminator) + else: + for i in range(args.count): + discriminators.append(random.randint(0x0000, 0x0FFF)) + + return discriminators + +# Append discriminator to each line of the passcode file +def append_discriminator(discriminator): + with open(OUT_FILE['pin_csv'], 'r') as fd: + lines = fd.readlines() + + lines[0] = ','.join([lines[0].strip(), 'Discriminator']) + for i in range(1, len(lines)): + lines[i] = ','.join([lines[i].strip(), str(discriminator[i - 1])]) + + with open(OUT_FILE['pin_disc_csv'], 'w') as fd: + fd.write('\n'.join(lines) + '\n') + + os.remove(OUT_FILE['pin_csv']) + +# Generates the csv file containing chip specific keys and keys provided by user in csv file +def generate_config_csv(args): + logging.info("Generating Config CSV...") + + csv_data = '' + for k, v in CHIP_KEY_MAP.items(): + csv_data += k + ',' + 'namespace,' + '\n' + for k1, v1 in v.items(): + csv_data += k1 + ',' + v1['type'] + ',' + v1['encoding'] + '\n' + + # Read data from the user provided csv file + if args.csv: + with open(args.csv, 'r') as f: + csv_data += f.read() + + with open(OUT_FILE['config_csv'], 'w') as f: + f.write(csv_data) + +def write_chip_mcsv_header(args): + logging.info('Writing chip manifest CSV header...') + + keys = list() + for _, v in CHIP_KEY_MAP.items(): + for k1, _ in v.items(): + keys.append(k1) + + mcsv_header = ','.join(keys) + '\n' + + with open(OUT_FILE['chip_mcsv'], 'w') as f: + f.write(mcsv_header) + +def append_chip_mcsv_row(row_data, args): + logging.info('Appending chip master CSV row...') + + with open(OUT_FILE['chip_mcsv'], 'a') as f: + f.write(row_data + '\n') + +# Convert the certificate in PEM format to DER format +def convert_x509_cert_from_pem_to_der(pem_file, out_der_file): + with open(pem_file, 'rb') as f: + pem_data = f.read() + + pem_cert = cryptography.x509.load_pem_x509_certificate(pem_data, cryptography.hazmat.backends.default_backend()) + der_cert = pem_cert.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.DER) + + with open(out_der_file, 'wb') as f: + f.write(der_cert) + +# Generate the Public and Private key pair binaries +def generate_keypair_bin(pem_file, out_privkey_bin, out_pubkey_bin): + with open(pem_file, 'rb') as f: + pem_data = f.read() + + key_pem = cryptography.hazmat.primitives.serialization.load_pem_private_key(pem_data, None) + private_number_val = key_pem.private_numbers().private_value + public_number_x = key_pem.public_key().public_numbers().x + public_number_y = key_pem.public_key().public_numbers().y + public_key_first_byte = 0x04 + + with open(out_privkey_bin, 'wb') as f: + f.write(private_number_val.to_bytes(32, byteorder='big')) + + with open(out_pubkey_bin, 'wb') as f: + f.write(public_key_first_byte.to_bytes(1, byteorder='big')) + f.write(public_number_x.to_bytes(32, byteorder='big')) + f.write(public_number_y.to_bytes(32, byteorder='big')) + +def generate_pai(args, ca_key, ca_cert, out_key, out_cert): + cmd = [ + TOOLS['chip-cert'], 'gen-att-cert', + '--type', 'i', + '--subject-cn', '"{} PAI {}"'.format(args.subject_cn_prefix, '00'), + '--out-key', out_key, + '--out', out_cert, + ] + + if args.lifetime: + cmd.extend(['--lifetime', str(args.lifetime)]) + if args.valid_from: + cmd.extend(['--valid-from', str(args.valid_from)]) + + cmd.extend([ + '--subject-vid', hex(args.vendor_id)[2:], + '--subject-pid', hex(args.product_id)[2:], + '--ca-key', ca_key, + '--ca-cert', ca_cert, + ]) + + execute_cmd(cmd) + logging.info('Generated PAI certificate: {}'.format(out_cert)) + logging.info('Generated PAI private key: {}'.format(out_key)) + +def generate_dac(iteration, args, discriminator, passcode, ca_key, ca_cert): + out_key_pem = os.sep.join([OUT_DIR['top'], UUIDs[iteration], 'internal', 'DAC_key.pem']) + out_cert_pem = out_key_pem.replace('key.pem', 'cert.pem') + out_cert_der = out_key_pem.replace('key.pem', 'cert.der') + out_private_key_bin = out_key_pem.replace('key.pem', 'private_key.bin') + out_public_key_bin = out_key_pem.replace('key.pem', 'public_key.bin') + + cmd = [ + TOOLS['chip-cert'], 'gen-att-cert', + '--type', 'd', + '--subject-cn', '"{} DAC {}"'.format(args.subject_cn_prefix, iteration), + '--out-key', out_key_pem, + '--out', out_cert_pem, + ] + + if args.lifetime: + cmd.extend(['--lifetime', str(args.lifetime)]) + if args.valid_from: + cmd.extend(['--valid-from', str(args.valid_from)]) + + cmd.extend(['--subject-vid', hex(args.vendor_id)[2:], + '--subject-pid', hex(args.product_id)[2:], + '--ca-key', ca_key, + '--ca-cert', ca_cert, + ]) + + execute_cmd(cmd) + logging.info('Generated DAC certificate: {}'.format(out_cert_pem)) + logging.info('Generated DAC private key: {}'.format(out_key_pem)) + + convert_x509_cert_from_pem_to_der(out_cert_pem, out_cert_der) + logging.info('Generated DAC certificate in DER format: {}'.format(out_cert_der)) + + generate_keypair_bin(out_key_pem, out_private_key_bin, out_public_key_bin) + logging.info('Generated DAC private key in binary format: {}'.format(out_private_key_bin)) + logging.info('Generated DAC public key in binary format: {}'.format(out_public_key_bin)) + + return out_cert_der, out_private_key_bin, out_public_key_bin + +def use_dac_from_args(args): + logging.info('Using DAC from command line arguments...') + logging.info('DAC Certificate: {}'.format(args.dac_cert)) + logging.info('DAC Private Key: {}'.format(args.dac_key)) + + # There should be only one UUID in the UUIDs list if DAC is specified + out_cert_der = os.sep.join([OUT_DIR['top'], UUIDs[0], 'internal', 'DAC_cert.der']) + out_private_key_bin = out_cert_der.replace('cert.der', 'private_key.bin') + out_public_key_bin = out_cert_der.replace('cert.der', 'public_key.bin') + + convert_x509_cert_from_pem_to_der(args.dac_cert, out_cert_der) + logging.info('Generated DAC certificate in DER format: {}'.format(out_cert_der)) + + generate_keypair_bin(args.dac_key, out_private_key_bin, out_public_key_bin) + logging.info('Generated DAC private key in binary format: {}'.format(out_private_key_bin)) + logging.info('Generated DAC public key in binary format: {}'.format(out_public_key_bin)) + + return out_cert_der, out_private_key_bin, out_public_key_bin + +def setup_out_dirs(vid, pid, count): + OUT_DIR['top'] = os.sep.join(['out', vid_pid_str(vid, pid)]) + OUT_DIR['stage'] = os.sep.join(['out', vid_pid_str(vid, pid), 'staging']) + + os.makedirs(OUT_DIR['top'], exist_ok=True) + os.makedirs(OUT_DIR['stage'], exist_ok=True) + + OUT_FILE['config_csv'] = os.sep.join([OUT_DIR['stage'], 'config.csv']) + OUT_FILE['chip_mcsv'] = os.sep.join([OUT_DIR['stage'], 'chip_master.csv']) + OUT_FILE['mcsv'] = os.sep.join([OUT_DIR['stage'], 'master.csv']) + OUT_FILE['pin_csv'] = os.sep.join([OUT_DIR['stage'], 'pin.csv']) + OUT_FILE['pin_disc_csv'] = os.sep.join([OUT_DIR['stage'], 'pin_disc.csv']) + + # Create directories to store the generated files + for i in range(count): + uuid_str = str(uuid.uuid4()) + UUIDs.append(uuid_str) + os.makedirs(os.sep.join([OUT_DIR['top'], uuid_str, 'internal']), exist_ok=True) + +def generate_passcodes_and_discriminators(args): + # Generate passcodes using spake2p tool + generate_passcodes(args) + # Randomly generate discriminators + discriminators = generate_discriminators(args) + # Append discriminators to passcodes file + append_discriminator(discriminators) + +def setup_csv_files(args): + generate_config_csv(args) + write_chip_mcsv_header(args) + +def setup_root_certs(args): + # If PAA is passed as input, then generate PAI certificate + if args.paa: + # output file names + PAI['cert_pem'] = os.sep.join([OUT_DIR['stage'], 'pai_cert.pem']) + PAI['cert_der'] = os.sep.join([OUT_DIR['stage'], 'pai_cert.der']) + PAI['key_pem'] = os.sep.join([OUT_DIR['stage'], 'pai_key.pem']) + + generate_pai(args, args.key, args.cert, PAI['key_pem'], PAI['cert_pem']) + convert_x509_cert_from_pem_to_der(PAI['cert_pem'], PAI['cert_der']) + logging.info('Generated PAI certificate in DER format: {}'.format(PAI['cert_der'])) + + # If PAI is passed as input, generate DACs + elif args.pai: + PAI['cert_pem'] = args.cert + PAI['key_pem'] = args.key + PAI['cert_der'] = os.sep.join([OUT_DIR['stage'], 'pai_cert.der']) + + convert_x509_cert_from_pem_to_der(PAI['cert_pem'], PAI['cert_der']) + logging.info('Generated PAI certificate in DER format: {}'.format(PAI['cert_der'])) + + # No need to verify else block as validate_args() already checks for this + +# Generates DACs, add the required items to the master.csv file, and +# generates the onboarding data i.e. manual codes and QR codes and image +def generate_dacs_and_onb_data(args): + with open(OUT_FILE['pin_disc_csv'], 'r') as csvf: + pin_disc_dict = csv.DictReader(csvf) + + # If any key is added to chip_nvs_keys.py make sure to maintain that order below + for row in pin_disc_dict: + row_items = list() + + row_items.append(row['Discriminator']) + # TODO: remove PIN Code, spec says it should not be included in nvs storage + row_items.append(row['PIN Code']) + row_items.append(row['Iteration Count']) + row_items.append(row['Salt']) + row_items.append(row['Verifier']) + + if args.dac_key is not None and args.dac_cert is not None: + dacs = use_dac_from_args(args) + else: + dacs = generate_dac(int(row['Index']), args, int(row['Discriminator']), int(row['PIN Code']), PAI['key_pem'], PAI['cert_pem']) + + # This appends DAC cert, private key, and public key + row_items.extend([os.path.abspath(x) for x in dacs]) + + row_items.append(os.path.abspath(PAI['cert_der'])) + row_items.append(os.path.abspath(args.cert_dclrn)) + + # Unique-id + row_items.append(binascii.b2a_hex(os.urandom(UNIQUE_ID_LEN)).decode('utf-8')) + + mcsv_row_data = ','.join(row_items) + append_chip_mcsv_row(mcsv_row_data, args) + + # Generate onboarding data + generate_onboarding_data(args, int(row['Index']), int(row['Discriminator']), int(row['PIN Code'])) + +def merge_chip_mcsv_and_user_mcsv(args): + logging.info('Merging chip master CSV and user master CSV...') + + with open(OUT_FILE['chip_mcsv'], 'r') as f: + chip_mcsv_data = f.readlines() + + # If user mcsv is present, merge it with chip mcsv + if args.mcsv: + logging.info('User manifest CSV is present. Merging...') + + with open(args.mcsv, 'r') as f: + user_mcsv_data = f.readlines() + + if (len(chip_mcsv_data) != len(user_mcsv_data)): + logging.error('Chip and user mcsv files have different number of rows') + sys.exit(1) + + chip_mcsv_data = [','.join((c_line.strip(), u_line.strip())) + '\n' for c_line, u_line in zip(chip_mcsv_data, user_mcsv_data)] + + with open(OUT_FILE['mcsv'], 'w') as f: + f.write(''.join(chip_mcsv_data)) + + os.remove(OUT_FILE['chip_mcsv']) + +def organize_output_files(suffix): + for i in range(len(UUIDs)): + dest_path = os.sep.join([OUT_DIR['top'], UUIDs[i]]) + internal_path = os.sep.join([dest_path, 'internal']) + + replace = os.sep.join([OUT_DIR['top'], 'bin', '{}-{}.bin'.format(suffix, str(i + 1))]) + replace_with = os.sep.join([dest_path, '{}-partition.bin'.format(UUIDs[i])]) + os.rename(replace, replace_with) + + replace = os.sep.join([OUT_DIR['top'], 'csv', '{}-{}.csv'.format(suffix, str(i + 1))]) + replace_with = os.sep.join([internal_path, 'partition.csv']) + os.rename(replace, replace_with) + + # Also copy the PAI certificate to the output directory + shutil.copy2(PAI['cert_der'], os.sep.join([internal_path, 'PAI_cert.der'])) + + logging.info('Generated output files at: {}'.format(os.sep.join([OUT_DIR['top'], UUIDs[i]]))) + + os.rmdir(os.sep.join([OUT_DIR['top'], 'bin'])) + os.rmdir(os.sep.join([OUT_DIR['top'], 'csv'])) + +def generate_partitions(suffix, size): + cmd = [ + TOOLS['mfg_gen'], 'generate', + OUT_FILE['config_csv'], OUT_FILE['mcsv'], + suffix, hex(size), '--outdir', OUT_DIR['top'] + ] + execute_cmd(cmd) + +def get_setup_payload_data(args, discriminator, passcode): + data = 'version 0\n' + data += 'vendorID ' + str(args.vendor_id) + '\n' + data += 'productID ' + str(args.product_id) + '\n' + data += 'commissioningFlow ' + str(args.commissioning_flow) + '\n' + data += 'rendezVousInformation ' + str(1 << args.discovery_mode) + '\n' + data += 'discriminator ' + str(discriminator) + '\n' + data += 'setUpPINCode ' + str(passcode) + '\n' + return data + +def get_chip_qrcode(file): + data = subprocess.check_output([TOOLS['qrcodetool'], 'generate-qr-code', '-f', file]) + data = data.decode('utf-8').splitlines() + qrcodeline = data[-1].split(': ') + + if qrcodeline[1] == 'QR Code': + return qrcodeline[-1] + else: + logging.error('Failed to generate QR code') + return None + +def get_chip_manualcode(file): + data = subprocess.check_output([TOOLS['qrcodetool'], 'generate-manual-code', '-f', file]) + data = data.decode('utf-8').splitlines() + manualcodeline = data[-1].split(': ') + + if manualcodeline[1] == 'Manual Code': + return manualcodeline[-1] + else: + logging.error('Failed to generate manual code') + return None + +def generate_onboarding_data(args, index, discriminator, passcode): + setup_payload_file = os.sep.join(['', 'tmp', 'setup_payload_{}.txt'.format(args.vendor_id, args.product_id)]) + + with open(setup_payload_file, 'w') as f: + f.write(get_setup_payload_data(args, discriminator, passcode)) + + chip_qrcode = get_chip_qrcode(setup_payload_file) + chip_manualcode = get_chip_manualcode(setup_payload_file) + + logging.info('Generated QR code: ' + chip_qrcode) + logging.info('Generated manual code: ' + chip_manualcode) + + # TODO: Append commissioning flow, discovery mode, passcode, discriminator as well? + csv_data = 'qrcode,manualcode\n' + csv_data += chip_qrcode + ',' + chip_manualcode + '\n' + + onboarding_data_file = os.sep.join([OUT_DIR['top'], UUIDs[index], '{}-onb_codes.csv'.format(UUIDs[index])]) + with open(onboarding_data_file, 'w') as f: + f.write(csv_data) + + # Create QR code image as mentioned in the spec + qrcode_file = os.sep.join([OUT_DIR['top'], UUIDs[index], '{}-qrcode.png'.format(UUIDs[index])]) + chip_qr = pyqrcode.create(chip_qrcode, version=2, error='M') + chip_qr.png(qrcode_file, scale=6) + + os.remove(setup_payload_file) + logging.info('Generated onboarding data and QR Code') + +def validate_args(args): + # csv and mcsv both should present or none + if (args.csv is not None) != (args.mcsv is not None): + logging.error("csv and mcsv should be both present or none") + sys.exit(1) + else: + # Read the number of lines in mcsv file + if args.mcsv is not None: + with open(args.mcsv, 'r') as f: + lines = sum(1 for line in f) + + # Subtract 1 for the header line + args.count = lines - 1 + + # Validate the passcode + if args.passcode is not None: + if ((args.passcode < 0x0000001 and args.passcode > 0x5F5E0FE) or (args.passcode in INVALID_PASSCODES)): + logging.error('Invalid passcode' + str(args.passcode)) + sys.exit(1) + + # Validate the discriminator + if (args.discriminator is not None) and (args.discriminator not in range(0x0000, 0x0FFF)): + logging.error('Invalid discriminator ' + str(args.discriminator)) + sys.exit(1) + + if args.unique_id is not None: + if len(args.unique_id) < (UNIQUE_ID_LEN * 2): + logging.error('Unique ID is too short') + sys.exit(1) + + if (len(args.unique_id) % 2) != 0: + logging.error('Unique ID is not even length') + sys.exit(1) + + # Check for a valid hex string + try: + int(args.unique_id, 16) + except ValueError: + logging.error('Unique ID is not a valid hex string') + sys.exit(1) + + # DAC key and DAC cert both should be present or none + if (args.dac_key is not None) != (args.dac_cert is not None): + logging.error("dac_key and dac_cert should be both present or none") + sys.exit(1) + else: + # Make sure PAI certificate is present if DAC is present + if (args.dac_key is not None) and (args.pai is False): + logging.error('Please provide PAI certificate along with DAC certificate and DAC key') + sys.exit(1) + + # If unique_id/discriminator/passcode/DAC is present then we are restricting + # the number of partitions to 1 + if (args.discriminator is not None + or args.passcode is not None + or args.unique_id is not None + or args.dac_key is not None): + if args.count > 1: + logging.error('Number of partitions should be 1 when unique_id or discriminator or passcode or DAC is present') + sys.exit(1) + + # Validate the input certificate type, if DAC is not present + if args.dac_key is None and args.dac_cert is None: + if args.paa: + logging.info('Input Root certificate type PAA') + elif args.pai: + logging.info('Input Root certificate type PAI') + else: + logging.error('Either PAA or PAI certificate is required') + sys.exit(1) + + # Check if Key and certificate are present + if args.key is None or args.cert is None: + logging.error('PAA key and certificate are required') + sys.exit(1) + + logging.info('Number of manufacturing NVS images to generate: {}'.format(args.count)) + +def get_args(): + def any_base_int(s): return int(s, 0) + + parser = argparse.ArgumentParser(description='Manufacuring partition generator tool') + parser.add_argument('-n', '--count', type=any_base_int, default=1, help='The number of manufacturing partition binaries to generate. Default is 1. If --csv and --mcsv are present, the number of lines in the mcsv file is used.') + parser.add_argument('-s', '--size', type=any_base_int, default=0x6000, help='The size of manufacturing partition binaries to generate. Default is 0x6000.') + parser.add_argument('-cn', '--subject-cn-prefix', type=str, default='ESP32', help='The common name prefix of the subject of the generated certificate.') + + parser.add_argument('-v', '--vendor-id', type=any_base_int, required=True, help='The vendor ID.') + parser.add_argument('-p', '--product-id', type=any_base_int, required=True, help='The product ID.') + + input_cert_group = parser.add_mutually_exclusive_group(required=True) + input_cert_group.add_argument('--paa', action='store_true', help='Use input certificate as PAA certificate.') + input_cert_group.add_argument('--pai', action='store_true', help='Use input certificate as PAI certificate.') + + # If DAC is present then PAI key is not required, so it is marked as not required here + # but, if DAC is not present then PAI key is required and that case is validated in validate_args() + parser.add_argument('-c', '--cert', type=str, required=True, help='The input certificate file in PEM format.') + parser.add_argument('-k', '--key', type=str, required=False, help='The input key file in PEM format.') + + parser.add_argument('-cd', '--cert-dclrn', type=str, required=True, help='The certificate declaration file in DER format.') + + parser.add_argument('--dac-cert', type=str, help='The input DAC certificate file in PEM format.') + parser.add_argument('--dac-key', type=str, help='The input DAC private key file in PEM format.'); + + parser.add_argument('-lt', '--lifetime', default=4294967295, type=any_base_int, help='Lifetime of the generated certificate. Default is 4294967295 if not specified, this indicate that certificate does not have well defined expiration date.') + parser.add_argument('-vf', '--valid-from', type=str, help='The start date for the certificate validity period in format --
[ :: ]. Default is current date.') + + parser.add_argument('--passcode', type=any_base_int, help='The passcode for pairing. Randomly generated if not specified.') + parser.add_argument('--discriminator', type=any_base_int, help='The discriminator for pairing. Randomly generated if not specified.') + + parser.add_argument('-cf', '--commissioning-flow', type=any_base_int, default=0, help='Device commissioning flow, 0:Standard, 1:User-Intent, 2:Custom. Default is 0.', choices=[0, 1, 2]) + parser.add_argument('-dm', '--discovery-mode', type=any_base_int, default=1, help='Commissionable device discovery netowrking technology. 0:WiFi-SoftAP, 1:BLE, 2:On-network. Default is BLE.', choices=[0, 1, 2]) + + parser.add_argument('--csv', type=str, help='CSV file containing the partition schema for extra options. [REF: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/mass_mfg.html#csv-configuration-file]') + parser.add_argument('--mcsv', type=str, help='Master CSV file containig optional/extra values specified by the user. [REF: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/storage/mass_mfg.html#master-value-csv-file]') + + parser.add_argument('-u', '--unique-id', type=str, help='Unique Identifier for the device. This should be at least 128-bit long. If not specified, a random unique id is generated.') + + return parser.parse_args() + +def main(): + logging.basicConfig(format='[%(asctime)s] [%(levelname)7s] - %(message)s', level=logging.INFO) + + args = get_args() + validate_args(args) + + check_tools_exists() + + setup_out_dirs(args.vendor_id, args.product_id, args.count) + generate_passcodes_and_discriminators(args) + setup_csv_files(args) + setup_root_certs(args) + generate_dacs_and_onb_data(args) + merge_chip_mcsv_and_user_mcsv(args) + generate_partitions('matter_partition', args.size) + organize_output_files('matter_partition') + +if __name__ == "__main__": + main() diff --git a/tools/mfg_tool/requirements.txt b/tools/mfg_tool/requirements.txt new file mode 100644 index 000000000..ef6ce14e3 --- /dev/null +++ b/tools/mfg_tool/requirements.txt @@ -0,0 +1,6 @@ +cryptography==36.0.2 +cffi==1.15.0 +future==0.18.2 +pycparser==2.21 +pypng==0.0.21 +PyQRCode==1.2.1