From 43576921917ec414fd2a9bccf6ac6c121ff76298 Mon Sep 17 00:00:00 2001 From: Maciej Bojczuk Date: Wed, 4 Jan 2023 17:54:02 +0100 Subject: [PATCH] [Telink] Change factory data generator utility (#24213) * [Telink] Manufacturing Partition Generator Utility This tool can be used to generate factory data for more bunch of devices. Signed-off-by: Maciej Bojczuk * [Telink] Add additional python requirements MFG tool require some additional python modules like: * future * pypng * PyQRCode Signed-off-by: Maciej Bojczuk * [Telink] MFG tool integration with build system Use new tool for factory data generation during building when below config is set: CONFIG_CHIP_FACTORY_DATA=y CONFIG_CHIP_FACTORY_DATA_BUILD=y Signed-off-by: Maciej Bojczuk * [Telink] Remove old, not needed scripts Remove old, not needed python scripts for factory data generation. Signed-off-by: Maciej Bojczuk Signed-off-by: Maciej Bojczuk --- config/telink/chip-module/CMakeLists.txt | 2 +- .../chip-module/generate_factory_data.cmake | 159 ++-- scripts/requirements.telink.txt | 3 + scripts/requirements.txt | 3 + .../generate_telink_chip_factory_data.py | 493 ------------ scripts/tools/telink/mfg_tool.py | 755 ++++++++++++++++++ scripts/tools/telink/readme.md | 212 +++++ scripts/tools/telink/requirements.txt | 6 + .../tools/telink/telink_factory_data.schema | 164 ---- .../tools/telink/telink_generate_partition.py | 169 ---- 10 files changed, 1041 insertions(+), 925 deletions(-) create mode 100644 scripts/requirements.telink.txt delete mode 100644 scripts/tools/telink/generate_telink_chip_factory_data.py create mode 100644 scripts/tools/telink/mfg_tool.py create mode 100644 scripts/tools/telink/readme.md create mode 100644 scripts/tools/telink/requirements.txt delete mode 100644 scripts/tools/telink/telink_factory_data.schema delete mode 100644 scripts/tools/telink/telink_generate_partition.py diff --git a/config/telink/chip-module/CMakeLists.txt b/config/telink/chip-module/CMakeLists.txt index 6113325da57f95..2328ef28c13893 100644 --- a/config/telink/chip-module/CMakeLists.txt +++ b/config/telink/chip-module/CMakeLists.txt @@ -327,7 +327,7 @@ endif() if (CONFIG_CHIP_FACTORY_DATA_MERGE_WITH_FIRMWARE) add_custom_target(merge_factory_data ALL COMMAND - dd if=${PROJECT_BINARY_DIR}/factory_data.bin of=${PROJECT_BINARY_DIR}/zephyr.bin bs=1024 seek=2000 + dd if=${PROJECT_BINARY_DIR}/factory/factory_data.bin of=${PROJECT_BINARY_DIR}/zephyr.bin bs=1024 seek=2000 ) if (CONFIG_CHIP_OTA_IMAGE_BUILD) add_dependencies(merge_factory_data merge_mcuboot) diff --git a/config/telink/chip-module/generate_factory_data.cmake b/config/telink/chip-module/generate_factory_data.cmake index a1c9f0ef9780a0..6b7681defc8224 100644 --- a/config/telink/chip-module/generate_factory_data.cmake +++ b/config/telink/chip-module/generate_factory_data.cmake @@ -15,49 +15,63 @@ # -# Create a JSON file based on factory data given via kConfigs. +# Create a binary file with factory data given via kConfigs. # -# This function creates a list of arguments for external script and then run it to write a JSON file. -# Created JSON file can be checked using JSON SCHEMA file if it is provided. +# This function creates a list of arguments for external script and then run it to write a factory data file. # # This script can be manipulated using following kConfigs: # - To merge generated factory data with final zephyr.hex file set kConfig CONFIG_CHIP_FACTORY_DATA_MERGE_WITH_FIRMWARE=y # - To use default certification paths set CONFIG_CHIP_FACTORY_DATA_USE_DEFAULTS_CERTS_PATH=y # -# During generation process a some file will be created in zephyr's build directory: -# - .json a file containing all factory data written in JSON format. +# During generation process a some file will be created in zephyr's build directory under factory subdirectory: +# - factory_data.bin +# - factory_data.hex +# - DAC_cert.der +# - DAC_cert.pem +# - DAC_key.pem +# - DAC_private_key.bin +# - DAC_public_key.bin +# - pai_cert.der +# - onb_codes.csv +# - pin_disc.csv +# - qrcode.png +# - summary.json # # [Args]: # factory_data_target - a name for target to generate factory_data. # script_path - a path to script that makes a JSON factory data file from given arguments. -# schema_path - a path to JSON schema file which can be used to verify generated factory data JSON file. -# This argument is optional, if you don't want to verify the JSON file put it empty "". # output_path - a path to output directory, where created JSON file will be stored. -function(telink_create_factory_data_json factory_data_target script_path schema_path output_path) +function(telink_create_factory_data_json factory_data_target script_path output_path) # set script args for future purpose set(script_args) ## generate all script arguments -string(APPEND script_args "--sn \"${CONFIG_CHIP_DEVICE_SERIAL_NUMBER}\"\n") -string(APPEND script_args "--date \"${CONFIG_CHIP_DEVICE_MANUFACTURING_DATE}\"\n") -string(APPEND script_args "--vendor_id ${CONFIG_CHIP_DEVICE_VENDOR_ID}\n") -string(APPEND script_args "--product_id ${CONFIG_CHIP_DEVICE_PRODUCT_ID}\n") -string(APPEND script_args "--vendor_name \"${CONFIG_CHIP_DEVICE_VENDOR_NAME}\"\n") -string(APPEND script_args "--product_name \"${CONFIG_CHIP_DEVICE_PRODUCT_NAME}\"\n") -string(APPEND script_args "--hw_ver ${CONFIG_CHIP_DEVICE_HARDWARE_VERSION}\n") -string(APPEND script_args "--hw_ver_str \"${CONFIG_CHIP_DEVICE_HARDWARE_VERSION_STRING}\"\n") +string(APPEND script_args "--serial-num \"${CONFIG_CHIP_DEVICE_SERIAL_NUMBER}\"\n") +string(APPEND script_args "--mfg-date \"${CONFIG_CHIP_DEVICE_MANUFACTURING_DATE}\"\n") +string(APPEND script_args "--vendor-id ${CONFIG_CHIP_DEVICE_VENDOR_ID}\n") +string(APPEND script_args "--product-id ${CONFIG_CHIP_DEVICE_PRODUCT_ID}\n") +string(APPEND script_args "--vendor-name \"${CONFIG_CHIP_DEVICE_VENDOR_NAME}\"\n") +string(APPEND script_args "--product-name \"${CONFIG_CHIP_DEVICE_PRODUCT_NAME}\"\n") +string(APPEND script_args "--hw-ver ${CONFIG_CHIP_DEVICE_HARDWARE_VERSION}\n") +string(APPEND script_args "--hw-ver-str \"${CONFIG_CHIP_DEVICE_HARDWARE_VERSION_STRING}\"\n") +string(APPEND script_args "--overwrite\n") +string(APPEND script_args "--in-tree\n") # check if Rotating Device Id Unique Id should be generated if(NOT CONFIG_CHIP_DEVICE_GENERATE_ROTATING_DEVICE_UID) if(NOT DEFINED CONFIG_CHIP_DEVICE_ROTATING_DEVICE_UID) message(FATAL_ERROR "CHIP_DEVICE_ROTATING_DEVICE_UID was not provided. To generate it use CONFIG_CHIP_DEVICE_GENERATE_ROTATING_DEVICE_UID=y") else() - string(APPEND script_args "--rd_uid \"${CONFIG_CHIP_DEVICE_ROTATING_DEVICE_UID}\"\n") + string(APPEND script_args "--rd-id-uid \"${CONFIG_CHIP_DEVICE_ROTATING_DEVICE_UID}\"\n") endif() else() - string(APPEND script_args "--generate_rd_uid\n") + string(APPEND script_args "--enable-rotating-device-id\n") endif() +# find chip cert tool +find_program(chip_cert_exe NAMES chip-cert REQUIRED) +string(APPEND script_args "--chip-cert-path ${chip_cert_exe}\n") + # for development purpose user can use default certs instead of generating or providing them if(CONFIG_CHIP_FACTORY_DATA_USE_DEFAULT_CERTS) # convert decimal PID to its hexadecimal representation to find out certification files in repository @@ -66,99 +80,57 @@ if(CONFIG_CHIP_FACTORY_DATA_USE_DEFAULT_CERTS) string(TOUPPER ${raw_pid} raw_pid_upper) # all certs are located in ${CHIP_ROOT}/credentials/development/attestation # it can be used during development without need to generate new certifications - string(APPEND script_args "--dac_cert \"${CHIP_ROOT}/credentials/development/attestation/Matter-Development-DAC-${raw_pid_upper}-Cert.der\"\n") - string(APPEND script_args "--dac_key \"${CHIP_ROOT}/credentials/development/attestation/Matter-Development-DAC-${raw_pid_upper}-Key.der\"\n") - string(APPEND script_args "--pai_cert \"${CHIP_ROOT}/credentials/development/attestation/Matter-Development-PAI-noPID-Cert.der\"\n") + string(APPEND script_args "--dac-cert \"${CHIP_ROOT}/credentials/development/attestation/Matter-Development-DAC-${raw_pid_upper}-Cert.pem\"\n") + string(APPEND script_args "--dac-key \"${CHIP_ROOT}/credentials/development/attestation/Matter-Development-DAC-${raw_pid_upper}-Key.pem\"\n") + string(APPEND script_args "--cert \"${CHIP_ROOT}/credentials/development/attestation/Matter-Development-PAI-noPID-Cert.pem\"\n") + string(APPEND script_args "--key \"${CHIP_ROOT}/credentials/development/attestation/Matter-Development-PAI-noPID-Key.pem\"\n") + string(APPEND script_args "-cd \"${CHIP_ROOT}/credentials/development/cd-certs/Chip-Test-CD-Cert.der\"\n") + string(APPEND script_args "--pai\n") else() - find_program(chip_cert_exe NAMES chip-cert REQUIRED) - string(APPEND script_args "--gen_cd\n") - string(APPEND script_args "--chip_cert_path ${chip_cert_exe}\n") + # generate PAI and DAC certs + string(APPEND script_args "--cert \"${CHIP_ROOT}/credentials/test/attestation/Chip-Test-PAA-NoVID-Cert.pem\"\n") + string(APPEND script_args "--key \"${CHIP_ROOT}/credentials/test/attestation/Chip-Test-PAA-NoVID-Key.pem\"\n") + string(APPEND script_args "-cd \"${CHIP_ROOT}/credentials/development/cd-certs/Chip-Test-CD-Cert.der\"\n") + string(APPEND script_args "--paa\n") endif() +# find chip tool requied for generating QRCode +find_program(chip_tool_exe NAMES chip-tool REQUIRED) +string(APPEND script_args "--chip-tool-path ${chip_tool_exe}\n") + # add Password-Authenticated Key Exchange parameters -string(APPEND script_args "--spake2_it \"${CONFIG_CHIP_DEVICE_SPAKE2_IT}\"\n") -string(APPEND script_args "--spake2_salt \"${CONFIG_CHIP_DEVICE_SPAKE2_SALT}\"\n") +string(APPEND script_args "--spake2-it \"${CONFIG_CHIP_DEVICE_SPAKE2_IT}\"\n") string(APPEND script_args "--discriminator ${CONFIG_CHIP_DEVICE_DISCRIMINATOR}\n") string(APPEND script_args "--passcode ${CONFIG_CHIP_DEVICE_SPAKE2_PASSCODE}\n") -string(APPEND script_args "--include_passcode\n") -string(APPEND script_args "--overwrite\n") -# check if spake2 verifier should be generated using script -if(CONFIG_CHIP_FACTORY_DATA_GENERATE_SPAKE2_VERIFIER) - # request script to generate a new spake2_verifier - # by adding an argument to script_args - find_program(spake_exe NAMES spake2p REQUIRED) - string(APPEND script_args "--spake2p_path ${spake_exe}\n") -else() - # Spake2 verifier should be provided using kConfig - string(APPEND script_args "--spake2_verifier \"${CONFIG_CHIP_DEVICE_SPAKE2_TEST_VERIFIER}\"\n") -endif() +# request spake2p to generate a new spake2_verifier +find_program(spake_exe NAMES spake2p REQUIRED) +string(APPEND script_args "--spake2-path ${spake_exe}\n") if(CONFIG_CHIP_DEVICE_ENABLE_KEY) # Add optional EnableKey that triggers user-specific action. -string(APPEND script_args "--enable_key \"${CONFIG_CHIP_DEVICE_ENABLE_KEY}\"\n") +string(APPEND script_args "--enable-key \"${CONFIG_CHIP_DEVICE_ENABLE_KEY}\"\n") endif() -# Set output JSON file and path to SCHEMA file to validate generated factory data -set(factory_data_json ${output_path}/${factory_data_target}.json) -string(APPEND script_args "-o \"${factory_data_json}\"\n") -string(APPEND script_args "-s \"${schema_path}\"\n") +string(APPEND script_args "--output \"${output_path}\"/factory\n") + +set(factory_data_bin ${output_path}/factory/factory_data.bin) -# execute first script to create a JSON file +# execute a script to create a factory data file separate_arguments(separated_script_args NATIVE_COMMAND ${script_args}) add_custom_command( - OUTPUT ${factory_data_json} + OUTPUT ${factory_data_bin} DEPENDS ${FACTORY_DATA_SCRIPT_PATH} COMMAND ${Python3_EXECUTABLE} ${FACTORY_DATA_SCRIPT_PATH} ${separated_script_args} COMMENT "Generating new Factory Data..." ) add_custom_target(${factory_data_target} ALL - DEPENDS ${factory_data_json} + DEPENDS ${factory_data_bin} ) endfunction() -# Create a .hex file with factory data in CBOR format. -# -# This function creates a .hex and .cbor files from given JSON factory data file. -# -# -# During generation process some files will be created in zephyr's build directory: -# - .hex a file containing all factory data in CBOR format. -# - .bin a binary file containing all raw factory data in CBOR format. -# - .cbor a file containing all factory data in CBOR format. -# -# [Args]: -# factory_data_hex_target - a name for target to generate factory data HEX file. -# factory_data_target - a name for target to generate factory data JSON file. -# script_path - a path to script that makes a factory data .hex file from given arguments. -# output_path - a path to output directory, where created JSON file will be stored. -function(telink_create_factory_data_hex_file factory_data_hex_target factory_data_target script_path output_path) - -# Pass the argument list via file -set(cbor_script_args "-i ${output_path}/${factory_data_target}.json\n") -string(APPEND cbor_script_args "-o ${output_path}/${factory_data_target}\n") -# get partition address and offset from partition manager during compilation -string(APPEND cbor_script_args "--offset 0x1f4000\n") -string(APPEND cbor_script_args "--size 0x1000\n") -string(APPEND cbor_script_args "-r\n") - -# execute second script to create a hex file containing factory data in cbor format -separate_arguments(separated_cbor_script_args NATIVE_COMMAND ${cbor_script_args}) -set(factory_data_hex ${output_path}/${factory_data_target}.hex) - -add_custom_command(OUTPUT ${factory_data_hex} - COMMAND ${Python3_EXECUTABLE} ${script_path} ${separated_cbor_script_args} - COMMENT "Generating factory data HEX file..." - DEPENDS ${factory_data_target} ${script_path} - ) -add_custom_target(${factory_data_hex_target} ALL - DEPENDS ${factory_data_hex} - ) - -endfunction() - # Generate factory data partition using given args # # @@ -176,21 +148,12 @@ message(FATAL_ERROR "CHIP_ROOT variable is not set, please add it to CMakeLists. endif() # Localize all scripts needed to generate factory data partition -set(FACTORY_DATA_SCRIPT_PATH ${CHIP_ROOT}/scripts/tools/telink/generate_telink_chip_factory_data.py) -set(GENERATE_CBOR_SCRIPT_PATH ${CHIP_ROOT}/scripts/tools/telink/telink_generate_partition.py) -set(FACTORY_DATA_SCHEMA_PATH ${CHIP_ROOT}/scripts/tools/telink/telink_factory_data.schema) +set(FACTORY_DATA_SCRIPT_PATH ${CHIP_ROOT}/scripts/tools/telink/mfg_tool.py) set(OUTPUT_FILE_PATH ${APPLICATION_BINARY_DIR}/zephyr) -# create a JSON file with all factory data +# create a binary file with all factory data telink_create_factory_data_json(factory_data - ${FACTORY_DATA_SCRIPT_PATH} - ${FACTORY_DATA_SCHEMA_PATH} + ${FACTORY_DATA_SCRIPT_PATH} ${OUTPUT_FILE_PATH}) -# create a .hex file with factory data in CBOR format based on the JSON file created previously -telink_create_factory_data_hex_file(factory_data_hex - factory_data - ${GENERATE_CBOR_SCRIPT_PATH} - ${OUTPUT_FILE_PATH}) - endfunction() diff --git a/scripts/requirements.telink.txt b/scripts/requirements.telink.txt new file mode 100644 index 00000000000000..7d2093551af69c --- /dev/null +++ b/scripts/requirements.telink.txt @@ -0,0 +1,3 @@ +future==0.18.2 +pypng==0.0.21 +PyQRCode==1.2.1 diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 6fec358caf1986..b39ec86833bf6e 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -20,6 +20,9 @@ virtualenv # TI -r requirements.ti.txt +# Telink +-r requirements.telink.txt + # cirque tests requests>=2.24.0 diff --git a/scripts/tools/telink/generate_telink_chip_factory_data.py b/scripts/tools/telink/generate_telink_chip_factory_data.py deleted file mode 100644 index 7605184700461a..00000000000000 --- a/scripts/tools/telink/generate_telink_chip_factory_data.py +++ /dev/null @@ -1,493 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2022 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. -# - -from os.path import exists -import os -import sys -import json -import jsonschema -import secrets -import argparse -import subprocess -import logging as log -import base64 -from collections import namedtuple -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.serialization import load_der_private_key - -# A user can not change the factory data version and must be coherent with -# the factory data version set in the Telink platform Kconfig file (CHIP_FACTORY_DATA_VERSION). -FACTORY_DATA_VERSION = 1 - -MATTER_ROOT = os.path.dirname(os.path.realpath(__file__))[:-len("/scripts/tools/telink")] -HEX_PREFIX = "hex:" -PUB_KEY_PREFIX = b'\x04' -INVALID_PASSCODES = [00000000, 11111111, 22222222, 33333333, 44444444, - 55555555, 66666666, 77777777, 88888888, 99999999, 12345678, 87654321] - - -def get_raw_private_key_der(der_file: str, password: str): - """ Split given der file to get separated key pair consisting of public and private keys. - - Args: - der_file (str): Path to .der file containing public and private keys - password (str): Password to decrypt Keys. It can be None, and then KEY is not encrypted. - - Returns: - hex string: return a hex string containing extracted and decrypted private KEY from given .der file. - """ - try: - with open(der_file, 'rb') as file: - key_data = file.read() - if password is None: - log.warning("KEY password has not been provided. It means that DAC key is not encrypted.") - keys = load_der_private_key(key_data, password, backend=default_backend()) - private_key = keys.private_numbers().private_value.to_bytes(32, byteorder='big') - - return private_key - - except IOError or ValueError: - return None - - -def gen_test_certs(chip_cert_exe: str, - output: str, - vendor_id: int, - product_id: int, - device_name: str, - generate_cd: bool = False, - cd_type: int = 1, - paa_cert_path: str = None, - paa_key_path: str = None): - """ - Generate Matter certificates according to given Vendor ID and Product ID using the chip-cert executable. - To use own Product Attestation Authority certificate provide paa_cert_path and paa_key_path arguments. - Without providing these arguments a PAA certificate will be get from /credentials/test/attestation directory - in the Matter repository. - - Args: - chip_cert_exe (str): path to chip-cert executable - output (str): output path to store a newly generated certificates (CD, DAC, PAI) - vendor_id (int): an identification number specific to Vendor - product_id (int): an identification number specific to Product - device_name (str): human-readable device name - generate_cd (bool, optional): Generate Certificate Declaration and store it in thee output directory. Defaults to False. - paa_cert_path (str, optional): provide PAA certification path. Defaults to None - a path will be set to /credentials/test/attestation directory. - paa_key_path (str, optional): provide PAA key path. Defaults to None - a path will be set to /credentials/test/attestation directory. - - Returns: - dictionary: ["PAI_CERT": (str), - "DAC_CERT": (str), - "DAC_KEY": (str)] - """ - - CD_PATH = MATTER_ROOT + "/credentials/test/certification-declaration/Chip-Test-CD-Signing-Cert.pem" - CD_KEY_PATH = MATTER_ROOT + "/credentials/test/certification-declaration/Chip-Test-CD-Signing-Key.pem" - PAA_PATH = paa_cert_path if paa_cert_path != None else MATTER_ROOT + "/credentials/test/attestation/Chip-Test-PAA-NoVID-Cert.pem" - PAA_KEY_PATH = paa_key_path if paa_key_path != None else MATTER_ROOT + "/credentials/test/attestation/Chip-Test-PAA-NoVID-Key.pem" - - attestation_certs = namedtuple("attestation_certs", ["dac_cert", "dac_key", "pai_cert"]) - - log.info("Generating new certificates using chip-cert...") - - if generate_cd: - # generate Certification Declaration - cmd = [chip_cert_exe, "gen-cd", - "--key", CD_KEY_PATH, - "--cert", CD_PATH, - "--out", output + "/CD.der", - "--format-version", "1", - "--vendor-id", hex(vendor_id), - "--product-id", hex(product_id), - "--device-type-id", "0", - "--certificate-id", "FFFFFFFFFFFFFFFFFFF", - "--security-level", "0", - "--security-info", "0", - "--certification-type", str(cd_type), - "--version-number", "0xFFFF", - ] - subprocess.run(cmd) - - new_certificates = {"PAI_CERT": output + "/PAI_cert", - "PAI_KEY": output + "/PAI_key", - "DAC_CERT": output + "/DAC_cert", - "DAC_KEY": output + "/DAC_key" - } - - # generate PAI - cmd = [chip_cert_exe, "gen-att-cert", - "-t", "i", - "-c", device_name, - "-V", hex(vendor_id), - "-C", PAA_PATH, - "-K", PAA_KEY_PATH, - "-o", new_certificates["PAI_CERT"] + ".pem", - "-O", new_certificates["PAI_KEY"] + ".pem", - "-l", str(10000), - ] - subprocess.run(cmd) - - # generate DAC - cmd = [chip_cert_exe, "gen-att-cert", - "-t", "d", - "-c", device_name, - "-V", hex(vendor_id), - "-P", hex(product_id), - "-C", new_certificates["PAI_CERT"] + ".pem", - "-K", new_certificates["PAI_KEY"] + ".pem", - "-o", new_certificates["DAC_CERT"] + ".pem", - "-O", new_certificates["DAC_KEY"] + ".pem", - "-l", str(10000), - ] - subprocess.run(cmd) - - # convert to .der files - for cert_k, cert_v in new_certificates.items(): - action_type = "convert-cert" if cert_k.find("CERT") != -1 else "convert-key" - log.info(cert_v + ".der") - cmd = [chip_cert_exe, action_type, - cert_v + ".pem", - cert_v + ".der", - "--x509-der", - ] - subprocess.run(cmd) - - return attestation_certs(new_certificates["DAC_CERT"] + ".der", - new_certificates["DAC_KEY"] + ".der", - new_certificates["PAI_CERT"] + ".der") - - -def gen_spake2p_verifier(passcode: int, it: int, salt: bytes) -> str: - """ Generate Spake2+ verifier using SPAKE2+ Python Tool - - Args: - passcode (int): Pairing passcode using in Spake2+ - it (int): Iteration counter for Spake2+ verifier generation - salt (str): Salt used to generate Spake2+ verifier - - Returns: - verifier encoded in Base64 - """ - - cmd = [ - os.path.join(MATTER_ROOT, 'scripts/tools/spake2p/spake2p.py'), 'gen-verifier', - '--passcode', str(passcode), - '--salt', base64.b64encode(salt).decode('ascii'), - '--iteration-count', str(it), - ] - return subprocess.check_output(cmd) - - -class FactoryDataGenerator: - """ - Class to generate factory data from given arguments and generate a JSON file - - """ - - def __init__(self, arguments) -> None: - """ - Args: - arguments (any):All input arguments parsed using ArgParse - """ - self._args = arguments - self._factory_data = list() - self._user_data = dict() - - try: - self._validate_args() - except AssertionError as e: - log.error(e) - sys.exit(-1) - - def _validate_args(self): - if self._args.user: - try: - self._user_data = json.loads(self._args.user) - except json.decoder.JSONDecodeError as e: - raise AssertionError("Provided wrong user data, this is not a JSON format! {}".format(e)) - assert self._args.spake2_verifier or self._args.passcode, \ - "Cannot find Spake2+ verifier, to generate a new one please provide passcode (--passcode)" - assert (self._args.chip_cert_path or (self._args.dac_cert and self._args.pai_cert and self._args.dac_key)), \ - "Cannot find paths to DAC or PAI certificates .der files. To generate a new ones please provide a path to chip-cert executable (--chip_cert_path)" - assert self._args.output.endswith(".json"), \ - "Output path doesn't contain .json file path. ({})".format(self._args.output) - assert not (self._args.passcode in INVALID_PASSCODES), \ - "Provided invalid passcode!" - - def generate_json(self): - """ - This function generates JSON data, .json file and validates it. - - To validate generated JSON data a scheme must be provided within script's arguments. - - - In the first part, if the rotating device id unique id has been not provided - as an argument, it will be created. - - If user-provided passcode and Spake2+ verifier have been not provided - as an argument, it will be created using an external script - - Passcode is not stored in JSON by default. To store it for debugging purposes, add --include_passcode argument. - - Validating output JSON is not mandatory, but highly recommended. - - """ - # generate missing data if needed - if not self._args.rd_uid: - if self._args.generate_rd_uid: - rd_uid = self._generate_rotating_device_uid() - else: - # rotating device ID unique ID was not provided, so do not store it in factory data. - rd_uid = None - else: - rd_uid = HEX_PREFIX + self._args.rd_uid - - if not self._args.spake2_verifier: - spake_2_verifier = self._generate_spake2_verifier() - else: - spake_2_verifier = self._args.spake2_verifier - - # convert salt to bytestring to be coherent with Spake2+ verifier type - spake_2_salt = self._args.spake2_salt - - if self._args.chip_cert_path: - certs = gen_test_certs(self._args.chip_cert_path, - self._args.output[:self._args.output.rfind("/")], - self._args.vendor_id, - self._args.product_id, - self._args.vendor_name + "_" + self._args.product_name, - self._args.gen_cd, - self._args.cd_type, - self._args.paa_cert, - self._args.paa_key) - dac_cert = certs.dac_cert - pai_cert = certs.pai_cert - dac_key = certs.dac_key - else: - dac_cert = self._args.dac_cert - dac_key = self._args.dac_key - pai_cert = self._args.pai_cert - - # try to read DAC public and private keys - dac_priv_key = get_raw_private_key_der(dac_key, self._args.dac_key_password) - if dac_priv_key is None: - log.error("Cannot read DAC keys from : {}".format(dac_key)) - sys.exit(-1) - - try: - json_file = open(self._args.output, "w+") - except FileNotFoundError: - print("Cannot create JSON file in this location: {}".format(self._args.output)) - sys.exit(-1) - with json_file: - # serialize data - self._add_entry("version", FACTORY_DATA_VERSION) - self._add_entry("sn", self._args.sn) - self._add_entry("vendor_id", self._args.vendor_id) - self._add_entry("product_id", self._args.product_id) - self._add_entry("vendor_name", self._args.vendor_name) - self._add_entry("product_name", self._args.product_name) - self._add_entry("product_label", self._args.product_label) - self._add_entry("product_url", self._args.product_url) - self._add_entry("part_number", self._args.part_number) - self._add_entry("date", self._args.date) - self._add_entry("hw_ver", self._args.hw_ver) - self._add_entry("hw_ver_str", self._args.hw_ver_str) - self._add_entry("dac_cert", self._process_der(dac_cert)) - self._add_entry("dac_key", dac_priv_key) - self._add_entry("pai_cert", self._process_der(pai_cert)) - if self._args.include_passcode: - self._add_entry("passcode", self._args.passcode) - self._add_entry("spake2_it", self._args.spake2_it) - self._add_entry("spake2_salt", spake_2_salt) - self._add_entry("spake2_verifier", spake_2_verifier) - self._add_entry("discriminator", self._args.discriminator) - if rd_uid: - self._add_entry("rd_uid", rd_uid) - if self._args.enable_key: - self._add_entry("enable_key", HEX_PREFIX + self._args.enable_key) - if self._args.user: - self._add_entry("user", self._args.user) - - factory_data_dict = dict(self._factory_data) - - json_object = json.dumps(factory_data_dict) - is_json_valid = True - - if self._args.schema: - is_json_valid = self._validate_output_json(json_object) - else: - log.warning("JSON Schema file has not been provided, the output file can be wrong. Be aware of that.") - try: - if is_json_valid: - json_file.write(json_object) - except IOError as e: - log.error("Cannot save output file into directory: {}".format(self._args.output)) - - def _add_entry(self, name: str, value: any): - """ Add single entry to list of tuples ("key", "value") """ - if(isinstance(value, bytes) or isinstance(value, bytearray)): - value = HEX_PREFIX + value.hex() - if value or (isinstance(value, int) and value == 0): - log.debug("Adding entry '{}' with size {} and type {}".format(name, sys.getsizeof(value), type(value))) - self._factory_data.append((name, value)) - - def _generate_spake2_verifier(self): - """ If verifier has not been provided in arguments list it should be generated via external script """ - return base64.b64decode(gen_spake2p_verifier(self._args.passcode, self._args.spake2_it, self._args.spake2_salt)) - - def _generate_rotating_device_uid(self): - """ If rotating device unique ID has not been provided it should be generated """ - log.warning("Cannot find rotating device UID in provided arguments list. A new one will be generated.") - rdu = secrets.token_bytes(16) - log.info("\n\nThe new rotate device UID: {}\n".format(rdu.hex())) - return rdu - - def _validate_output_json(self, output_json: str): - """ - Validate output JSON data with provided .scheme file - This function will raise error if JSON does not match schema. - - """ - try: - with open(self._args.schema) as schema_file: - log.info("Validating JSON with schema...") - schema = json.loads(schema_file.read()) - validator = jsonschema.Draft202012Validator(schema=schema) - validator.validate(instance=json.loads(output_json)) - except IOError as e: - log.error("Provided JSON schema file is wrong: {}".format(self._args.schema)) - return False - else: - log.info("Validate OK") - return True - - def _process_der(self, path: str): - log.debug("Processing der file...") - try: - with open(path, 'rb') as f: - data = f.read() - return data - except IOError as e: - log.error(e) - raise e - - -def main(): - parser = argparse.ArgumentParser(description="Telink Factory Data NVS generator tool") - - def allow_any_int(i): return int(i, 0) - def base64_str(s): return base64.b64decode(s) - - mandatory_arguments = parser.add_argument_group("Mandatory keys", "These arguments must be provided to generate JSON file") - optional_arguments = parser.add_argument_group( - "Optional keys", "These arguments are optional and they depend on the user-purpose") - parser.add_argument("-s", "--schema", type=str, - help="JSON schema file to validate JSON output data") - parser.add_argument("-o", "--output", type=str, required=True, - help="Output path to store .json file, e.g. my_dir/output.json") - parser.add_argument("-v", "--verbose", action="store_true", - help="Run this script with DEBUG logging level") - parser.add_argument("--include_passcode", action="store_true", - help="Include passcode in factory data. By default, it is used only for generating Spake2+ verifier.") - parser.add_argument("--overwrite", action="store_true", - help="If output JSON file exist this argument allows to generate new factory data and overwrite it.") - # Json known-keys values - # mandatory keys - mandatory_arguments.add_argument("--sn", type=str, required=True, - help="[ascii string] Serial number of a device which can be used to identify \ - the serial number field in the Matter certificate structure. \ - Maximum length of serial number is 20 bytes. \ - Strings longer than 20 bytes will be declined in script") - mandatory_arguments.add_argument("--vendor_id", type=allow_any_int, - help="[int | hex int] Provide Vendor Identification Number") - mandatory_arguments.add_argument("--product_id", type=allow_any_int, - help="[int | hex int] Provide Product Identification Number") - mandatory_arguments.add_argument("--vendor_name", type=str, - help="[string] provide human-readable vendor name") - mandatory_arguments.add_argument("--product_name", type=str, - help="[string] provide human-readable product name") - mandatory_arguments.add_argument("--date", type=str, required=True, - help="[ascii string] Provide manufacturing date \ - A manufacturing date specifies the date that the Node was manufactured. \ - Used format for providing a manufacturing date is ISO 8601 e.g. YYYY-MM-DD.") - mandatory_arguments.add_argument("--hw_ver", type=allow_any_int, required=True, - help="[int | hex int] Provide hardware version in int format.") - mandatory_arguments.add_argument("--hw_ver_str", type=str, required=True, - help="[ascii string] Provide hardware version in string format.") - mandatory_arguments.add_argument("--spake2_it", type=allow_any_int, required=True, - help="[int | hex int] Provide Spake2+ iteration count.") - mandatory_arguments.add_argument("--spake2_salt", type=base64_str, required=True, - help="[base64 string] Provide Spake2+ salt.") - mandatory_arguments.add_argument("--discriminator", type=allow_any_int, required=True, - help="[int] Provide BLE pairing discriminator. \ - A 12-bit value matching the field of the same name in \ - the setup code. Discriminator is used during a discovery process.") - - # optional keys - optional_arguments.add_argument("--product_url", type=str, - help="[string] provide link to product-specific web page") - optional_arguments.add_argument("--product_label", type=str, - help="[string] provide human-readable product label") - optional_arguments.add_argument("--part_number", type=str, - help="[string] provide human-readable product number") - optional_arguments.add_argument("--chip_cert_path", type=str, - help="Generate DAC and PAI certificates instead giving a path to .der files. This option requires a path to chip-cert executable." - "By default you can find chip-cert in connectedhomeip/src/tools/chip-cert directory and build it there.") - optional_arguments.add_argument("--dac_cert", type=str, - help="[.der] Provide the path to .der file containing DAC certificate.") - optional_arguments.add_argument("--dac_key", type=str, - help="[.der] Provide the path to .der file containing DAC keys.") - optional_arguments.add_argument("--generate_rd_uid", action="store_true", - help="Generate a new rotating device unique ID, print it out to console output and store it in factory data.") - optional_arguments.add_argument("--dac_key_password", type=str, - help="Provide a password to decode dac key. If dac key is not encrypted do not provide this argument.") - optional_arguments.add_argument("--pai_cert", type=str, - help="[.der] Provide the path to .der file containing PAI certificate.") - optional_arguments.add_argument("--rd_uid", type=str, - help="[hex string] [128-bit hex-encoded] Provide the rotating device unique ID. If this argument is not provided a new rotating device id unique id will be generated.") - optional_arguments.add_argument("--passcode", type=allow_any_int, - help="[int | hex] Default PASE session passcode. (This is mandatory to generate Spake2+ verifier).") - optional_arguments.add_argument("--spake2_verifier", type=base64_str, - help="[base64 string] Provide Spake2+ verifier without generating it.") - optional_arguments.add_argument("--enable_key", type=str, - help="[hex string] [128-bit hex-encoded] The Enable Key is a 128-bit value that triggers manufacturer-specific action while invoking the TestEventTrigger Command." - "This value is used during Certification Tests, and should not be present on production devices.") - optional_arguments.add_argument("--user", type=str, - help="[string] Provide additional user-specific keys in JSON format: {'name_1': 'value_1', 'name_2': 'value_2', ... 'name_n', 'value_n'}.") - optional_arguments.add_argument("--gen_cd", action="store_true", default=False, - help="Generate a new Certificate Declaration in .der format according to used Vendor ID and Product ID. This certificate will not be included to the factory data.") - optional_arguments.add_argument("--cd_type", type=int, default=1, - help="[int] Type of generated Certification Declaration: 0 - development, 1 - provisional, 2 - official") - optional_arguments.add_argument("--paa_cert", type=str, - help="Provide a path to the Product Attestation Authority (PAA) certificate to generate the PAI certificate. Without providing it, a testing PAA stored in the Matter repository will be used.") - optional_arguments.add_argument("--paa_key", type=str, - help="Provide a path to the Product Attestation Authority (PAA) key to generate the PAI certificate. Without providing it, a testing PAA key stored in the Matter repository will be used.") - args = parser.parse_args() - - if args.verbose: - log.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=log.DEBUG) - else: - log.basicConfig(format='[%(levelname)s] %(message)s', level=log.INFO) - - # check if json file already exist - if(exists(args.output) and not args.overwrite): - log.error("Output file: {} already exist, to create a new one add argument '--overwrite'. By default overwriting is disabled".format(args.output)) - return - - generator = FactoryDataGenerator(args) - generator.generate_json() - - -if __name__ == "__main__": - main() diff --git a/scripts/tools/telink/mfg_tool.py b/scripts/tools/telink/mfg_tool.py new file mode 100644 index 00000000000000..2c4390c0d0eed3 --- /dev/null +++ b/scripts/tools/telink/mfg_tool.py @@ -0,0 +1,755 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2022 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. +# + +import argparse +import base64 +import binascii +import csv +import json +import os +import random +import shutil +import subprocess +import logging as logger +import sys +import cryptography.hazmat.backends +import cryptography.x509 +import pyqrcode +import cbor2 as cbor +from intelhex import IntelHex + +TOOLS = { + 'spake2p': None, + 'chip-cert': None, + 'chip-tool': None, +} + +INVALID_PASSCODES = [00000000, 11111111, 22222222, 33333333, 44444444, 55555555, + 66666666, 77777777, 88888888, 99999999, 12345678, 87654321] + +FACTORY_DATA_VERSION = 1 +SERIAL_NUMBER_LEN = 32 + +# Lengths for manual pairing codes and qrcode +SHORT_MANUALCODE_LEN = 11 +LONG_MANUALCODE_LEN = 21 +QRCODE_LEN = 22 +ROTATING_DEVICE_ID_UNIQUE_ID_LEN_BITS = 128 +HEX_PREFIX = "hex:" +DEV_SN_CSV_HDR = "Serial Number,\n" + +NVS_MEMORY = dict() + + +def nvs_memory_append(key, value): + if isinstance(value, str): + NVS_MEMORY[key] = value.encode("utf-8") + else: + NVS_MEMORY[key] = value + + +def nvs_memory_update(key, value): + if isinstance(value, str): + NVS_MEMORY.update({key: value.encode("utf-8")}) + else: + NVS_MEMORY.update({key: value}) + + +def check_tools_exists(args): + if args.spake2_path: + TOOLS['spake2p'] = shutil.which(args.spake2_path) + else: + TOOLS['spake2p'] = shutil.which('spake2p') + + if TOOLS['spake2p'] is None: + logger.error('spake2p not found, please specify --spake2-path argument') + sys.exit(1) + # if the certs and keys are not in the generated partitions or the specific dac cert and key are used, + # the chip-cert is not needed. + if args.paa or (args.pai and (args.dac_cert is None and args.dac_key is None)): + if args.chip_cert_path: + TOOLS['chip-cert'] = shutil.which(args.chip_cert_path) + else: + TOOLS['chip-cert'] = shutil.which('chip-cert') + if TOOLS['chip-cert'] is None: + logger.error('chip-cert not found, please specify --chip-cert-path argument') + sys.exit(1) + + if args.chip_tool_path: + TOOLS['chip-tool'] = shutil.which(args.chip_tool_path) + else: + TOOLS['chip-tool'] = shutil.which('chip-tool') + if TOOLS['chip-tool'] is None: + logger.error('chip-tool not found, please specify --chip-tool-path argument') + sys.exit(1) + + logger.debug('Using following tools:') + logger.debug('spake2p: {}'.format(TOOLS['spake2p'])) + logger.debug('chip-cert: {}'.format(TOOLS['chip-cert'])) + logger.debug('chip-tool: {}'.format(TOOLS['chip-tool'])) + + +def execute_cmd(cmd): + logger.debug('Executing Command: {}'.format(cmd)) + status = subprocess.run(cmd, capture_output=True) + + try: + status.check_returncode() + except subprocess.CalledProcessError as e: + if status.stderr: + logger.error('[stderr]: {}'.format(status.stderr.decode('utf-8').strip())) + logger.error('Command failed with error: {}'.format(e)) + sys.exit(1) + + +def check_str_range(s, min_len, max_len, name): + if s and ((len(s) < min_len) or (len(s) > max_len)): + logger.error('%s must be between %d and %d characters', name, min_len, max_len) + sys.exit(1) + + +def check_int_range(value, min_value, max_value, name): + if value and ((value < min_value) or (value > max_value)): + logger.error('%s is out of range, should be in range [%d, %d]', name, min_value, max_value) + sys.exit(1) + + +def vid_pid_str(vid, pid): + return '_'.join([hex(vid)[2:], hex(pid)[2:]]) + + +def read_der_file(path: str): + logger.debug("Reading der file {}...", path) + try: + with open(path, 'rb') as f: + data = f.read() + return data + except IOError as e: + logger.error(e) + raise e + + +def read_key_bin_file(path: str): + try: + with open(path, 'rb') as file: + key_data = file.read() + + return key_data + + except IOError or ValueError: + return None + + +def setup_out_dir(out_dir_top, args, serial: str): + out_dir = os.sep.join([out_dir_top, vid_pid_str(args.vendor_id, args.product_id)]) + + if args.in_tree: + out_dir = out_dir_top + + os.makedirs(out_dir, exist_ok=True) + + dirs = { + 'output': os.sep.join([out_dir, serial]), + 'internal': os.sep.join([out_dir, serial, 'internal']), + } + + if args.in_tree: + dirs['output'] = out_dir + dirs['internal'] = os.sep.join([out_dir, 'internal']) + + os.makedirs(dirs['output'], exist_ok=True) + os.makedirs(dirs['internal'], exist_ok=True) + + return dirs + + +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) + + +def generate_passcode(args, out_dirs): + salt_len_max = 32 + + cmd = [ + TOOLS['spake2p'], 'gen-verifier', + '--iteration-count', str(args.spake2_it), + '--salt-len', str(salt_len_max), + '--out', os.sep.join([out_dirs['output'], 'pin.csv']) + ] + + # If passcode is provided, use it + if (args.passcode): + cmd.extend(['--pin-code', str(args.passcode)]) + + execute_cmd(cmd) + + +def generate_discriminator(args, out_dirs): + # If discriminator is provided, use it + if args.discriminator: + disc = args.discriminator + else: + disc = random.randint(0x0000, 0x0FFF) + # Append discriminator to each line of the passcode file + with open(os.sep.join([out_dirs['output'], '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(disc)]) + + with open(os.sep.join([out_dirs['output'], 'pin_disc.csv']), 'w') as fd: + fd.write('\n'.join(lines) + '\n') + + os.remove(os.sep.join([out_dirs['output'], 'pin.csv'])) + + +def generate_pai_certs(args, ca_key, ca_cert, out_key, out_cert): + cmd = [ + TOOLS['chip-cert'], 'gen-att-cert', + '--type', 'i', + '--subject-cn', '"{} PAI {}"'.format(args.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) + logger.info('Generated PAI certificate: {}'.format(out_cert)) + logger.info('Generated PAI private key: {}'.format(out_key)) + + +def setup_root_certificates(args, dirs): + pai_cert = { + 'cert_pem': None, + 'cert_der': None, + 'key_pem': None, + } + # If PAA is passed as input, then generate PAI certificate + if args.paa: + # output file names + pai_cert['cert_pem'] = os.sep.join([dirs['internal'], 'pai_cert.pem']) + pai_cert['cert_der'] = os.sep.join([dirs['internal'], 'pai_cert.der']) + pai_cert['key_pem'] = os.sep.join([dirs['internal'], 'pai_key.pem']) + + generate_pai_certs(args, args.key, args.cert, pai_cert['key_pem'], pai_cert['cert_pem']) + convert_x509_cert_from_pem_to_der(pai_cert['cert_pem'], pai_cert['cert_der']) + logger.info('Generated PAI certificate in DER format: {}'.format(pai_cert['cert_der'])) + + # If PAI is passed as input, generate DACs + elif args.pai: + pai_cert['cert_pem'] = args.cert + pai_cert['key_pem'] = args.key + pai_cert['cert_der'] = os.sep.join([dirs['internal'], 'pai_cert.der']) + + convert_x509_cert_from_pem_to_der(pai_cert['cert_pem'], pai_cert['cert_der']) + logger.info('Generated PAI certificate in DER format: {}'.format(pai_cert['cert_der'])) + + return pai_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_dac_cert(iteration, args, out_dirs, discriminator, passcode, ca_key, ca_cert): + out_key_pem = os.sep.join([out_dirs['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.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) + logger.info('Generated DAC certificate: {}'.format(out_cert_pem)) + logger.info('Generated DAC private key: {}'.format(out_key_pem)) + + convert_x509_cert_from_pem_to_der(out_cert_pem, out_cert_der) + logger.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) + logger.info('Generated DAC private key in binary format: {}'.format(out_private_key_bin)) + logger.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_cert_from_args(args, out_dirs): + logger.info('Using DAC from command line arguments...') + logger.info('DAC Certificate: {}'.format(args.dac_cert)) + logger.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_dirs['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) + logger.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) + logger.info('Generated DAC private key in binary format: {}'.format(out_private_key_bin)) + logger.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 get_manualcode_args(vid, pid, flow, discriminator, passcode): + payload_args = list() + payload_args.append('--discriminator') + payload_args.append(str(discriminator)) + payload_args.append('--setup-pin-code') + payload_args.append(str(passcode)) + payload_args.append('--version') + payload_args.append('0') + payload_args.append('--vendor-id') + payload_args.append(str(vid)) + payload_args.append('--product-id') + payload_args.append(str(pid)) + payload_args.append('--commissioning-mode') + payload_args.append(str(flow)) + return payload_args + + +def get_qrcode_args(vid, pid, flow, discriminator, passcode, disc_mode): + payload_args = get_manualcode_args(vid, pid, flow, discriminator, passcode) + payload_args.append('--rendezvous') + payload_args.append(str(1 << disc_mode)) + return payload_args + + +def get_chip_qrcode(chip_tool, vid, pid, flow, discriminator, passcode, disc_mode): + payload_args = get_qrcode_args(vid, pid, flow, discriminator, passcode, disc_mode) + cmd_args = [chip_tool, 'payload', 'generate-qrcode'] + cmd_args.extend(payload_args) + data = subprocess.check_output(cmd_args) + + # Command output is as below: + # \x1b[0;32m[1655386003372] [23483:7823617] CHIP: [TOO] QR Code: MT:Y.K90-WB010E7648G00\x1b[0m + return data.decode('utf-8').split('QR Code: ')[1][:QRCODE_LEN] + + +def get_chip_manualcode(chip_tool, vid, pid, flow, discriminator, passcode): + payload_args = get_manualcode_args(vid, pid, flow, discriminator, passcode) + cmd_args = [chip_tool, 'payload', 'generate-manualcode'] + cmd_args.extend(payload_args) + data = subprocess.check_output(cmd_args) + + # Command output is as below: + # \x1b[0;32m[1655386909774] [24424:7837894] CHIP: [TOO] Manual Code: 749721123365521327689\x1b[0m\n + # OR + # \x1b[0;32m[1655386926028] [24458:7838229] CHIP: [TOO] Manual Code: 34972112338\x1b[0m\n + # Length of manual code depends on the commissioning flow: + # For standard commissioning flow it is 11 digits + # For User-intent and custom commissioning flow it is 21 digits + manual_code_len = LONG_MANUALCODE_LEN if flow else SHORT_MANUALCODE_LEN + return data.decode('utf-8').split('Manual Code: ')[1][:manual_code_len] + + +def generate_onboarding_data(args, out_dirs, discriminator, passcode): + chip_manualcode = get_chip_manualcode(TOOLS['chip-tool'], args.vendor_id, args.product_id, + args.commissioning_flow, discriminator, passcode) + chip_qrcode = get_chip_qrcode(TOOLS['chip-tool'], args.vendor_id, args.product_id, + args.commissioning_flow, discriminator, passcode, args.discovery_mode) + + logger.info('Generated QR code: ' + chip_qrcode) + logger.info('Generated manual code: ' + chip_manualcode) + + csv_data = 'qrcode,manualcode,discriminator,passcode\n' + csv_data += chip_qrcode + ',' + chip_manualcode + ',' + str(discriminator) + ',' + str(passcode) + '\n' + + onboarding_data_file = os.sep.join([out_dirs['output'], 'onb_codes.csv']) + 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_dirs['output'], 'qrcode.png']) + chip_qr = pyqrcode.create(chip_qrcode, version=2, error='M') + chip_qr.png(qrcode_file, scale=6) + + logger.info('Generated onboarding data and QR Code') + + +# This function generates the DACs, picks the commissionable data from the already present csv file, +# and generates the onboarding payloads, and writes everything to the master csv +def write_device_unique_data(args, out_dirs, pai_cert): + with open(os.sep.join([out_dirs['output'], 'pin_disc.csv']), 'r') as csvf: + pin_disc_dict = csv.DictReader(csvf) + row = pin_disc_dict.__next__() + + nvs_memory_append('discriminator', int(row['Discriminator'])) + nvs_memory_append('spake2_it', int(row['Iteration Count'])) + nvs_memory_append('spake2_salt', base64.b64decode(row['Salt'])) + nvs_memory_append('spake2_verifier', base64.b64decode(row['Verifier'])) + nvs_memory_append('passcode', int(row['PIN Code'])) + + if args.paa or args.pai: + if args.dac_key is not None and args.dac_cert is not None: + dacs = use_dac_cert_from_args(args, out_dirs) + else: + dacs = generate_dac_cert(int(row['Index']), args, out_dirs, int(row['Discriminator']), + int(row['PIN Code']), pai_cert['key_pem'], pai_cert['cert_pem']) + + nvs_memory_append('dac_cert', read_der_file(dacs[0])) + nvs_memory_append('dac_key', read_key_bin_file(dacs[1])) + nvs_memory_append('pai_cert', read_der_file(pai_cert['cert_der'])) + + nvs_memory_append('cert_dclrn', read_der_file(args.cert_dclrn)) + + if (args.enable_rotating_device_id is True) and (args.rd_id_uid is None): + nvs_memory_update('rd_uid', binascii.b2a_hex(os.urandom( + int(ROTATING_DEVICE_ID_UNIQUE_ID_LEN_BITS / 8))).decode('utf-8')) + + # Generate onboarding data + generate_onboarding_data(args, out_dirs, int(row['Discriminator']), int(row['PIN Code'])) + + return dacs + + +def generate_partition(args, out_dirs): + logger.info('Generating partition image: offset: 0x{:X} size: 0x{:X}'.format(args.offset, args.size)) + cbor_data = cbor.dumps(NVS_MEMORY) + # Create hex file + if len(cbor_data) > args.size: + raise ValueError("generated CBOR file exceeds declared maximum partition size! {} > {}".format(len(cbor_data), args.size)) + ih = IntelHex() + ih.putsz(args.offset, cbor_data) + ih.write_hex_file(os.sep.join([out_dirs['output'], 'factory_data.hex']), True) + ih.tobinfile(os.sep.join([out_dirs['output'], 'factory_data.bin'])) + + +def generate_json_summary(args, out_dirs, pai_certs, dacs_cert, serial_num: str): + json_dict = dict() + + json_dict['serial_num'] = serial_num + + for key, nvs_value in NVS_MEMORY.items(): + if (not isinstance(nvs_value, bytes) and not isinstance(nvs_value, bytearray)): + json_dict[key] = nvs_value + + with open(os.sep.join([out_dirs['output'], 'pin_disc.csv']), 'r') as csvf: + pin_disc_dict = csv.DictReader(csvf) + row = pin_disc_dict.__next__() + json_dict['passcode'] = row['PIN Code'] + json_dict['spake2_salt'] = row['Salt'] + json_dict['spake2_verifier'] = row['Verifier'] + + with open(os.sep.join([out_dirs['output'], 'onb_codes.csv']), 'r') as csvf: + pin_disc_dict = csv.DictReader(csvf) + row = pin_disc_dict.__next__() + for key, value in row.items(): + json_dict[key] = value + + for key, value in pai_certs.items(): + json_dict[key] = value + + if dacs_cert is not None: + json_dict['dac_cert'] = dacs_cert[0] + json_dict['dac_priv_key'] = dacs_cert[1] + json_dict['dac_pub_key'] = dacs_cert[2] + + json_dict['cert_dclrn'] = args.cert_dclrn + + # Format vid & pid as hex + json_dict['vendor_id'] = hex(json_dict['vendor_id']) + json_dict['product_id'] = hex(json_dict['product_id']) + + with open(os.sep.join([out_dirs['output'], 'summary.json']), 'w') as json_file: + json.dump(json_dict, json_file, indent=4) + + +def add_additional_kv(args, serial_num): + # Device instance information + if args.vendor_id is not None: + nvs_memory_append('vendor_id', args.vendor_id) + if args.vendor_name is not None: + nvs_memory_append('vendor_name', args.vendor_name) + if args.product_id is not None: + nvs_memory_append('product_id', args.product_id) + if args.product_name is not None: + nvs_memory_append('product_name', args.product_name) + if args.hw_ver is not None: + nvs_memory_append('hw_ver', args.hw_ver) + if args.hw_ver_str is not None: + nvs_memory_append('hw_ver_str', args.hw_ver_str) + if args.mfg_date is not None: + nvs_memory_append('date', args.mfg_date) + if args.enable_rotating_device_id: + nvs_memory_append('rd_uid', args.rd_id_uid) + + # Add the serial-num + nvs_memory_append('sn', serial_num) + + nvs_memory_append('version', FACTORY_DATA_VERSION) + + if args.enable_key: + nvs_memory_append('enable_key', args.enable_key) + + # Keys from basic clusters + if args.product_label is not None: + nvs_memory_append('product_label', args.product_label) + if args.product_url is not None: + nvs_memory_append('product_url', args.product_url) + if args.part_number is not None: + nvs_memory_append('part_number', args.part_number) + + +def get_and_validate_args(): + def allow_any_int(i): return int(i, 0) + def base64_str(s): return base64.b64decode(s) + + parser = argparse.ArgumentParser(description='Manufacuring partition generator tool', + formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=50)) + mandatory_args = parser.add_argument_group("Mandatory keys", "These arguments must be provided to generate JSON file") + optional_args = parser.add_argument_group("Optional keys", "These arguments are optional and they depend on the user-purpose") + + # General options + general_args = parser.add_argument_group('General options') + general_args.add_argument('-n', '--count', type=allow_any_int, default=1, + help='The number of manufacturing partition binaries to generate. Default is 1.') + general_args.add_argument("--output", type=str, required=False, default="out", + help="[string] Output path where generated data will be stored.") + general_args.add_argument("--spake2-path", type=str, required=False, + help="[string] Provide Spake2+ tool path") + general_args.add_argument("--chip-tool-path", type=str, required=False, + help="[string] Provide chip-tool path") + general_args.add_argument("--chip-cert-path", type=str, required=False, + help="[string] Provide chip-cert path") + general_args.add_argument("--overwrite", action="store_true", default=False, + help="If output directory exist this argument allows to generate new factory data and overwrite it.") + general_args.add_argument("--in-tree", action="store_true", default=False, + help="Use it only when building factory data from Matter source code.") + general_args.add_argument("--enable-key", type=str, + help="[hex string] [128-bit hex-encoded] The Enable Key is a 128-bit value that triggers manufacturer-specific action while invoking the TestEventTrigger Command." + "This value is used during Certification Tests, and should not be present on production devices.") + # Commissioning options + commissioning_args = parser.add_argument_group('Commisioning options') + commissioning_args.add_argument('--passcode', type=allow_any_int, + help='The passcode for pairing. Randomly generated if not specified.') + commissioning_args.add_argument("--spake2-it", type=allow_any_int, default=1000, + help="[int] Provide Spake2+ iteration count.") + commissioning_args.add_argument('--discriminator', type=allow_any_int, + help='The discriminator for pairing. Randomly generated if not specified.') + commissioning_args.add_argument('-cf', '--commissioning-flow', type=allow_any_int, default=0, + help='Device commissioning flow, 0:Standard, 1:User-Intent, 2:Custom. \ + Default is 0.', choices=[0, 1, 2]) + commissioning_args.add_argument('-dm', '--discovery-mode', type=allow_any_int, default=1, + help='Commissionable device discovery networking technology. \ + 0:WiFi-SoftAP, 1:BLE, 2:On-network. Default is BLE.', choices=[0, 1, 2]) + + # Device insrance information + dev_inst_args = parser.add_argument_group('Device instance information options') + dev_inst_args.add_argument('-v', '--vendor-id', type=allow_any_int, required=False, help='Vendor id') + dev_inst_args.add_argument('--vendor-name', type=str, required=False, help='Vendor name') + dev_inst_args.add_argument('-p', '--product-id', type=allow_any_int, required=False, help='Product id') + dev_inst_args.add_argument('--product-name', type=str, required=False, help='Product name') + dev_inst_args.add_argument('--hw-ver', type=allow_any_int, required=False, help='Hardware version') + dev_inst_args.add_argument('--hw-ver-str', type=str, required=False, help='Hardware version string') + dev_inst_args.add_argument('--mfg-date', type=str, required=False, help='Manufacturing date in format YYYY-MM-DD') + dev_inst_args.add_argument('--serial-num', type=str, required=False, help='Serial number in hex format') + dev_inst_args.add_argument('--enable-rotating-device-id', action='store_true', + help='Enable Rotating device id in the generated binaries') + dev_inst_args.add_argument('--rd-id-uid', type=str, required=False, + help='128-bit unique identifier for generating rotating device identifier, provide 32-byte hex string, e.g. "1234567890abcdef1234567890abcdef"') + + dac_args = parser.add_argument_group('Device attestation credential options') + # 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() + dac_args.add_argument('-c', '--cert', type=str, required=False, help='The input certificate file in PEM format.') + dac_args.add_argument('-k', '--key', type=str, required=False, help='The input key file in PEM format.') + dac_args.add_argument('-cd', '--cert-dclrn', type=str, required=True, help='The certificate declaration file in DER format.') + dac_args.add_argument('--dac-cert', type=str, help='The input DAC certificate file in PEM format.') + dac_args.add_argument('--dac-key', type=str, help='The input DAC private key file in PEM format.') + dac_args.add_argument('-cn', '--cn-prefix', type=str, default='Telink', + help='The common name prefix of the subject of the generated certificate.') + dac_args.add_argument('-lt', '--lifetime', default=4294967295, type=allow_any_int, + help='Lifetime of the generated certificate. Default is 4294967295 if not specified, \ + this indicate that certificate does not have well defined expiration date.') + dac_args.add_argument('-vf', '--valid-from', type=str, + help='The start date for the certificate validity period in format --
[ :: ]. \ + Default is current date.') + input_cert_group = dac_args.add_mutually_exclusive_group(required=False) + 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.') + + basic_args = parser.add_argument_group('Few more Basic clusters options') + basic_args.add_argument('--product-label', type=str, required=False, help='Product label') + basic_args.add_argument('--product-url', type=str, required=False, help='Product URL') + basic_args.add_argument('--part_number', type=str, required=False, help='Provide human-readable product number') + + part_gen_args = parser.add_argument_group('Partition generator options') + part_gen_args.add_argument('--offset', type=allow_any_int, default=0x1F4000, + help='Partition offset - an address in devices NVM memory, where factory data will be stored') + part_gen_args.add_argument('--size', type=allow_any_int, default=0x1000, + help='The maximum partition size') + + args = parser.parse_args() + + # Validate in-tree parameter + if args.count > 1 and args.in_tree: + logger.error('Option --in-tree can not be use together with --count > 1') + sys.exit(1) + + # Validate discriminator and passcode + check_int_range(args.discriminator, 0x0000, 0x0FFF, 'Discriminator') + if args.passcode is not None: + if ((args.passcode < 0x0000001 and args.passcode > 0x5F5E0FE) or (args.passcode in INVALID_PASSCODES)): + logger.error('Invalid passcode' + str(args.passcode)) + sys.exit(1) + + # Validate the device instance information + check_int_range(args.product_id, 0x0000, 0xFFFF, 'Product id') + check_int_range(args.vendor_id, 0x0000, 0xFFFF, 'Vendor id') + check_int_range(args.hw_ver, 0x0000, 0xFFFF, 'Hardware version') + check_int_range(args.spake2_it, 1, 10000, 'Spake2+ iteration count') + check_str_range(args.serial_num, 1, SERIAL_NUMBER_LEN, 'Serial number') + check_str_range(args.vendor_name, 1, 32, 'Vendor name') + check_str_range(args.product_name, 1, 32, 'Product name') + check_str_range(args.hw_ver_str, 1, 64, 'Hardware version string') + check_str_range(args.mfg_date, 8, 16, 'Manufacturing date') + check_str_range(args.rd_id_uid, 32, 32, 'Rotating device Unique id') + + # Validates the attestation related arguments + # DAC key and DAC cert both should be present or none + if (args.dac_key is not None) != (args.dac_cert is not None): + logger.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): + logger.error('Please provide PAI certificate along with DAC certificate and DAC key') + 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: + logger.info('Input Root certificate type PAA') + elif args.pai: + logger.info('Input Root certificate type PAI') + else: + logger.info('Do not include the device attestation certificates and keys in partition binaries') + + # Check if Key and certificate are present + if (args.paa or args.pai) and (args.key is None or args.cert is None): + logger.error('CA key and certificate are required to generate DAC key and certificate') + sys.exit(1) + + check_str_range(args.product_label, 1, 64, 'Product Label') + check_str_range(args.product_url, 1, 256, 'Product URL') + check_str_range(args.part_number, 1, 32, 'Part Number') + + return args + + +def main(): + logger.basicConfig(format='[%(asctime)s] [%(levelname)7s] - %(message)s', level=logger.INFO) + args = get_and_validate_args() + check_tools_exists(args) + + if os.path.exists(args.output): + if args.overwrite: + logger.info("Output directory already exists. All data will be overwritten.") + shutil.rmtree(args.output) + else: + logger.error("Output directory exists! Please use different or remove existing.") + exit(1) + + # If serial number is not passed, then generate one + if args.serial_num is None: + serial_num_int = int(binascii.b2a_hex(os.urandom(SERIAL_NUMBER_LEN)), 16) + logger.info("Serial number not provided. Using generated one: {}".format(hex(serial_num_int))) + else: + serial_num_int = int(args.serial_num, 16) + + out_dir_top = os.path.realpath(args.output) + os.makedirs(out_dir_top, exist_ok=True) + + dev_sn_file = open(os.sep.join([out_dir_top, "device_sn.csv"]), "w") + dev_sn_file.write(DEV_SN_CSV_HDR) + + for i in range(args.count): + pai_cert = {} + serial_num_str = format(serial_num_int + i, 'x') + logger.info("Generating for {}".format(serial_num_str)) + dev_sn_file.write(serial_num_str + '\n') + out_dirs = setup_out_dir(out_dir_top, args, serial_num_str) + add_additional_kv(args, serial_num_str) + generate_passcode(args, out_dirs) + generate_discriminator(args, out_dirs) + if args.paa or args.pai: + pai_cert = setup_root_certificates(args, out_dirs) + dacs_cert = write_device_unique_data(args, out_dirs, pai_cert) + generate_partition(args, out_dirs) + generate_json_summary(args, out_dirs, pai_cert, dacs_cert, serial_num_str) + + dev_sn_file.close() + + +if __name__ == "__main__": + main() diff --git a/scripts/tools/telink/readme.md b/scripts/tools/telink/readme.md new file mode 100644 index 00000000000000..d3539b590d3591 --- /dev/null +++ b/scripts/tools/telink/readme.md @@ -0,0 +1,212 @@ +# Manufacturing Partition Generator Utility + +This tool is designed to generate factory partitions for mass production. + +## Dependencies + +Please make sure you have had the following tools before using the generator +tool. + +- [CHIP Certificate Tool](https://github.com/project-chip/connectedhomeip/tree/master/src/tools/chip-cert) + +- [SPAKE2P Parameters Tool](https://github.com/project-chip/connectedhomeip/tree/master/src/tools/spake2p) + +- [chip-tool](https://github.com/project-chip/connectedhomeip/tree/master/examples/chip-tool) + +### [Build Matter tools](https://github.com/project-chip/connectedhomeip/blob/master/docs/guides/BUILDING.md#build-for-the-host-os-linux-or-macos) + +1. Using the following commands to generate chip-tool, spake2p and chip-cert at + `path/to/connectedhomeip/build/out/host`. + + ```shell + cd path/to/connectedhomeip + source scripts/activate.sh + gn gen build/out/host + ninja -C build/out/host + ``` + +2. Add the tools path to \$PATH + + ```shell + export PATH="$PATH:path/to/connectedhomeip/build/out/host" + ``` + +### Install python dependencies + +```shell +cd path/to/connectedhomeip/scripts/tools/telink/ +python3 -m pip install -r requirements.txt +``` + +## Usage + +The following commands generate factory partitions using the default testing PAI +keys, certificates, and CD in Matter project. You can make it using yours +instead in real production. + +### Generate a factory partition + +```shell +python3 mfg_tool.py -v 0xFFF2 -p 0x8001 \ +--serial-num AABBCCDDEEFF11223344556677889900 \ +--vendor-name "Telink Semiconductor" \ +--product-name "not-specified" \ +--mfg-date 2022-12-12 \ +--hw-ver 1 \ +--hw-ver-str "prerelase" \ +--pai \ +--key /path/to/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Key.pem \ +--cert /path/to/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Cert.pem \ +-cd /path/to/connectedhomeip/credentials/test/certification-declaration/Chip-Test-CD-FFF2-8001.der \ +--spake2-path /path/to/spake2p \ +--chip-tool-path /path/to/chip-tool \ +--chip-cert-path /path/to/chip-cert +``` + +### Generate 5 factory partitions [Optional argument : --count] + +```shell +python3 mfg_tool.py --count 5 -v 0xFFF2 -p 0x8001 \ +--serial-num AABBCCDDEEFF11223344556677889900 \ +--vendor-name "Telink Semiconductor" \ +--product-name "not-specified" \ +--mfg-date 2022-02-02 \ +--hw-ver 1 \ +--hw-ver-str "prerelase" \ +--pai \ +--key /path/to/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Key.pem \ +--cert /path/to/connectedhomeip/credentials/test/attestation/Chip-Test-PAI-FFF2-8001-Cert.pem \ +-cd /path/to/connectedhomeip/credentials/test/certification-declaration/Chip-Test-CD-FFF2-8001.der \ +--spake2-path /path/to/spake2p \ +--chip-tool-path /path/to/chip-tool \ +--chip-cert-path /path/to/chip-cert +``` + +## Output files and directory structure + +``` +out +├── device_sn.csv +└── fff2_8001 + ├── aabbccddeeff11223344556677889900 + │   ├── factory_data.bin + │   ├── factory_data.hex + │   ├── internal + │   │   ├── DAC_cert.der + │   │   ├── DAC_cert.pem + │   │   ├── DAC_key.pem + │   │   ├── DAC_private_key.bin + │   │   ├── DAC_public_key.bin + │   │   └── pai_cert.der + │   ├── onb_codes.csv + │   ├── pin_disc.csv + │   ├── qrcode.png + │   └── summary.json + ├── aabbccddeeff11223344556677889901 + │   ├── factory_data.bin + │   ├── factory_data.hex + │   ├── internal + │   │   ├── DAC_cert.der + │   │   ├── DAC_cert.pem + │   │   ├── DAC_key.pem + │   │   ├── DAC_private_key.bin + │   │   ├── DAC_public_key.bin + │   │   └── pai_cert.der + │   ├── onb_codes.csv + │   ├── pin_disc.csv + │   ├── qrcode.png + │   └── summary.json + ├── aabbccddeeff11223344556677889902 + │   ├── factory_data.bin + │   ├── factory_data.hex + │   ├── internal + │   │   ├── DAC_cert.der + │   │   ├── DAC_cert.pem + │   │   ├── DAC_key.pem + │   │   ├── DAC_private_key.bin + │   │   ├── DAC_public_key.bin + │   │   └── pai_cert.der + │   ├── onb_codes.csv + │   ├── pin_disc.csv + │   ├── qrcode.png + │   └── summary.json + └── aabbccddeeff11223344556677889903 + ├── factory_data.bin + ├── factory_data.hex + ├── internal + │   ├── DAC_cert.der + │   ├── DAC_cert.pem + │   ├── DAC_key.pem + │   ├── DAC_private_key.bin + │   ├── DAC_public_key.bin + │   └── pai_cert.der + ├── onb_codes.csv + ├── pin_disc.csv + ├── qrcode.png + └── summary.json +``` + +Tool generates following output files: + +- Partition Binary : `factory_data.bin` and `factory_data.hex` +- Partition JSON : `summary.json` +- Onboarding codes : `onb_codes.csv` +- QR Code image : `qrcode.png` + +Other intermediate files are stored in `internal/` directory: + +- PAI Certificate : `pai_cert.der` +- DAC Certificates : `DAC_cert.der` and `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 SN. + +## Flashing the factory partition FW into Matter App + +You can try one of these factory partition FW on developing stage. + +1. Prepare a Matter App FW with empty factory data partition. + + For example, `lighting-app`. Please generate the FW as below: + + ```shell + cd path/to/connectedhomeip/example/ligting-app/telink/ + west build -- -DCONFIG_CHIP_FACTORY_DATA=y + ``` + + The output FW is stored at `./build/zephyr/zephyr.bin`. + +2. Then flash Matter App FW onto B91 board. + +3. Then flash the `factory_data.bin` generated from the generator tool at + specific address: + + > Note: The offset for Matter + > [v1.0-branch](https://github.com/telink-semi/zephyr/blob/telink_matter_v1.0-branch/boards/riscv/tlsr9518adk80d/tlsr9518adk80d.dts) + > is `0xF4000` and for + > [master branch](https://github.com/telink-semi/zephyr/blob/telink_matter/boards/riscv/tlsr9518adk80d/tlsr9518adk80d.dts) + > is `0x1F4000`. You can check the `factory_partition` reg at + > `tlsr9518adk80d.dts` for details. + + For example, the `factory_data_bin` with serial number + `aabbccddeeff11223344556677889900`. Here is the expected output in logging: + + ```shell + ... + I: 947 [DL]Device Configuration: + I: 951 [DL] Serial Number: aabbccddeeff11223344556677889900 + I: 957 [DL] Vendor Id: 65522 (0xFFF2) + I: 961 [DL] Product Id: 32769 (0x8001) + I: 965 [DL] Hardware Version: 1 + I: 969 [DL] Setup Pin Code (0 for UNKNOWN/ERROR): 93320241 + I: 975 [DL] Setup Discriminator (0xFFFF for UNKNOWN/ERROR): 3008 (0xBC0) + I: 983 [DL] Manufacturing Date: 2022-02-02 + I: 988 [DL] Device Type: 65535 (0xFFFF) + I: 993 [SVR]SetupQRCode: [MT:634J042C00O-KB7Z-10] + I: 999 [SVR]Copy/paste the below URL in a browser to see the QR Code: + I: 1006 [SVR]https://project-chip.github.io/connectedhomeip/qrcode.html?data=MT%3A634J042C00O-KB7Z-10 + I: 1017 [SVR]Manual pairing code: [26251356956] + ... + ``` diff --git a/scripts/tools/telink/requirements.txt b/scripts/tools/telink/requirements.txt new file mode 100644 index 00000000000000..68323409dd2f6c --- /dev/null +++ b/scripts/tools/telink/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 \ No newline at end of file diff --git a/scripts/tools/telink/telink_factory_data.schema b/scripts/tools/telink/telink_factory_data.schema deleted file mode 100644 index 561bf4d5d72682..00000000000000 --- a/scripts/tools/telink/telink_factory_data.schema +++ /dev/null @@ -1,164 +0,0 @@ -{ - "$id": "Telink_Factory_Data_schema", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "A representation of all factory data used in Matter's Telink device", - "type": "object", - "required": [ - "version", - "sn", - "vendor_id", - "product_id", - "vendor_name", - "product_name", - "date", - "hw_ver", - "hw_ver_str", - "dac_cert", - "dac_key", - "pai_cert", - "spake2_it", - "spake2_salt", - "spake2_verifier", - "discriminator" - ], - "properties": { - "version": { - "description": "Current version of the factory data set", - "type": "integer", - "minimum": 0, - "maximum": 255 - }, - "sn": { - "description": "Serial number of device", - "type": "string", - "maxLength": 32 - }, - "vendor_id": { - "description": "Vendor Identifier", - "type": "integer", - "minimum": 0, - "maximum": 65524 - }, - "product_id": { - "description": "Product Identifier", - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "vendor_name": { - "description": "human-readable vendor name", - "type": "string", - "maxLength": 32 - }, - "product_name": { - "description": "human-readable product name", - "type": "string", - "maxLength": 32 - }, - "product_label": { - "description": "more user-friendly human-readable product name", - "type": "string", - "maxLength": 64 - }, - "product_url": { - "description": "link to product-specific web page", - "type": "string", - "maxLength": 256 - }, - "part_number": { - "description": "human-readable vendor assigned part number", - "type": "string", - "maxLength": 32 - }, - "date": { - "description": "Manufacturing date according to ISO 8601 in notation YYYY-MM-DD", - "type": "string", - "format": "date", - "minLength": 10, - "maxLength": 10, - "pattern": "^\\d{4}-\\d{2}-\\d{2}$" - }, - "hw_ver": { - "description": "Hardware version - integer", - "type": "integer", - "minimum": 0, - "maximum": 65536 - }, - "hw_ver_str": { - "description": "A string representation of hardware version", - "type": "string", - "minLength": 1, - "maxLength": 64 - }, - "rd_uid": { - "description": "A randomly-generated 128-bit or longer octet string. Length has been expanded with 'hex:' prefix", - "type": "string", - "pattern": "^hex:([0-9A-Fa-f]{2}){16,}$", - "minLength": 20, - "minLength": 36 - }, - "dac_cert": { - "description": "DAC certificate in hex-string format", - "type": "string", - "pattern": "^hex:([0-9A-Fa-f]{2})+$", - "minLength": 6, - "maxLength": 1204 - }, - "dac_key": { - "description": "DAC Private Key in hex-string format", - "type": "string", - "pattern": "^hex:([0-9A-Fa-f]{2}){32}$", - "minLength": 68, - "maxLength": 68 - }, - "pai_cert": { - "description": "PAI certificate in hex-string format", - "type": "string", - "pattern": "^hex:([0-9A-Fa-f]{2})+$", - "minLength": 6, - "maxLength": 1204 - }, - "passcode": { - "description": "A default PASE session passcode", - "type": "integer", - "minimum": 1, - "maximum": 99999998 - }, - "spake2_it": { - "description": "An Iteration counter for the Symmetric Password-Authenticated Key Exchange", - "type": "integer", - "minimum": 1000, - "maximum": 100000 - }, - "spake2_salt": { - "description": "A key-derivation function for the Symmetric Password-Authenticated Key Exchange.", - "type": "string", - "pattern": "^hex:([0-9A-Fa-f]{2})+$", - "minLength": 36, - "maxLength": 68 - }, - "spake2_verifier": { - "description": "A verifier for the Symmetric Password-Authenticated Key Exchange", - "type": "string", - "pattern": "^hex:([0-9A-Fa-f]{2})+$", - "minLength": 97 - }, - "discriminator": { - "description": "The Discriminator value helps to further identify potential devices during the setup process.", - "type": "integer", - "minimum": 0, - "maximum": 4095 - }, - "enable_key": { - "description": "The Enable Key is a 128-bit value that triggers manufacturer-specific action while invoking the TestEventTrigger Command", - "type": "string", - "pattern": "^hex:([0-9A-Fa-f]{2}){16}$", - "minLength": 36, - "maxLength": 36 - }, - "user": { - "description": "A user-specific additional data which should be added to factory data. This should be a Json format.", - "type": "object" - } - } -} diff --git a/scripts/tools/telink/telink_generate_partition.py b/scripts/tools/telink/telink_generate_partition.py deleted file mode 100644 index ddd3b63acc4a7f..00000000000000 --- a/scripts/tools/telink/telink_generate_partition.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2022 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. -# - -import codecs -import sys -from intelhex import IntelHex -import argparse -import json -import logging as log -import cbor2 as cbor - -HEX_PREFIX = "hex:" - - -class PartitionCreator: - """ - Class to create telink partition containing FactoryData - - :param offset: This is a partition offset where data will be stored in device's flash memory - :param length: This is a maximum partition size - :param input: This is a path to input JSON file - :param output: This is a path to output directory - - """ - - def __init__(self, offset: int, length: int, input: str, output: str) -> None: - self._ih = IntelHex() - self._length = length - self._offset = offset - self._data_ready = False - self._output = output - self._input = input - try: - self.__data_to_save = self._convert_to_dict(self._load_json()) - except IOError: - sys.exit(-1) - - def generate_cbor(self): - """ - Generates .cbor file using cbor2 library. - It generate a CBORTag 55799 which is user-specific tag - - """ - if self.__data_to_save: - # prepare raw data from Json - cbor_data = cbor.dumps(self.__data_to_save) - return cbor_data - - def create_hex(self, data: bytes): - """ - Creates .hex file from CBOR. - This file can be write directly to device. - - """ - if len(data) > self._length: - raise ValueError("generated CBOR file exceeds declared maximum partition size! {} > {}".format(len(data), self._length)) - self._ih.putsz(self._offset, data) - self._ih.write_hex_file(self._output + ".hex", True) - self._data_ready = True - return True - - def create_bin(self): - """ - Creates raw binary data of created previously .hex file - - """ - if not self._data_ready: - log.error("Please create hex file first!") - return False - self._ih.tobinfile(self._output + ".bin") - return True - - @staticmethod - def _convert_to_dict(data): - """ - Converts a list containing tuples ("key_name", "key_value") to a dictionary - - If "key_value" of data entry is a string-type variable and contains a HEX_PREFIX algorithm decodes it - to hex format to be sure that a cbor file will contain proper bytes. - - If "key_value" of data entry is a dictionary, algorithm appends it to the created dictionary. - """ - output_dict = dict() - for entry in data: - if not isinstance(entry, dict): - log.debug("Processing entry {}".format(entry)) - if isinstance(data[entry], str) and data[entry].startswith(HEX_PREFIX): - output_dict[entry] = codecs.decode(data[entry][len(HEX_PREFIX):], "hex") - elif isinstance(data[entry], str): - output_dict[entry] = data[entry].encode("utf-8") - else: - output_dict[entry] = data[entry] - else: - output_dict[entry] = entry - return output_dict - - def _load_json(self): - """ - Loads file containing a JSON data and converts it to JSON format - - :raises IOError: if provided JSON file can not be read out. - """ - try: - with open(self._input, "rb") as json_file: - return json.loads(json_file.read()) - except IOError as e: - log.error("Can not read Json file {}".format(self._input)) - raise e - - -def print_flashing_help(): - print("\nTo flash the generated hex/bin containing factory data, use BDT tool") - - -def main(): - - def allow_any_int(i): return int(i, 0) - - parser = argparse.ArgumentParser(description="Telink Factory Data NVS partition generator tool") - parser.add_argument("-i", "--input", type=str, required=True, - help="Path to input .json file") - parser.add_argument("-o", "--output", type=str, required=True, - help="Prefix for output file paths, e.g. setting dir/output causes creation of the following files: dir/output.hex, and dir/output.bin") - parser.add_argument("--offset", type=allow_any_int, required=True, - help="Partition offset - an address in device's NVM memory, where factory data will be stored") - parser.add_argument("--size", type=allow_any_int, required=True, - help="The maximum partition size") - parser.add_argument("-v", "--verbose", action="store_true", - help="Run this script with DEBUG logging level") - parser.add_argument("-r", "--raw", action="store_true", - help="Do not print flashing help and other logs, only generate a .hex file. It can be useful when the script is used by other script.") - args = parser.parse_args() - - if args.verbose: - log.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=log.DEBUG) - elif args.raw: - log.basicConfig(format='%(message)s', level=log.ERROR) - else: - log.basicConfig(format='[%(asctime)s] %(message)s', level=log.INFO) - - partition_creator = PartitionCreator(args.offset, args.size, args.input, args.output) - cbor_data = partition_creator.generate_cbor() - try: - if not args.raw: - print("Generating .hex file: {}.hex with offset: {} and size: {}".format(args.output, hex(args.offset), hex(args.size))) - if partition_creator.create_hex(cbor_data) and partition_creator.create_bin(): - if not args.raw: - print_flashing_help() - except ValueError as e: - log.error(e) - sys.exit(-1) - - -if __name__ == "__main__": - main()