Building CRA-ready nRF devices¶
Secure boot and signed firmware updates, end to end, on Nordic nRF. Nordic's b0/NSIB verifies MCUboot, MCUboot verifies the Zephyr application — and every signing key stays inside the HSM.
This is the design pattern behind the CRA's essential requirements on tamper resistance and update authenticity: a device that boots only firmware you authorised, and accepts only updates you signed.
The chain¶
Three stages, each verifying the next. Trust starts in ROM, which cannot be changed after manufacturing, and is handed forward one link at a time.
-
b0 / NSIB
Stage 1 · ROM
Nordic's immutable first-stage bootloader. Cannot be updated. Enforces the whole chain.
Trusts — an ECDSA-P256 public key provisioned into a fixed flash slot at manufacturing
Verifies — MCUboot, the next stage
-
MCUboot
Stage 2 · flash
Second-stage bootloader. Updatable, and signed so b0 will accept it.
Trusts — an ECDSA-P256 public key embedded in the MCUboot binary at build time
Verifies — the Zephyr app, the next stage
-
Zephyr app
Stage 3 · app slot
The application. Updatable, signed via
imgtoolso MCUboot will accept it.Provides — the signature MCUboot verifies at each boot
Core property — HSM-resident keys
The two ECDSA-P256 private keys that anchor this chain live in the HSM and never leave it. What the device holds is the matching pair of public keys — one written into b0's provisioning slot at manufacturing, one built into the MCUboot binary. At boot, verification uses only those public keys: LAAVAT is not in the loop, and a device in the field never contacts the platform to boot.
Both keys are ECDSA on prime256v1 (P-256, secp256r1). Nordic's b0
hardware-accelerates only P-256, and imgtool supports it natively. See
key custody for how the private
keys are held.
The LAAVAT product¶
One product exposes both signing operations. Each binds an HSM-held ECDSA-P256 key to a specific role in the chain, and both gate on the same approval rule.
-
nRF MCUboot signing
operationType: DigestSigningThe raw-digest primitive. You compute the 32-byte b0 pre-image hash locally and submit it; the HSM signs it with ECDSA-P256 and returns a 64-byte signature, which you assemble into b0's validation-info block.
Key —
ECDSAP256, "Digest Signing key" -
nRF Zephyr app signing
operationType: SignMcuBootThe
imgtoolbatch primitive. You submit a.tgzwith a smallrequest.jsonplus the unsigned.bin; LAAVAT runsimgtool signinternally with the HSM key and returns a signed MCUboot-format image.Key —
ECDSAP256, "MCUboot sign"
Operation type versus CLI argument
The operationType in the product definition is SignMcuBoot, but the
imagesigning add command takes MCUBoot. Both are correct — they are
different identifiers for the same operation.
Example product definition¶
Group UUIDs are per-tenant; substitute your own.
{
"name": "nRF52840 NSIB",
"description": "nRF52840 with signing of MCUboot for b0/NSIB and the Zephyr app for MCUboot",
"productType": "Production",
"enabled": true,
"caInfo": [],
"rndKeys": [],
"productOperations": [
{
"name": "nRF Zephyr app signing",
"description": "Sign nRF Zephyr app",
"operationType": "SignMcuBoot",
"token": {
"name": "MCUboot sign",
"description": "Signing key for nRF MCUboot and Zephyr app",
"keyType": "ECDSAP256"
},
"approvalRule": {
"name": "Test rule",
"description": "Rule used for testing",
"allowedGroups": ["<allowed-group-uuid>"],
"approvalGroups": ["<approval-group-uuid>"],
"blanketGroups": []
}
},
{
"name": "nRF MCUboot signing",
"description": "Digest signing with ECC key",
"operationType": "DigestSigning",
"token": {
"name": "Digest Signing key",
"description": "Digest signing key",
"keyType": "ECDSAP256"
},
"approvalRule": {
"name": "Rule for access",
"description": "Rule used for testing",
"allowedGroups": ["<allowed-group-uuid>"],
"approvalGroups": ["<approval-group-uuid>"],
"blanketGroups": []
}
}
]
}
Token handling
The examples below pipe the token in on stdin with -t @-, which suits CI
secret managers. A config file created with config-init works equally
well — see secure token handling.
Hop A: signing MCUboot for b0¶
b0 has no "sign this binary" tool. It has a fixed on-flash structure — the validation-info block — that it parses at boot to decide whether stage 2 is authentic. Producing that block with an HSM-held key splits into two parts: assemble everything locally except the signature, then have the HSM sign the one hash that ties it together.
What b0 reads at boot¶
A 176-byte block appended to the MCUboot image, in five fields:
u8[12] magic // NCS-defined constant, identifies a b0 info block
u32 fw_address // where MCUboot lives in flash (boot slot start)
u8[32] fw_hash // SHA-256 over the MCUboot bytes b0 will boot
u8[64] pubkey // P-256 uncompressed X‖Y
u8[64] signature // ECDSA-P256 over SHA-256(magic ‖ addr ‖ fw_hash ‖ pubkey)
Everything except the signature can be computed on the build host. The signature is the only part that needs the private key.
1. Fetch the public key and assemble the pre-image¶
Fetch the public key once, then pack
magic ‖ fw_address_LE ‖ SHA-256(mcuboot.bin) ‖ pubkey into 112 bytes and hash
it to a 32-byte digest.
printf '%s' "$TOKEN" | signing-tool -c -t @- \
-a https://app.laavat.io/<CustomerName>/api/v1 \
product getpubkey \
-P <product-id> \
--operid <digest-oper-id> \
-O /tmp/laavat_b0_pub.pem
2. Send only the digest¶
Submit the 32 bytes, base64-encoded, to the DigestSigning operation. The
image itself never leaves your build environment — only its hash goes over
the wire.
REQ_ID=$(printf '%s' "$TOKEN" | signing-tool --json -c -t @- \
-a https://app.laavat.io/<CustomerName>/api/v1 \
imagesigning add DigestSigning \
-P <product-id> \
--operid <digest-oper-id> \
-p <base64-digest> \
-H SHA256 \
-N nrf-b0-mcuboot \
-D "b0 signing" | jq -r '.id')
The request is asynchronous: it returns an ID and moves from Created to
Ready. Capture the ID with --json, then download the 64-byte signature —
--wait blocks until the request is ready rather than polling by hand.
printf '%s' "$TOKEN" | signing-tool -c -t @- \
-a https://app.laavat.io/<CustomerName>/api/v1 \
imagesigning get -I "$REQ_ID" --wait \
-O /tmp/b0_sig.bin
The signature may come back as raw r‖s or DER-encoded — detect which from the
first byte and unwrap if needed.
3. Assemble the block¶
Concatenate the 64-byte signature onto the 112-byte pre-image to form the
176-byte validation-info block, and append it to mcuboot.bin. That is the
artifact b0 verifies at boot.
The image now boots on any device provisioned with the matching public key, with no further contact with LAAVAT.
Hop B: signing the application for MCUboot¶
MCUboot has its own signing tool, imgtool, producing a well-known format: a
fixed header, the payload, and a trailing TLV block carrying the signature and
metadata. This hop delegates the whole imgtool invocation to LAAVAT, so the
private key never appears on the build host.
1. Package what imgtool needs¶
Build a .tgz containing the unsigned zephyr.bin and a request.json
carrying the usual imgtool parameters:
{
"images": [{
"filename": "zephyr.bin",
"output": "signed-zephyr.bin",
"align": "8",
"version": "1.6.44+1628771740",
"header_size": "0x200",
"slot_size": "0xea000",
"pad_header": true,
"pad": true,
"confirm": true,
"security_counter": "auto"
}]
}
2. Submit to the MCUboot operation¶
LAAVAT unpacks the archive, runs imgtool sign inside the HSM boundary with
the key bound to this operation, and returns a .tgz containing the signed
image.
printf '%s' "$TOKEN" | signing-tool -c -t @- \
-a https://app.laavat.io/<CustomerName>/api/v1 \
imagesigning add MCUBoot \
-P <product-id> \
--operid <app-oper-id> \
-F app-submit.tgz \
-N nrf-zephyr-app \
-D "Zephyr app signing"
3. Extract the signed image¶
Untar the response; the signed binary is at signed/signed-zephyr.bin. It is a
standard MCUboot-format image — the on-device bootloader, imgtool verify and
mcumgr all treat it identically to a locally signed one.
For the request and response formats in detail, see MCUboot signing.
Variant: with TF-M in the chain¶
Some builds split the application region into a Secure Processing Environment (TF-M, Trusted Firmware-M) and a Non-Secure Processing Environment (the Zephyr app). MCUboot then verifies two images at boot instead of one, and both need signing.
graph LR
A["<b>b0 / NSIB</b><br/>stage 1 · ROM"]
B["<b>MCUboot</b><br/>stage 2 · flash"]
C["<b>TF-M</b><br/>stage 3a · secure"]
D["<b>Zephyr app</b><br/>stage 3b · non-secure"]
A -->|verifies| B
B -->|verifies| C
B -->|verifies| D
b0's role is unchanged — it still verifies only MCUboot. The extension sits entirely inside MCUboot's verify domain.
Append one more entry to productOperations, the same shape as the Zephyr app
operation with a different name and key:
{
"name": "nRF TF-M signing",
"description": "Sign nRF TF-M secure image",
"operationType": "SignMcuBoot",
"token": {
"name": "TF-M sign",
"description": "Signing key for nRF TF-M",
"keyType": "ECDSAP256"
},
"approvalRule": {
"name": "Test rule",
"description": "Rule used for testing",
"allowedGroups": ["<allowed-group-uuid>"],
"approvalGroups": ["<approval-group-uuid>"],
"blanketGroups": []
}
}
The signing flow gains one more submit-poll-download cycle, identical in shape to the application submission but pointed at the TF-M operation with the TF-M binary as input.
Verifying without hardware¶
Both hops can be checked in a build environment, before any silicon exists. The two verifiers have different natures, so they are checked differently.
Hop A: b0 verification is simulated¶
b0 lives in ROM, so without silicon it cannot actually run. But everything b0 does before jumping to stage 2 is public in the NCS source and can be replayed in ordinary Python. Read the 176-byte block back off the signed image and run the same five checks:
- Parse the block. Split the last 176 bytes into magic (12),
fw_address(4, little-endian),fw_hash(32), pubkey (64,X‖Y) and signature (64,r‖s). - Magic constant matches. The 12 bytes must equal
CONFIG_SB_VALIDATION_INFO_MAGICfrom NCS. On mismatch b0 treats the region as not a valid info block and refuses to boot. - Embedded pubkey equals the provisioned pubkey. b0 has a P-256 public key
in a fixed flash slot from manufacturing; the one in the block must match it
byte for byte. Use the key from
product getpubkeyas the reference — that is exactly what a manufacturer programs into the device. - Firmware hash matches. Hash the MCUboot bytes b0 would boot (everything
before the appended block) and compare with
fw_hash. A single changed byte fails here. - Signature verifies. Reassemble
magic ‖ fw_address_LE ‖ fw_hash ‖ pubkeyfrom the parsed block, take its SHA-256, and verify the 64-byte signature against it with the embedded pubkey. This is the same ECDSA-P256 verification the hardware accelerator performs.
If all five pass, the image would boot on a real device.
Hop B: MCUboot verification is real¶
MCUboot ships its verifier as a Python CLI, running the same maths the on-device bootloader executes at boot. No simulation is needed, because the tool is the reference implementation.
printf '%s' "$TOKEN" | signing-tool -c -t @- \
-a https://app.laavat.io/<CustomerName>/api/v1 \
product getpubkey \
-P <product-id> \
--operid <app-oper-id> \
-O /tmp/laavat_app_pub.pem
imgtool verify --key /tmp/laavat_app_pub.pem signed-app.bin
imgtool dumpinfo signed-app.bin
If imgtool verify exits zero, MCUboot on a real device would accept the
image. dumpinfo parses the header and TLV block, so you can confirm the
version, signature algorithm, key hash and TLV list are what you asked for.
Where off-hardware verification stops¶
Worth stating plainly, because a passing test suite is easy to over-read:
| Claim | Off-hardware | What would close the gap |
|---|---|---|
| MCUboot signature is cryptographically correct for b0 | Fully proven | — |
| App signature is cryptographically correct for MCUboot | Fully proven | — |
| b0 parses the block at its expected flash offset | Partial | Boot on an nRF52840-DK with the pubkey provisioned |
| Struct magic, field layout and endianness match your NCS release | Guarded by defaults, not pinned | Cross-reference subsys/bootloader/bl_validation/bl_validation.c in your sdk-nrf checkout |
| The provisioning flow puts the pubkey in b0's slot correctly | Not tested | Manufacturing tooling on real silicon |
| Both signings survive concurrent submissions | Not stressed | A parallel-submission test, if throughput matters |
Related¶
- Secure boot and CRA — how this supports the CRA's integrity and update requirements
- MCUboot signing — request
and response formats for the
SignMcuBootoperation - MCUboot example flow — a single-image walkthrough, from product creation to verification
- Security and architecture — where the keys live and who can reach them
References¶
bl_validation.cin sdk-nrf —struct fw_validation_info, the authoritative definition of the validation-info block and its field offsets- MCUboot design — the image format and what the bootloader verifies at boot
- MCUboot imgtool — the signing and verification tool used in Hop B