Skip to content

Commit 4cae173

Browse files
evenlrlubos
authored andcommitted
scripts: generator script for key provisioning json files
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 <[email protected]> Signed-off-by: Håkon Amundsen <[email protected]>
1 parent 2aa5697 commit 4cae173

File tree

2 files changed

+276
-0
lines changed

2 files changed

+276
-0
lines changed

CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,7 @@
689689
/scripts/print_toolchain_checksum.sh @nrfconnect/ncs-ci
690690
/scripts/sdp/ @nrfconnect/ncs-ll-ursus
691691
/scripts/twister/alt/zephyr/tests/drivers/mspi/api/testcase.yaml @nrfconnect/ncs-ll-ursus
692+
/scripts/generate_psa_key_attributes.py @nrfconnect/ncs-aurora
692693

693694
/scripts/docker/*.rst @nrfconnect/ncs-doc-leads
694695
/scripts/hid_configurator/*.rst @nrfconnect/ncs-si-bluebagel-doc
+275
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
#
2+
# Copyright (c) 2025 Nordic Semiconductor ASA
3+
#
4+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
5+
#
6+
7+
"""
8+
Module for generating PSA key attribute binary blobs
9+
"""
10+
11+
import argparse
12+
import struct
13+
import binascii
14+
import math
15+
import json
16+
import sys
17+
from pathlib import Path
18+
from enum import IntEnum
19+
from cryptography.hazmat.primitives import serialization
20+
21+
class PsaKeyType(IntEnum):
22+
"""The type of the key"""
23+
24+
AES = 0x2400
25+
ECC_TWISTED_EDWARDS = 0x4142
26+
RAW_DATA = 0x1001
27+
28+
29+
class PsaKeyBits(IntEnum):
30+
"""Number of bits in the key"""
31+
32+
AES = 256
33+
EDDSA = 255
34+
35+
36+
class PsaUsage(IntEnum):
37+
"""Permitted usage on a key"""
38+
39+
VERIFY_MESSAGE_EXPORT = 0x0801
40+
ENCRYPT_DECRYPT = 0x0300
41+
USAGE_DERIVE = 0x4000
42+
43+
class PsaCracenUsageSceme(IntEnum):
44+
NONE = 0xff
45+
PROTECTED = 0
46+
SEED = 1
47+
ENCRYPTED = 2
48+
RAW = 3
49+
50+
class PsaKeyLifetime(IntEnum):
51+
"""Lifetime and location for storing key"""
52+
53+
PERSISTENT_CRACEN = 0x804E0001
54+
PERSISTENT_CRACEN_KMU = 0x804E4B01
55+
56+
57+
class PsaAlgorithm(IntEnum):
58+
"""Algorithm that can be associated with a key. Not used for AES"""
59+
60+
NONE = 0
61+
CBC = 0x04404000
62+
EDDSA_PURE = 0x06000800
63+
64+
65+
class PlatformKeyAttributes:
66+
def __init__(self,
67+
key_type: PsaKeyType,
68+
identifier: int,
69+
location: PsaKeyLifetime,
70+
usage: PsaUsage,
71+
algorithm: PsaAlgorithm,
72+
size: int,
73+
cracen_usage: PsaCracenUsageSceme = PsaCracenUsageSceme.NONE):
74+
75+
self.key_type = key_type
76+
self.lifetime = location
77+
self.usage = usage
78+
self.alg0 = algorithm
79+
self.alg1 = PsaAlgorithm.NONE
80+
self.bits = size
81+
self.identifier = identifier
82+
83+
if location == PsaKeyLifetime.PERSISTENT_CRACEN_KMU:
84+
if cracen_usage == PsaCracenUsageSceme.NONE:
85+
print("--cracen_usage must be set if location target is PERSISTENT_CRACEN_KMU")
86+
return
87+
self.identifier = 0x7fff0000 | (cracen_usage << 12) | (identifier & 0xff)
88+
89+
if self.key_type == PsaKeyType.AES:
90+
self.alg1 = PsaAlgorithm.NONE
91+
elif self.key_type == PsaKeyType.ECC_TWISTED_EDWARDS:
92+
self.alg1 = PsaAlgorithm.NONE
93+
elif self.key_type == PsaKeyType.RAW_DATA:
94+
self.alg1 = PsaAlgorithm.NONE
95+
else:
96+
raise RuntimeError("Invalid key type")
97+
98+
def pack(self):
99+
"""Builds a binary blob compatible with the psa_key_attributes_s C struct"""
100+
101+
return struct.pack(
102+
"<hhIIIIII",
103+
self.key_type,
104+
self.bits,
105+
self.lifetime,
106+
# Policy
107+
self.usage,
108+
self.alg0,
109+
self.alg1,
110+
self.identifier,
111+
0, # Reserved, only used if key id encodes owner id
112+
)
113+
114+
def is_valid_hexa_code(string):
115+
try:
116+
int(string, 16)
117+
return True
118+
except ValueError:
119+
return False
120+
121+
def main() -> None:
122+
parser = argparse.ArgumentParser(
123+
description="Generate PSA key attributes and write to stdout or"
124+
"create or append the information including the key to a"
125+
"nrfutil compatible json file. Also supports reading key"
126+
"from a PEM file in some cases. Key source can either be"
127+
"a RAW key using the --key argument, a randomly generated"
128+
"key using the --trng-key argument or a public key can be"
129+
"read from a .PEM file. These are mutual exclusive.",
130+
allow_abbrev=False,
131+
)
132+
133+
parser.add_argument(
134+
"--usage",
135+
help="Key usage",
136+
type=str,
137+
required=True,
138+
choices=[x.name for x in PsaUsage],
139+
)
140+
141+
parser.add_argument(
142+
"--id",
143+
help="Key identifier",
144+
type=lambda number_string: int(number_string, 0),
145+
required=True,
146+
)
147+
148+
parser.add_argument(
149+
"--type",
150+
help="Key type",
151+
type=str,
152+
required=True,
153+
choices=[x.name for x in PsaKeyType],
154+
)
155+
156+
parser.add_argument(
157+
"--size",
158+
help="Key size in bits",
159+
type=lambda number_string: int(number_string, 0),
160+
required=True,
161+
)
162+
163+
parser.add_argument(
164+
"--algorithm",
165+
help="Key algorithm",
166+
type=str,
167+
required=False,
168+
default="NONE",
169+
choices=[x.name for x in PsaAlgorithm],
170+
)
171+
172+
parser.add_argument(
173+
"--location",
174+
help="Storage location",
175+
type=str,
176+
required=True,
177+
choices=[x.name for x in PsaKeyLifetime],
178+
)
179+
180+
parser.add_argument(
181+
"--key",
182+
help="Key value",
183+
type=str,
184+
required=False,
185+
)
186+
187+
parser.add_argument(
188+
"--trng-key",
189+
help="Generate key randomly",
190+
action="store_true",
191+
required=False,
192+
)
193+
194+
parser.add_argument(
195+
"--key-from-file",
196+
help="Read key from PEM file",
197+
type=argparse.FileType(mode="rb"),
198+
required=False,
199+
)
200+
201+
parser.add_argument(
202+
"--bin-output",
203+
help="Output metadata as binary",
204+
action="store_true",
205+
required=False,
206+
)
207+
208+
parser.add_argument(
209+
"--file",
210+
help="JSON file to create or modify",
211+
type=str,
212+
required=False,
213+
)
214+
215+
parser.add_argument(
216+
"--cracen_usage",
217+
help="CRACEN KMU Slot usage scheme",
218+
type=str,
219+
required=False,
220+
default="NONE",
221+
choices=[x.name for x in PsaCracenUsageSceme],
222+
)
223+
224+
args = parser.parse_args()
225+
226+
metadata = PlatformKeyAttributes(key_type=PsaKeyType[args.type],
227+
identifier=args.id,
228+
location=PsaKeyLifetime[args.location],
229+
usage=PsaUsage[args.usage],
230+
algorithm=PsaAlgorithm[args.algorithm],
231+
size=args.size,
232+
cracen_usage=PsaCracenUsageSceme[args.cracen_usage]).pack()
233+
234+
metadata_str = binascii.hexlify(metadata).decode('utf-8').upper()
235+
236+
if args.file and Path(args.file).is_file():
237+
with open(args.file, encoding="utf-8") as file:
238+
json_data= json.load(file)
239+
else:
240+
json_data= json.loads('{ "version": 0, "keyslots": [ ]}')
241+
242+
if args.trng_key:
243+
value = f'TRNG:{int(math.ceil(args.size / 8))}'
244+
elif args.key:
245+
key = args.key.strip("0x")
246+
if not is_valid_hexa_code(key):
247+
print("Invalid KEY value")
248+
return
249+
value = f'0x{key}'
250+
elif args.key_from_file:
251+
key_data = args.key_from_file.read()
252+
key = serialization.load_pem_public_key(key_data)
253+
key = key.public_bytes(
254+
encoding=serialization.Encoding.Raw,
255+
format=serialization.PublicFormat.Raw
256+
)
257+
value = f'0x{key.hex()}'
258+
else:
259+
print("Expecting either --key, --trng-key or --key-from-file")
260+
return
261+
262+
json_data["keyslots"].append({"metadata": f'0x{metadata_str}', "value": f'{value}'})
263+
output = json.dumps(json_data, indent=4)
264+
265+
if args.file:
266+
with open(args.file, "w", encoding="utf-8") as file:
267+
file.write(output)
268+
elif args.bin_output:
269+
sys.stdout.buffer.write(metadata)
270+
else:
271+
print(output)
272+
273+
274+
if __name__ == "__main__":
275+
main()

0 commit comments

Comments
 (0)