diff --git a/config/esp32/components/chip/CMakeLists.txt b/config/esp32/components/chip/CMakeLists.txt index a3ad2d9c438619..34910753c7572e 100644 --- a/config/esp32/components/chip/CMakeLists.txt +++ b/config/esp32/components/chip/CMakeLists.txt @@ -30,6 +30,7 @@ if(NOT CHIP_ROOT) endif() include(${CMAKE_CURRENT_LIST_DIR}/ota-image.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/factory.cmake) set(CHIP_REQUIRE_COMPONENTS esp_eth freertos lwip bt mbedtls fatfs app_update console openthread nvs_flash spi_flash) @@ -521,6 +522,11 @@ if(CONFIG_ENABLE_PW_RPC) endforeach() endif() +if(CONFIG_ENABLE_BUILD_TIME_PARTITION_SCRIPT) + generate_build_time_partition(fctry esp_secure_cert ${CHIP_ROOT} FLASH_IN_PROJECT) + add_dependencies(build_time_partition app) +endif() + # Build Matter OTA image if (CONFIG_CHIP_OTA_IMAGE_BUILD) chip_ota_image(chip-ota-image diff --git a/config/esp32/components/chip/Kconfig b/config/esp32/components/chip/Kconfig index 8ba2c1b3986e15..53b20fa21cb6cc 100644 --- a/config/esp32/components/chip/Kconfig +++ b/config/esp32/components/chip/Kconfig @@ -883,7 +883,7 @@ menu "CHIP Device Layer" NVS namespace. If this option is enabled, the application can use an API to set a CD, the configured CD will be used for subsequent CD reads. - config ENABLE_ESP_INSIGHTS_TRACE + config ENABLE_ESP_INSIGHTS_TRACE bool "Enable Matter ESP Insights" depends on ESP_INSIGHTS_ENABLED default y @@ -892,12 +892,21 @@ menu "CHIP Device Layer" Enabling the above option will enable the esp32 specific tracing functionality and report the diagnostic information to the insights cloud. - config ENABLE_ESP_INSIGHTS_SYSTEM_STATS + config ENABLE_ESP_INSIGHTS_SYSTEM_STATS bool "Enable System Stats for insights" depends on ESP_INSIGHTS_ENABLED default n help This option enables the system statistics to be sent to the insights cloud. + + config ENABLE_BUILD_TIME_PARTITION_SCRIPT + bool "Enable script to build fctry and esp_secure_cert partition in during build time." + default n + help + Enables the cmake script to generate and map the factory parition bin and esp-secure-cert + partition bin to their corresponding addresses specified in partitions.csv and flash the partitons + in a single idf.py flash command. + endmenu diff --git a/config/esp32/components/chip/factory.cmake b/config/esp32/components/chip/factory.cmake new file mode 100644 index 00000000000000..e42764c173608a --- /dev/null +++ b/config/esp32/components/chip/factory.cmake @@ -0,0 +1,122 @@ +# +# Copyright (c) 2023 Project CHIP Authors +# +# 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. +# + +function(generate_build_time_partition fctry_partition esp_secure_cert_partition chip_root) + set(options FLASH_IN_PROJECT) + set(multi DEPENDS) + cmake_parse_arguments(arg "${options}" "" "${multi}" "${ARGN}") + get_filename_component(chip_root_abs_path ${chip_root} ABSOLUTE) + + set(generate_esp32_chip_factory_bin.py ${PYTHON} ${chip_root}/scripts/tools/generate_esp32_chip_factory_bin.py) + + partition_table_get_partition_info(fctry_partition_size "--partition-name ${fctry_partition}" "size") + partition_table_get_partition_info(fctry_partition_offset "--partition-name ${fctry_partition}" "offset") + + partition_table_get_partition_info(secure_cert_partition_size "--partition-name ${esp_secure_cert_partition}" "size") + partition_table_get_partition_info(secure_cert_partition_offset "--partition-name ${esp_secure_cert_partition}" "offset") + + message(STATUS "fctry_partition_size : ${fctry_partition_size}") + message(STATUS "fctry_partition_offset : ${fctry_partition_offset}") + message(STATUS "secure_cert_partition_size : ${secure_cert_partition_size}") + message(STATUS "secure_cert_partition_offset : ${secure_cert_partition_offset}") + + if("${fctry_partition_size}" AND "${fctry_partition_offset}") + set(DEFAULT_DEVICE_NAME "My bulb") + set(DEFAULT_VENDOR_NAME "Test-vendor") + set(DEFAULT_HARDWARE_VERSION 1) + set(DEFAULT_HARDWARE_VERSION_STR "Devkit") + set(DEFAULT_VENDOR_ID 0xFFF2) + set(DEFAULT_PRODUCT_ID 0x8001) + set(DEFAULT_DAC_CERT "${chip_root_abs_path}/credentials/test/attestation/Chip-Test-DAC-FFF2-8001-0008-Cert.der") + set(DEFAULT_DAC_KEY "${chip_root_abs_path}/credentials/test/attestation/Chip-Test-DAC-FFF2-8001-0008-Key.der") + set(DEFAULT_PAI_CERT "${chip_root_abs_path}/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Cert.der") + set(DEFAULT_CERT_DCLRN "${chip_root_abs_path}/credentials/test/certification-declaration/Chip-Test-CD-FFF2-8001.der") + set(DEFAULT_PASSCODE 20202020) + set(DEFAULT_DISCRIMINATOR 3841) + + set(DEVICE_NAME ${DEFAULT_DEVICE_NAME} CACHE STRING "My bulb") + set(VENDOR_NAME ${DEFAULT_VENDOR_NAME} CACHE STRING "Test-vendor") + set(HARDWARE_VERSION ${DEFAULT_HARDWARE_VERSION} CACHE STRING 1) + set(HARDWARE_VERSION_STR ${DEFAULT_HARDWARE_VERSION_STR} CACHE STRING "Devkit") + set(VENDOR_ID ${DEFAULT_VENDOR_ID} CACHE STRING 0xFFF2) + set(PRODUCT_ID ${DEFAULT_PRODUCT_ID} CACHE STRING 0x8001) + set(DAC_CERT ${DEFAULT_DAC_CERT} CACHE STRING "${chip_root_abs_path}/credentials/test/attestation/Chip-Test-DAC-FFF2-8001-0008-Cert.der") + set(DAC_KEY ${DEFAULT_DAC_KEY} CACHE STRING "${chip_root_abs_path}/credentials/test/attestation/Chip-Test-DAC-FFF2-8001-0008-Key.der") + set(PAI_CERT ${DEFAULT_PAI_CERT} CACHE STRING "${chip_root_abs_path}/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Cert.der") + set(CERT_DCLRN ${DEFAULT_CERT_DCLRN} CACHE STRING "${chip_root_abs_path}/credentials/test/certification-declaration/Chip-Test-CD-FFF2-8001.der") + set(PASSCODE ${DEFAULT_PASSCODE} CACHE STRING 20202020) + set(DISCRIMINATOR ${DEFAULT_DISCRIMINATOR} CACHE STRING 3841) + + message(STATUS "Bulb Name: ${DEVICE_NAME}") + message(STATUS "Vendor Name: ${VENDOR_NAME}") + message(STATUS "Hardware Version: ${HARDWARE_VERSION}") + message(STATUS "Hardware Version String: ${HARDWARE_VERSION_STR}") + message(STATUS "Vendor ID: ${VENDOR_ID}") + message(STATUS "Product ID: ${PRODUCT_ID}") + message(STATUS "DAC Cert: ${DAC_CERT}") + message(STATUS "DAC Key: ${DAC_KEY}") + message(STATUS "PAI Cert: ${PAI_CERT}") + message(STATUS "Certification Declaration: ${CERT_DCLRN}") + message(STATUS "Passcode: ${PASSCODE}") + message(STATUS "Discriminator: ${DISCRIMINATOR}") + + + # Execute Factory partition image generation; this always executes as there is no way to specify for CMake to watch for + # contents of the base dir changing. + add_custom_target(build_time_partition ALL + COMMAND ${generate_esp32_chip_factory_bin.py} -d ${DISCRIMINATOR} + -p ${PASSCODE} + --product-name "${DEVICE_NAME}" + --vendor-name "${VENDOR_NAME}" + --vendor-id ${VENDOR_ID} + --product-id ${PRODUCT_ID} + --hw-ver ${HARDWARE_VERSION} + --hw-ver-str "${HARDWARE_VERSION_STR}" + --dac-cert ${DAC_CERT} + --dac-key ${DAC_KEY} + --pai-cert ${PAI_CERT} + --cd ${CERT_DCLRN} + --dac-in-secure-cert + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + + + set(factory_partition_bin ${CMAKE_BINARY_DIR}/bin/factory_partition.bin) + set(esp_secure_cert_partition_bin ${CMAKE_BINARY_DIR}/bin/esp_secure_cert_partititon.bin) + idf_component_get_property(main_args esptool_py FLASH_ARGS) + idf_component_get_property(sub_args esptool_py FLASH_SUB_ARGS) + + esptool_py_flash_target(${fctry_partition}-flash "${main_args}" "${sub_args}" ALWAYS_PLAINTEXT) + esptool_py_flash_to_partition(${fctry_partition}-flash "${fctry_partition}" "${factory_partition_bin}") + + esptool_py_flash_target(${esp_secure_cert_partition}-flash "${main_args}" "${sub_args}" ALWAYS_PLAINTEXT) + esptool_py_flash_to_partition(${esp_secure_cert_partition}-flash "${esp_secure_cert_partition}" "${esp_secure_cert_partition_bin}") + + add_dependencies(${fctry_partition}-flash build_time_partition) + add_dependencies(${esp_secure_cert_partition}-flash build_time_partition) + + if(arg_FLASH_IN_PROJECT) + esptool_py_flash_to_partition(flash "${fctry_partition}" "${factory_partition_bin}") + esptool_py_flash_to_partition(flash "${esp_secure_cert_partition}" "${esp_secure_cert_partition_bin}") + add_dependencies(flash build_time_partition) + endif() + else() + set(message "Failed to create Factory partition image for partition '${partition}'. " + "Check project configuration if using the correct partition table file.") + fail_at_build_time(factory_${partition}_bin "${message}") + + endif() +endfunction() diff --git a/scripts/setup/requirements.esp32.txt b/scripts/setup/requirements.esp32.txt index d0d6d924ff8ddb..955ea52195d13f 100644 --- a/scripts/setup/requirements.esp32.txt +++ b/scripts/setup/requirements.esp32.txt @@ -9,6 +9,8 @@ reedsolo>=1.5.3,<=1.5.4 bitstring>=3.1.6,<4 ecdsa>=0.16.0 construct==2.10.54 +pypng==0.0.21 +PyQRCode==1.2.1 python-socketio<5 itsdangerous<2.1 ; python_version < "3.11" esp_idf_monitor==1.1.1 diff --git a/scripts/tools/generate_esp32_chip_factory_bin.py b/scripts/tools/generate_esp32_chip_factory_bin.py index 37ef6beac3cfb3..e1cb65b1b99c0d 100755 --- a/scripts/tools/generate_esp32_chip_factory_bin.py +++ b/scripts/tools/generate_esp32_chip_factory_bin.py @@ -18,12 +18,16 @@ import argparse import base64 +import csv import enum +import hashlib +import json import logging import os +import pyqrcode import sys -from types import SimpleNamespace +from types import SimpleNamespace import cryptography.x509 from bitarray import bitarray from bitarray.util import ba2int @@ -32,6 +36,8 @@ CHIP_TOPDIR = os.path.dirname(os.path.realpath(__file__))[:-len(os.path.join('scripts', 'tools'))] sys.path.insert(0, os.path.join(CHIP_TOPDIR, 'scripts', 'tools', 'spake2p')) from spake2p import generate_verifier # noqa: E402 isort:skip +sys.path.insert(0,os.path.join(CHIP_TOPDIR, 'src', 'setup_payload','python')) +from generate_setup_payload import SetupPayload, CommissioningFlow if os.getenv('IDF_PATH'): sys.path.insert(0, os.path.join(os.getenv('IDF_PATH'), @@ -48,11 +54,13 @@ TOOLS = {} - FACTORY_PARTITION_CSV = 'nvs_partition.csv' FACTORY_PARTITION_BIN = 'factory_partition.bin' NVS_KEY_PARTITION_BIN = 'nvs_key_partition.bin' ESP_SECURE_CERT_PARTITION_BIN = 'esp_secure_cert_partititon.bin' +CONFIG_FILE = 'config.json' +ONBOARDING_DATA_FILE = 'onboarding_codes.csv' +QROCDE_FILE = 'qrcode.png' FACTORY_DATA = { # CommissionableDataProvider @@ -167,6 +175,25 @@ } +def save_config(args): + with open(CONFIG_FILE, 'w') as config_file: + json.dump(vars(args), config_file) + +def load_config(): + try: + with open(CONFIG_FILE, 'r') as config_file: + return json.load(config_file) + except FileNotFoundError: + return None + +def calculate_hash(data): + return hashlib.sha256(data.encode('utf-8')).hexdigest() + + +def args_changed(current_args, saved_args): + return calculate_hash(json.dumps(vars(current_args))) != calculate_hash(json.dumps(saved_args)) + + class CalendarTypes(enum.Enum): Buddhist = 0 Chinese = 1 @@ -602,6 +629,12 @@ def any_base_int(s): return int(s, 0) help='Do not generate the factory partition binary') parser.add_argument('--output_dir', type=str, default='bin', help='Created image output file path') + 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 networking technology. \ + 0:WiFi-SoftAP, 1:BLE, 2:On-network. Default is BLE.', choices=[0, 1, 2]) parser.set_defaults(generate_bin=True) return parser.parse_args() @@ -632,13 +665,42 @@ def generate_factory_partiton_binary(args): def set_up_out_dirs(args): os.makedirs(args.output_dir, exist_ok=True) +def generate_onboarding_data(args): + payloads = SetupPayload(args.discriminator, args.passcode, args.discovery_mode, CommissioningFlow(args.commissioning_flow), + args.vendor_id, args.product_id) + logging.info('Discovery mode' + str(args.discovery_mode)) + chip_qrcode = payloads.generate_qrcode() + chip_manualcode = payloads.generate_manualcode() + # ToDo: remove this if qrcode tool can handle the standard manual code format + if args.commissioning_flow == CommissioningFlow.Standard: + chip_manualcode = chip_manualcode[:4] + '-' + chip_manualcode[4:7] + '-' + chip_manualcode[7:] + else: + chip_manualcode = '"' + chip_manualcode[:4] + '-' + chip_manualcode[4:7] + '-' + chip_manualcode[7:11] + '\n' + chip_manualcode[11:15] + '-' + chip_manualcode[15:18] + '-' + chip_manualcode[18:20] + '-' + chip_manualcode[20:21] + '"' + + logging.info('Generated QR code: ' + chip_qrcode) + logging.info('Generated manual code: ' + chip_manualcode) + + csv_data = 'qrcode,manualcode,discriminator,passcode\n' + csv_data += chip_qrcode + ',' + chip_manualcode + ',' + str(args.discriminator) + ',' + str(args.passcode) + '\n' + + with open(os.path.join(args.output_dir,ONBOARDING_DATA_FILE), 'w') as f: + f.write(csv_data) + + chip_qr = pyqrcode.create(chip_qrcode, version=2, error='M') + chip_qr.png(os.path.join(args.output_dir,QROCDE_FILE), scale=6) + def main(): + saved_args = load_config() args = get_args() - set_up_out_dirs(args) - set_up_factory_data(args) - generate_factory_partiton_binary(args) - + if saved_args is None or args_changed(args, saved_args): + set_up_out_dirs(args) + set_up_factory_data(args) + generate_factory_partiton_binary(args) + generate_onboarding_data(args) + save_config(args) + else: + logging.info("No changes in arguments. Skipping partition generation.") if __name__ == "__main__": logging.basicConfig(format='[%(asctime)s] [%(levelname)7s] - %(message)s', level=logging.INFO)