4"""Sign the secure_boot_ns_hil Non-Secure image and merge it with the Secure hex.
6The BLXNS root-of-trust proof (#172) needs the NS image SIGNED: the Secure verifier
7reads the NS image's ``.ns_rot_header`` for the body length, locates the appended
8``ra8_rot_trailer_t`` at ns_base + body_len, and checks the ECDSA-P256 signature
9before BLXNS. This build step produces TWO flashable merged hexes from the Secure
10hex + the NS image body:
12 * ``<genuine>`` = Secure hex + signed NS image (verify passes -> NS runs)
13 * ``<tampered>`` = Secure hex + signed NS image with (digest mismatch -> denied)
14 one body byte flipped after signing
16Signing needs the held-out RoT private key (``scripts/secrets/rot_sign.py``). This step
17DEGRADES GRACEFULLY: if the key is not present it prints the exact command to run
18later (with the key) and exits 0 -- the unsigned NS body is left for reference and
19the build still succeeds. Run this script by hand (``--key <path>``) once the key
20is available, or set ``RA8_ROT_KEY`` before the build.
22It shells out to ``objcopy`` (bin<->ihex), ``scripts/secrets/rot_sign.py`` (sign), and
23``scripts/gen/merge_ihex.py`` (merge); no third-party Python packages.
29from pathlib
import Path
32NS_LOAD_ADDR = 0x02080000
43NS_ROT_HEADER_OFFSET = 0x40
44NS_ROT_HEADER_MAGIC = 0x3152534E
47def _read_header_body_len(body: bytes) -> int:
48 """Return the body_len the NS linker recorded in the .ns_rot_header.
50 The header (magic + body_len) sits at NS_ROT_HEADER_OFFSET. This is the
51 authoritative signed-body length the firmware verifier reads; the raw objcopy
52 output can be SHORTER when a trailing empty section (e.g. an empty .data)
53 contributes only alignment, so the caller pads up to this length before
54 signing -- guaranteeing the trailer lands at ns_base + body_len.
56 off = NS_ROT_HEADER_OFFSET
57 if len(body) < off + 8:
58 _die = f
"NS image too small ({len(body)} B) to hold the .ns_rot_header"
59 raise SystemExit(_die)
60 magic = int.from_bytes(body[off : off + 4],
"little")
61 if magic != NS_ROT_HEADER_MAGIC:
62 _die = f
"NS .ns_rot_header magic {magic:#010x} != {NS_ROT_HEADER_MAGIC:#010x} (NSR1)"
63 raise SystemExit(_die)
64 return int.from_bytes(body[off + 4 : off + 8],
"little")
67def _run(cmd: list[str]) ->
None:
68 """Run a subprocess, echoing the command; raise on failure."""
69 sys.stdout.write(
" $ " +
" ".join(str(c)
for c
in cmd) +
"\n")
70 subprocess.run(cmd, check=
True)
73def _bin_to_hex(objcopy: str, bin_path: Path, hex_path: Path) ->
None:
74 """Convert a raw binary at NS_LOAD_ADDR into an Intel HEX file."""
82 f
"--change-addresses={NS_LOAD_ADDR:#x}",
89def _sign(rot_sign: str, key: Path, body: Path, out: Path) ->
None:
90 """Append a signed ra8_rot_trailer_t to ``body`` -> ``out``."""
108def _merge(merge_tool: str, secure_hex: Path, ns_hex: Path, out_hex: Path) ->
None:
109 """Merge the Secure hex and an NS hex into one flashable hex."""
110 _run([sys.executable, merge_tool, str(secure_hex), str(ns_hex), str(out_hex)])
113def _tampered_copy(signed: Path, out: Path) ->
None:
114 """Copy ``signed`` and flip one body byte so the RoT digest check fails."""
115 data = bytearray(signed.read_bytes())
116 data[TAMPER_OFFSET] ^= TAMPER_XOR
117 out.write_bytes(data)
120def _emit_manual_recipe(args: argparse.Namespace, ns_bin: Path) ->
None:
121 """Print the exact command to sign+merge later, when the key is available."""
123 "sign_and_merge: RoT signing key not found -- NS image left UNSIGNED.\n"
124 f
" (looked for: {args.key or '<none given>'})\n"
125 f
" Unsigned NS body: {ns_bin}\n"
126 " The genuine/tampered merged hexes were NOT produced. On the bench,\n"
127 " with the key present, run:\n\n"
128 f
" python3 {Path(__file__).resolve()} \\\n"
129 f
" --secure-hex {args.secure_hex} \\\n"
130 f
" --ns-elf {args.ns_elf} \\\n"
131 f
" --objcopy {args.objcopy} \\\n"
132 f
" --rot-sign {args.rot_sign} \\\n"
133 f
" --merge {args.merge} \\\n"
134 f
" --out-genuine {args.out_genuine} \\\n"
135 f
" --out-tampered {args.out_tampered} \\\n"
136 " --key $HOME/ra8d2-rot-signing-key.pem\n\n"
137 " Then flash --out-genuine (NS runs) and --out-tampered (NS denied).\n"
142 """Sign the NS image and emit the genuine and tampered merged hexes.
144 Pipeline: extract the raw NS body from its ELF with objcopy, pad it to the
145 `body_len` the linker recorded in `.ns_rot_header`, sign the padded body,
146 convert to ihex at the LMA, and merge with the Secure hex. The tampered
147 output is the same signed image with one body byte flipped AFTER signing, so
148 it differs from the genuine one only in that the digest no longer matches.
150 The padding step is not incidental. objcopy drops a trailing empty section's
151 alignment tail, so the raw output can be a few bytes short of the linker's
152 loaded-image extent. Signing the short body would put the trailer at the
153 wrong address and hash different bytes than the firmware does -- verification
154 would fail on a correctly-signed image. Padding to `body_len` makes the
155 signed body exactly what the firmware hashes.
157 Degrades deliberately when the RoT key is absent: it prints the exact
158 command to run later with the key and returns 0, leaving the unsigned NS
159 body for reference so the build still succeeds. **A 0 return therefore does
160 not mean the images were produced.** Check for the output files, not the
164 0 on success, and also 0 on the no-key degrade path.
167 SystemExit: The NS body is larger than the header's `body_len`, or a
168 subprocess (objcopy, rot_sign, merge_ihex) failed.
170 parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
171 parser.add_argument(
"--secure-hex", required=
True, help=
"Secure ELF's ihex")
172 parser.add_argument(
"--ns-elf", required=
True, help=
"Non-Secure ELF")
173 parser.add_argument(
"--objcopy", required=
True, help=
"arm-none-eabi-objcopy path")
174 parser.add_argument(
"--rot-sign", required=
True, help=
"scripts/secrets/rot_sign.py path")
175 parser.add_argument(
"--merge", required=
True, help=
"scripts/gen/merge_ihex.py path")
176 parser.add_argument(
"--out-genuine", required=
True, help=
"output genuine merged hex")
177 parser.add_argument(
"--out-tampered", required=
True, help=
"output tampered merged hex")
178 parser.add_argument(
"--key", default=
"", help=
"RoT private-key PEM (empty -> degrade)")
179 args = parser.parse_args()
181 out_dir = Path(args.out_genuine).parent
182 ns_bin = out_dir /
"secure_boot_ns_hil_ns.bin"
185 _run([args.objcopy,
"-O",
"binary", str(args.ns_elf), str(ns_bin)])
192 raw = ns_bin.read_bytes()
193 body_len = _read_header_body_len(raw)
194 if len(raw) > body_len:
195 _die = f
"NS body {len(raw)} B exceeds header body_len {body_len} B"
196 raise SystemExit(_die)
197 body = out_dir /
"secure_boot_ns_hil_ns_body.bin"
198 body.write_bytes(raw + b
"\x00" * (body_len - len(raw)))
200 key = Path(args.key)
if args.key
else None
201 if key
is None or not key.is_file():
202 _emit_manual_recipe(args, ns_bin)
206 signed = out_dir /
"secure_boot_ns_hil_ns_signed.bin"
207 signed_hex = out_dir /
"secure_boot_ns_hil_ns_signed.hex"
208 _sign(args.rot_sign, key, body, signed)
209 _bin_to_hex(args.objcopy, signed, signed_hex)
210 _merge(args.merge, Path(args.secure_hex), signed_hex, Path(args.out_genuine))
213 tampered = out_dir /
"secure_boot_ns_hil_ns_tampered.bin"
214 tampered_hex = out_dir /
"secure_boot_ns_hil_ns_tampered.hex"
215 _tampered_copy(signed, tampered)
216 _bin_to_hex(args.objcopy, tampered, tampered_hex)
217 _merge(args.merge, Path(args.secure_hex), tampered_hex, Path(args.out_tampered))
220 "sign_and_merge: OK\n"
221 f
" genuine -> {args.out_genuine} (flash: NS liveness advances)\n"
222 f
" tampered -> {args.out_tampered} "
223 f
"(body byte {TAMPER_OFFSET:#x} flipped: RoT denies, NS never runs)\n"
228if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.