diff --git a/.github/workflows/examples-nrfconnect.yaml b/.github/workflows/examples-nrfconnect.yaml index 0e2c7f4c4f0090..ac05d5577f5dba 100644 --- a/.github/workflows/examples-nrfconnect.yaml +++ b/.github/workflows/examples-nrfconnect.yaml @@ -83,6 +83,9 @@ jobs: - name: Update nRF Connect SDK revision to the currently recommended. timeout-minutes: 10 run: scripts/run_in_build_env.sh "python3 scripts/setup/nrfconnect/update_ncs.py --update --shallow" + - name: Run unit tests of factory data generation script + timeout-minutes: 10 + run: 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: 10 diff --git a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py index 9cf7a10341c1ee..be06dad9bd355d 100644 --- a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py +++ b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py @@ -170,14 +170,14 @@ def gen_test_certs(chip_cert_exe: str, 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 +def gen_spake2p_params(spake2p_path: str, passcode: int, it: int, salt: bytes) -> dict: + """ Generate Spake2+ params using external spake2p tool Args: - spake2p_path (str): path to spake2 executable - passcode (int): Pairing passcode using in SPAKE 2 - it (int): Iteration counter for SPAKE2 Verifier generation - salt (str): Salt used to generate SPAKE2 password + spake2p_path (str): path to spake2p executable + passcode (int): Pairing passcode using in Spake2+ + it (int): Iteration counter for Spake2+ verifier generation + salt (str): Salt used to generate Spake2+ verifier Returns: dict: dictionary containing passcode, it, salt, and generated Verifier @@ -186,7 +186,7 @@ def gen_spake2p_params(spake2p_path: str, passcode: int, it: int, salt: str) -> cmd = [ spake2p_path, 'gen-verifier', '--iteration-count', str(it), - '--salt', str(salt), + '--salt', base64.b64encode(salt), '--pin-code', str(passcode), '--out', '-', ] @@ -197,7 +197,7 @@ def gen_spake2p_params(spake2p_path: str, passcode: int, it: int, salt: str) -> class FactoryDataGenerator: """ - Class to generate factory data from given arguments and generate a Json file + Class to generate factory data from given arguments and generate a JSON file """ @@ -221,11 +221,11 @@ def _validate_args(self): 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)) + raise AssertionError("Provided wrong user data, this is not a JSON format! {}".format(e)) assert (self._args.spake2_verifier or (self._args.passcode and self._args.spake2p_path)), \ - "Can not find spake2 verifier, to generate a new one please provide passcode (--passcode) and path to spake2p script (--spake2p_path)" + "Cannot find Spake2+ verifier, to generate a new one please provide passcode (--passcode) and path to spake2p tool (--spake2p_path)" assert (self._args.chip_cert_path or (self._args.dac_cert and self._args.pai_cert and self._args.dac_key)), \ - "Can not 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)" + "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), \ @@ -233,14 +233,14 @@ def _validate_args(self): def generate_json(self): """ - This function generates JSON data, .json file and validate it + This function generates JSON data, .json file and validates it. - To validate generated JSON data a scheme must be provided within script's arguments + 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 + 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. @@ -256,12 +256,12 @@ def generate_json(self): rd_uid = HEX_PREFIX + self._args.rd_uid if not self._args.spake2_verifier: - spake_2_verifier = base64.b64decode(self._generate_spake2_verifier()) + spake_2_verifier = self._generate_spake2_verifier() else: - spake_2_verifier = base64.b64decode(self._args.spake2_verifier) + spake_2_verifier = self._args.spake2_verifier - # convert salt to bytestring to be coherent with spake2 verifier type - spake_2_salt = bytes(self._args.spake2_salt, 'utf-8') + # 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, @@ -283,13 +283,13 @@ def generate_json(self): # 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("Can not read DAC keys from : {}".format(dac_key)) + log.error("Cannot read DAC keys from : {}".format(dac_key)) sys.exit(-1) try: json_file = open(self._args.output, "w+") except FileNotFoundError: - print("Can not create JSON file in this location: {}".format(self._args.output)) + print("Cannot create JSON file in this location: {}".format(self._args.output)) sys.exit(-1) with json_file: # serialize data @@ -313,9 +313,10 @@ def generate_json(self): self._add_entry("discriminator", self._args.discriminator) if rd_uid: self._add_entry("rd_uid", rd_uid) - self._add_entry("enable_key", HEX_PREFIX + self._args.enable_key) - # add user-specific data - self._add_entry("user", self._args.user) + 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) @@ -325,12 +326,12 @@ def generate_json(self): 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.") + 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("Can not save output file into directory: {}".format(self._args.output)) + 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") """ @@ -344,11 +345,11 @@ def _generate_spake2_verifier(self): """ If verifier has not been provided in arguments list it should be generated via external script """ spake2_params = gen_spake2p_params(self._args.spake2p_path, self._args.passcode, self._args.spake2_it, self._args.spake2_salt) - return spake2_params["Verifier"] + return base64.b64decode(spake2_params["Verifier"]) def _generate_rotating_device_uid(self): """ If rotating device unique ID has not been provided it should be generated """ - log.warning("Can not find rotating device UID in provided arguments list. A new one will 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 @@ -361,12 +362,12 @@ def _validate_output_json(self, output_json: str): """ try: with open(self._args.schema) as schema_file: - log.info("Validating Json with schema...") + 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)) + log.error("Provided JSON schema file is wrong: {}".format(self._args.schema)) return False else: log.info("Validate OK") @@ -387,18 +388,19 @@ def main(): parser = argparse.ArgumentParser(description="NrfConnect 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") + 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") + 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="passcode is used only for generating Spake2 Verifier to include it in factory data add this argument") + 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 @@ -425,9 +427,9 @@ def allow_any_int(i): return int(i, 0) 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 Counter.") - mandatory_arguments.add_argument("--spake2_salt", type=str, required=True, - help="[ascii string] Provide Spake2 Salt.") + 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 \ @@ -450,16 +452,16 @@ def allow_any_int(i): return int(i, 0) 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).") + help="[int | hex] Default PASE session passcode. (This is mandatory to generate Spake2+ verifier).") optional_arguments.add_argument("--spake2p_path", type=str, help="[string] Provide a path to spake2p. By default You can find spake2p in connectedhomeip/src/tools/spake2p directory and build it there.") - optional_arguments.add_argument("--spake2_verifier", type=str, - help="[ascii string] Provide Spake2 Verifier without generating it.") + 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'}.") + 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, diff --git a/scripts/tools/nrfconnect/tests/test_generate_factory_data.py b/scripts/tools/nrfconnect/tests/test_generate_factory_data.py new file mode 100755 index 00000000000000..8b45a0e81556ac --- /dev/null +++ b/scripts/tools/nrfconnect/tests/test_generate_factory_data.py @@ -0,0 +1,198 @@ +#!/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 base64 +import json +import os +import subprocess +import tempfile +import unittest + +TOOLS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + +DAC_DER_KEY = bytes([0x30, 0x77, 0x02, 0x01, 0x01, 0x04, 0x20, 0xbf, 0x26, 0xd5, 0xd2, 0x25, + 0xeb, 0x6b, 0x09, 0x6d, 0xd5, 0xa6, 0xb9, 0x03, 0x04, 0x8e, 0xf2, 0xd7, + 0x6e, 0xf2, 0xe8, 0x56, 0x25, 0x39, 0x0b, 0xd5, 0x70, 0xb2, 0xf1, 0x65, + 0x99, 0x30, 0xeb, 0xa0, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, + 0x03, 0x01, 0x07, 0xa1, 0x44, 0x03, 0x42, 0x00, 0x04, 0x48, 0x36, 0x32, + 0x85, 0x68, 0x70, 0x00, 0x9e, 0xd6, 0x8e, 0x78, 0xc0, 0xd9, 0x4b, 0xe7, + 0xd9, 0xb5, 0x97, 0xb7, 0x88, 0x1d, 0xfb, 0x96, 0x00, 0xbb, 0x47, 0x1d, + 0x8b, 0x70, 0xbb, 0xce, 0x1d, 0xf4, 0x47, 0xa7, 0x93, 0x60, 0x2e, 0x14, + 0xde, 0x07, 0xdb, 0x80, 0xef, 0x75, 0xd8, 0x6c, 0x55, 0x6c, 0x7a, 0xc4, + 0xb4, 0x06, 0x0d, 0x50, 0xe1, 0x0f, 0xe2, 0x26, 0x06, 0xb4, 0xdd, 0x1b, + 0x4f]) + +DAC_RAW_KEY = bytes([0xbf, 0x26, 0xd5, 0xd2, 0x25, 0xeb, 0x6b, 0x09, 0x6d, 0xd5, 0xa6, 0xb9, + 0x03, 0x04, 0x8e, 0xf2, 0xd7, 0x6e, 0xf2, 0xe8, 0x56, 0x25, 0x39, 0x0b, + 0xd5, 0x70, 0xb2, 0xf1, 0x65, 0x99, 0x30, 0xeb]) + +DAC_DER_CERT = bytes([0x30, 0x82, 0x01, 0xe8, 0x30, 0x82, 0x01, 0x8e, 0xa0, 0x03, 0x02, 0x01, + 0x02, 0x02, 0x08, 0x49, 0x2e, 0x20, 0xdb, 0x59, 0x76, 0xa8, 0x90, 0x30, + 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, + 0x3e, 0x31, 0x26, 0x30, 0x24, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x1d, + 0x4e, 0x6f, 0x72, 0x64, 0x69, 0x63, 0x20, 0x53, 0x65, 0x6d, 0x69, 0x63, + 0x6f, 0x6e, 0x64, 0x75, 0x63, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x53, 0x41, + 0x5f, 0x4c, 0x6f, 0x63, 0x6b, 0x31, 0x14, 0x30, 0x12, 0x06, 0x0a, 0x2b, + 0x06, 0x01, 0x04, 0x01, 0x82, 0xa2, 0x7c, 0x02, 0x01, 0x0c, 0x04, 0x31, + 0x32, 0x37, 0x46, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x32, 0x30, 0x37, 0x31, + 0x39, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x34, 0x39, + 0x31, 0x32, 0x30, 0x33, 0x32, 0x33, 0x35, 0x39, 0x35, 0x39, 0x5a, 0x30, + 0x54, 0x31, 0x26, 0x30, 0x24, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x1d, + 0x4e, 0x6f, 0x72, 0x64, 0x69, 0x63, 0x20, 0x53, 0x65, 0x6d, 0x69, 0x63, + 0x6f, 0x6e, 0x64, 0x75, 0x63, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x53, 0x41, + 0x5f, 0x4c, 0x6f, 0x63, 0x6b, 0x31, 0x14, 0x30, 0x12, 0x06, 0x0a, 0x2b, + 0x06, 0x01, 0x04, 0x01, 0x82, 0xa2, 0x7c, 0x02, 0x01, 0x0c, 0x04, 0x31, + 0x32, 0x37, 0x46, 0x31, 0x14, 0x30, 0x12, 0x06, 0x0a, 0x2b, 0x06, 0x01, + 0x04, 0x01, 0x82, 0xa2, 0x7c, 0x02, 0x02, 0x0c, 0x04, 0x41, 0x42, 0x43, + 0x44, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, + 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, + 0x03, 0x42, 0x00, 0x04, 0x48, 0x36, 0x32, 0x85, 0x68, 0x70, 0x00, 0x9e, + 0xd6, 0x8e, 0x78, 0xc0, 0xd9, 0x4b, 0xe7, 0xd9, 0xb5, 0x97, 0xb7, 0x88, + 0x1d, 0xfb, 0x96, 0x00, 0xbb, 0x47, 0x1d, 0x8b, 0x70, 0xbb, 0xce, 0x1d, + 0xf4, 0x47, 0xa7, 0x93, 0x60, 0x2e, 0x14, 0xde, 0x07, 0xdb, 0x80, 0xef, + 0x75, 0xd8, 0x6c, 0x55, 0x6c, 0x7a, 0xc4, 0xb4, 0x06, 0x0d, 0x50, 0xe1, + 0x0f, 0xe2, 0x26, 0x06, 0xb4, 0xdd, 0x1b, 0x4f, 0xa3, 0x60, 0x30, 0x5e, + 0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, + 0x30, 0x00, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, + 0x04, 0x04, 0x03, 0x02, 0x07, 0x80, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, + 0x0e, 0x04, 0x16, 0x04, 0x14, 0xbc, 0x6a, 0xa5, 0x79, 0x3c, 0x51, 0xa7, + 0x60, 0x18, 0x38, 0x66, 0x4b, 0x26, 0xa7, 0xd3, 0xec, 0x25, 0x87, 0x46, + 0x18, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, + 0x80, 0x14, 0x4b, 0x5b, 0xd0, 0x91, 0x60, 0xa3, 0x0e, 0xac, 0x2f, 0x94, + 0xa4, 0x82, 0x6b, 0xd7, 0x6e, 0x96, 0x39, 0xca, 0xd3, 0xb3, 0x30, 0x0a, + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x48, + 0x00, 0x30, 0x45, 0x02, 0x20, 0x38, 0xf4, 0x2f, 0xd7, 0x06, 0x9a, 0xbc, + 0xfc, 0x83, 0x2b, 0x74, 0xe1, 0xb6, 0x11, 0xb0, 0x2f, 0x72, 0xfd, 0xc2, + 0x75, 0x59, 0xdd, 0x7d, 0x04, 0x9c, 0x81, 0x37, 0x01, 0x74, 0x98, 0x77, + 0x22, 0x02, 0x21, 0x00, 0xb8, 0x9d, 0x63, 0x5b, 0xe2, 0xd3, 0x03, 0xe3, + 0xbc, 0xcb, 0x7e, 0x95, 0x18, 0xc2, 0xbb, 0x0c, 0x1d, 0xff, 0x3b, 0x3d, + 0x37, 0x41, 0x72, 0x2a, 0xd3, 0x4d, 0x38, 0x5c, 0x64, 0x2b, 0xc1, 0x46]) + +PAI_DER_CERT = bytes([0x30, 0x82, 0x01, 0xb4, 0x30, 0x82, 0x01, 0x5a, 0xa0, 0x03, 0x02, 0x01, + 0x02, 0x02, 0x08, 0x09, 0x10, 0x34, 0x50, 0x40, 0x83, 0x6a, 0x05, 0x30, + 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, + 0x1a, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x0f, + 0x4d, 0x61, 0x74, 0x74, 0x65, 0x72, 0x20, 0x54, 0x65, 0x73, 0x74, 0x20, + 0x50, 0x41, 0x41, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x32, 0x30, 0x37, 0x31, + 0x39, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x34, 0x39, + 0x31, 0x32, 0x30, 0x33, 0x32, 0x33, 0x35, 0x39, 0x35, 0x39, 0x5a, 0x30, + 0x3e, 0x31, 0x26, 0x30, 0x24, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x1d, + 0x4e, 0x6f, 0x72, 0x64, 0x69, 0x63, 0x20, 0x53, 0x65, 0x6d, 0x69, 0x63, + 0x6f, 0x6e, 0x64, 0x75, 0x63, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x53, 0x41, + 0x5f, 0x4c, 0x6f, 0x63, 0x6b, 0x31, 0x14, 0x30, 0x12, 0x06, 0x0a, 0x2b, + 0x06, 0x01, 0x04, 0x01, 0x82, 0xa2, 0x7c, 0x02, 0x01, 0x0c, 0x04, 0x31, + 0x32, 0x37, 0x46, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, + 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, + 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xd7, 0x0c, 0x57, 0xcd, 0xac, 0x0c, + 0x8b, 0x0b, 0x25, 0xfc, 0x64, 0x70, 0xa4, 0x2f, 0xb3, 0xf1, 0x37, 0xf7, + 0x5f, 0x65, 0x2c, 0xd0, 0xb2, 0x15, 0xf1, 0xfe, 0x13, 0x53, 0x52, 0x3f, + 0x59, 0x81, 0xd2, 0x3d, 0xf7, 0xf1, 0x59, 0x88, 0xbd, 0xce, 0xe4, 0x3a, + 0x20, 0x84, 0xe6, 0x1d, 0xe7, 0x3c, 0x83, 0xfb, 0xc4, 0x86, 0x5e, 0x5c, + 0xb8, 0x45, 0x5e, 0x2b, 0xa3, 0x70, 0x08, 0xfb, 0x05, 0x1b, 0xa3, 0x66, + 0x30, 0x64, 0x30, 0x12, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, + 0x04, 0x08, 0x30, 0x06, 0x01, 0x01, 0xff, 0x02, 0x01, 0x01, 0x30, 0x0e, + 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, + 0x01, 0x06, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, + 0x14, 0x4b, 0x5b, 0xd0, 0x91, 0x60, 0xa3, 0x0e, 0xac, 0x2f, 0x94, 0xa4, + 0x82, 0x6b, 0xd7, 0x6e, 0x96, 0x39, 0xca, 0xd3, 0xb3, 0x30, 0x1f, 0x06, + 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0x78, 0x5c, + 0xe7, 0x05, 0xb8, 0x6b, 0x8f, 0x4e, 0x6f, 0xc7, 0x93, 0xaa, 0x60, 0xcb, + 0x43, 0xea, 0x69, 0x68, 0x82, 0xd5, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, + 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x48, 0x00, 0x30, 0x45, 0x02, + 0x20, 0x60, 0x22, 0xfc, 0xeb, 0x83, 0x4c, 0x6f, 0xb1, 0x4b, 0xa0, 0x72, + 0x3b, 0xcd, 0x8f, 0x68, 0x51, 0x5b, 0x29, 0x04, 0xa9, 0x6f, 0x5d, 0xb7, + 0xec, 0xe1, 0xf2, 0x30, 0x93, 0xd7, 0x49, 0x7e, 0xee, 0x02, 0x21, 0x00, + 0xdf, 0xe7, 0x72, 0xe6, 0xdc, 0x1a, 0xad, 0xf0, 0x2c, 0x58, 0x7a, 0x0d, + 0xde, 0x3d, 0xc0, 0x14, 0x3a, 0x97, 0xe1, 0x35, 0x38, 0xf7, 0xff, 0x76, + 0x05, 0x5e, 0xbf, 0x27, 0x90, 0x6f, 0x50, 0x0f]) + + +def write_file(path: str, content: bytes) -> None: + with open(path, 'wb') as f: + f.write(content) + + +def read_json(path: str) -> object: + with open(path) as f: + return json.load(f) + + +def bytes_to_json(content: bytes) -> str: + return f'hex:{content.hex()}' + + +def base64_to_json(content: str) -> str: + return bytes_to_json(base64.b64decode(content)) + + +class TestGenerateFactoryData(unittest.TestCase): + + def test_generate_factory_data_all_specified(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'), + '--include_passcode', + '--sn', 'SN:12345678', + '--vendor_id', '0x127F', + '--product_id', '0xABCD', + '--vendor_name', 'Nordic Semiconductor ASA', + '--product_name', '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', '0123456789ABCDEF', + '--enable_key', '00112233445566778899aabbccddeeff', + '-o', os.path.join(outdir, 'fd.json') + ]) + + factory_data = read_json(os.path.join(outdir, 'fd.json')) + + self.assertEqual(factory_data.get('version'), 1) + self.assertEqual(factory_data.get('sn'), 'SN:12345678') + self.assertEqual(factory_data.get('vendor_id'), 0x127F) + self.assertEqual(factory_data.get('product_id'), 0xABCD) + self.assertEqual(factory_data.get('vendor_name'), 'Nordic Semiconductor ASA') + self.assertEqual(factory_data.get('product_name'), 'Lock') + self.assertEqual(factory_data.get('date'), '2022-07-20') + self.assertEqual(factory_data.get('hw_ver'), 101) + self.assertEqual(factory_data.get('hw_ver_str'), 'v1.1') + self.assertEqual(factory_data.get('dac_key'), bytes_to_json(DAC_RAW_KEY)) + self.assertEqual(factory_data.get('dac_cert'), bytes_to_json(DAC_DER_CERT)) + self.assertEqual(factory_data.get('pai_cert'), bytes_to_json(PAI_DER_CERT)) + self.assertEqual(factory_data.get('spake2_it'), 2000) + self.assertEqual(factory_data.get('spake2_salt'), base64_to_json('U1BBS0UyUCBLZXkgU2FsdA==')) + self.assertEqual(factory_data.get('spake2_verifier'), base64_to_json( + 'WN0SgEXLfUN19BbJqp6qn4pS69EtdNLReIMZwv/CIM0ECMP7ytiAJ7txIYJ0Ovlha/rQ3E+88mj3qaqqnviMaZzG+OyXEdSocDIT9ZhmkTCgWwERaHz4Vdh3G37RT6kqbw==')) + self.assertEqual(factory_data.get('discriminator'), 0xFED) + self.assertEqual(factory_data.get('passcode'), 13243546) + self.assertEqual(factory_data.get('rd_uid'), 'hex:0123456789ABCDEF') + self.assertEqual(factory_data.get('enable_key'), 'hex:00112233445566778899aabbccddeeff') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/include/platform/CHIPDeviceConfig.h b/src/include/platform/CHIPDeviceConfig.h index f6b2818e683bf8..f59f53df8048ab 100644 --- a/src/include/platform/CHIPDeviceConfig.h +++ b/src/include/platform/CHIPDeviceConfig.h @@ -960,7 +960,7 @@ #error "Non-default Spake2+ salt configured but verifier left unchanged" #endif -// Generated with: spake2p gen-verifier -o - -i 1000 -s "SPAKE2P Key Salt" -p 20202021 +// Generated with: spake2p gen-verifier -o - -i 1000 -s "U1BBS0UyUCBLZXkgU2FsdA==" -p 20202021 #define CHIP_DEVICE_CONFIG_USE_TEST_SPAKE2P_VERIFIER \ "uWFwqugDNGiEck/po7KHwwMwwqZgN10XuyBajPGuyzUEV/iree4lOrao5GuwnlQ65CJzbeUB49s31EH+NEkg0JVI5MGCQGMMT/SRPFNRODm3wH/MBiehuFc6FJ/" \ "NH6Rmzw==" diff --git a/src/tools/spake2p/Cmd_GenVerifier.cpp b/src/tools/spake2p/Cmd_GenVerifier.cpp index e39a9939b8dd48..3ba83571ca84c3 100644 --- a/src/tools/spake2p/Cmd_GenVerifier.cpp +++ b/src/tools/spake2p/Cmd_GenVerifier.cpp @@ -136,11 +136,12 @@ OptionSet *gCmdOptionSets[] = }; // clang-format on -uint32_t gCount = 1; -uint32_t gPinCode = chip::kSetupPINCodeUndefinedValue; -uint32_t gIterationCount = 0; +uint32_t gCount = 1; +uint32_t gPinCode = chip::kSetupPINCodeUndefinedValue; +uint32_t gIterationCount = 0; +uint8_t gSalt[BASE64_MAX_DECODED_LEN(BASE64_ENCODED_LEN(chip::kSpake2p_Max_PBKDF_Salt_Length))]; +uint8_t gSaltDecodedLen = 0; uint8_t gSaltLen = 0; -const char * gSalt = nullptr; const char * gOutFileName = nullptr; bool HandleOption(const char * progName, OptionSet * optSet, int id, const char * name, const char * arg) @@ -186,12 +187,28 @@ bool HandleOption(const char * progName, OptionSet * optSet, int id, const char break; case 's': - gSalt = arg; - if (!(strlen(gSalt) >= chip::kSpake2p_Min_PBKDF_Salt_Length && strlen(gSalt) <= chip::kSpake2p_Max_PBKDF_Salt_Length)) + if (strlen(arg) > BASE64_ENCODED_LEN(chip::kSpake2p_Max_PBKDF_Salt_Length)) { - fprintf(stderr, "%s: Invalid legth of the specified salt parameter: %s\n", progName, arg); + fprintf(stderr, "%s: Salt parameter too long: %s\n", progName, arg); return false; } + + gSaltDecodedLen = static_cast(chip::Base64Decode32(arg, static_cast(strlen(arg)), gSalt)); + + // The first check was just to make sure Base64Decode32 would not write beyond the buffer. + // Now double-check is the length is correct. + if (gSaltDecodedLen > chip::kSpake2p_Max_PBKDF_Salt_Length) + { + fprintf(stderr, "%s: Salt parameter too long: %s\n", progName, arg); + return false; + } + + if (gSaltDecodedLen < chip::kSpake2p_Min_PBKDF_Salt_Length) + { + fprintf(stderr, "%s: Salt parameter too short: %s\n", progName, arg); + return false; + } + break; case 'o': @@ -227,19 +244,19 @@ bool Cmd_GenVerifier(int argc, char * argv[]) return false; } - if (gSalt == nullptr && gSaltLen == 0) + if (gSaltDecodedLen == 0 && gSaltLen == 0) { fprintf(stderr, "Please specify at least one of the 'salt' or 'salt-len' parameters.\n"); return false; } - if (gSalt != nullptr && gSaltLen != 0 && gSaltLen != strlen(gSalt)) + if (gSaltDecodedLen != 0 && gSaltLen != 0 && gSaltDecodedLen != gSaltLen) { fprintf(stderr, "The specified 'salt-len' doesn't match the length of 'salt' parameter.\n"); return false; } if (gSaltLen == 0) { - gSaltLen = static_cast(strlen(gSalt)); + gSaltLen = gSaltDecodedLen; } if (gOutFileName == nullptr) @@ -271,7 +288,7 @@ bool Cmd_GenVerifier(int argc, char * argv[]) for (uint32_t i = 0; i < gCount; i++) { uint8_t salt[chip::kSpake2p_Max_PBKDF_Salt_Length]; - if (gSalt == nullptr) + if (gSaltDecodedLen == 0) { CHIP_ERROR err = chip::Crypto::DRBG_get_bytes(salt, gSaltLen); if (err != CHIP_NO_ERROR) @@ -318,8 +335,8 @@ bool Cmd_GenVerifier(int argc, char * argv[]) } // On the next iteration the PIN Code and Salt will be randomly generated. - gPinCode = chip::kSetupPINCodeUndefinedValue; - gSalt = nullptr; + gPinCode = chip::kSetupPINCodeUndefinedValue; + gSaltDecodedLen = 0; } return true; diff --git a/src/tools/spake2p/README.md b/src/tools/spake2p/README.md index 47eb56e0d59d80..b895f1284bfa40 100644 --- a/src/tools/spake2p/README.md +++ b/src/tools/spake2p/README.md @@ -22,7 +22,7 @@ Specify '--help' option for detail instructions on command usage: Example command that generates spake2p verifier for a given PIN code: ``` -./spake2p gen-verifier --pin-code 45502684 --iteration-count 1000 --salt "SPAKE2P Key Salt 1" --out spake2p-provisioning-data.csv +./spake2p gen-verifier --pin-code 45502684 --iteration-count 1000 --salt "U1BBS0UyUCBLZXkgU2FsdA==" --out spake2p-provisioning-data.csv ``` Example command that generates 100 sets of spake2p parameters (random PIN Codes,