From 4cae17351ffbeffc2d8e733fa73bd616ed195d95 Mon Sep 17 00:00:00 2001 From: Even Falch-Larsen Date: Fri, 14 Feb 2025 10:14:19 +0100 Subject: [PATCH] scripts: generator script for key provisioning json files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This python script is used to generate json files that contain key data and metadata that is used by nrfutil provisioning keys into nrf54h and nrf54l devices. Signed-off-by: Even Falch-Larsen Signed-off-by: HÃ¥kon Amundsen --- CODEOWNERS | 1 + scripts/generate_psa_key_attributes.py | 275 +++++++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 scripts/generate_psa_key_attributes.py diff --git a/CODEOWNERS b/CODEOWNERS index b0c1fa9e393c..46383b97e05b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -689,6 +689,7 @@ /scripts/print_toolchain_checksum.sh @nrfconnect/ncs-ci /scripts/sdp/ @nrfconnect/ncs-ll-ursus /scripts/twister/alt/zephyr/tests/drivers/mspi/api/testcase.yaml @nrfconnect/ncs-ll-ursus +/scripts/generate_psa_key_attributes.py @nrfconnect/ncs-aurora /scripts/docker/*.rst @nrfconnect/ncs-doc-leads /scripts/hid_configurator/*.rst @nrfconnect/ncs-si-bluebagel-doc diff --git a/scripts/generate_psa_key_attributes.py b/scripts/generate_psa_key_attributes.py new file mode 100644 index 000000000000..b0abfa36b571 --- /dev/null +++ b/scripts/generate_psa_key_attributes.py @@ -0,0 +1,275 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +""" +Module for generating PSA key attribute binary blobs +""" + +import argparse +import struct +import binascii +import math +import json +import sys +from pathlib import Path +from enum import IntEnum +from cryptography.hazmat.primitives import serialization + +class PsaKeyType(IntEnum): + """The type of the key""" + + AES = 0x2400 + ECC_TWISTED_EDWARDS = 0x4142 + RAW_DATA = 0x1001 + + +class PsaKeyBits(IntEnum): + """Number of bits in the key""" + + AES = 256 + EDDSA = 255 + + +class PsaUsage(IntEnum): + """Permitted usage on a key""" + + VERIFY_MESSAGE_EXPORT = 0x0801 + ENCRYPT_DECRYPT = 0x0300 + USAGE_DERIVE = 0x4000 + +class PsaCracenUsageSceme(IntEnum): + NONE = 0xff + PROTECTED = 0 + SEED = 1 + ENCRYPTED = 2 + RAW = 3 + +class PsaKeyLifetime(IntEnum): + """Lifetime and location for storing key""" + + PERSISTENT_CRACEN = 0x804E0001 + PERSISTENT_CRACEN_KMU = 0x804E4B01 + + +class PsaAlgorithm(IntEnum): + """Algorithm that can be associated with a key. Not used for AES""" + + NONE = 0 + CBC = 0x04404000 + EDDSA_PURE = 0x06000800 + + +class PlatformKeyAttributes: + def __init__(self, + key_type: PsaKeyType, + identifier: int, + location: PsaKeyLifetime, + usage: PsaUsage, + algorithm: PsaAlgorithm, + size: int, + cracen_usage: PsaCracenUsageSceme = PsaCracenUsageSceme.NONE): + + self.key_type = key_type + self.lifetime = location + self.usage = usage + self.alg0 = algorithm + self.alg1 = PsaAlgorithm.NONE + self.bits = size + self.identifier = identifier + + if location == PsaKeyLifetime.PERSISTENT_CRACEN_KMU: + if cracen_usage == PsaCracenUsageSceme.NONE: + print("--cracen_usage must be set if location target is PERSISTENT_CRACEN_KMU") + return + self.identifier = 0x7fff0000 | (cracen_usage << 12) | (identifier & 0xff) + + if self.key_type == PsaKeyType.AES: + self.alg1 = PsaAlgorithm.NONE + elif self.key_type == PsaKeyType.ECC_TWISTED_EDWARDS: + self.alg1 = PsaAlgorithm.NONE + elif self.key_type == PsaKeyType.RAW_DATA: + self.alg1 = PsaAlgorithm.NONE + else: + raise RuntimeError("Invalid key type") + + def pack(self): + """Builds a binary blob compatible with the psa_key_attributes_s C struct""" + + return struct.pack( + " None: + parser = argparse.ArgumentParser( + description="Generate PSA key attributes and write to stdout or" + "create or append the information including the key to a" + "nrfutil compatible json file. Also supports reading key" + "from a PEM file in some cases. Key source can either be" + "a RAW key using the --key argument, a randomly generated" + "key using the --trng-key argument or a public key can be" + "read from a .PEM file. These are mutual exclusive.", + allow_abbrev=False, + ) + + parser.add_argument( + "--usage", + help="Key usage", + type=str, + required=True, + choices=[x.name for x in PsaUsage], + ) + + parser.add_argument( + "--id", + help="Key identifier", + type=lambda number_string: int(number_string, 0), + required=True, + ) + + parser.add_argument( + "--type", + help="Key type", + type=str, + required=True, + choices=[x.name for x in PsaKeyType], + ) + + parser.add_argument( + "--size", + help="Key size in bits", + type=lambda number_string: int(number_string, 0), + required=True, + ) + + parser.add_argument( + "--algorithm", + help="Key algorithm", + type=str, + required=False, + default="NONE", + choices=[x.name for x in PsaAlgorithm], + ) + + parser.add_argument( + "--location", + help="Storage location", + type=str, + required=True, + choices=[x.name for x in PsaKeyLifetime], + ) + + parser.add_argument( + "--key", + help="Key value", + type=str, + required=False, + ) + + parser.add_argument( + "--trng-key", + help="Generate key randomly", + action="store_true", + required=False, + ) + + parser.add_argument( + "--key-from-file", + help="Read key from PEM file", + type=argparse.FileType(mode="rb"), + required=False, + ) + + parser.add_argument( + "--bin-output", + help="Output metadata as binary", + action="store_true", + required=False, + ) + + parser.add_argument( + "--file", + help="JSON file to create or modify", + type=str, + required=False, + ) + + parser.add_argument( + "--cracen_usage", + help="CRACEN KMU Slot usage scheme", + type=str, + required=False, + default="NONE", + choices=[x.name for x in PsaCracenUsageSceme], + ) + + args = parser.parse_args() + + metadata = PlatformKeyAttributes(key_type=PsaKeyType[args.type], + identifier=args.id, + location=PsaKeyLifetime[args.location], + usage=PsaUsage[args.usage], + algorithm=PsaAlgorithm[args.algorithm], + size=args.size, + cracen_usage=PsaCracenUsageSceme[args.cracen_usage]).pack() + + metadata_str = binascii.hexlify(metadata).decode('utf-8').upper() + + if args.file and Path(args.file).is_file(): + with open(args.file, encoding="utf-8") as file: + json_data= json.load(file) + else: + json_data= json.loads('{ "version": 0, "keyslots": [ ]}') + + if args.trng_key: + value = f'TRNG:{int(math.ceil(args.size / 8))}' + elif args.key: + key = args.key.strip("0x") + if not is_valid_hexa_code(key): + print("Invalid KEY value") + return + value = f'0x{key}' + elif args.key_from_file: + key_data = args.key_from_file.read() + key = serialization.load_pem_public_key(key_data) + key = key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw + ) + value = f'0x{key.hex()}' + else: + print("Expecting either --key, --trng-key or --key-from-file") + return + + json_data["keyslots"].append({"metadata": f'0x{metadata_str}', "value": f'{value}'}) + output = json.dumps(json_data, indent=4) + + if args.file: + with open(args.file, "w", encoding="utf-8") as file: + file.write(output) + elif args.bin_output: + sys.stdout.buffer.write(metadata) + else: + print(output) + + +if __name__ == "__main__": + main()