Skip to content

Commit

Permalink
Merge 3832c09 into 4cdecc9
Browse files Browse the repository at this point in the history
  • Loading branch information
Damian-Nordic authored Jul 19, 2022
2 parents 4cdecc9 + 3832c09 commit 1212196
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 56 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/examples-nrfconnect.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
84 changes: 43 additions & 41 deletions scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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', '-',
]
Expand All @@ -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
"""

Expand All @@ -221,26 +221,26 @@ 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), \
"Provided invalid passcode!"

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.
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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)

Expand All @@ -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") """
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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 \
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 1212196

Please sign in to comment.