Skip to content

Commit e16136e

Browse files
committed
scripts: Update ncs-provision west command to use nrfutil to upload keys
nrfutil provides a command to upload public keys to KMU. West ncs-provision command should use nrfutil instead of nrfprovision python library. Signed-off-by: Lukasz Fundakowski <lukasz.fundakowski@nordicsemi.no>
1 parent 53cb100 commit e16136e

File tree

1 file changed

+114
-60
lines changed

1 file changed

+114
-60
lines changed

scripts/west_commands/ncs_provision.py

+114-60
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
#
44
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
55

6-
from __future__ import annotations
7-
8-
import re
6+
import json
97
import subprocess
108
import sys
9+
import tempfile
10+
from dataclasses import asdict, dataclass
1111
from pathlib import Path
1212

1313
from cryptography.hazmat.primitives.serialization import load_pem_private_key
@@ -24,13 +24,78 @@
2424
NRF54L15_KEY_POLICIES: dict[str, str] = {"revokable": "REVOKED", "lock": "LOCKED"}
2525

2626

27+
@dataclass
28+
class SlotParams:
29+
id: int
30+
value: str
31+
rpolicy: str
32+
algorithm: str = ALGORITHM
33+
dest: str = KMU_KEY_SLOT_DEST_ADDR
34+
metadata: str = KEY_SLOT_METADATA
35+
36+
def asdict(self) -> dict[str, str]:
37+
return asdict(self)
38+
39+
40+
class NrfutilWrapper:
41+
42+
def __init__(
43+
self,
44+
slots: list[SlotParams],
45+
device_id: str | None = None,
46+
output_dir: str | None = None,
47+
*,
48+
dry_run: bool = False
49+
) -> None:
50+
self.device_id = device_id
51+
self.dry_run = dry_run
52+
self.data = {
53+
"version": 0,
54+
"keyslots": [slot.asdict() for slot in slots]
55+
}
56+
self.output_dir = output_dir or tempfile.mkdtemp(prefix="nrfutil_")
57+
58+
def run_command(self):
59+
command = self._build_command()
60+
print(' '.join(command), file=sys.stderr)
61+
if self.dry_run:
62+
return
63+
result = subprocess.run(command, stderr=subprocess.PIPE, text=True)
64+
if result.returncode:
65+
print(result.stderr, file=sys.stderr)
66+
sys.exit("Uploading failed!")
67+
68+
def _make_json_file(self) -> str:
69+
"""Create JSON file and return path to it."""
70+
json_file = Path(self.output_dir).joinpath("keyfile.json").resolve().expanduser()
71+
with open(json_file, "w") as file:
72+
json.dump(self.data, file, indent=2)
73+
print(f"Keys file saved as {json_file}", file=sys.stderr)
74+
return str(json_file)
75+
76+
def _build_command(self) -> list[str]:
77+
json_file_path = self._make_json_file()
78+
command = [
79+
"nrfutil",
80+
"device",
81+
"x-provision-nrf54l-keys",
82+
"--key-file",
83+
json_file_path,
84+
"--verify",
85+
]
86+
if self.device_id:
87+
command += ["--serial-number", self.device_id]
88+
89+
return command
90+
91+
2792
class NcsProvision(WestCommand):
2893

2994
def __init__(self):
3095
super().__init__(
31-
"ncs-provision",
32-
"NCS provision",
33-
"NCS provision utility tool.",
96+
name="ncs-provision",
97+
help="NCS provision",
98+
description="NCS provision utility tool.",
3499
)
35100

36101
def do_add_parser(self, parser_adder):
@@ -52,7 +117,8 @@ def do_add_parser(self, parser_adder):
52117
choices=KEY_SLOTS.keys(),
53118
# default value for backward compatibility
54119
default="UROT_PUBKEY",
55-
help="Key name to upload",
120+
type=lambda x: x.upper(),
121+
help="Key name to upload (default: %(default)s)",
56122
)
57123
upload_parser.add_argument(
58124
"-p",
@@ -64,70 +130,58 @@ def do_add_parser(self, parser_adder):
64130
"revokable: keys can be revoked each by one. "
65131
"lock: all keys stay as they are. "
66132
"lock-last: last key is uploaded as locked, "
67-
"others as revokable",
133+
"others as revokable (default=%(default)s)",
68134
)
69135
upload_parser.add_argument(
70136
"-s", "--soc", type=str, help="SoC",
71137
choices=["nrf54l05", "nrf54l10", "nrf54l15"], required=True
72138
)
73139
upload_parser.add_argument("--dev-id", help="Device serial number")
140+
upload_parser.add_argument(
141+
"--build-dir", metavar="PATH",
142+
help="Path to output directory where keyfile.json will be saved. "
143+
"If not specified, temporary directory will be used.",
144+
)
145+
upload_parser.add_argument(
146+
"--dry-run", default=False, action="store_true",
147+
help="Generate upload command and keyfile without executing the command"
148+
)
74149

75150
return parser
76151

77152
def do_run(self, args, unknown_args):
78153
if args.command == "upload":
79154
if args.soc in ["nrf54l05", "nrf54l10", "nrf54l15"]:
80-
keyname = args.keyname
81-
if len(args.keys) > len(KEY_SLOTS[keyname]):
82-
sys.exit(
83-
"Error: requested upload of more keys than there are designated slots.")
84-
for slot_idx, keyfile in enumerate(args.keys):
85-
with open(keyfile, "rb") as f:
86-
priv_key = load_pem_private_key(
87-
f.read(), password=None)
88-
pub_key = priv_key.public_key()
89-
if args.policy == "lock-last":
90-
if slot_idx == (len(args.keys) - 1):
91-
key_policy = NRF54L15_KEY_POLICIES["lock"]
92-
else:
93-
key_policy = NRF54L15_KEY_POLICIES["revokable"]
94-
else:
95-
key_policy = NRF54L15_KEY_POLICIES[args.policy]
96-
dev_id = args.dev_id
97-
pub_key_hex = pub_key.public_bytes_raw().hex()
98-
slot_id = str(KEY_SLOTS[keyname][slot_idx])
99-
command = self._build_command(
100-
dev_id=dev_id, key_policy=key_policy, pub_key=pub_key_hex, slot_id=slot_id
101-
)
102-
nrfprovision = subprocess.run(
103-
command, stderr=subprocess.PIPE, text=True
104-
)
105-
stderr = nrfprovision.stderr
106-
print(stderr, file=sys.stderr)
107-
if re.search("fail", stderr) or nrfprovision.returncode:
108-
sys.exit("Uploading failed!")
155+
self._upload_keys(args)
156+
157+
def _upload_keys(self, args) -> None:
158+
slots: list[SlotParams] = []
159+
keyname = args.keyname
160+
if len(args.keys) > len(KEY_SLOTS[keyname]):
161+
sys.exit(
162+
"Error: requested upload of more keys than there are designated slots."
163+
)
164+
for slot_idx, keyfile in enumerate(args.keys):
165+
pub_key_hex = self._get_public_key_hex(keyfile)
166+
if args.policy == "lock-last":
167+
if slot_idx == (len(args.keys) - 1):
168+
key_policy = NRF54L15_KEY_POLICIES["lock"]
169+
else:
170+
key_policy = NRF54L15_KEY_POLICIES["revokable"]
171+
else:
172+
key_policy = NRF54L15_KEY_POLICIES[args.policy]
173+
slot_id = KEY_SLOTS[keyname][slot_idx]
174+
slots.append(SlotParams(id=slot_id, value=pub_key_hex, rpolicy=key_policy))
175+
176+
runner = NrfutilWrapper(
177+
slots=slots, device_id=args.dev_id, output_dir=args.build_dir, dry_run=args.dry_run
178+
)
179+
runner.run_command()
109180

110181
@staticmethod
111-
def _build_command(
112-
key_policy: str, pub_key: str, slot_id: str, dev_id: str | None
113-
) -> list[str]:
114-
command = [
115-
"nrfprovision",
116-
"provision",
117-
"--rpolicy",
118-
key_policy,
119-
"--value",
120-
pub_key,
121-
"--metadata",
122-
KEY_SLOT_METADATA,
123-
"--id",
124-
slot_id,
125-
"--algorithm",
126-
ALGORITHM,
127-
"--dest",
128-
KMU_KEY_SLOT_DEST_ADDR,
129-
"--verify",
130-
]
131-
if dev_id:
132-
command.extend(["--snr", dev_id])
133-
return command
182+
def _get_public_key_hex(keyfile: str) -> str:
183+
with open(keyfile, "rb") as f:
184+
priv_key = load_pem_private_key(f.read(), password=None)
185+
pub_key = priv_key.public_key()
186+
pub_key_hex = f'0x{pub_key.public_bytes_raw().hex()}'
187+
return pub_key_hex

0 commit comments

Comments
 (0)