Skip to content

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.

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.

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.

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: FIT signing, OP-TEE TA signing

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.

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

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/>BootgenPSK · PSK1<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.

printf '%s' "$TOKEN" | signing-tool -c -t @- -a https://app.laavat.io/<tenant>/api/v1 \
    product add -T mpsoc-gateway-product.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": "BootgenPSK",
            "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 (BootgenPSK) Signs the SPK in every certificate when ppk_select = 0 Hash of the PPK in the PPK0 eFUSEs (see the note below); 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.

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 EFusePPKBits 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. That is the client the manufacturing line runs. Registering it is a one-time step, approved like any other request:

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
EFusePPKBits PPK0 The PSK0 public key
EFusePPK0Bits PPK1 The PSK1 public key

Check the slot before you burn

The names do not follow the slot numbers. EFusePPKBits has no index because it predates devices with two PPK slots; the field with the 0 suffix is the second slot, PPK1. eFUSEs are one-time programmable, so burn each value into the slot in the table above, and cross-check it against the value recomputed from the public key in Verifying before shipping.

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.

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.

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.

{
  "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

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 EFusePPKBits 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 EFusePPK0Bits 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.

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:

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 EFusePPKBits 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 EFusePPK0Bits. 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
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 sets out which requirements the platform supports and which remain yours.

References