> Markdown version of https://docs.laavat.io/solutions/cra-ready-nrf/ from the LAAVAT PKI and Signing Platform documentation. All pages: https://docs.laavat.io/llms.txt

# 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.

<div class="grid cards stages" markdown>

-   __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 `imgtool` so MCUboot will accept it.

    **Provides** — the signature MCUboot verifies at each boot

</div>

> **Abstract: 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](https://docs.laavat.io/security/#key-custody-and-export) 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](https://docs.laavat.io/usage/productguide/product/).

<div class="grid cards" markdown>

-   __nRF MCUboot signing__

    ---

    `operationType: DigestSigning`

    The 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: SignMcuBoot`

    The `imgtool` batch primitive. You submit a `.tgz` with a small
    `request.json` plus the unsigned `.bin`; LAAVAT runs `imgtool sign`
    internally with the HSM key and returns a signed MCUboot-format image.

    **Key** — `ECDSAP256`, "MCUboot sign"

</div>

> **Note: 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.

```json
{
  "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": []
      }
    }
  ]
}
```

> **Note: 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](https://docs.laavat.io/democlient/usage/#providing-the-token).

## 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:

```text
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.

```bash
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.

```bash
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.

```bash
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:

```json
{
  "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.

```bash
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](https://docs.laavat.io/usage/signing-encryption/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.

```mermaid
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:

```json
{
  "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:

1. **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`).
2. **Magic constant matches.** The 12 bytes must equal
   `CONFIG_SB_VALIDATION_INFO_MAGIC` from NCS. On mismatch b0 treats the region
   as not a valid info block and refuses to boot.
3. **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 getpubkey` as the reference — that
   is exactly what a manufacturer programs into the device.
4. **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.
5. **Signature verifies.** Reassemble `magic ‖ fw_address_LE ‖ fw_hash ‖ pubkey`
   from 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.

```bash
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](https://docs.laavat.io/cra-compliance/secure-boot-cra/) — how this
  supports the CRA's integrity and update requirements
- [MCUboot signing](https://docs.laavat.io/usage/signing-encryption/mcuboot-signing/) — request
  and response formats for the `SignMcuBoot` operation
- [MCUboot example flow](https://docs.laavat.io/exampleflows/mcubootsigning/) — a single-image
  walkthrough, from product creation to verification
- [Building CRA-ready STM32H5 controllers](https://docs.laavat.io/solutions/cra-ready-stm32h5/) — the same
  `SignMcuBoot` flow under ST's OEMiRoT
- [Security and architecture](https://docs.laavat.io/security/) — where the keys live and
  who can reach them

## References

- [`bl_validation.c` in sdk-nrf](https://github.com/nrfconnect/sdk-nrf/blob/main/subsys/bootloader/bl_validation/bl_validation.c)
  — `struct fw_validation_info`, the authoritative definition of the
  validation-info block and its field offsets
- [MCUboot design](https://docs.mcuboot.com/design.html) — the image format and
  what the bootloader verifies at boot
- [MCUboot imgtool](https://docs.mcuboot.com/imgtool.html) — the signing and
  verification tool used in Hop B
