diff --git a/config/nrfconnect/chip-module/Kconfig b/config/nrfconnect/chip-module/Kconfig index 6550dca841b51e..32f7b2ad2f67ba 100644 --- a/config/nrfconnect/chip-module/Kconfig +++ b/config/nrfconnect/chip-module/Kconfig @@ -123,7 +123,7 @@ config CHIP_FACTORY_DATA_MERGE_WITH_FIRMWARE factory data. # Use default certificates without generating or providing them -config CHIP_FACTORY_DATA_USE_DEFAULTS_CERTS +config CHIP_FACTORY_DATA_USE_DEFAULT_CERTS bool "Use default certificates located in Matter repository" default y help diff --git a/config/nrfconnect/chip-module/generate_factory_data.cmake b/config/nrfconnect/chip-module/generate_factory_data.cmake index 02b6e6ce4c0ac0..004b67d9c224f7 100644 --- a/config/nrfconnect/chip-module/generate_factory_data.cmake +++ b/config/nrfconnect/chip-module/generate_factory_data.cmake @@ -55,10 +55,12 @@ if(NOT CONFIG_CHIP_DEVICE_GENERATE_ROTATING_DEVICE_UID) else() string(APPEND script_args "--rd_uid \"${CONFIG_CHIP_DEVICE_ROTATING_DEVICE_UID}\"\n") endif() +else() + string(APPEND script_args "--generate_rd_uid\n") endif() # for development purpose user can use default certs instead of generating or providing them -if(CONFIG_CHIP_FACTORY_DATA_USE_DEFAULTS_CERTS) +if(CONFIG_CHIP_FACTORY_DATA_USE_DEFAULT_CERTS) # convert decimal PID to its hexadecimal representation to find out certification files in repository math(EXPR LOCAL_PID "${CONFIG_CHIP_DEVICE_PRODUCT_ID}" OUTPUT_FORMAT HEXADECIMAL) string(SUBSTRING ${LOCAL_PID} 2 -1 raw_pid) @@ -68,14 +70,9 @@ if(CONFIG_CHIP_FACTORY_DATA_USE_DEFAULTS_CERTS) string(APPEND script_args "--dac_key \"${CHIP_ROOT}/credentials/development/attestation/Matter-Development-DAC-${raw_pid}-Key.der\"\n") string(APPEND script_args "--pai_cert \"${CHIP_ROOT}/credentials/development/attestation/Matter-Development-PAI-noPID-Cert.der\"\n") else() - # try to generate a new DAC and PAI certs and DAC key - # request script to generate a new certificates - # by adding an argument to script_args - find_program(chip-cert NAMES chip-cert) - if(NOT chip-cert) - message(FATAL_ERROR "Could not find chip_cert_path executable in PATH") - endif() - string(APPEND script_args "--chip_cert_path ${chip-cert}\n") + 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") endif() # add Password-Authenticated Key Exchange parameters @@ -88,10 +85,7 @@ string(APPEND script_args "--passcode ${CONFIG_CHIP_DEVICE_SPAKE2_PASSCODE}\n") 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) - if(NOT spake_exe) - message(FATAL_ERROR "Could not find spake2p executable in PATH") - endif() + find_program(spake_exe NAMES spake2p REQUIRED) string(APPEND script_args "--spake2p_path ${spake_exe}\n") else() # Spake2 verifier should be provided using kConfig diff --git a/docs/guides/nrfconnect_factory_data_configuration.md b/docs/guides/nrfconnect_factory_data_configuration.md index 9e587c85030fdb..3dc71cd58ddd92 100644 --- a/docs/guides/nrfconnect_factory_data_configuration.md +++ b/docs/guides/nrfconnect_factory_data_configuration.md @@ -228,6 +228,9 @@ $ python scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py -h --passcode --spake2p_path ``` + > Note: To generate new SPAKE2+ verifier you need `spake2p` executable. See + > the note at the end of this section to learn how to get it. + - Manual: ``` @@ -235,7 +238,19 @@ $ python scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py -h ``` d. Add paths to `.der` files that contain PAI and DAC certificates and the - DAC private key (replace the respective variables with the file names): + DAC private key (replace the respective variables with the file names) using + one of the following methods: + + - Automatic: + + ``` + --chip_cert_path + ``` + + > Note: To generate new certificates, you need the `chip-cert` executable. + > See the note at the end of this section to learn how to get it. + + - Manual: ``` --dac_cert .der --dac_key .der --pai_cert .der @@ -328,6 +343,17 @@ If the script finishes successfully, go to the location you provided with the > 3. Add the `connectedhomeip/src/tools/spake2p/out/spake2p` path as an > argument of `--spake2p_path` for the Python script. +> Note: Generating new certificates is optional if default vendor and product +> IDs are used and requires providing a path to the `chip-cert` executable. To +> get it, complete the following steps: +> +> 1. Navigate to the `connectedhomeip` root directory. +> 2. In a terminal, run the command: +> `cd src/tools/chip-cert && gn gen out && ninja -C out chip-cert` to build +> the executable. +> 3. Add the `connectedhomeip/src/tools/chip-cert/out/chip-cert` path as an +> argument of `--chip_cert_path` for the Python script. + > Note: By default, overwriting the existing JSON file is disabled. This means > that you cannot create a new JSON file with the same name in the exact > location as an existing file. To allow overwriting, add the `--overwrite` @@ -625,6 +651,27 @@ $ west build -b nrf52840dk_nrf52840 -- \ -DCONFIG_CHIP_FACTORY_DATA_MERGE_WITH_FIRMWARE=y ``` +You can also build an example with auto-generation of new CD, DAC and PAI +certificates. The newly generated certificates will be added to factory data set +automatically. To generate new certificates disable using default certificates +by building an example with the additional option +`-DCHIP_FACTORY_DATA_USE_DEFAULT_CERTS=n`: + +``` +$ west build -b nrf52840dk_nrf52840 -- \ +-DCONFIG_CHIP_FACTORY_DATA=y \ +-DCONFIG_CHIP_FACTORY_DATA_BUILD=y \ +-DCONFIG_CHIP_FACTORY_DATA_MERGE_WITH_FIRMWARE=y \ +-DCONFIG_CHIP_FACTORY_DATA_USE_DEFAULT_CERTS=n +``` + +> Note: To generate new certificates using the nRF Connect platform build +> system, you need the `chip-cert` executable in your system variable PATH. To +> learn how to get `chip-cert`, go to the note at the end of +> [creating the factory data partition with the second script](#creating-the-factory-data-partition-with-the-second-script) +> section, and then add the newly built executable to the system variable PATH. +> The Cmake build system will find this executable automatically. + After that, use the following command from the example's directory to write firmware and newly generated factory data at the same time: diff --git a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py index 62e9fa303e051b..76329650f834a1 100644 --- a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py +++ b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py @@ -16,6 +16,7 @@ # from os.path import exists +import os import sys import json import jsonschema @@ -24,6 +25,7 @@ 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 @@ -31,6 +33,7 @@ # the factory data version set in the nRF Connect platform Kconfig file (CHIP_FACTORY_DATA_VERSION). FACTORY_DATA_VERSION = 1 +MATTER_ROOT = os.path.dirname(os.path.realpath(__file__))[:-len("/scripts/tools/nrfconnect")] HEX_PREFIX = "hex:" PUB_KEY_PREFIX = b'\x04' INVALID_PASSCODES = [00000000, 11111111, 22222222, 33333333, 44444444, @@ -61,6 +64,112 @@ def get_raw_private_key_der(der_file: str, password: str): 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, + 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", str(1), + "--vendor-id", hex(vendor_id), + "--product-id", hex(product_id), + "--device-type-id", "0xA", + "--certificate-id", "ZIG20142ZB330003-24", + "--security-level", str(0), + "--security-info", str(0), + "--certification-type", str(0), + "--version-number", "0x2694", + ] + 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_params(spake2p_path: str, passcode: int, it: int, salt: str) -> dict: """ Generate spake2 params using external spake2p script @@ -151,12 +260,29 @@ def generate_json(self): spake_2_verifier = base64.b64decode(self._args.spake2_verifier) # convert salt to bytestring to be coherent with spake2 verifier type - spake_2_salt = base64.b64decode(self._args.spake2_salt) + spake_2_salt = bytes(self._args.spake2_salt, 'utf-8') + + 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.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(self._args.dac_key, self._args.dac_key_password) + dac_priv_key = get_raw_private_key_der(dac_key, self._args.dac_key_password) if dac_priv_key is None: - log.error("Can not read DAC keys from : {}".format(self._args.dac_key)) + log.error("Can not read DAC keys from : {}".format(dac_key)) sys.exit(-1) try: @@ -175,9 +301,9 @@ def generate_json(self): 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(self._args.dac_cert)) + 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(self._args.pai_cert)) + 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) @@ -329,6 +455,12 @@ def allow_any_int(i): return int(i, 0) help="[ascii string] Provide Spake2 Verifier without generating it.") 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("--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: @@ -336,10 +468,6 @@ def allow_any_int(i): return int(i, 0) else: log.basicConfig(format='[%(levelname)s] %(message)s', level=log.INFO) - if(args.chip_cert_path): - log.error("Generating DAC and PAI certificates is not supported yet") - return - # 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))