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

# Building CRA-ready i.MX8M gateways

Secure boot and trusted firmware updates, end to end, on an NXP i.MX8M
gateway. The boot ROM verifies the bootloader through HAB, u-boot verifies the
kernel FIT, OP-TEE verifies its Trusted Applications, and RAUC verifies every
update bundle — and every signing key stays inside the HSM.

The Cyber Resilience Act's Annex I requires that a product protects the
integrity of its software and that security updates can be installed securely.
On an i.MX8M gateway both come down to the same discipline: every stage of the
boot chain and every update bundle is signed, and each is verified by the stage
that loads it against a trust anchor placed on the device once. This guide
explains what that takes, and how the manufacturer operates it on the LAAVAT
platform in the most common configuration.

> **Note: Scope**
> This guide covers the signing of the boot chain and of RAUC update
> bundles, the keys and certificate hierarchy behind them, and how the
> device verifies the result. It does not cover the update *delivery*
> infrastructure — hawkBit or another deployment server, the download and
> rollout process, or the device-side slot, rollback and health-check
> policies; those are configured in RAUC and the fleet management system
> independently of how bundles are signed. Applications delivered as
> containers on top of the root filesystem are a separate trust chain with
> its own signing and verification tooling; the
> [OCI container signing example](https://docs.laavat.io/exampleflows/ocisigning/) describes
> it.

## Secure boot and secure firmware update

**Secure boot** means a device only runs software whose signature it can
verify, starting from code that cannot be changed. The chain begins in the
processor's boot ROM, which verifies the first bootloader stage against a key
hash burned into one-time-programmable fuses; that stage verifies the next, and
so on through the bootloader, the operating system kernel and the root
filesystem. Each stage carries a signature made by the manufacturer and is
checked against a trust anchor that an earlier stage brought with it. Break any
link and the device stops, or refuses the image, rather than running modified
code.

**Secure firmware update** extends the same rule to the software installed
after the device has left the factory. An update bundle is signed by the
manufacturer, verified by the device against a certificate chain it already
trusts, and installed atomically into an inactive slot, so that a failed or
rejected update leaves the running system untouched and the device can fall
back.

The two are inseparable. Secure boot without secure update leaves no safe way
to fix a vulnerability; secure update without secure boot verifies the bundle
once and then trusts whatever is in flash at the next power-on. Together they
are secure by design in the Act's sense: integrity is a property of the
product's architecture and lifecycle, not a check added after release. What
runs on the device is what the manufacturer released, from the first
instruction to the latest update.

### What it takes

None of the mechanisms have to be invented. The i.MX8M boot ROM implements HAB
and verifies the first stage in silicon; u-boot verifies FIT images; OP-TEE
verifies its Trusted Applications; the Linux kernel enforces dm-verity; RAUC
installs verified A/B updates. These are mature, widely deployed and openly
documented, and a Yocto-based BSP integrates them. The building blocks are not
the hard part. The hard part is everything around them that has to be right
for the lifetime of the product, and it falls into three areas.

- **Key management.** The Super Root Keys behind the fuse hash must exist for
  the lifetime of the product and must never leak; a compromised SRK cannot be
  replaced on devices already in the field. The FIT, TA and update signing
  keys must be protected just as well, used only through controlled, approved
  and logged operations, and never present on a developer's machine.
- **Root-of-trust delivery.** Every verifier needs its anchor delivered
  securely and embedded at the right point: the SRK hash to the manufacturing
  line for fusing, the kernel-signing public key into the u-boot control DTB,
  the TA-signing key into OP-TEE core, the update CA chain into the root
  filesystem. Each of these is a build-time input, and each must be the
  genuine one.
- **Signing the artifacts.** Every release must have the bootloader stages,
  the kernel FIT, the Trusted Applications and the update bundle signed with
  the correct tool for each format, in the right order, with an audit trail
  of who signed what.

This is where LAAVAT comes in. The manufacturer keeps the design of its chain
of trust and its build; LAAVAT provides the platform that holds the keys in an
HSM, runs each signing operation with role-based access, approval and audit,
operates the certificate hierarchy behind the update bundles, and delivers the
trust anchors the device and the production line need.

## LAAVAT's part in each link

Each artifact on the gateway is verified by the component that loads it,
against a trust anchor placed on the device once. The table shows, link by
link, what verifies what, where the anchor lives, and what LAAVAT does — and
does not do — for it. The design of the chain, the build, the placement of the
trust anchors and the verification on the device remain with the manufacturer,
and nothing on the device talks to LAAVAT at boot or at update time.

| Artifact | Verified by | Trust anchor on the device | LAAVAT role |
| --- | --- | --- | --- |
| Bootloader: SPL + u-boot FIT | Boot ROM, via HAB4 | SRK fuses (hash of the SRK table) | Signs with NXP CST, provides the SRK table and fuse hash |
| Kernel FIT | u-boot | Public key in the u-boot control DTB | Signs with `mkimage`, provides the public key |
| Root filesystem | Kernel via dm-verity | Root hash in the signed kernel FIT | None |
| Kernel modules | Kernel module loader | Per-build key built into the kernel | None; the key is generated by the build |
| OP-TEE Trusted Applications | OP-TEE core | Public key built into OP-TEE core | Signs with `sign_encrypt.py`, provides the public key |
| RAUC update bundle | RAUC on the device | CA keyring in the rootfs | Bundles and signs with a one-time key, provides the CA keyring |

> **Abstract: Core property — HSM-resident keys**
> The four Super Root Keys, the kernel and OP-TEE signing keys and the RAUC
> issuing CA all live in the HSM and never leave it. What the device holds
> is public material only: the SRK hash in fuses, two public keys in the
> boot image, and a CA chain in the root filesystem. **LAAVAT is not in the
> loop** at boot or at update time. See
> [key custody](https://docs.laavat.io/security/#key-custody-and-export) for how the
> private keys are held.

The manufacturer brings the build artifacts; LAAVAT returns them signed,
together with the material the device needs to verify them.

```mermaid
graph LR
    subgraph you["You bring"]
        B1["<b>Secure boot artifacts</b><br/>SPL · u-boot FIT<br/>kernel FIT · OP-TEE TAs"]
        B2["<b>Update bundle</b><br/>rootfs · appfs · manifest<br/>verity format"]
    end
    subgraph laavat["LAAVAT platform"]
        L1["<b>Secure-boot signing</b><br/>SignHAB · SignKernel<br/>OP-TEESigning"]
        L2["<b>RAUC bundle signing</b><br/>one-time signer<br/>under your RAUC CA"]
    end
    subgraph out["Signed and verifiable"]
        O1["<b>Signed boot artifacts</b><br/>+ SRK fuse hash<br/>+ FIT and OP-TEE keys"]
        O2["<b>Signed RAUC bundle</b><br/>verified on the device<br/>against the CA keyring"]
    end
    B1 --> L1 --> O1
    B2 --> L2 --> O2
```

One product, two flows, with the same workflow and governance applied to
both: HSM-protected keys, role-based access, an approval gate and a full audit
trail on every operation.

## Where signing happens

Signing is part of the release build. The CI pipeline produces the artifacts
and calls the LAAVAT platform for each one; the signed results are what gets
flashed at the factory and what gets shipped as field updates. In a typical
integration this happens through the LAAVAT-provided Yocto scripts, which wrap
the platform calls into the build. This guide shows the same steps as explicit
`signing-tool` commands so that each one is visible.

```mermaid
graph TD
    A["<b>Build outputs</b><br/>SPL + u-boot FIT · kernel FIT · OP-TEE TAs · RAUC bundle input"]
    B["<b>CI pipeline</b><br/>signing-tool, or the LAAVAT Yocto scripts"]
    C["<b>i.MX8M Gateway product</b><br/>SignHAB · SignKernel · OP-TEESigning · RaucBundleSigningWithOneTimeKey"]
    D["<b>Boot image</b><br/>factory flash"]
    E["<b>Kernel FIT + TAs</b><br/>in the boot image / rootfs"]
    F["<b>RAUC bundle</b><br/>field update"]
    A --> B --> C
    C --> D
    C --> E
    C --> F
```

## The LAAVAT product

A LAAVAT *product* groups the keys and the signing operations for one device
type. The i.MX8M Gateway product carries the HAB key tree (four Super Root
Keys, each with a CSF and an IMG signing certificate), HSM tokens for FIT and
OP-TEE signing, and the RAUC issuing CA. Certificate *profiles* define what
each certificate looks like; the product template references them by id.

### Certificate profiles

Four profiles are needed: a Root and an End-Entity profile for the HAB tree,
and a Sub CA and an End-Entity profile for the RAUC hierarchy. They are added
through the console or with `signing-tool`:

```bash
printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    profile add -F imx8m-habv4-rootca-profile.yaml -N "i.MX8M HABv4 Root CA Profile" -T ROOT
printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    profile add -F imx8m-habv4-endentity-profile.yaml -N "i.MX8M HABv4 End-Entity Profile" -T END
# the two RAUC profiles likewise; each upload returns the profile id used in the product template
```

> **Note: Validity periods**
> The HAB certificates are deliberately long-lived: the SRKs cannot be
> replaced on fused devices, and the boot ROM does not evaluate certificate
> validity. The RAUC validities are examples; an issuer must not expire
> before the certificates it issues.

#### i.MX8M HABv4 Root CA Profile

```yaml
Name: i.MX8M HABv4 Root CA Profile
DN:
  Organization: Manufacturer
ProfileType: 3                 # Root
SignatureAlgorithm: 4          # SHA256WithRSA
KeyAlgorithm: 1                # RSA
ValidityPeriod: "1752000h"     # 200 years
BasicConstraints:
  BasicConstraintsValid: true
  IsCA: true
  MaxPathLen: 0
  MaxPathLenZero: false
AddSKI: true
AddAKI: true
KeyUsage:
  - 32                         # CertSign
ExtendedKeyUsage:
```

#### i.MX8M HABv4 End-Entity Profile

```yaml
Name: i.MX8M HABv4 End-Entity Profile
DN:
  Organization: Manufacturer
ProfileType: 1                 # End entity (CSF and IMG certificates)
SignatureAlgorithm: 4          # SHA256WithRSA
KeyAlgorithm: 1                # RSA
ValidityPeriod: "1752000h"     # 200 years
BasicConstraints:
  BasicConstraintsValid: true
  IsCA: false
AddSKI: true
AddAKI: true
KeyUsage:
  - 1                          # DigitalSignature
ExtendedKeyUsage:
ExtraExtensions: true
SANUsage: true
```

#### i.MX8M RAUC Sub CA Profile

```yaml
Name: i.MX8M RAUC Sub CA Profile
DN:
  Organization: Manufacturer
  Country: FI
ProfileType: 2                 # Sub CA
SignatureAlgorithm: 10         # ECDSAWithSHA256
KeyAlgorithm: 2                # ECDSA
ValidityPeriod: "219000h"      # 25 years, example
BasicConstraints:
  BasicConstraintsValid: true
  IsCA: true
  MaxPathLen: 0                # issues end-entity certificates only
  MaxPathLenZero: true
AddSKI: true
AddAKI: true
KeyUsage:
  - 32                         # CertSign
  - 64                         # CRLSign
ExtendedKeyUsage:
```

#### i.MX8M RAUC End-Entity Profile

```yaml
Name: i.MX8M RAUC End-Entity Profile
DN:
  Organization: Manufacturer
  Country: FI
ProfileType: 1                 # End entity (ephemeral bundle signer)
SignatureAlgorithm: 10         # ECDSAWithSHA256
KeyAlgorithm: 2                # ECDSA
ValidityPeriod: "87600h"       # 10 years, example
BasicConstraints:
  BasicConstraintsValid: true
  IsCA: false
AddSKI: true
AddAKI: true
KeyUsage:
  - 1                          # DigitalSignature
  - 2                          # ContentCommitment
ExtendedKeyUsage:
  - 3                          # CodeSigning
ExtraExtensions: true
SANUsage: true
```

### Example product definition

The product is created from the JSON template below with `signing-tool` and
approved in the console. Identifiers are placeholders; the group ids are the
RBAC groups allowed to submit and to approve each operation. In a CI setup the
release group may also be listed under `blanketGroups` for auto-approval.

```bash
printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    product add -T imx8m-gateway-product.json
```

```json
{
  "name": "i.MX8M Gateway",
  "description": "i.MX8M-based reference gateway product. Covers HAB secure boot (SPL + u-boot), FIT kernel signing, OP-TEE Trusted Application signing and RAUC bundle signing.",
  "productType": "Production",
  "enabled": true,
  "caInfo": [
    {
      "CN": "SRK0",
      "description": "CA used for i.MX8M HAB",
      "useCase": "HABCA",
      "profileID": "<hab-root-profile-uuid>",
      "keyType": "RSA4096",
      "certificateType": "ROOT",
      "leafs": [
        { "CN": "CSF0", "description": "CSF End-Entity certificate", "useCase": "HABCA",
          "keyType": "RSA4096", "certificateType": "ENDENTITY", "profileID": "<hab-ee-profile-uuid>" },
        { "CN": "IMG0", "description": "IMG signing certificate", "useCase": "HABCA",
          "keyType": "RSA4096", "certificateType": "ENDENTITY", "profileID": "<hab-ee-profile-uuid>" }
      ]
    },
    {
      "CN": "SRK1",
      "description": "CA used for i.MX8M HAB",
      "useCase": "HABCA",
      "profileID": "<hab-root-profile-uuid>",
      "keyType": "RSA4096",
      "certificateType": "ROOT",
      "leafs": [
        { "CN": "CSF1", "description": "CSF End-Entity certificate", "useCase": "HABCA",
          "keyType": "RSA4096", "certificateType": "ENDENTITY", "profileID": "<hab-ee-profile-uuid>" },
        { "CN": "IMG1", "description": "IMG signing certificate", "useCase": "HABCA",
          "keyType": "RSA4096", "certificateType": "ENDENTITY", "profileID": "<hab-ee-profile-uuid>" }
      ]
    },
    {
      "CN": "SRK2",
      "description": "CA used for i.MX8M HAB",
      "useCase": "HABCA",
      "profileID": "<hab-root-profile-uuid>",
      "keyType": "RSA4096",
      "certificateType": "ROOT",
      "leafs": [
        { "CN": "CSF2", "description": "CSF End-Entity certificate", "useCase": "HABCA",
          "keyType": "RSA4096", "certificateType": "ENDENTITY", "profileID": "<hab-ee-profile-uuid>" },
        { "CN": "IMG2", "description": "IMG signing certificate", "useCase": "HABCA",
          "keyType": "RSA4096", "certificateType": "ENDENTITY", "profileID": "<hab-ee-profile-uuid>" }
      ]
    },
    {
      "CN": "SRK3",
      "description": "CA used for i.MX8M HAB",
      "useCase": "HABCA",
      "profileID": "<hab-root-profile-uuid>",
      "keyType": "RSA4096",
      "certificateType": "ROOT",
      "leafs": [
        { "CN": "CSF3", "description": "CSF End-Entity certificate", "useCase": "HABCA",
          "keyType": "RSA4096", "certificateType": "ENDENTITY", "profileID": "<hab-ee-profile-uuid>" },
        { "CN": "IMG3", "description": "IMG signing certificate", "useCase": "HABCA",
          "keyType": "RSA4096", "certificateType": "ENDENTITY", "profileID": "<hab-ee-profile-uuid>" }
      ]
    },
    {
      "CN": "RAUC Issuing Sub CA",
      "description": "Sub CA that issues the ephemeral RAUC bundle-signer certificates. Provisioned with externalRoot: the CSR is signed by the RAUC Root CA and the certificate is uploaded to this slot.",
      "useCase": "RaucCodeSigning",
      "keyType": "ECDSAP256",
      "certificateType": "SUBCA",
      "externalRoot": true,
      "profileID": "<rauc-subca-profile-uuid>"
    }
  ],
  "rndKeys": [],
  "productOperations": [
    {
      "name": "HAB CST signing",
      "description": "HAB CST signing for SPL and u-boot images (i.MX8M).",
      "operationType": "SignHAB",
      "approvalRule": {
        "name": "HAB signing approval rule",
        "description": "Approval rule for HAB CST signing.",
        "allowedGroups":  ["<release-group-uuid>"],
        "approvalGroups": ["<approver-group-uuid>"],
        "blanketGroups":  []
      }
    },
    {
      "name": "Kernel signing",
      "description": "FIT image kernel signing.",
      "operationType": "SignKernel",
      "token": { "name": "Kernel", "description": "HSM token for FIT image kernel signing.", "keyType": "RSA2048" },
      "approvalRule": {
        "name": "Kernel signing approval rule",
        "description": "Approval rule for kernel FIT signing.",
        "allowedGroups":  ["<release-group-uuid>"],
        "approvalGroups": ["<approver-group-uuid>"],
        "blanketGroups":  []
      }
    },
    {
      "name": "OP-TEE TA image signing",
      "description": "OP-TEESigning operation for .ta artifacts (RSA-PSS-SHA256, 4096-bit).",
      "operationType": "OP-TEESigning",
      "token": { "name": "OP-TEE signing key", "description": "RSA 4096 key for OP-TEE TA signing.", "keyType": "RSA4096" },
      "approvalRule": {
        "name": "OP-TEE signing approval rule",
        "description": "Approval rule for OP-TEE TA signing.",
        "allowedGroups":  ["<release-group-uuid>"],
        "approvalGroups": ["<approver-group-uuid>"],
        "blanketGroups":  []
      }
    },
    {
      "name": "RAUC bundle signing",
      "description": "RAUC update-bundle CMS signing (ECDSA P-256). Each call generates an ephemeral bundle-signer key in the HSM, has the RAUC Issuing Sub CA issue its certificate using the End-Entity profile, signs the bundle, and discards the key.",
      "operationType": "RaucBundleSigningWithOneTimeKey",
      "profileID": "<rauc-ee-profile-uuid>",
      "token": { "name": "RAUC one-time signing key", "description": "Ephemeral key for RAUC bundle-signer certificates.", "keyType": "ECDSAP256" },
      "approvalRule": {
        "name": "RAUC signing approval rule",
        "description": "Approval rule for RAUC bundle signing.",
        "allowedGroups":  ["<release-group-uuid>"],
        "approvalGroups": ["<approver-group-uuid>"],
        "blanketGroups":  []
      }
    }
  ],
  "productConfigItems": [
    { "name": "FusemapType", "value": "IMX8" }
  ]
}
```

`FusemapType: IMX8` matters: it is what makes `SignHAB` recalculate the Image
Vector Table for FIT images, which i.MX8M requires. See
[Product management](https://docs.laavat.io/usage/productguide/product/) for the configuration
items.

## The RAUC update PKI

Update bundles are verified on the device against a CA chain, so they need a
PKI of their own.

> **Note: Why the Sub CA is marked `externalRoot` in the product template**
> A CA declared in a product template is normally created self-signed, as a
> root that belongs to that product. For the RAUC Sub CA that is the wrong
> shape: the device keyring should anchor on a Root that lives outside any
> product, stays untouched by day-to-day signing, and can sign a replacement
> Sub CA later without a new keyring on fielded devices. `externalRoot:
> true` tells the platform to generate the Sub CA's key and a *certificate
> signing request* instead of a self-signed certificate; the request is
> then signed by the RAUC Root CA and the resulting certificate uploaded to
> the Sub CA slot. The Sub CA stays inside the product, where the RAUC
> operation can use it, but its trust comes from the Root.

<div class="grid cards stages" markdown>

-   __RAUC Root CA__

    ---

    *Outside the product*

    ECDSA P-256, self-signed, created in the console. Signs the Sub CA's CSR
    once and is otherwise untouched.

    **On the device** — in the keyring

-   __RAUC Issuing Sub CA__

    ---

    *Inside the product*

    ECDSA P-256, `externalRoot`, `MaxPathLen 0`. Issues one certificate per
    bundle signing.

    **On the device** — in the keyring

-   __Bundle signer__

    ---

    *Ephemeral*

    ECDSA P-256, `CN` = random UUID, EKU CodeSigning. Key discarded after use.

    **On the device** — no; travels inside each bundle

</div>

### Creating the hierarchy

1. Upload the two RAUC profiles (Sub CA and End-Entity).
2. Create the **RAUC Root CA** in the console as a self-signed Root, using a
   Root profile of the same key type, and approve it.
3. Create the product from the template. The **RAUC Issuing Sub CA** appears
   in *pending* state, holding a CSR.
4. Download the Sub CA's CSR. The download is the DER request encoded as
   base64; convert it to PEM before use:

    ```bash
    printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
        ca getcsr --caid <rauc-subca-uuid> -O "RAUC Issuing Sub CA.csr"
    base64 -d < "RAUC Issuing Sub CA.csr" | openssl req -inform DER -outform PEM -out subca.csr.pem
    ```

5. On the RAUC Root CA choose *Issue certificate*, paste the PEM CSR and select
   the RAUC Sub CA profile, then approve the request. The result is a PKCS#7
   containing the Sub CA certificate and the Root.
6. Upload that certificate to the pending Sub CA. It becomes active, chained
   under the Root.

The console dialogs are the same as for the device-identity hierarchy;
[Trusted device identities](https://docs.laavat.io/solutions/trusted-device-identities/#creating-the-pki-in-the-console)
walks through them. The externally rooted Sub CA pattern is also shown in the
[advanced PKI example flow](https://docs.laavat.io/exampleflows/advancedpki/).

From then on every RAUC signing request generates a fresh key inside the HSM,
has the Sub CA issue a certificate for it, signs the bundle, and discards the
key. Re-signing an existing bundle works the same way and leaves the payload,
manifest and verity data byte-identical.

## Signing the release artifacts

In a typical integration the LAAVAT-provided Yocto scripts perform these steps
inside the build. They are shown here as explicit `signing-tool` commands for
clarity. The pattern is the same for every operation: submit the input, poll
until the request is *Ready*, download the result. With an approval rule the
request waits in *ApprovalRequired* until an approver releases it in the
console; with blanket approval the platform signs immediately.

> **Note: Token handling**
> Every command below reads the bearer token from `$TOKEN` on stdin with
> `-t @-` and targets the tenant's API with `-a`. A config file created with
> `config-init` works equally well — see
> [secure token handling](https://docs.laavat.io/democlient/usage/#providing-the-token).

```bash
export TOKEN=<bearer-token>

# poll a request until Ready (state 16); 2 = ApprovalRequired, 8 = InProcess, 32/64 = Rejected/Failed
printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    --json imagesigning get -I <request-uuid> | jq .state
```

`imagesigning get --wait` blocks until the request is ready rather than
polling by hand — see [Reference client usage](https://docs.laavat.io/democlient/usage/).

### Bootloader: SPL and u-boot in one request — `SignHAB`

The combined boot image carries two HAB images, SPL at offset 0 and u-boot at
its own IVT. Both are signed in one archive-mode request: a flat `.tar.gz`
holding the binary and a `request.json` with one CSF per stage. The
authenticate blocks are the regions the ROM hashes; the SPL CSF also carries
the *Unlock MID* command. The offsets come from the BSP build — the CSF files
CST would have used.

```json
{
  "csfs": [
    {
      "id": "spl",
      "binaryFilename": "imx-boot-gateway.bin",
      "unlock": { "features": ["MID"] },
      "signatureOffset": "0x31e00",
      "authenticate": { "blocks": [
        { "address": "0x91ffc0", "offset": "0x0", "length": "0x31e00" } ] }
    },
    {
      "id": "uboot",
      "binaryFilename": "imx-boot-gateway.bin",
      "signatureOffset": "0x59020",
      "csfRegionSize": "0x1FE0",
      "authenticate": { "blocks": [
        { "address": "0x401fcdc0", "offset": "0x58000",  "length": "0x1020"  },
        { "address": "0x40200000", "offset": "0x5B000",  "length": "0xD6B88" },
        { "address": "0x402D6B88", "offset": "0x131B88", "length": "0x9568"  },
        { "address": "0x970000",   "offset": "0x13B0F0", "length": "0xA150"  } ] }
    }
  ]
}
```

```bash
tar -czf boot-signing.tar.gz request.json imx-boot-gateway.bin

printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    imagesigning add SignHAB -P <product-uuid> --operid <signhab-operation-uuid> \
    -N "gateway-1.4.0-boot" -D "SPL + u-boot, release 1.4.0" -F boot-signing.tar.gz
Request sent. Request ID: <request-uuid>, state: Created

printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    imagesigning get -I <request-uuid> --skipBase64 -O boot-signed.tar.gz
Downloading signed binary to: boot-signed.tar.gz
File Downloaded

tar -tzf boot-signed.tar.gz
response.json
signed/imx-boot-gateway.bin
```

`response.json` lists one entry per CSF; the last entry's `sha256_output` is
the SHA-256 of the delivered binary. Each stage now ends with a CSF: the SRK
table, the CSF and IMG certificates, and two CMS signatures. A single image (an
SPL on its own, a u-boot FIT on its own) can also be submitted raw, without a
request file; the platform reads the IVT and signs the one image it finds, and
returns the result base64-encoded. The request format is documented in full in
[CST signing](https://docs.laavat.io/usage/signing-encryption/cstsigning/).

### Kernel FIT — `SignKernel`

The FIT is submitted as-is. The platform signs every configuration's signature
node with the product's Kernel key (`mkimage`) and returns the re-packed FIT.
The signature node's algorithm and `key-name-hint` come from the FIT itself, so
the build's `.its` must declare the key size the product actually has: a FIT
declaring `sha256,rsa4096` signed with an RSA-2048 key cannot verify.

```bash
printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    imagesigning add SignKernel -P <product-uuid> --operid <signkernel-operation-uuid> \
    -N "gateway-1.4.0-kernel" -D "kernel FIT, release 1.4.0" -F fitImage
Request sent. Request ID: <request-uuid>, state: Created

printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    imagesigning get -I <request-uuid> --skipBase64 -O fitImage.signed
```

See [FIT signing](https://docs.laavat.io/usage/signing-encryption/fitsigning/) for preparing the
`.its` and embedding the public key into u-boot.

### OP-TEE Trusted Applications — `OP-TEESigning`

Each `.ta` is submitted raw; the platform re-signs the TA header with the
product's OP-TEE key (RSA-PSS, SHA-256) and returns it. The payload hash inside
the header is unchanged; only the signature differs.

```bash
printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    imagesigning add OP-TEESigning -P <product-uuid> --operid <optee-operation-uuid> \
    -N "gateway-1.4.0-ta" -D "TA <ta-uuid>, release 1.4.0" -F <ta-uuid>.ta
Request sent. Request ID: <request-uuid>, state: Created

printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    imagesigning get -I <request-uuid> --skipBase64 -O <ta-uuid>.signed.ta
```

### RAUC update bundle — `RaucBundleSigningWithOneTimeKey`

The input is a `.tar.gz` with a `request.json` and a directory holding the
manifest and the images. The verity format is the usual choice: the root
filesystem is protected by a dm-verity hash tree, and the manifest carrying the
tree's root hash is signed.

```ini
# request.json
{ "format": "verity", "directory": "data" }

# data/manifest.raucm
[bundle]
format=verity

[update]
compatible=gateway-v1
version=1.4.0

[image.rootfs]
filename=rootfs.ext4
```

```bash
tar -czf update-1.4.0.tar.gz request.json data/

printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    imagesigning add RaucBundleSigningWithOneTimeKey -P <product-uuid> --operid <rauc-operation-uuid> \
    -N "gateway-1.4.0-update" -D "RAUC bundle, release 1.4.0" -F update-1.4.0.tar.gz
Request sent. Request ID: <request-uuid>, state: Created

printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    imagesigning get -I <request-uuid> --skipBase64 -O gateway-1.4.0.raucb
```

The result is the `.raucb` bundle with an inline CMS signature made by the
ephemeral signer. The signer's certificate is embedded in the bundle; the Sub
CA and Root are not, which is why the device keyring must contain both. The
input formats — plain, verity, crypt, and re-signing — are documented in
[RAUC signing](https://docs.laavat.io/usage/signing-encryption/raucsigning/).

## What goes on the device

| Trust anchor | Source on the platform | Where it goes |
| --- | --- | --- |
| SRK fuse hash | Product secrets (`SRKHASH`, with the SRK table as `SRKTABLE`) | Burned into the SRK_HASH OTP fuses at manufacturing. One-time; verify against the signed image before burning |
| Kernel public key | `product getpubkey` for the SignKernel operation | u-boot control DTB, key node named after the FIT's `key-name-hint`, `required = "conf"` |
| OP-TEE public key | `product getpubkey` for the OP-TEESigning operation | Built into OP-TEE core (TA verification key) |
| RAUC CA keyring | Certificate chain of the RAUC Issuing Sub CA (Sub CA + Root) | `/etc/rauc/keyring.pem`, referenced from `system.conf` |

### SRK fuse hash

The SRK table and its hash are part of the product's secrets. They are
retrieved with a registered manufacturing client (client certificate
authentication), which keeps the fuse material off developer accounts:

```bash
printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    secrets get -P <product-uuid> -C client.private -O product-secrets.json
jq -r .SRKHASH product-secrets.json
<64 hex characters>
```

How the hash is burned depends on the fusing tool in the production line; the
value and its format are described in the
[HAB signing example flow](https://docs.laavat.io/exampleflows/habsigning/) and
[CST signing](https://docs.laavat.io/usage/signing-encryption/cstsigning/). Registering the
client is covered in [Clients](https://docs.laavat.io/usage/clientguide/clients/).

### FIT and OP-TEE public keys

```bash
printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    product getpubkey -P <product-uuid> --operid <signkernel-operation-uuid> -O kernel-pub.pem
public key written to: kernel-pub.pem
SHA-256 (DER SubjectPublicKeyInfo): <64 hex characters>

printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    product getpubkey -P <product-uuid> --operid <optee-operation-uuid> -O optee-pub.pem
```

`kernel-pub.pem` is wrapped into the u-boot control DTB by the BSP build
(`mkimage -K`, key node `key-<hint>` with `required = "conf"`);
`optee-pub.pem` is the `TA_PUBLIC_KEY` OP-TEE core is built with.

### RAUC keyring

The keyring is the Sub CA certificate together with the Root. Export the RAUC
Issuing Sub CA's certificate from the console as PKCS#7 (the export carries the
chain), or take it from the product secrets, then convert to PEM:

```bash
# from the console export
openssl pkcs7 -in rauc-ca-chain.p7 -print_certs -out keyring.pem

# or from the product secrets (caInfo entry of the RAUC Issuing Sub CA)
jq -r '.caInfo[] | select(.cn=="RAUC Issuing Sub CA") | .pkcs7chain' product-secrets.json \
    | base64 -d | openssl pkcs7 -inform DER -print_certs -out keyring.pem

openssl x509 -in keyring.pem -noout -subject   # first cert
subject=C = FI, O = Manufacturer, CN = RAUC Issuing Sub CA
```

Install it on the device and reference it from RAUC's `system.conf`:

```ini
[keyring]
path=/etc/rauc/keyring.pem
check-purpose=codesign
```

## Verifying before shipping

Everything the device will check can be checked on a build host first, with
the same tools, against the public material LAAVAT provides. The gateway
release pipeline can do this as a smoke test after every signing round.

### Boot image (HAB)

```text
# one run per stage; the offsets are the IVT locations from the request: SPL at 0x0, u-boot at 0x59000
hab4_image_verifier signed/imx-boot-gateway.bin 0x0
  IVT Tag: 0xD1
  CSF Length: 88 bytes
    Command Tag: 0xBE   Processing 'Install Key' command...        (SRK table)
    Command Tag: 0xBE   Processing 'Install Key' command...        (CSF certificate)
    Command Tag: 0xCA   Processing 'Authenticate Data' command...  (CSF signature)
    Command Tag: 0xB2   Processing 'Unlock' command...
    Command Tag: 0xBE   Processing 'Install Key' command...        (IMG certificate)
    Command Tag: 0xCA   Processing 'Authenticate Data' command...
      Block Start: 0x91FFC0   Block Length: 204288 bytes
    Command Tag: 0xB2   Processing 'Unlock' command...
  SRK Table Hash: <64 hex characters>
  Signature 1:  issuer: O=Manufacturer, CN=SRK0
  Signature 2:  issuer: O=Manufacturer, CN=SRK0
```

What to check: the SRK Table Hash equals the product's `SRKHASH`; the
authenticate blocks match the request; both stages carry two signatures issued
by the intended SRK. This confirms structure and key material only: the
signature itself is verified by the boot ROM, and no host tool stands in for
it.

### Kernel FIT

```text
fit_check_sign -f fitImage.signed -k kernel-verify.dtb
Signature check OK
Verifying Hash Integrity for node 'conf-gateway.dtb'... sha256,rsa2048:<hint>+
Verified OK, loading images
```

`kernel-verify.dtb` is a control DTB carrying `kernel-pub.pem` with
`required = "conf"`, the same form u-boot uses. The unsigned input must fail
against it (*Signature check bad*); that is what proves the check has teeth.

### OP-TEE Trusted Application

```text
python3 sign_encrypt.py verify --uuid <ta-uuid> --in <ta-uuid>.signed.ta --key optee-pub.pem
TA UUID: <ta-uuid>

python3 sign_encrypt.py display --in <ta-uuid>.signed.ta | grep hash
hash: <64 hex characters>   (identical to the input's)
```

### RAUC bundle

```text
rauc info -c system.conf gateway-1.4.0.raucb
Verifying bundle signature...
Verified inline signature by 'C = FI, O = Manufacturer, CN = <signer-uuid>'
Compatible:     'gateway-v1'
Version:        '1.4.0'
Bundle Format:  verity
  Verity Hash:  '<64 hex characters>'
Certificate Chain:
 0 Subject: C = FI, O = Manufacturer, CN = <signer-uuid>
   Issuer:  C = FI, O = Manufacturer, CN = RAUC Issuing Sub CA
 1 Subject: C = FI, O = Manufacturer, CN = RAUC Issuing Sub CA
   Issuer:  C = FI, O = Manufacturer, CN = RAUC Root CA
 2 Subject: C = FI, O = Manufacturer, CN = RAUC Root CA
   Issuer:  C = FI, O = Manufacturer, CN = RAUC Root CA

rauc extract -c system.conf gateway-1.4.0.raucb /tmp/bundle   # verifies the hash tree as well
```

`system.conf` here is the device's, or a minimal copy with the keyring path and
`check-purpose=codesign`. A bundle with a modified payload fails on
`rauc extract`; a bundle signed under a different CA fails already on
`rauc info`.

### Where off-device verification stops

Worth stating plainly, because a passing smoke test is easy to over-read:

| Claim | Off-device | What would close the gap |
| --- | --- | --- |
| Kernel FIT signature is cryptographically correct for u-boot | Fully proven | — |
| TA signature is cryptographically correct for OP-TEE core | Fully proven | — |
| Bundle signature chains to the keyring and the hash tree is intact | Fully proven | — |
| HAB signatures are correct for the boot ROM | Structure and key material only | The ROM is the only verifier; boot on fused silicon |
| The SRK hash in fuses matches the signed image | Not tested | Read back the fuses on the production line before closing them |
| dm-verity holds at mount time and the slot switch completes | Not tested | `rauc install` on a device, then reboot into the new slot |

## Mapping to the CRA

| Annex I requirement | Covered by |
| --- | --- |
| Integrity of the software and its configuration is protected; unauthorised modification is detected | HAB secure boot from ROM; signed kernel FIT and TAs; dm-verity root filesystem |
| Security updates are provided and can be installed securely | RAUC bundles signed under the manufacturer's own CA, verified on the device before installation |
| Keys are protected; access is controlled and logged | All signing keys in the HSM; per-operation roles, approval gates and audit trail on the platform |
| Products are identifiable and can be onboarded securely | IDevID and LDevID issuance — see [Trusted device identities](https://docs.laavat.io/solutions/trusted-device-identities/) |

Building a device this way does not by itself make a product CRA compliant.
The [Annex I mapping](https://docs.laavat.io/cra-compliance/annex-i-mapping/) sets out which
requirements the platform supports and which remain yours.

## Related

- [Secure boot and CRA](https://docs.laavat.io/cra-compliance/secure-boot-cra/) — how this
  supports the CRA's integrity and update requirements
- [CST signing](https://docs.laavat.io/usage/signing-encryption/cstsigning/) — the archive-mode
  `request.json` reference for `SignHAB`
- [HAB signing example flow](https://docs.laavat.io/exampleflows/habsigning/) — a single-image
  walkthrough, from product creation to the SRK hash
- [RAUC signing](https://docs.laavat.io/usage/signing-encryption/raucsigning/) and the
  [RAUC signing example flow](https://docs.laavat.io/exampleflows/raucsigning/) — bundle formats
  and a worked example
- [FIT signing](https://docs.laavat.io/usage/signing-encryption/fitsigning/) — preparing the
  `.its` and embedding the key into u-boot
- [Product management](https://docs.laavat.io/usage/productguide/product/) — templates,
  operations and the `FusemapType` configuration item
- [Advanced PKI example flow](https://docs.laavat.io/exampleflows/advancedpki/) — externally
  rooted Sub CAs in a product
- [Trusted device identities](https://docs.laavat.io/solutions/trusted-device-identities/) — the companion
  design for IDevID and LDevID issuance
- [CRA-ready Zynq UltraScale+ gateways](https://docs.laavat.io/solutions/cra-ready-zynq-ultrascale/) — the
  same chain from U-Boot onwards, with a Bootgen-signed boot image before it
- [CRA-ready Raspberry Pi gateways](https://docs.laavat.io/solutions/cra-ready-raspberry-pi/) — signed boot
  with encrypted updates and a signed manifest instead of RAUC
- [Security and architecture](https://docs.laavat.io/security/) — where the keys live and
  who can reach them

## References

- NXP AN4581, *Secure Boot on i.MX 8M using HABv4* — the application note for
  the boot ROM's verification of the first stage, available from NXP's
  documentation portal
- [RAUC: bundle formats and security](https://rauc.readthedocs.io/en/latest/advanced.html#security)
  and the
  [system configuration reference](https://rauc.readthedocs.io/en/latest/reference.html#system-configuration-file)
- [OP-TEE: Trusted Application signing](https://optee.readthedocs.io/en/latest/building/trusted_applications.html#offline-signing-of-tas)
- [u-boot: FIT signature verification](https://docs.u-boot.org/en/latest/usage/fit/signature.html)
- [Regulation (EU) 2024/2847, Cyber Resilience Act](https://eur-lex.europa.eu/eli/reg/2024/2847/oj),
  Annex I
