diff --git a/.github/workflows/examples-nrfconnect.yaml b/.github/workflows/examples-nrfconnect.yaml index 4f0efb889359c8..929d8af509aeba 100644 --- a/.github/workflows/examples-nrfconnect.yaml +++ b/.github/workflows/examples-nrfconnect.yaml @@ -99,7 +99,9 @@ jobs: run: scripts/run_in_build_env.sh "python3 scripts/setup/nrfconnect/update_ncs.py --check" - name: Run unit tests of factory data generation script timeout-minutes: 15 - run: scripts/run_in_build_env.sh "./scripts/tools/nrfconnect/tests/test_generate_factory_data.py" + run: | + scripts/run_in_build_env.sh 'pip3 install -r scripts/setup/requirements.nrfconnect.txt' + scripts/run_in_build_env.sh "./scripts/tools/nrfconnect/tests/test_generate_factory_data.py" - name: Build example nRF Connect SDK Lock App on nRF52840 DK if: github.event_name == 'push' || steps.changed_paths.outputs.nrfconnect == 'true' timeout-minutes: 15 diff --git a/config/nrfconnect/chip-module/Kconfig b/config/nrfconnect/chip-module/Kconfig index 390a99b1556e77..e5fbdb405e365d 100644 --- a/config/nrfconnect/chip-module/Kconfig +++ b/config/nrfconnect/chip-module/Kconfig @@ -139,6 +139,14 @@ config CHIP_FACTORY_DATA_MERGE_WITH_FIRMWARE when flashing the firmware using the west tool, includes the factory data as well. +config CHIP_FACTORY_DATA_GENERATE_ONBOARDING_CODES + bool "Generate onboarding codes during the generation of a factory data set" + help + Enables generation of onboarding codes (manual pairing code and QR code) + during the generation of a factory data set. You can provide the + onboarding codes a Matter controller to commission a device to a Matter + network. + # Select source of the certificates choice CHIP_FACTORY_DATA_CERT_SOURCE prompt "Attestation certificate file source" diff --git a/config/nrfconnect/chip-module/generate_factory_data.cmake b/config/nrfconnect/chip-module/generate_factory_data.cmake index b1df43fa59cfce..a754f22873c13d 100644 --- a/config/nrfconnect/chip-module/generate_factory_data.cmake +++ b/config/nrfconnect/chip-module/generate_factory_data.cmake @@ -91,6 +91,10 @@ string(APPEND script_args "--passcode ${CONFIG_CHIP_DEVICE_SPAKE2_PASSCODE}\n") string(APPEND script_args "--include_passcode\n") string(APPEND script_args "--overwrite\n") +if(CONFIG_CHIP_FACTORY_DATA_GENERATE_ONBOARDING_CODES) + string(APPEND script_args "--generate_onboarding\n") +endif() + # check if spake2 verifier should be generated using script if(NOT CONFIG_CHIP_FACTORY_DATA_GENERATE_SPAKE2_VERIFIER) # 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 dd881349683a0c..16a9210c41fb8c 100644 --- a/docs/guides/nrfconnect_factory_data_configuration.md +++ b/docs/guides/nrfconnect_factory_data_configuration.md @@ -42,6 +42,8 @@ data secure by applying hardware write protection. - [Option 1: Using the php-json-schema tool](#option-1-using-the-php-json-schema-tool) - [Option 2: Using a website validator](#option-2-using-a-website-validator) - [Option 3: Using the nRF Connect Python script](#option-3-using-the-nrf-connect-python-script) + - [Generating onboarding codes](#generating-onboarding-codes) + - [Enabling onboarding codes generation within the build system](#enabling-onboarding-codes-generation-within-the-build-system) - [Preparing factory data partition on a device](#preparing-factory-data-partition-on-a-device) - [Creating a factory data partition with the second script](#creating-a-factory-data-partition-with-the-second-script) - [Building an example with factory data](#building-an-example-with-factory-data) @@ -493,6 +495,59 @@ as an additional argument. To do this, complete the following steps: > **Note:** To learn more about the JSON schema, visit > [this unofficial JSON Schema tool usage website](https://json-schema.org/understanding-json-schema/). +### Generating onboarding codes + +The +[generate_nrfconnect_chip_factory_data.py](../../scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py) +script lets you generating a manual code and a QR code from the given factory +data parameters. You can use these codes to perform commissioning to the Matter +network over Bluetooth LE since they include all the pairing data required by +the Matter controller. You can place these codes on the device packaging or on +the device itself during production. + +To generate a manual pairing code and a QR code, complete the following steps: + +1. Install all required Python dependencies for Matter: + + ``` + $ python -m pip install -r ./scripts/setup/requirements.nrfconnect.txt + ``` + +2. Complete steps 1, 2, and 3 from the + [Creating the factory data JSON file with the first script](#creating-the-factory-data-json-file-with-the-first-script) + section to prepare the final invocation of the Python script. + +3. Add the `--generate_onboarding` argument to the Python script final + invocation. + +4. Run the script. + +5. Navigate to the output directory provided as the `-o` argument. + +The output directory contains the following files you need: + +- JSON file containing the latest factory data set. +- Test file containing the generated manual code and the text version of the + QR Code. +- PNG file containing the generated QR Code as an image. + +#### Enabling onboarding codes generation within the build system + +You can generate onboarding codes using the nRF Connect platform build system +described in +[Building an example with factory data](#building-an-example-with-factory-data), +and build an example with the following additional option: +`-DCONFIG_CHIP_FACTORY_DATA_GENERATE_ONBOARDING_CODES=y`. + +For example, the build command for the nRF52840 DK could look like this: + +``` +$ west build -b nrf52840dk_nrf52840 -- \ +-DCONFIG_CHIP_FACTORY_DATA=y \ +-DCONFIG_CHIP_FACTORY_DATA_BUILD=y \ +-DCONFIG_CHIP_FACTORY_DATA_GENERATE_ONBOARDING_CODES=y +``` + ### Preparing factory data partition on a device The factory data partition is an area in the device's persistent storage where a diff --git a/scripts/setup/requirements.nrfconnect.txt b/scripts/setup/requirements.nrfconnect.txt index bf221a74da354a..c0c3c285441136 100644 --- a/scripts/setup/requirements.nrfconnect.txt +++ b/scripts/setup/requirements.nrfconnect.txt @@ -1,3 +1,6 @@ jsonschema>=4.4.0 cbor2>=5.4.3 -ecdsa>=0.18.0 \ No newline at end of file +ecdsa>=0.18.0 +qrcode==7.4.2 +bitarray==2.6.0 +python_stdnum==1.18 diff --git a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py index aea949dc6190c1..38e11967e744c1 100644 --- a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py +++ b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py @@ -29,6 +29,22 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import load_der_private_key +try: + import qrcode + from generate_setup_payload import CommissioningFlow, SetupPayload +except ImportError: + SDK_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) + sys.path.append(os.path.join(SDK_ROOT, "src/setup_payload/python")) + try: + import qrcode + from generate_setup_payload import CommissioningFlow, SetupPayload + except ModuleNotFoundError or ImportError: + no_onboarding_modules = True + else: + no_onboarding_modules = False +else: + no_onboarding_modules = False + try: import jsonschema except ImportError: @@ -327,6 +343,9 @@ def generate_json(self): except IOError: log.error("Cannot save output file into directory: {}".format(self._args.output)) + if self._args.generate_onboarding: + self._generate_onboarding_data() + 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)): @@ -375,6 +394,19 @@ def _process_der(self, path: str): log.error(e) raise e + def _generate_onboarding_data(self): + setup_payload = SetupPayload(discriminator=self._args.discriminator, + pincode=self._args.passcode, + rendezvous=2, # fixed pairing BLE + flow=CommissioningFlow.Standard, + vid=self._args.vendor_id, + pid=self._args.product_id) + with open(self._args.output[:-len(".json")] + ".txt", "w") as manual_code_file: + manual_code_file.write("Manualcode : " + setup_payload.generate_manualcode() + "\n") + manual_code_file.write("QRCode : " + setup_payload.generate_qrcode()) + qr = qrcode.make(setup_payload.generate_qrcode()) + qr.save(self._args.output[:-len(".json")] + ".png") + def main(): parser = argparse.ArgumentParser(description="NrfConnect Factory Data NVS generator tool") @@ -481,6 +513,9 @@ def base64_str(s): return base64.b64decode(s) 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.")) + optional_arguments.add_argument("--generate_onboarding", action="store_true", + help=("Generate a Manual Code and QR Code according to provided factory data set." + "As a result a PNG image containing QRCode and a .txt file containing Manual Code will be available within output directory")) args = parser.parse_args() if args.verbose: @@ -501,6 +536,12 @@ def base64_str(s): return base64.b64decode(s) "-r ./scripts/requirements.nrfconnect.txt from the Matter root directory.")) return + if args.generate_onboarding and no_onboarding_modules: + log.error(("Requested generation of onboarding codes, but the some modules are not installed. \n" + "Install all dependencies for Matter by invoking: pip3 install " + "-r ./scripts/requirements.nrfconnect.txt from the Matter root directory.")) + return + generator = FactoryDataGenerator(args) generator.generate_json() diff --git a/scripts/tools/nrfconnect/tests/test_generate_factory_data.py b/scripts/tools/nrfconnect/tests/test_generate_factory_data.py index 3ffa4c75b6bf0b..0be21f8fcb8bad 100755 --- a/scripts/tools/nrfconnect/tests/test_generate_factory_data.py +++ b/scripts/tools/nrfconnect/tests/test_generate_factory_data.py @@ -21,6 +21,7 @@ import subprocess import tempfile import unittest +from os.path import exists TOOLS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) @@ -120,6 +121,10 @@ 0xde, 0x3d, 0xc0, 0x14, 0x3a, 0x97, 0xe1, 0x35, 0x38, 0xf7, 0xff, 0x76, 0x05, 0x5e, 0xbf, 0x27, 0x90, 0x6f, 0x50, 0x0f]) +TEST_MANUAL_CODE = "Manualcode : 35442608082" + +TEST_QR_CODE = "QRCode : MT:KAYA3EYF15ND8B1OA00" + def write_file(path: str, content: bytes) -> None: with open(path, 'wb') as f: @@ -258,6 +263,50 @@ def test_generate_spake2p_verifier_default(self): '--raw' ]) + def test_generate_onboarding_codes(self): + with tempfile.TemporaryDirectory() as outdir: + write_file(os.path.join(outdir, 'DAC_key.der'), DAC_DER_KEY) + write_file(os.path.join(outdir, 'DAC_cert.der'), DAC_DER_CERT) + write_file(os.path.join(outdir, 'PAI_cert.der'), PAI_DER_CERT) + + subprocess.check_call(['python3', os.path.join(TOOLS_DIR, 'generate_nrfconnect_chip_factory_data.py'), + '-s', os.path.join(TOOLS_DIR, 'nrfconnect_factory_data.schema'), + '--include_passcode', + '--sn', 'SN:12345678', + '--vendor_id', '0x127F', + '--product_id', '0xABCD', + '--vendor_name', 'Nordic Semiconductor ASA', + '--product_name', 'Lock Gen2', + '--part_number', 'PCA10056', + '--product_url', 'https://example.com/lock', + '--product_label', 'Lock', + '--date', '2022-07-20', + '--hw_ver', '101', + '--hw_ver_str', 'v1.1', + '--dac_key', os.path.join(outdir, 'DAC_key.der'), + '--dac_cert', os.path.join(outdir, 'DAC_cert.der'), + '--pai_cert', os.path.join(outdir, 'PAI_cert.der'), + '--spake2_it', '2000', + '--spake2_salt', 'U1BBS0UyUCBLZXkgU2FsdA==', + '--passcode', '13243546', + '--spake2_verifier', ('WN0SgEXLfUN19BbJqp6qn4pS69EtdNLReIMZwv/CIM0ECMP7ytiAJ7txIYJ0Ovlha/' + 'rQ3E+88mj3qaqqnviMaZzG+OyXEdSocDIT9ZhmkTCgWwERaHz4Vdh3G37RT6kqbw=='), + '--discriminator', '0xFED', + '--rd_uid', '91a9c12a7c80700a31ddcfa7fce63e44', + '--enable_key', '00112233445566778899aabbccddeeff', + '--user', '{"name": "product_name", "version": 123, "revision": "0x123"}', + '-o', os.path.join(outdir, 'fd.json'), + '--generate_onboarding' + ]) + + self.assertTrue(exists(os.path.join(outdir, 'fd.txt'))) + self.assertTrue(exists(os.path.join(outdir, 'fd.png'))) + + with open(os.path.join(outdir, 'fd.txt'), 'r') as onboarding_code_file: + onboarding = onboarding_code_file.readlines() + self.assertEqual(onboarding[0][:-1], TEST_MANUAL_CODE) + self.assertEqual(onboarding[1], TEST_QR_CODE) + if __name__ == '__main__': unittest.main()