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

# Building CRA-ready Zynq UltraScale+ MPSoC gateways

A Zynq UltraScale+ MPSoC can be set up so that it boots only what the gateway
manufacturer signed: the boot ROM checks the primary public key against a hash
in eFUSEs, and every partition of the boot image carries an RSA signature.

This guide is an example of that setup with the signing keys held on the LAAVAT
platform: how the boot image is signed, how the eFUSE value reaches
manufacturing, and how the result is verified before it ships. Take the parts
that match your own design.

> **Note: How to read this**
> This is an example configuration, not a design guide. It shows how one
> platform operation signs a complete Bootgen boot image — PMU firmware,
> FSBL, bitstream, ARM Trusted Firmware, U-Boot — and how the eFUSE value
> reaches manufacturing. From U-Boot onwards the chain is the same as on an
> i.MX8M gateway: the kernel FIT and OP-TEE trusted applications are signed
> exactly as described in [CRA-ready i.MX8M gateways](https://docs.laavat.io/solutions/cra-ready-imx8m/).

## Secure boot on the Zynq UltraScale+

**The boot chain** on the MPSoC starts in the boot ROM, code in silicon that
cannot change. The ROM reads the boot image from the boot medium and loads two
things: the PMU firmware and the first-stage boot loader (FSBL). The FSBL, a
small program generated by the Vitis tools for the specific board, initialises
the DDR memory, clocks and I/O, programs the FPGA bitstream, and loads the ARM
Trusted Firmware and U-Boot into memory before handing over. Because the FSBL
is the first code that is not in silicon, and because it loads everything after
it, it is the link the ROM has to verify and the link that verifies the rest.

**Secure boot** is the ROM refusing an FSBL it cannot verify. With the RSA_EN
eFUSE set, the ROM loads the FSBL only if the boot image carries a certificate
whose primary public key (PPK) hashes to the value burned into the PPK eFUSEs,
and whose secondary public key (SPK) is signed by that PPK. The FSBL then
applies the same check to every partition it loads: the bitstream, the ARM
Trusted Firmware and U-Boot each carry their own certificate, signed by the
SPK. Two independent PPK eFUSE slots exist, usually both burned at
manufacturing, and an image is signed under either.

Xilinx calls this scheme *authentication* and its signature blocks
*authentication certificates*; this guide keeps those nouns and says *sign* for
what the platform does.

**Bootgen** is Xilinx's tool for assembling the boot image from a BIF
description. Given the private keys, it signs each partition and writes the
certificates. In the configuration described here that signing step no longer
runs on the build host: the BIF is sent to the platform without any key
material, and the platform runs Bootgen with keys that exist only inside its
HSM.

This is where LAAVAT comes in. The manufacturer keeps its BIF and its
components; LAAVAT holds the PSK and SSK keys in an HSM, runs Bootgen as a
product operation, enforces who may use it, and hands back the boot image
together with the public keys the manufacturing line needs.

## LAAVAT's part in each link

Each partition is verified by the component that loads it, against the keys
carried in the image and anchored in the eFUSEs. Nothing on the device talks to
LAAVAT at boot.

| Artifact | Verified by | Trust anchor on the device | LAAVAT role |
| --- | --- | --- | --- |
| Boot header, PMU firmware, FSBL | Boot ROM | PPK hash in the PPK0 (or PPK1) eFUSEs; RSA_EN fuse | Assembles and signs the image (`SignBootgenImage`) with the PSK and SSK in the HSM; provides the PPK for the eFUSE hash |
| Bitstream, ARM Trusted Firmware, U-Boot partitions | FSBL | The SPK in each partition's certificate, itself signed by the PPK | Same operation, one certificate per partition |
| Kernel FIT, OP-TEE trusted applications | U-Boot, OP-TEE | As on the i.MX8M gateway | The same operations as in [CRA-ready i.MX8M gateways](https://docs.laavat.io/solutions/cra-ready-imx8m/): FIT signing, OP-TEE TA signing |

> **Abstract: Core property — HSM-resident keys**
> The primary and secondary signing keys live in the HSM and never leave
> it; Bootgen runs inside the platform against them. What the device holds
> is public material only: the PPK hash in eFUSEs, and the PPK and SPK
> carried in every boot image. See
> [key custody](https://docs.laavat.io/security/#key-custody-and-export).

The manufacturer brings the BIF, converted to a JSON request, and the
components; LAAVAT returns the signed boot image and the public keys.

```mermaid
graph LR
    subgraph you["You bring"]
        B1["<b>Boot image request</b><br/>request.json<br/>+ ELFs · bitstream"]
    end
    subgraph laavat["LAAVAT platform"]
        L2["<b>Key operations</b><br/>BootgenPSK0 · BootgenPSK1<br/>BootgenSSK · RSA-4096"]
        L1["<b>Boot image signing</b><br/>SignBootgenImage<br/>Bootgen + HSM keys"]
    end
    subgraph out["Signed and verifiable"]
        O1["<b>boot.bin</b><br/>signed header and<br/>every partition"]
        O2["<b>Public keys</b><br/>primary.pub (PPK)<br/>secondary.pub (SPK)"]
    end
    B1 --> L1
    L2 -.->|"keys"| L1
    L1 --> O1
    L1 --> O2
```

One product: the boot image operation, and the key operations behind it. Every
request runs with HSM-protected keys, role-based access, an approval gate and a
full audit trail.

## The LAAVAT product

The product has one signing operation and a set of key operations that exist
only to own keys: two primary keys, one per PPK eFUSE slot, and one or more
secondary keys, each RSA-4096 as the boot ROM expects. It is created from the
template below with `signing-tool` and approved.

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

```json
{
    "name": "MPSoC gateway",
    "description": "Zynq UltraScale+ MPSoC boot image signing with Bootgen",
    "productType": "Production",
    "enabled": true,
    "caInfo": [],
    "rndKeys": [],
    "productOperations": [
        {
            "name": "Boot image signing",
            "description": "Assembles boot.bin from request.json + components and signs every partition",
            "operationType": "SignBootgenImage",
            "approvalRule": {
                "name": "Two-person", "description": "Boot image requests are released by an approver",
                "allowedGroups":  ["<release-group-uuid>"],
                "approvalGroups": ["<approver-group-uuid>"],
                "blanketGroups":  []
            }
        },
        {
            "name": "PSK0",
            "description": "Primary key for ppk_select = 0; its hash goes into the PPK0 eFUSEs",
            "operationType": "BootgenPSK0",
            "token": { "name": "PSK0", "description": "RSA-4096", "keyType": "RSA4096" }
        },
        {
            "name": "PSK1",
            "description": "Primary key for ppk_select = 1; its hash goes into the PPK1 eFUSEs",
            "operationType": "BootgenPSK1",
            "token": { "name": "PSK1", "description": "RSA-4096", "keyType": "RSA4096" }
        },
        {
            "name": "SSK",
            "description": "Secondary key: signs the partitions; its public half (SPK) is signed by the PPK in every certificate",
            "operationType": "BootgenSSK",
            "token": { "name": "SSK", "description": "RSA-4096", "keyType": "RSA4096" }
        }
    ]
}
```

The key operations have no approval rule because they are never called
directly; the boot image operation is the only one that uses them, and it is
the one to gate. Further secondary keys are added as `BootgenSSK2` …
`BootgenSSK8` operations.

## The keys and where their other halves go

The PPK hash is anchored once, at manufacturing; every later trust decision on
the device derives from it through the certificates inside the boot image.

| Key (operation) | Used for | What the device holds | Placed by |
| --- | --- | --- | --- |
| PSK0 (`BootgenPSK0`) | Signs the SPK in every certificate when `ppk_select` = 0 | Hash of the PPK in the PPK0 eFUSEs; the PPK itself travels in the image | Manufacturing, once, with the RSA-enable fuse |
| PSK1 (`BootgenPSK1`) | The same role when `ppk_select` = 1 | Hash in the PPK1 eFUSEs | Manufacturing, usually together with PPK0; the two slots are independent |
| SSK (`BootgenSSK`…) | Signs the boot header and every partition | Nothing: the SPK is inside each certificate and checked against the PPK | The image |

Only the PPK hash is provisioned; everything else is carried by the boot image.

> **Warning: The PPK hash is not NIST SHA3-384**
> The PPK hash is SHA3-384 in AMD's original Keccak form. The parts the ROM
> verifies — boot header, PPK hash, boot image — use the Keccak padding,
> while partitions loaded later use NIST SHA3. A generic SHA3 tool therefore
> gives the wrong value; Bootgen's `-efuseppkbits` and the platform's
> `EFusePPK0Bits` and `EFusePPK1Bits` both compute it correctly.

The platform provides the eFUSE values as part of the product secrets, a bundle
that only a client of type `ProductionPC` registered on the product with an RSA
or EC public key can open — see [Clients](https://docs.laavat.io/usage/clientguide/clients/).
That is the client the manufacturing line runs. Registering it is a one-time
step, approved like any other request:

```bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out station.key
openssl pkey -in station.key -pubout -out station.pub

printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    client add -N "manufacturing-station" -D "reads the eFUSE values" -K station.pub \
    -U "appid:<station-identity-app-id>" -T ProductionPC -p <product-uuid>
Client Add request sent. Request ID: <request-uuid> state: ApprovalRequired

printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    client approve -I <request-uuid>
Client approved

printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    secrets get -P <product-uuid> -C station.key -O product-secrets
Full secret payload written to: product-secrets
```

The product secrets are JSON; the hashes are base64 text in
`productConfigItems`:

| Config item | eFUSE slot | Hash of |
| --- | --- | --- |
| `EFusePPK0Bits` | **PPK0** | The PSK0 public key |
| `EFusePPK1Bits` | **PPK1** | The PSK1 public key |

`ppk_select` chooses per request which slot an image is signed for, and a
device accepts it only if that slot holds the matching hash.

## Signing the boot image

One request per boot image. Submit the archive, wait until the request is
*Ready*, download the result. Where the operation carries an approval rule, the
request waits in *ApprovalRequired* until an approver releases it. The request
and archive formats are documented in full in
[Xilinx signing](https://docs.laavat.io/usage/signing-encryption/xilinxsigning/).

> **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>

# imagesigning get --wait polls until the request is Ready (state 16), then downloads the result;
# it keeps polling through ApprovalRequired (2) for up to --wait-timeout seconds (default 1800).
```

### The request: the BIF without keys

The request is the Bootgen BIF written as JSON, with every key and
authentication attribute removed; the platform adds those. Partition attributes
are passed through as they would appear in the BIF.

```json
{
  "arch": "zynqmp",
  "ppk_select": 0,
  "spk_id": 0,
  "images": [
    {
      "output": "boot.bin",
      "items": [
        { "filename": "pmufw.elf", "attributes": "pmufw_image" },
        { "filename": "fsbl.elf",  "attributes": "bootloader" },
        { "filename": "system.bit","attributes": "destination_device = pl" },
        { "filename": "bl31.elf",  "attributes": "destination_cpu = a53-0, exception_level = el-3, trustzone" },
        { "filename": "u-boot.elf","attributes": "destination_cpu = a53-0, exception_level = el-2" }
      ]
    }
  ]
}
```

`ppk_select` chooses the primary key, and therefore the eFUSE slot the image is
meant for. Unfused development boards can add
`"fsbl_config": ["bh_auth_enable"]`, which makes the ROM verify the PPK from the
boot header instead of the eFUSEs.

### Submit and download — `SignBootgenImage`

```bash
tar czf boot-request.tar.gz request.json pmufw.elf fsbl.elf system.bit bl31.elf u-boot.elf

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

# the operation carries an approval rule, so the request moves to ApprovalRequired;
# the pipeline waits here until a member of the approval group releases it
printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    imagesigning get -I <request-uuid> --wait --skipBase64 -O boot-signed.tar.gz

# the approver, with their own token, from their own session (or from the approvals view in the UI)
printf '%s' "$APPROVER_TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    imagesigning approve -I <request-uuid>
Image signing request <request-uuid> approved

# ... and the pipeline's wait returns
Downloading signed binary to: boot-signed.tar.gz
File Downloaded

tar xzf boot-signed.tar.gz
boot.bin  primary.pub  secondary.pub
```

The returned archive is raw binary, hence `--skipBase64`. `boot.bin` is a
complete Zynq UltraScale+ boot image with a signature — an authentication
certificate, in Xilinx's term — on the header and on every partition;
`primary.pub` is the PPK the image was signed under and `secondary.pub` the
SPK.

## What goes on the device

| Trust anchor | Source on the platform | Where it goes |
| --- | --- | --- |
| PPK0 hash | `EFusePPK0Bits` from `secrets get` with the manufacturing client, cross-checked with `bootgen -efuseppkbits` | PPK0 eFUSEs, with RSA_EN set; one-time programmable, written at manufacturing |
| PPK1 hash | `EFusePPK1Bits` from the same bundle; cross-checked from the PSK1 public key | PPK1 eFUSEs, usually burned together with PPK0 |
| `boot.bin` | The signed request | The boot medium (QSPI, SD, eMMC); carries the PPK, the SPK and every signature |

Nothing else is provisioned: the device learns the keys from the image and
trusts them because of the fused hash.

## Verifying before shipping

Bootgen performs the same certificate checks as the ROM and the FSBL, so a
signed image can be verified on the build host before it goes anywhere.

```text
bootgen -arch zynqmp -verify boot.bin
Verifying Header Authentication Certificate
    SPK Signature Verified
    Header Signature Verified
Verifying Partition 'fsbl.elf.0' Authentication Certificate
    BootHeader Signature Verified
    SPK Signature Verified
    Partition Signature Verified
… one block per partition …
Authentication is verified on bootimage boot.bin

bootgen -arch zynqmp -read boot.bin
# dumps the boot header, every partition header (authentication [rsa]) and every certificate: ppk_mod, spk_mod, spk_id, signatures
```

Two further checks close the loop with the eFUSEs. The key the image was signed
under, `primary.pub` in the returned archive, is the same key as
`product getpubkey` returns for the PSK operation, and its modulus is the
`ppk_mod` in every certificate. And the eFUSE value from the product secrets can
be recomputed from that public key alone:

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

cat > ppk0.bif <<'EOF'
the_ROM_image:
{
  [ppkfile] psk0-pub.pem
  [bootloader] fsbl.elf
}
EOF
bootgen -arch zynqmp -image ppk0.bif -efuseppkbits ppk0-efuse-hash.txt -w on
# ppk0-efuse-hash.txt: 96 hex characters = the 384-bit value for the PPK0 eFUSEs; must equal EFusePPK0Bits from the secrets
```

When the recomputed value equals the platform's, a device fused with it boots
this image. For the PPK1 slot the same is done with the PSK1 key, and the
result must equal `EFusePPK1Bits`. A flipped byte in any partition or
certificate makes `bootgen -verify` report a failed signature.

### Where off-device verification stops

| Claim | Off-device | What would close the gap |
| --- | --- | --- |
| Every certificate and signature in `boot.bin` is valid | Fully proven, by `bootgen -verify` | — |
| The eFUSE value matches the key the image was signed under | Fully proven, by recomputing it with `-efuseppkbits` | — |
| The ROM accepts the image on fused silicon | Not tested | Boot on a board with RSA_EN and the PPK hash burned |
| The hash was burned into the intended slot | Not tested | Read back the eFUSEs on the production line before setting RSA_EN |

## Mapping to the CRA

| Annex I requirement | Covered by |
| --- | --- |
| Integrity of the software and its configuration is protected; unauthorised modification is detected | RSA-signed boot image anchored in the PPK eFUSEs; every partition verified by the ROM or the FSBL |
| Security updates are provided and can be installed securely | A new boot image is verified by the ROM the same way as the first; RAUC update bundles and the kernel FIT are signed as in [CRA-ready i.MX8M gateways](https://docs.laavat.io/solutions/cra-ready-imx8m/) |
| Keys are protected; access is controlled and logged | PSK and SSK keys in the HSM; the boot image operation behind roles, an approval gate and the audit trail |
| Compromise can be recovered from | Two independent PPK slots, usually both fused; images can be signed under either |

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

- [Xilinx signing](https://docs.laavat.io/usage/signing-encryption/xilinxsigning/) — the
  `request.json` format, the archive contents and the BIF conversion
- [CRA-ready i.MX8M gateways](https://docs.laavat.io/solutions/cra-ready-imx8m/) — the kernel FIT, OP-TEE
  and RAUC half of the chain, from U-Boot onwards
- [FIT signing](https://docs.laavat.io/usage/signing-encryption/fitsigning/) — preparing the
  kernel FIT for U-Boot
- [Clients](https://docs.laavat.io/usage/clientguide/clients/) — registering the manufacturing
  station
- [Product management](https://docs.laavat.io/usage/productguide/product/) — templates,
  operations and the `EFusePPK0Bits` and `EFusePPK1Bits` configuration items
- [Security and architecture](https://docs.laavat.io/security/) — where the keys live and
  who can reach them

## References

- [Bootgen User Guide (UG1283)](https://docs.amd.com/r/en-US/ug1283-bootgen-user-guide)
  — authentication, BIF attributes, `-efuseppkbits`, `-verify`
- [Zynq UltraScale+ Device TRM (UG1085)](https://docs.amd.com/r/en-US/ug1085-zynq-ultrascale-trm)
  — secure boot and eFUSEs
- [Regulation (EU) 2024/2847, Cyber Resilience Act](https://eur-lex.europa.eu/eli/reg/2024/2847/oj),
  Annex I
