User Guide - Secure Boot on Nitrogen93 with EZ BSP

  1. Introduction
  2. Scope and audience
  3. The chain of trust
    1. What each link protects
  4. Prerequisites
    1. Host
    2. Fetch the BSP
    3. Boot switches
  5. Key material
    1. The SIG_DATA_PATH tree
    2. The shipped demonstration keys
    3. Generating a production tree
    4. spsdk_ahab.yaml
    5. Pointing the build at your keys
    6. Signing without private keys on the build host
  6. Building a signed image
    1. Known issue: the secure build fails on a sysroot collision
    2. What lands in the deploy directory
    3. Signing parameters, for the record
  7. Verify on the host
    1. Confirm signing actually ran
    2. Verify the containers
    3. Confirm the FIT verification key is in U-Boot
    4. Confirm the FIT is signed
    5. Confirm the update package is signed
    6. Verification checklist
  8. Programming a device
    1. SD card
    2. eMMC — via SWUpdate
    3. USB recovery
  9. Getting a console, and reading ahab_status
    1. Why there is no prompt
    2. Why you cannot just change the environment
    3. The rescue image — a prompt with the ahab_* commands
    4. Reading ahab_status
    5. In a signed production build
  10. Provisioning the root of trust (POINT OF NO RETURN)
    1. What gets programmed
    2. The safe sequence
    3. How fuses get programmed on the i.MX93
    4. Closing the device
    5. Lifecycle states
  11. Root filesystem integrity and runtime secrets
    1. Partition layout and what protects what
    2. How the dm-verity root hash is authenticated
    3. OP-TEE secure storage as configured in 7.2
    4. RPMB is off by design — you are expected to enable it
    5. PKCS#11 on the target
  12. Signed firmware updates
    1. A/B updates and rollback
    2. A note on keys
  13. Recovery and boot modes
  14. Production checklist
  15. Appendix A — Coming from the old CST flow
  16. Appendix B — How EZ BSP signing is wired
    1. Why both are required
    2. What happens when both are set
    3. What you still have to do yourself
  17. Appendix C — The N93 versus the N8MP
  18. Appendix D — Design note: SGK versus SRK signing
  19. Appendix E — Reference
    1. Command index
    2. Artifact index
    3. Offsets and key variables
    4. Where the behaviour is defined
    5. Applying this guide to the N91 and N95
    6. External references

Introduction

AHAB secure boot on the Nitrogen93 SMARC SOM

ProductNitrogen93 SMARC SOM (NXP i.MX93 · SPSDK target mimx9352)
BSPEZ BSP 7.2 — Yocto Walnascar · U-Boot 2026.07-14.25.0.3 · kernel 6.18.20
ManifestEzurio/EZ-BSP-Yocto-Release-Packages, branch wrynose-summit-7.2.0.x, nitrogen_7.2.0.5.xml
SecurityAHAB + EdgeLock secure enclave (ELE) · SPSDK signing · dm-verity · signed SWUpdate
Version2.0 · August 2026
OwnerJon Powell · Embedded Linux Support / SOM FAE

This document supersedes the earlier Nitrogen Advanced Secure Boot Guide and the published application note Nitrogen93 SMARC — Yocto U-Boot Secure Boot Guide (AHAB, i.MX93). Both were written against the pre-EZ-BSP tree, where you built U-Boot by hand and signed containers yourself with the NXP Code Signing Tool. EZ BSP signs the boot chain for you, inside the Yocto build. If you follow the old CST/CSF procedure on EZ BSP you will be fighting the build system.

Scope and audience

This guide covers the production secure-boot lifecycle for the Nitrogen93 on EZ BSP:

  • what EZ BSP signs automatically, and what you must still do yourself
  • generating and organising your signing keys
  • producing a fully signed image
  • verifying every signature before you commit anything irreversible to hardware
  • programming devices (SD, eMMC, USB recovery)
  • provisioning the hardware root of trust (fuses) and closing the device
  • how the boot chain protects the kernel, the root filesystem, and firmware updates at runtime

It is written for engineers integrating a Nitrogen93 into a product. Fuse maps, keywriter internals and lifecycle-transition mechanics are governed by NXP licensing; where this guide reaches that boundary it points you at NXP AN13575 or at Ezurio Secure Provisioning rather than reproducing NDA-bound material.

How to read it. Sections 3 to 13 are the procedure, in the order you carry it out: set up, make keys, build, verify, flash, get a console, fuse, then the runtime and update layers. The appendices hold the background — Appendix A if you are migrating from the old CST flow, Appendix B for how the signing is wired, Appendix C for why i.MX8M instructions do not apply here, Appendix D on key rotation, Appendix E for the command and offset reference.

Platform. This guide is written and measured against the Nitrogen93. The N91 and N95 use the same AHAB flow and almost the same configuration — Applying this guide to the N91 and N95 lists exactly what changes, and what does not transfer.

The chain of trust

i.MX93 boot ROM
-> verifies Container 0 (ELE firmware, NXP-signed — never touch it)
-> ELE verifies the OEM container(s) (SPL + DDR fw, then ATF/bl31 + OP-TEE + U-Boot)
against the SRK hash in your fuses
-> U-Boot verifies the kernel FIT against the public key in its own control DTB
-> the FIT's boot script hands dm-verity the root hash it was signed with
-> kernel mounts the squashfs rootfs through dm-verity
-> SWUpdate accepts only CMS-signed .swu packages for later updates
-> OP-TEE holds runtime secrets outside the verified rootfs

Two properties of this chain are worth internalising, because they differ from the old BSP:

The kernel FIT is signed on every build, secure or not. UBOOT_SIGN_ENABLE = "1" is set unconditionally in the machine configuration, so even a default summitsom-wayland image ships a signed FIT and a U-Boot whose control DTB carries the verification key marked required.

The AHAB container is not. Container signing is opt-in (Appendix B — How EZ BSP signing is wired). In a default build the ROM→SPL and SPL→U-Boot/ATF/OP-TEE hops are unauthenticated: the kernel is verified by a bootloader that nothing verified. That is fine for development and useless as security. Section 4 is how you close it.

LinkArtifactMechanismActive by default?
ROM → ELEmx93a1-ahab-container.img at flash.bin+0x0NXP signatureAlways
ROM/ELE → SPLSPL + LPDDR4 training firmwareAHAB container, your SRK — ROM authenticates the SPLNo — needs Appendix B — How EZ BSP signing is wired
SPL → runtimebl31.bin (ATF), tee.bin (OP-TEE), u-boot.binAHAB container, your SRK — the SPL authenticates U-BootNo — needs Appendix B — How EZ BSP signing is wired
U-Boot → kernelkernel.itb — kernel + DTB/overlays + boot scriptFIT signature, sha256,rsa2048, PSS, required = "conf"Yes
kernel → rootfs*.squashfs-zst.veritydm-verity; root hash carried inside the signed FITYes
field update*.swuSWUpdate CMS signatureOnly on a -secure DISTRO
runtime secretsOP-TEE secure storageHUK-derived encryptionYes

Prerequisites

Host

Ubuntu 22.04 LTS, 300 GB free for a Yocto build. Host packages:

sudo apt install autoconf bash bc bison bsdmainutils build-essential bzip2 ca-certificates \
  chrpath cmake coccinelle cpio diffstat dosfstools flex gawk gdisk gperf gzip help2man \
  libncurses-dev libtool-bin locales lz4 make openssh-client patch perl python-is-python3 \
  python3 rsync screen sed sudo tar texinfo unzip wget whois xxd zstd
sudo apt install autoconf bash bc bison bsdmainutils build-essential bzip2 ca-certificates \

  chrpath cmake coccinelle cpio diffstat dosfstools flex gawk gdisk gperf gzip help2man \

  libncurses-dev libtool-bin locales lz4 make openssh-client patch perl python-is-python3 \

  python3 rsync screen sed sudo tar texinfo unzip wget whois xxd zstd

For host-side verification (see Verify on the host) you also want SPSDK and the device-tree tools:

pip install spsdk
sudo apt install device-tree-compiler

nxpimage is the SPSDK entry point used throughout this guide. The build has its own copy (python3-spsdk-native), but a host copy lets you inspect artifacts outside BitBake.

You do not need a cross toolchain, and you do not need the NXP CST. Both were requirements of the old flow. If you intend to generate your own SRK keys with NXP’s tooling you will need CST for that step alone (Generating a production tree).

Fetch the BSP

mkdir -p ~/work/imx93-smarc-yocto && cd ~/work/imx93-smarc-yocto
repo init -u https://github.com/Ezurio/EZ-BSP-Yocto-Release-Packages \
  -b wrynose-summit-7.2.0.x -m nitrogen_7.2.0.5.xml
repo sync --no-clone-bundle --force-sync --prune

Boot switches

TargetCarrier SwitchesSOM SW1 Switch
SD cardOFFON
onboard eMMCOFFOFF

Key material

The SIG_DATA_PATH tree

All signing inputs live under one directory, pointed at by SIG_DATA_PATH. EZ BSP expects this layout, which is the structure NXP’s tooling produces:

<SIG_DATA_PATH>/
├── spsdk_ahab.yaml            SPSDK AHAB signing configuration
├── crts/
│   ├── SRK1..SRK4_*_usr_crt.pem   the four SRK public certificates
│   ├── CA1_*_ca_crt.pem
│   ├── SRK_1_2_3_4_table.bin      the SRK table embedded in the container
│   └── SRK_1_2_3_4_fuse.bin       the SRK hash — this is what you fuse (Provisioning the root of trust (POINT OF NO RETURN))
└── keys/
    ├── SRK1..SRK4_*_usr_key.pem   the SRK private keys
    ├── CA1_*_ca_key.pem
    ├── key_pass.txt               passphrase for the private keys
    ├── dev.key / dev.crt          kernel FIT signing key
    └── update_signing.key / .crt  SWUpdate package signing key

Three separate trust roots live in that one tree, and it is worth being explicit about which is which:

KeySignsVerified by
SRK<n> (ECC)the AHAB boot containerthe ELE, against the SRK hash in fuses
dev (RSA-2048)the kernel FITU-Boot, against the key in its control DTB
update_signing (RSA-2048)the .swu packageSWUpdate on-target, against /etc/swupdate/dev.crt

The shipped demonstration keys

SIG_DATA_PATH defaults to meta-summit-som/keys/nitrogen/ahab — a complete, working, and entirely public key tree committed to the layer. It exists so that a secure build works out of the box for evaluation. Its properties:

  • SRKs are ECC secp256r1 with sha256 hashing, flag_ca: false
  • srk_set: oem, used_srk_id: 0 (i.e. signing with SRK1 of 4)
  • key_pass.txt contains a well-known passphrase
  • update_signing.key is a symlink to dev.key — the update key and the kernel key are the same key

Never ship these. Anyone with the BSP has the private keys. A device fused to this SRK hash will boot images signed by anyone. Treat a build that used the default SIG_DATA_PATH as unsigned, and never program its SRK hash into a device you intend to close.

The demo tree is still useful: it lets you rehearse the entire build-and-verify flow (see Building a signed image and Verify on the host) before your production keys exist.

Generating a production tree

Key generation is governed by NXP tooling. The Code Signing Tool ships ahab_pki_tree.sh, which produces exactly the crts/ + keys/ structure above; srktool builds SRK_*_table.bin and SRK_*_fuse.bin from the certificates. CST requires an NXP account and licence agreement — see https://www.nxp.com/webapp/Download?colCode=IMX_CST_TOOL_NEW. SPSDK can also generate keys and SRK tables (nxpcrypto, nxpimage ahab) if you prefer to stay in one toolchain.

When ahab_pki_tree.sh asks whether the SRKs should be certificate authorities, answer no. That produces leaf signing certificates (*_usr_crt.pem) with no SGK, which is the configuration the i.MX93 ELE supports — see Appendix D — Design note: SGK versus SRK signing. In spsdk_ahab.yaml the same decision appears as flag_ca: false.

The kernel and update keys are ordinary RSA keys, and unlike the SRKs you can generate them yourself:

openssl genrsa -out dev.key 2048
openssl req -batch -new -x509 -days 7300 -key dev.key -out dev.crt
openssl genrsa -out update_signing.key 2048
openssl req -batch -new -x509 -days 7300 -key update_signing.key -out update_signing.crt

Use two different keys. The demo tree conflates them; a production tree should not, because the kernel key must be available to every build while the update key ideally is not.

spsdk_ahab.yaml

The configuration the build feeds to nxpimage:

family: mimx9352            # rewritten by the build from SPSDK_FAMILY — see note
revision: latest
srk_set: oem
used_srk_id: 0              # which of the four SRKs signs this image
signer: SRK1_sha256_secp256r1_v3_usr_key.pem
srk_table:
  flag_ca: false
  hash_algorithm: sha256
  srk_array:
    - SRK1_sha256_secp256r1_v3_usr_crt.pem
    - SRK2_sha256_secp256r1_v3_usr_crt.pem
    - SRK3_sha256_secp256r1_v3_usr_crt.pem
    - SRK4_sha256_secp256r1_v3_usr_crt.pem

Note on family. The build overwrites this field from SPSDK_FAMILY, which is mimx9352 for the i.MX93. The value you write in the file is ignored — the shipped example says mimx9131 (i.MX91), which is harmless. If you invoke nxpimage by hand, set it correctly yourself.

used_srk_id selects which SRK signs this build. All four public certificates go into the SRK table regardless, and the fuse hash covers all four — which is what gives you three spare roots for rotation and revocation (Appendix D — Design note: SGK versus SRK signing).

Pointing the build at your keys

SIG_DATA_PATH can be set three ways, in increasing order of preference for real projects:

# on the command line

SIG_DATA_PATH=/path/to/sig/data bitbake image-summitsom-weston

# in build/conf/local.conf

SIG_DATA_PATH = "/path/to/sig/data"

# in your own board's MACHINE configuration, in your own meta layer

SIG_DATA_PATH = "/path/to/sig/data"

The third is the right answer for a product: your carrier-board layer carries its own machine config, and the key location travels with it.

Signing without private keys on the build host

For production, keeping SRK private keys on a shared build server is the weak point in an otherwise sound design. EZ BSP supports AWS KMS as an alternative: set AWS_KMS_AHAB_KEY_ARN (or the blanket AWS_KMS_KEY_ARN) and the build swaps the file-based signature provider for a PKCS#11 one backed by KMS, so the signing operation happens in KMS and no exportable private key ever lands on the host. The FIT and HAB4 paths have equivalent support. This is optional and unnecessary for evaluation.

Building a signed image

Two things switch signing on, and you need both of them:

  • SECURE_BOOT = "1"
  • a DISTRO carrying the -secure suffix

A -secure DISTRO on its own is not enough. Without SECURE_BOOT=1 you get a read-only rootfs, a signed kernel FIT and a CMS-signed .swu — but the AHAB boot container stays unsigned. That is a build which looks secure and is not. SECURE_BOOT is assigned nowhere in the layers; it reaches BitBake only through the environment passthrough in setup-environment-nxp, so you must set it yourself. Appendix B has the gating chain if you want the mechanism.

cd ~/work/imx93-smarc-yocto

export SIG_DATA_PATH=/path/to/sig/data      # omit to use the demo keys (evaluation only)
export SECURE_BOOT=1

MACHINE=imx93-nitrogen-smarc DISTRO=summitsom-wayland-secure \
  . setup-environment-nxp build

bitbake image-summitsom-weston

Image and DISTRO choices:

DISTROImageNotes
summitsom-cmd-secureimage-summitsom-cmdheadless
summitsom-wayland-secureimage-summitsom-westongraphical (Weston)
summitsom-xwayland-secureimage-summitsom-westongraphical + X11 compatibility

Known issue: the secure build fails on a sysroot collision

As of EZ BSP 7.2 (nitrogen_7.2.0.5.xml) a secure AHAB build does not complete out of the box. bitbake aborts while staging U-Boot’s dependencies:

ERROR: u-boot-imx-summit-14.25.0.3-r0 do_prepare_recipe_sysroot: The file
/usr/lib/python3.14/site-packages/tests/__init__.py is installed by both
python3-cryptography-native and python3-x690-native, aborting

Cause: python3-spsdk-native lists both python3-cryptography-native and python3-x690-native in its RDEPENDS, and both of those install a top-level site-packages/tests/ package. Staging both into one recipe sysroot hits a duplicate-file abort. Because every AHAB platform pulls python3-spsdk-native, this affects the N91, N93 and N95 secure builds alike — it is not board-specific and not caused by your configuration.

This was introduced by the move to Wrynose, and a fix is in review (meta-summit-som PR #59). Check whether it has landed in your release before applying the workaround below.

Workaround until the fix lands — drop the stray tests package from one of the two recipes. In your own layer, add recipes-devtools/python/python3-x690_%.bbappend:

do_install:append() {
    rm -rf ${D}${PYTHON_SITEPACKAGES_DIR}/tests
}

Neither package’s tests directory is used at build or run time, so removing it is safe. With that appended, a full secure image build completes cleanly — 8187 tasks, no errors — and signing runs normally. Confirm the fix is what unblocked you rather than a stale sysroot by checking that bitbake-layers show-appends python3-x690 lists your bbappend.

What lands in the deploy directory

build/tmp/deploy/images/imx93-nitrogen-smarc/:

Note that on a -secure DISTRO the image artifacts gain a -secure suffix in their filenames (image-summitsom-weston-secure-imx93-nitrogen-smarc.swu, and so on), because IMAGE_BASENAME:summit-secure appends it. The bootloader artifacts below do not.

ArtifactWhat it is
signed-flash.binthe AHAB-signed boot container — SPL, DDR firmware, ATF, OP-TEE, U-Boot
flash.binsymlink to signed-flash.bin
unsigned-flash.binthe pre-signing image — a symlink to flash-<machine>-<ver>.img. This is your recovery image; copy it with cp -L
flash.bin.taggedsigned image plus a size tag, for tooling that needs it
kernel.itbthe signed kernel FIT — kernel, DTB, 17 overlays, boot script
*.squashfs-zst.veritythe read-only rootfs with its dm-verity hash tree appended
*.squashfs-zst.verity.envroot hash, salt, block counts — build-side bookkeeping
*.swuthe CMS-signed SWUpdate package
*.wic.bz2, *.wic.bmapfull-disk SD image
mksdcard.shSD card writer (SD card)

If signed-flash.bin and unsigned-flash.bin are absent, signing did not run. Re-read Why both are required.

Do not try to tell signed from unsigned by file size. In a secure build both are the same size — CONFIG_AHAB_BOOT=y makes the container layout reserve room for the signature blocks up front, so signing fills reserved space rather than growing the file. (Against a non-secure build the sizes do differ, because that build reserves nothing.) Use the steps in Verify on the host to tell them apart.

Signing parameters, for the record

ItemValue
AHAB containerSPSDK nxpimage ahab sign, family mimx9352, srk_set: oem
SRK keysECC secp256r1, SHA-256, four slots, flag_ca: false
Kernel FITsha256,rsa2048, PSS padding, key dev, configuration-level signatures
FIT enforcementrequired = "conf" in U-Boot’s control DTB
rootfssquashfs-zst + dm-verity, SHA-256
.swuCMS (RSA), key update_signing, verified against /etc/swupdate/dev.crt

Note that FIT signing uses PSS padding on this machine, not the OpenEmbedded default of PKCS#1 v1.5. If you verify signatures with your own tooling, match the padding.

Verify on the host

Everything in this section is non-destructive and runs on the build host. Do all of it before programming a single fuse. A signature problem found here costs you a rebuild; the same problem found after fusing costs you a SOM.

Confirm signing actually ran

cd build/tmp/deploy/images/imx93-nitrogen-smarc
ls -l flash.bin signed-flash.bin unsigned-flash.bin flash.bin.tagged

A signed build looks like this:

flash.bin -> signed-flash.bin
flash.bin.tagged                                    1470504
flash-imx93-nitrogen-smarc-14.25.0.3-r0.img         1470464
signed-flash.bin                                    1470464
unsigned-flash.bin -> flash-imx93-nitrogen-smarc-14.25.0.3-r0.img

If you see a plain flash.bin and no signed-*/unsigned-* pair, the AHAB step did not run — re-read Why both are required.

Verify the containers

nxpimage ahab verify is the command that answers “is this actually signed, and by whom”:

nxpimage ahab verify -b signed-flash.bin -f mimx9352

The lines that matter, from a real signed image:

AHAB Image:
  Family:             mimx9352, Revision: latest
  Target memory:      sd_emmc
  Containers count:   2

  Container 0
    SRK Set:            nxp. Signed by NXP keys
    Container authenticity(Succeeded): nxp, Signed by NXP keys
    Container offset(Succeeded): 0x0000.

  Container 1
    Container authenticity(Succeeded): oem, Signed by OEM keys
    Container signing(Succeeded)
      Revoke keys(Succeeded): No SRK key is revoked
      Used SRK key ID(Succeeded): SRK ID 0
      SRK Table & Signature block presence(Succeeded):
      Signed source(Succeeded): SRK key
      Signature(Succeeded): 81725f0842ff87cd...
    Container offset(Succeeded): 0x0400.

Summary table of verifier results:
| Succeeded | Warning | Error |
|    366    |    1    |   0   |

The same command on unsigned-flash.bin, for contrast:

SRK Set:            none. Image is not signed
      SRK Table(Succeeded): Not used
      Signature(Succeeded): Not used
    Container authenticity(Succeeded): none, Image is not signed

Pass criteria: Error count is 0, Container 0 reports nxp, and every OEM container reports oem, Signed by OEM keys with an SRK table and signature present. Used SRK key ID should match the used_srk_id in your spsdk_ahab.yaml.

Expect exactly one Warning, and do not chase it. The overall result prints as Warning, not Succeeded, because of this on Container 0:

Image Encryption (Warning)
  Decrypted data(Warning): The NXP image can't be verified

That is SPSDK saying it cannot verify NXP’s own encrypted ELE firmware image — which it cannot, because it does not have NXP’s keys and never will. It says nothing about your signing. An overall result of Warning with 0 errors is a pass.

“Containers count: 2” does not mean a container was missed. SPSDK reports the primary boot container set. A Nitrogen93 flash.bin holds a third container further in (around 0x4ac00) that the SPL authenticates at runtime, and it is signed too — the section Confirm both OEM containers are signed shows how to confirm it.

Confirm both OEM containers are signed

The boot chain has two OEM containers: SPL + DDR firmware, and then ATF + OP-TEE + U-Boot. Both must be signed or the chain has a hole. Decoding the container headers directly is the quickest check — the SRK-set field lives in the low two bits of the flags word at header offset 4:

ContainerOffsetUnsignedSignedImages
ELE firmware (NXP)0x000000nxp, len 544nxp, len 544 — identical1
SPL + DDR firmware0x000400none, len 160oem, len 5441
ATF + U-Boot + OP-TEE0x04ac00none, len 416oem, len 8003

Signing adds 384 bytes (an SRK table plus a signature block) to each OEM container header and leaves the NXP container byte-identical. The three images in the last container are bl31.bin, u-boot.bin and tee.bin.

This table is N93-specific. Do not carry these offsets, lengths or image counts to the N91 or N95 — the N95 in particular uses AHAB V2 and carries an extra image. See Applying this guide to the N91 and N95, and re-measure on your own part.

Container offsets shift between secure and non-secure builds, because CONFIG_AHAB_BOOT=y changes the layout. Read them from your own image rather than hardcoding them.

For the record, the signature block sits at +0x90 inside the SPL container — the same 0x400 + 0x90 = 0x490 that the old CST flow made you compute and type into a .csf by hand. The number has not changed; you simply no longer have to know it.

Confirm the FIT verification key is in U-Boot

The kernel FIT is only as good as U-Boot’s willingness to enforce it. Check the control DTB:

fdtget u-boot-imx93-nitrogen-smarc.dtb /signature/key-dev required algo key-name-hint

On a stock build this returns:

conf
sha256,rsa2048
dev

together with rsa,modulus, rsa,exponent, rsa,num-bits = 0x800 (2048) and the other RSA parameters. The required = "conf" property is the one that matters — it makes bootm and source reject any FIT configuration that does not verify. Without it, U-Boot would happily boot an unsigned FIT.

fdtget -l u-boot-imx93-nitrogen-smarc.dtb /signature      # should list key-dev

Confirm the FIT is signed

mkimage -l kernel.itb | head -40

Every configuration node should carry a signature. The .its in the deploy directory shows the structure directly:

signature-1 {
        algo = "sha256,rsa2048";
        key-name-hint = "dev";
        padding = "pss";
        sign-images = "kernel", "fdt", "script";
};

sign-images must include script. That is the mechanism by which the dm-verity root hash is authenticated (How the dm-verity root hash is authenticated) — if script were omitted, an attacker could swap the boot script and point dm-verity at a different root hash.

Confirm the update package is signed

cpio -itv < image-summitsom-weston-secure-imx93-nitrogen-smarc.swu

A CMS-signed package carries sw-description.sig immediately after sw-description:

sw-description                                         7347
sw-description.sig                                     1438
erase_data_emmc.sh / erase_data.sh / copy_partitions.sh
flash.bin                                           1470464
uboot.env                                              8192
image-...-secure-...squashfs-zst.verity            107040768
kernel.itb                                          6220664

A non-secure build has no .sig member, which is a quick way to confirm your DISTRO actually carried the -secure suffix. Note the flash.bin inside the package is the signed container.

To prove the signature rather than just its presence, extract the two files and verify them against the update certificate:

cpio -id < image-...-secure-....swu sw-description sw-description.sig
openssl cms -verify -in sw-description.sig -inform DER -content sw-description \
    -CAfile "$SIG_DATA_PATH/keys/update_signing.crt" -purpose any -binary -out /dev/null

CMS Verification successful

The -binary flag is mandatory, and omitting it looks like a security failure. Without it OpenSSL applies S/MIME text canonicalisation to the content, the digest no longer matches, and you get:

CMS Verification failure
CMS routines:CMS_SignerInfo_verify_content:verification failure

That is a verification artefact, not a bad package. The signature is sha256 + rsaEncryption over the raw bytes of sw-description, so the content must be treated as binary. -purpose any is needed because the demo certificate is a generic self-signed cert with no codesigning EKU.

Verification checklist

CheckPass condition
signed-flash.bin exists, flash.bin points at itConfirm signing actually ran
nxpimage ahab verify reports 0 errors (one benign warning is fine)Verify the containers
Container 0 = nxp, byte-identical to the unsigned imageVerify the containers
Both OEM containers = oem, SRK table and signature presentConfirm both OEM containers are signed
Used SRK key ID matches used_srk_id in your YAMLVerify the containers
/signature/key-dev present with required = "conf"Confirm the FIT verification key is in U-Boot
FIT configurations signed, sign-images includes scriptConfirm the FIT is signed
.swu contains sw-description.sigConfirm the update package is signed

Only when all eight pass should you move on.

Programming a device

The cardinal rule, restated for this BSP: the boot container must land at the offset the ROM reads from. On EZ BSP that is:

MediumContainer locationSet by
SD card / eMMC user area32 KiB (0x8000)IMX_BOOT_SEEK = "32"
eMMC hardware boot partition (mmcblk0boot0/boot1)offset 0EMMC_BOOT_OFFSET = "0"
U-Boot environment0x3F0000 within the same boot partitionUBOOT_ENV_OFFSET = "4128768"

If you have notes that say 0x400, they are describing the container header’s offset inside flash.bin, not its offset on the media. The old guide conflated the two. flash.bin begins with the ELE container at its own offset 0; the second container header sits at +0x400. On the media, the whole file goes to 32 KiB (or to 0 in a boot partition).

The SPL uses the i.MX ROM API (CONFIG_SPL_BOOTROM_SUPPORT) to fetch U-Boot proper, which is why one flash.bin works unmodified at both offsets — it reuses whatever medium and offset the ROM already used.

Also note: the .wks file is named summit-imx-uboot-spl-bootpart.wks, but “bootpart” there means “this layout has a FAT /boot partition” — it is a meta-freescale naming convention. The WIC image never touches the eMMC hardware boot partitions. Only the SWUpdate path does.

SD card

cd build/tmp/deploy/images/imx93-nitrogen-smarc
lsblk -o NAME,SIZE,VENDOR,MODEL,TRAN          # identify the card. Get this wrong and you
                                              # overwrite your own disk.
sudo ./mksdcard.sh /dev/sdX

Options: -r <MiB> rootfs_data size, -b <MiB> boot, -p <MiB> perm, -w <MiB> swap, -u copy the .swu into rootfs_data (it appears at /rw on the target).

The script writes flash.bin with dd … bs=1k seek=32 to the whole device, then creates a FAT boot partition holding uboot.env and kernel.itb, swap, perm, the raw verity rootfs, and rootfs_data_a.

mksdcard.sh -s (“Secure boot”) has no effect. The flag is parsed and never used. Whether the resulting card is secure depends entirely on whether the flash.bin you built was signed (Building a signed image), not on this flag. Ignore it.

Alternatively write the full-disk image:

sudo bmaptool copy image-summitsom-weston-imx93-nitrogen-smarc.wic.bz2 /dev/sdX
# or
bzcat image-*.wic.bz2 | sudo dd of=/dev/sdX bs=4M iflag=fullblock conv=fsync status=progress
sudo partprobe /dev/sdX

On a Windows workstation, write the .wic with Etcher or Win32DiskImager.

eMMC — via SWUpdate

This is the supported path, and it is a genuine change from the old guide: you no longer raw-write a .wic to eMMC, and you no longer script UUU to place partitions. You hand a signed .swu to SWUpdate running on the target, and it writes the container to mmcblk0boot0, the kernel FIT to the kernel partition, and the verity rootfs to the rootfs partition — in the correct places, with per-artifact SHA-256 checks and readback verification.

Boot the board from SD first, then:

Push (firewall-friendly; preferred, and the only option that works cleanly over WSL). The USB ethernet gadget is hard-coded to 10.10.3.1:

# BSD netcat
nc -N -W 5 "${IP}" 9000 < image-summitsom-weston-imx93-nitrogen-smarc.swu
# GNU netcat
nc -q 0 -w 5 "${IP}" 9000 < image-summitsom-weston-imx93-nitrogen-smarc.swu

Pull — serve the deploy directory and fetch from the target:

# on the host

cd build/tmp/deploy/images/imx93-nitrogen-smarc && python3 -m http.server

# on the target

fw_update http://<host-ip>:8000/image-summitsom-weston-imx93-nitrogen-smarc.swu

For a first-time eMMC provision, fw_update -m complete lays down the full GPT A/B layout:

PartitionLabelSizeContents
p1kernel_a64 Msigned kernel FIT (A)
p2rootfs_a512 Mverity rootfs (A)
p3rootfs_data_a256 Mwritable data (A)
p4kernel_b64 Msigned kernel FIT (B)
p5rootfs_b512 Mverity rootfs (B)
p6rootfs_data_b256 Mwritable data (B)
p7perm5 Gpersistent data, OP-TEE secure storage

plus flash.bin + uboot.env written to both mmcblk0boot0 and mmcblk0boot1, giving you A/B bootloader redundancy. mmcblk0boot0 is then selected as the active boot partition. Partition sizes are configurable — adjust them to your storage and use case during integration.

If SWUpdate does not answer, check the service on the target:

systemctl status swupdate

Then move the switches to eMMC boot (Boot switches) and restart.

USB recovery

If the board will not boot at all, uuu recovers it over USB serial download. Note what this does and does not do on EZ BSP: uuu does not write storage here. It streams a rescue boot container into RAM and starts a rescue Linux; SWUpdate then does the writing. A rescue build produces flash-rescue.bin, fitImage-rescue and imx-rescue.uuu:

MACHINE=imx93-nitrogen-smarc DISTRO=summitsom-rescue-initramfs \
  . setup-environment-nxp build-rescue
bitbake image-summitsom-rescue-initramfs-main

Put the board in USB serial-download mode and run:

sudo uuu -v imx-rescue.uuu

The script is short and worth understanding:

uuu_version 1.4.165
SDPS: boot -f flash-rescue.bin          # ROM streams the image, SPL fetches U-Boot via ROM API
FB: ucmd setenv fastboot_buffer ${loadaddr}
FB: download -f fitImage-rescue          # copies the rescue FIT into DDR
FB: acmd bootm ${fastboot_buffer}        # and boots it
FB: done

Once the rescue system is up, push a .swu to it as in eMMC — via SWUpdate.

The Signed firmware updates section uses this same rescue image for a different purpose — it is also how you get a U-Boot prompt.

Getting a console, and reading ahab_status

The installed EZ BSP U-Boot gives you neither an interactive prompt nor the ahab_* commands. Both are deliberate, and both have a clean workaround. Understanding why matters, because the obvious workarounds do not work.

Why there is no prompt

The U-Boot shell is compiled in — CONFIG_CMDLINE=y, CONFIG_HUSH_PARSER=y, and neither CONFIG_SILENT_CONSOLE nor CONFIG_DISABLE_CONSOLE is set. The console is live at 115200 on ttyLP0 and prints normally. What stops you reaching the prompt is:

  • CONFIG_BOOTDELAY=-2. In common/autoboot.c, a value of -2 short-circuits the keypress check entirely — abortboot() is never called, so no key is ever sampled. The familiar “Hit any key to stop autoboot” string is in the binary but never printed.
  • Every failure path resets. bootcmd ends in reset, and each branch of loadimage ends in reset or || reset. main_loop() only falls through to cli_loop() if bootcmd returns, and it never does.
  • No preboot. CONFIG_USE_PREBOOT is unset, so a preboot variable is ignored.

Why you cannot just change the environment

The intuitive fix — fw_setenv bootdelay 3 from Linux — does not work, and neither does rewriting uboot.env or raw-writing the environment block. The build sets CONFIG_ENV_WRITEABLE_LIST=y, and include/configs/nitrogen_imx93.h defines a static whitelist:

ethaddr, eth1addr, bootside, rescueside, fips, fips_wifi, version, conf, RegDomain

env/flags.c rejects any name outside that list when it is imported with the H_EXTERNAL flag, and every stored-environment importer (FAT and MMC) passes exactly that flag. So bootdelay, bootcmd, bootargs and verify are silently discarded when read from storage, no matter how you write them. This is intentional hardening, and it is load-bearing: it is what stops an attacker with write access to the boot partition from turning off FIT verification by setting verify=no.

The nine names on that list are there for operational reasons rather than convenience — bootside and rescueside drive A/B slot selection, conf is how set-mode selects the device-tree overlay configuration, and RegDomain exists because for Infineon radios the regulatory domain has to be fixed in software to satisfy Ezurio’s certification. Note that Ezurio does not supply tooling for changing the regulatory domain.

Do not try to defeat the whitelist on a production unit. Use the steps in The rescue image — a prompt with the ahab_* commands instead.

The rescue image — a prompt with the ahab_* commands

EZ BSP already ships the answer. The summitsom-rescue-initramfs DISTRO applies a U-Boot config fragment that sets, for exactly this purpose:

CONFIG_AHAB_BOOT=y            # compiles in ahab_status, ahab_close, fuse-related ELE commands
CONFIG_BOOTDELAY=3            # interruptible
CONFIG_FASTBOOT_UUU_SUPPORT=y
CONFIG_FASTBOOT_OEM_RUN=y
CONFIG_ENV_IS_NOWHERE=y       # no stored env, so no whitelist to fight

Build it and RAM-boot it over USB (USB recovery). You get a three-second interruptible prompt with the full AHAB command set, running entirely from DDR — nothing on the board is modified, and a power cycle returns the unit to its normal image. This is the right way to inspect a device’s secure-boot state, on the bench or in the field.

=> ahab_status
=> fuse read <bank> <word> <count>

Never ship a rescue image on a production unit. CONFIG_FASTBOOT_OEM_RUN=y is arbitrary U-Boot command execution over USB. It is entirely appropriate for a recovery tool and entirely unacceptable in a deployed device.

Reading ahab_status

=> ahab_status
lifecycle 0x00000008, NXP closed
No SRK fuse programmed

That is a virgin part: open, nothing fused, as shipped. Interpretation:

OutputMeaning
lifecycle 0x8, no SRK fuseAs-shipped. Open. Signed and unsigned both boot.
lifecycle 0x8, SRK fused, no eventsYour signed image authenticates. This is the green light for Closing the device.
lifecycle 0x8, SRK fused, events listedSignature is failing. Still boots because the part is open — diagnose and iterate here.
lifecycle 0x80OEM closed. Enforcing.

The events list is your diagnostic surface, and it only exists while the part is open. Use it.

In a signed production build

When you build with SECURE_BOOT=1, ahab_status; is prepended to bootcmd, so a signed device reports its AHAB state on every boot to the serial console — without needing a prompt at all. For most verification purposes, reading the boot log is enough.

Provisioning the root of trust (POINT OF NO RETURN)

POINT OF NO RETURN. Fuses are one-time programmable. A wrong SRK-hash burn, or closing a device before a signed image has been proven to authenticate on it, permanently bricks the SOM — there is no recovery path, no rework, no RMA. Read this entire section before programming anything, and keep a spare SOM on the bench.

Signing (Building a signed image) and provisioning (this section) are separate stages, and conflating them is the most common conceptual error. Signing happens at build time and is completely reversible. Provisioning happens once per device and is not.

What gets programmed

Two things, in this order, with a validation step between them:

  1. The SRK hash — a SHA-256 digest over your four SRK public keys, held in <SIG_DATA_PATH>/crts/SRK_1_2_3_4_fuse.bin (32 bytes = eight 32-bit fuse words). This tells the ELE which root keys to trust. Programming it does not yet enforce anything: the device stays open and continues to boot unsigned images.
  2. The lifecycle transition to OEM-closed — this is what turns enforcement on. After it, the ROM requires a valid signature and unsigned images are rejected.

The gap between them is your entire safety margin. Use it.

The safe sequence

1. Program the SRK-hash fuses. <- irreversible, but not yet enforcing
2. Do NOT close the device yet.
3. Flash your signed image (Programming a device).
4. Boot it and read ahab_status (Getting a console, and reading ahab_status). <- the device is still open, so a bad
signature still boots and you can retry
5. Confirm: signature authenticates, no AHAB events reported.
6. Only then consider closing (Closing the device).

Step 4 is the whole point. On an open device a signature failure is a diagnostic; on a closed device it is a brick.

How fuses get programmed on the i.MX93

On the i.MX93 the fuses sit behind the EdgeLock enclave, so every write is ELE-mediated. In practice the SRK-hash write itself is straightforward: you get to a U-Boot prompt and program the fuse bank with the words from <SIG_DATA_PATH>/crts/SRK_1_2_3_4_fuse.bin, using U-Boot’s fuse command (CONFIG_CMD_FUSE=y is set in the stock build, and its i.MX9 backend goes through the ELE):

=> fuse prog <bank> <word> 0xXXXXXXXX      # repeat per word
=> fuse read <bank> 0 8                    # read back and compare before going further

Note the subcommand is fuse prog — this U-Boot has read, cmp, readm, sense, prog and override, but no write. The correct bank and word range for the SRK hash come from NXP AN13575 for your part and ELE firmware revision; this guide does not reproduce the fuse map boundary — it is vendor material under NXP licensing, and it varies by firmware. See Appendix C — The N93 versus the N8MP

The catch on EZ BSP: a stock image gives you no prompt to type that at (bootdelay=-2, and the environment whitelist prevents changing it — Getting a console, and reading ahab_status). So before you can fuse anything by hand you need a route to a prompt, which The rescue image — a prompt with the ahab_* commands covers. This is the main reason most projects should not be doing this by hand at all:

You have three routes, in the order most projects should consider them:

  1. Ezurio Secure Provisioning (recommended for production). Ezurio offers fuse programming, secure boot enablement, root-of-trust provisioning, and flashing of signed production images as a manufacturing service. For most customers this is the right answer: the irreversible step happens in a controlled factory process, with keys handled under an agreed procedure, and you never build the tooling yourself. Talk to your Ezurio contact.
  2. Ezurio factory provisioning tooling. Ezurio’s WBx3 manufacturing platform integrates SPSDK for i.MX93 secure-boot provisioning. If you are running your own line with Ezurio tooling, this is the supported path.
  3. Directly with SPSDK. If you are provisioning in-house, SPSDK is the tool. The exact fuse indices, the ELE message sequence and the lifecycle-transition mechanics are specified in NXP AN13575 (Secure Boot on i.MX9 using AHAB) and the SPSDK documentation. This guide deliberately does not reproduce the fuse map: it is silicon-vendor material, subject to NXP licensing, and it varies with ELE firmware revision. Read it from the source that matches your part and firmware.

The classic brick. Whatever route you take, the fuse words must match the SRK hash byte-for-byte, in the byte order your tool expects. The historical failure mode is reading raw bytes out of SRK_*_fuse.bin and pasting them into 32-bit hex literals, which byte-swaps every word. A byte-swapped hash makes the ELE reject every signature you will ever produce, and because the fuses are already burned the device is unrecoverable. Prefer an automated path that handles the endianness for you, and if you must do it manually, verify each word against your tool’s own readback before writing. This single mistake accounts for more dead i.MX9 parts than any other.

Closing the device

Closing is the second irreversible step. Preconditions, all of them:

Only then transition the lifecycle to OEM-closed. In a U-Boot with CONFIG_AHAB_BOOT=y that is ahab_close, which will make you confirm:

=> ahab_close
Warning: Please ensure your sample is in NXP closed state, OEM SRK hash has been fused,
and you are able to boot a signed image successfully without any SECO events reported.
If not, your sample will be unrecoverable.
Really perform this operation? <y/N>

Reading a refusal. If closing fails like this:

Error: ele_forward_lifecycle: ret -5, life_cycle 0x8, response 0xbb29
Error in forward lifecycle to OEM closed

response 0xBB29 means the ELE is correctly refusing because a signed image cannot authenticate against the fused hash. This is not a firmware limitation and not something to work around — it is the enclave preventing you from bricking a board that would never boot again. Fix the signature-or-fuse mismatch (Verify the containers, and the byte-order warning above) until ahab_status is clean, then close.

ahab_lock goes a step further and locks the device against further provisioning. Do not run it until you are certain you will never need to change the device’s provisioning state.

A supported auto-close is coming. Work in review for meta-summit-som (PR #59) adds a mechanism that attempts to close the processor automatically on first boot of a signed image. When that lands it becomes the recommended route, because it removes the manual prompt step this section otherwise depends on. Check whether it is in your release before building a manual close into your factory process.

Lifecycle states

LifecycleMeaningYour position
0x8NXP closedNormal as-shipped state. NXP finished provisioning; the part is fully OEM-open. Safe. Signed and unsigned images both boot.
0x80OEM closedThe enforcing state. The ROM requires a valid signature. One-way door.
—OEM lockedEnforcing, plus further restrictions on debug and provisioning.

A fresh dev kit reporting lifecycle 0x00000008, NXP closed is expected and safe. It does not mean the part is locked against you. 0x80 is the state to be careful about.

Root filesystem integrity and runtime secrets

Partition layout and what protects what

RegionProtection
mmcblk0boot0 / boot1AHAB signature over the container (Appendix B — How EZ BSP signing is wired)
kernel_a / kernel_bFIT signature, verified by U-Boot
rootfs_a / rootfs_bdm-verity, read-only; root hash authenticated via the FIT
rootfs_data_a / _bwritable overlay; not integrity-protected
permpersistent data; hosts OP-TEE secure storage
RPMBhardware replay-protected block — present in the eMMC, not currently used (see RPMB is off by design)

A -secure DISTRO additionally makes the rootfs genuinely read-only (read-only-rootfs) and drops the inittype=overlay boot argument, so the running system cannot mutate its own verified root. Confirmed in the generated boot script: the secure build’s bootargs contains no inittype=overlay, where the non-secure build’s does.

How the dm-verity root hash is authenticated

This is an elegant part of the design and worth understanding, because it is where the boot chain crosses from bootloader to kernel.

veritysetup appends a hash tree to the squashfs image at build time and emits the root hash and salt. The build bakes those values, verbatim, into a U-Boot boot script:

dm_table="vroot,<uuid>,,ro,0 207440 verity 1 ${boot_dev} ${boot_dev} 4096 4096 25930 25931 \
          sha256 <root-hash> <salt> 0"
setenv bootargs "${bootargs} ... dm-mod.create=\"${dm_table}\" dm-mod.waitfor=${boot_dev} \
                 root=/dev/dm-0 rootwait rootfstype=squashfs ro"

That script is then embedded in the kernel FIT as an image node named script, and — critically — added to each configuration’s sign-images list:

sign-images = "kernel", "fdt", "script";

So the root hash is not protected by a signature of its own. It is protected by being inside the signed FIT configuration. U-Boot verifies the configuration, sources the script, and the script hands the kernel a root hash that could not have been altered without breaking the FIT signature. There is no separate .verity.sig to manage, and the .env file in the deploy directory is build-side bookkeeping only — nothing signs it and nothing on the target reads it.

One consequence: the kernel command line is part of your trust boundary. Anything that can inject bootargs can influence how the rootfs is mounted, which is another reason the environment whitelist in Why you cannot just change the environment matters.

OP-TEE secure storage as configured in 7.2

OP-TEE 4.10.0 runs as tee.bin inside the AHAB container, so on a signed device the TEE itself is covered by the boot signature.

What is enabled on the Nitrogen93:

  • REE_FS secure storage, rooted at /perm/tee (on the persistent perm partition)
  • the trusted_keys early TA (f04a0fe7-1f5d-4b9b-abf7-619b85b4ce8c), backing Linux kernel trusted keys with the TEE
  • optee-client on the rootfs, providing /dev/tee0 and the client API

No device-tree work is required. /dev/tee0 appears even though the board DTS contains no OP-TEE node — this is correct and expected. OP-TEE is loaded from the boot container and patches the device tree at runtime before Linux starts, injecting the /firmware/optee node and the secure-world and shared-memory reserved-memory regions. The Linux driver binds once it sees those nodes and gets a valid response to its SMC probe. Do not hand-add an OP-TEE node — you will conflict with the runtime patch.

RPMB is off by design — you are expected to enable it

REE_FS stores secure objects as files on the normal Linux filesystem, encrypted with a key derived from the Hardware Unique Key inside the TEE. The threat model deserves a precise statement, because it is easy to get wrong in both directions:

  • REE_FS does not expose plaintext key material to a root attacker. The data is HUK-encrypted, and the HUK is not reachable from the normal world.
  • The real weakness is rollback / replay. A root attacker can restore an older copy of the secure-storage files, and OP-TEE cannot detect it. Revoked credentials can be resurrected; monotonic counters can be wound back.

RPMB defeats this, because the eMMC’s replay-protected block has a hardware monotonic write counter. In EZ BSP the RPMB filesystem is deliberately left disabled — CFG_RPMB_FS, CFG_RPMB_FS_DEV_ID, CFG_REE_FS=n and CFG_RPMB_WRITE_KEY are all present but commented out in meta-summit-som/recipes-security/optee/optee-os_%.bbappend. Confirmed resolved config: CFG_REE_FS 1, CFG_RPMB_FS unset.

This is a design decision, not an oversight. Writing the RPMB authentication key is a one-way, one-time operation, and the details are easy to get wrong. Rather than make that decision on your behalf in a shared layer, EZ BSP leaves it to you to turn on deliberately, in your own layer, when you have decided your storage design.

That fits the recommended project structure: your product carries a thin meta layer on top of meta-summit-som which defines your MACHINE, DISTRO, device trees and application logic — and which is also the right place for SIG_DATA_PATH (Pointing the build at your keys) and for enabling RPMB. In your layer:

# <your-layer>/recipes-security/optee/optee-os_%.bbappend
RPMB_FS_DEV_ID ?= "${EMMC_DEVICE}"

EXTRA_OEMAKE:append = " \
    CFG_RPMB_FS=y \
    CFG_RPMB_FS_DEV_ID=${RPMB_FS_DEV_ID} \
    CFG_REE_FS=n \
"

EMMC_DEVICE is 0 on the Nitrogen93. Add CFG_RPMB_WRITE_KEY=y only for the build that is allowed to provision the key.

The RPMB key write is one-time and irreversible. Once written, that device’s RPMB is bound to a key derived from its HUK, and there is no way back. Gate it behind a deliberate factory step rather than enabling it in a general-purpose development image, and validate the whole flow before you enable CFG_RPMB_WRITE_KEY on units you intend to ship.

One consequence worth knowing during bring-up: because RPMB is off in the stock configuration, you cannot accidentally burn the RPMB key while evaluating. That safety net disappears the moment you enable it in your own layer.

PKCS#11 on the target

The intended architecture is a chain, and it is worth understanding as one:

your application
-> OpenSSL (pkcs11-provider)
-> libckteec (PKCS#11 API over the TEE client API)
-> OP-TEE PKCS#11 TA
-> OP-TEE secure storage

pkcs11-provider is an OpenSSL 3 provider — it offloads cryptographic operations to a backend that exposes a PKCS#11 API. The OP-TEE PKCS#11 TA is that backend. They are not alternatives; they work in tandem. The typical workflow is to import a private key into OP-TEE secure storage through the TA (with pkcs11-tool), then use it from OpenSSL through the provider, so the key never exists as a file.

A genuinely useful capability here: pkcs11-provider supports PEM wrapper files — a .pem that contains nothing but a PKCS#11 URI. That lets applications which insist on a real key file path (web servers, for example) use a key that actually lives in OP-TEE secure storage.

What is present on a stock 7.2 image, and what you must add

Verified against a built image-summitsom-weston-secure rootfs:

ComponentStatus
libckteec.so.0, libckteec.so.0.1.0Present at /usr/lib/ (shipped inside the optee-client package, which is why it does not appear as its own manifest entry)
pkcs11-providerPresent at /usr/lib/ossl-modules/pkcs11.so
p11-kitPresent (/usr/lib/pkcs11/p11-kit-*.so)
OP-TEE PKCS#11 TABuilt but not installed. CFG_PKCS11_TA 1 is on by OP-TEE default, and the TA binary is produced as fd02c9da-306c-48c7-a49c-bbd827ae86ee.ta — but the optee-os-ta package is not in the image, so /lib/optee_armtz is empty on the target
pkcs11-tool (opensc)Not installed

So the client-side plumbing ships and works, but on a stock image the TA cannot be loaded and there is no pkcs11-tool to drive it. To use this chain, add both to your image:

IMAGE_INSTALL:append = " optee-os-ta opensc"

Then confirm the token appears:

ls /lib/optee_armtz/                       # expect fd02c9da-...ta
pkcs11-tool --module /usr/lib/libckteec.so.0 -L
openssl list -providers                    # expect pkcs11-provider

Correction to the previous guide. The old document gave the TA UUID as db1b1f42-4207-5bf8-bb8e-14597543f63d. The OP-TEE PKCS#11 TA UUID is fd02c9da-306c-48c7-a49c-bbd827ae86ee. Its pkcs11-tool --module /usr/lib/libckteec.so.0 command line is otherwise correct — that path does exist on 7.2 — but the command will fail on a stock image until optee-os-ta and opensc are installed, and it also told you libckteec was not the provider you wanted. It is.

Failure signatures, and what they mean:

SymptomCause
ckteec_invoke_init: TEEC init context failed / C_Initialize … CKR_DEVICE_ERROROP-TEE not running, or the TA is not in /lib/optee_armtz
pkcs11-tool: command not foundopensc not installed
dmesg \| grep optee empty, no /dev/tee0the OP-TEE driver has not bound — see OP-TEE secure storage as configured in 7.2

Ezurio’s general recommendation holds regardless of interface: keep TLS private keys, device identity certificates and cloud onboarding secrets in OP-TEE secure storage rather than in the filesystem. Secrets held there survive a rootfs update, stay outside the dm-verity image, and are never exposed as plaintext files.

Signed firmware updates

Updates are the part of the chain most often left unprotected, and on EZ BSP they are covered by default on a -secure build.

Packages are .swu files signed with CMS using update_signing.key. The matching certificate is installed into the image at /etc/swupdate/dev.crt, and SWUpdate is configured with -k /etc/swupdate/dev.crt, so it verifies every package before applying it and refuses anything it cannot authenticate. On top of the signature, each artifact inside the package carries its own SHA-256, and the complete mode adds explicit readback verification of what was written.

The update trust chain:

signed .swu
-> SWUpdate CMS verification (is this package from us?)
-> written to the inactive A/B slot
-> AHAB verification of the new container (on next boot)
-> FIT verification of the new kernel
-> dm-verity verification of the new rootfs

A/B updates and rollback

The device keeps two complete sets of partitions. The running system occupies one slot; the update is written to the other. Only after a fully successful install does SWUpdate switch the active eMMC boot partition — the boot-partition switch is gated on the post-install stage specifically so that a failed update cannot leave the device pointing at a half-written slot. If the updated slot fails to boot, the bootloader falls back to the previous known-good one.

Applying an update in the field is the same command as provisioning (eMMC — via SWUpdate):

fw_update https://updates.example.com/image-summitsom-weston-imx93-nitrogen-smarc.swu

A note on keys

In the demonstration key tree update_signing.key is a symlink to dev.key — the update key and the kernel key are the same key. Split them in production. The kernel signing key must be present for every build; the update signing key ideally lives only where releases are cut, so that a compromised build host cannot forge field updates.

Recovery and boot modes

Keep an escape hatch at every stage. Concretely:

StageRecovery
Before fusingAnything. Reflash SD or eMMC freely; boot either medium via the switches.
SRK fused, device openUnsigned images still boot. Reflash unsigned-flash.bin if needed.
OEM closedOnly correctly signed images run. Recovery requires a properly signed image.

Practical measures:

  • Keep unsigned-flash.bin from every build. It is your open-device recovery image, and the build produces it for free.
  • Keep a signed recovery image once you start closing devices. After OEM-close, an unsigned recovery image is useless to you.
  • Boot the other medium. Because the SD and eMMC paths are independent, a bad eMMC image is recoverable by moving the switches (Boot switches) and booting SD.
  • USB serial download is the last resort and works on any open device (USB recovery).
  • Archive your keys and SRK_*_fuse.bin alongside each release. If you lose the private key matching a fused SRK hash, every device fused to it becomes unupdatable.

Production checklist

Before you ship:

Appendix A — Coming from the old CST flow

Old procedureOn EZ BSP
git clone boundarydevices/u-boot, make imx93_nitrogen_smarc_defconfig, make -j (twice)Nothing. U-Boot is a Yocto recipe (u-boot-imx-summit); flash.bin is built by binman inside it.
ahab_pki_tree.sh, answer n to the CA promptStill generate a PKI tree, but the CA flag is now the flag_ca: false field in spsdk_ahab.yaml.
Hand-write flash-c1.csf, compute Offsets = 0x400 0x490, run cst -i … -o … per containerSet two variables. nxpimage ahab sign runs automatically in do_deploy and signs every OEM container.
binman ls fails, use nxpimage to inspectReversed — binman builds the image here. Use nxpimage ahab parse to inspect (still true).
Keep a copy of flash.bin as your recovery imageThe build keeps it for you, as unsigned-flash.bin.
Composite .wic, split-WIC over UUU, raw dd of the whole diskmksdcard.sh for SD; signed .swu through SWUpdate for eMMC. UUU only RAM-boots a rescue image.
Container header at 0x400 / MBR at LBA 0flash.bin is written at 32 KiB (SD / eMMC user area) or at offset 0 of mmcblk0boot0.
fuse prog and ahab_close at the U-Boot promptThe installed U-Boot has no reachable prompt. Fuses are programmed with SPSDK or by Ezurio.
pkcs11-tool --module /usr/lib/libckteec.so.0libckteec is not built. On-target PKCS#11 is the OpenSSL pkcs11-provider.
RPMB secure storage, Found working RPMB device as a factory gateRPMB is not enabled in EZ BSP 7.2. OP-TEE storage is REE_FS under /perm/tee.

Appendix B — How EZ BSP signing is wired

This is the most important section in the guide. EZ BSP has three independent controls, and enabling secure boot needs two of them set together.

ControlSet byWhat it turns onDefault
UBOOT_SIGN_ENABLE = "1"the machine config, unconditionallyKernel FIT signing; insertion of the public key into U-Boot’s control DTBAlready on
DISTRO = summitsom-*-secureyou, when choosing a DISTRORead-only rootfs, CMS-signed .swu, /data split — and it makes the AHAB include visibleOff
SECURE_BOOT = "1"you, through the environmentAHAB signing of the boot containerOff

Why both are required

The recipe chain is a two-stage gate:

u-boot-imx-summit.bb
  include u-boot-imx-summit-secure.inc          if 'summit-secure' in OVERRIDES
      include u-boot-imx-summit-secure-ahab.inc     if 'ahab' in OVERRIDES and SECURE_BOOT == '1'

summit-secure comes from the -secure DISTRO. ahab comes from the machine (MACHINEOVERRIDES =. "mx93:summitsom:ahab:"). SECURE_BOOT comes from you.

So:

  • SECURE_BOOT=1 with DISTRO=summitsom-wayland — does nothing. The outer include never fires.
  • DISTRO=summitsom-wayland-secure without SECURE_BOOT — you get a read-only rootfs and a signed .swu, but an unsigned boot container. This is the trap: the build looks secure and the boot chain’s foundation is open.
  • Both set — full chain.

SECURE_BOOT is not assigned anywhere in any layer. It reaches BitBake only because setup-environment-nxp adds it to BB_ENV_PASSTHROUGH_ADDITIONS:

BB_ENV_PASSTHROUGH_ADDITIONS="${BB_ENV_PASSTHROUGH_ADDITIONS} SIG_DATA_PATH SECURE_BOOT"

You may set it in the environment before sourcing the setup script, or in build/conf/local.conf.

Check your build. If conf/local.conf has no SECURE_BOOT and your DISTRO has no -secure suffix, your flash.bin is unsigned. Confirm as shown in the section Verify on the host.

This split is intentional: SECURE_BOOT=1 is the switch that enables the signing mechanism, and it is expected to be set by you rather than implied by the DISTRO. It simply has not been documented anywhere until now, which is why it is the first thing this guide covers.

What happens when both are set

do_configure appends CONFIG_AHAB_BOOT=y to the U-Boot config. That single symbol does three things:

  1. Compiles the ahab_* command family into U-Boot (ahab_status, ahab_close, ahab_lock, ahab_dump, ahab_commit, auth_cntr, ele_message, …). Without it these commands do not exist — see Getting a console, and reading ahab_status.
  2. Makes the SPL actually authenticate the next container (spl_imx_container.c calls ahab_auth_cntr_hdr() / ahab_verify_cntr_image()). Confirmed by Ezurio engineering: the hardware authenticates the SPL, the SPL authenticates U-Boot, and so on down the chain — each stage verifies the next.
  3. Prepends ahab_status; to the built-in bootcmd, so every boot logs its AHAB state.

Then do_deploy runs the signing step, which:

  1. copies your spsdk_ahab.yaml, forcing family: mimx9352, rewriting signer: into an SPSDK signature-provider string (type=file;file_path=<your key>;password=<keys/key_pass.txt>), and absolutising the srk_array certificate paths into <SIG_DATA_PATH>/crts/;
  2. runs the signing command against the binman-produced image:
  3. nxpimage ahab sign -c spsdk_ahab_sign.yaml -b flash.bin -o signed-flash.bin --force
  4. deploys signed-flash.bin, renames the unsigned original to unsigned-flash.bin, repoints flash.bin at the signed image, and writes flash.bin.tagged.

Everything downstream — the .wic, the .swu, mksdcard.sh — consumes flash.bin and therefore picks up the signed container with no further action.

What you still have to do yourself

EZ BSP signs. It does not provision. You are responsible for:

Signing without provisioning gives you a signed image that any open device will happily boot alongside an unsigned one. Provisioning is what makes the signature mean something.

Appendix C — The N93 versus the N8MP

The i.MX8M secure-boot documentation does not apply to the i.MX93. Different silicon, different security architecture, different tools.

Nitrogen91 / 93 / 95 (i.MX91/93/95)Nitrogen8M Mini / Plus (i.MX8M)
SchemeAHABHABv4
Crypto arbiterEdgeLock secure enclave (ELE)CAAM
Image formatMultiple independently signed containersSingle image with an appended CSF
Signing toolNXP SPSDK (nxpimage ahab sign)NXP CST
When signing happensAfter image generation, as a post-processing stepDuring image generation, inside binman
SPSDK targetmimx9352 (N93); mimx9131 (N91), mimx9596 (N95)n/a
Fuse programmingELE-mediatedOCOTP
U-Boot status commandahab_statushab_status
EZ BSP switchSECURE_BOOT=1 + an ahab machineSECURE_BOOT=1 + a hab4 machine

Do not follow the i.MX8M HAB blog posts or mx8m_secure_boot.txt for the N93 — wrong tooling, key formats, container layout and fuse flow. If older notes say hab_status, on this platform read it as ahab_status.

EZ BSP reflects the split in its own layers: u-boot-imx-summit-secure-ahab.inc for the i.MX9 family, u-boot-imx-summit-secure-hab4.inc for the i.MX8M family, selected by the machine’s ahab / hab4 override. The CST recipe (cst-native) is COMPATIBLE_MACHINE = "(hab4)" and is never even built for a Nitrogen93.

Authoritative references. NXP AN13575 (Secure Boot on i.MX9 using AHAB) is the canonical silicon-vendor document. The SPSDK documentation at https://spsdk.readthedocs.io/ covers nxpimage and the AHAB configuration schema.

Appendix D — Design note: SGK versus SRK signing

A natural instinct from the i.MX8/8X world is a two-tier PKI — SRKs as offline roots, an SGK (signing key) as the operational signer — so that the signing key can be rotated without touching fuses. On the i.MX93 this does not work today.

Current i.MX91 / i.MX93 / i.MX8ULP ELE firmware does not support SGK; SGK-signed containers fail AHAB authentication on these parts. Use SRK-only signing — which is exactly what EZ BSP does, and what flag_ca: false in spsdk_ahab.yaml expresses.

Two misconceptions worth heading off:

  • “The tooling falls back to SRK if SGK is absent.” No. SRK-only and SGK are distinct configurations. Choosing SRK-only is not a workaround or a misconfiguration; it is the correct choice for this silicon.
  • “We can migrate to SGK later and keep the same SRK hash.” No. Regenerating the PKI with the CA flag set produces different certificates, therefore a different SRK table, therefore a different SRK hash. Devices already fused to the original hash would reject the new containers, with no recovery path — and the SGK containers would not authenticate on i.MX93 ELE anyway.

What to do instead — operational key hygiene:

  • Keep SRK private keys offline or in an HSM between signing operations; consider the AWS KMS path (Signing without private keys on the build host) so no exportable private key sits on the build host.
  • Plan rotation across SRK1→SRK4. AHAB revocation is at the SRK level, and the currently-booting SRK cannot revoke itself: rotate by booting with SRK2 to revoke SRK1, then SRK3 to revoke SRK2, and so on. You have four roots, which means three rotations. Budget them.
  • Use the container software-version fields for anti-rollback.
  • Treat SGK as a watch-this-space item, gated on a future ELE firmware release. If NXP adds support, re-evaluate against what that firmware actually implements rather than against the i.MX8 model.

Appendix E — Reference

Command index

PurposeCommand
Fetch the BSPrepo init -u …/EZ-BSP-Yocto-Release-Packages -b wrynose-summit-7.2.0.x -m nitrogen_7.2.0.5.xml
Configure a secure buildexport SECURE_BOOT=1 SIG_DATA_PATH=… then MACHINE=imx93-nitrogen-smarc DISTRO=summitsom-wayland-secure . setup-environment-nxp build
Buildbitbake image-summitsom-weston
Verify a containernxpimage ahab verify -b signed-flash.bin -f mimx9352
Inspect a containernxpimage ahab parse -b signed-flash.bin -f mimx9352 -o parse_out
Sign a container by handnxpimage ahab sign -c <yaml> -b flash.bin -o signed-flash.bin
Check the FIT key in U-Bootfdtget u-boot-imx93-nitrogen-smarc.dtb /signature/key-dev required algo
List FIT contentsmkimage -l kernel.itb
Check .swu signaturecpio -itv < image-*.swu → look for sw-description.sig
Write an SD cardsudo ./mksdcard.sh /dev/sdX
Update over the network (push)nc -N -W 5 <ip> 9000 < image-*.swu
Update over the network (pull)fw_update http://<host>:8000/image-*.swu
First eMMC provisionfw_update -m complete <url>
USB recoverysudo uuu -v imx-rescue.uuu
Check secure-boot stateahab_status (rescue U-Boot, or the boot log of a signed build)
Close the deviceahab_close — irreversible, see Closing the device

Artifact index

FileRole
signed-flash.binAHAB-signed boot container (SPL, DDR fw, ATF, OP-TEE, U-Boot)
unsigned-flash.binpre-signing image — open-device recovery
flash.binsymlink to whichever of the above this build produced
kernel.itbsigned kernel FIT: kernel, DTB, overlays, verity boot script
*.squashfs-zst.verityread-only rootfs with appended dm-verity hash tree
*.verity.envroot hash / salt / block counts (build-side only)
*.swuCMS-signed SWUpdate package
uboot.envU-Boot environment blob (whitelist-protected on import)
SRK_1_2_3_4_table.binSRK table embedded in the container
SRK_1_2_3_4_fuse.binSRK hash — the value programmed into fuses
mx93a1-ahab-container.imgNXP-signed ELE firmware container

Offsets and key variables

ItemValue
Container on SD / eMMC user area32 KiB (IMX_BOOT_SEEK = "32")
Container in eMMC boot partitionoffset 0 (EMMC_BOOT_OFFSET = "0")
U-Boot environment0x3F0000, size 0x2000 (UBOOT_ENV_OFFSET = "4128768")
Containers inside flash.bin (secure build)0x0 ELE, NXP-signed · 0x400 SPL + DDR fw, OEM · 0x4ac00 ATF + U-Boot + OP-TEE, OEM (3 images). Offsets differ in a non-secure build — read them from your image
SPSDK familymimx9352
FIT algorithmssha256, rsa2048, PSS padding, key dev
Serial console115200 8N1, ttyLP0

Where the behaviour is defined

For anyone who needs to confirm a claim in this document against the BSP:

TopicFile
All signing variables for the machinemeta-summit-som/conf/machine/include/imx93-nitrogen.inc
AHAB signing stepmeta-summit-som/dynamic-layers/imx-bsp/recipes-bsp/u-boot-imx/u-boot-imx-summit-secure-ahab.inc
Secure-include gating…/u-boot-imx-summit-secure.inc, …/u-boot-imx-summit.bb
SECURE_BOOT passthroughmeta-summit-som/tools/setup-environment-nxp
Demo keys and SPSDK configmeta-summit-som/keys/nitrogen/ahab/
Rescue U-Boot config fragment…/u-boot-imx/files/mx93-generic-bsp/uboot-initramfs.cfg
dm-verity generationmeta-summit-som/classes-recipe/dm-verity.bbclass
FIT generation and signingmeta-summit-som/classes-recipe/summit-kernel-fitimage.bbclass
.swu signingmeta-summit-som/classes-recipe/image-summitsom-swu-gen.bbclass
eMMC layout and write planmeta-summit-som/recipes-summit/images/files/imx-generic-bsp/sw-description
SD layoutmeta-summit-som/files/wic/summit-imx-uboot-spl-bootpart.wks.in
OP-TEE configurationmeta-summit-som/recipes-security/optee/optee-os_%.bbappend, optee-client_%.bbappend
Environment whitelistU-Boot include/configs/nitrogen_imx93.h

Applying this guide to the N91 and N95

The Nitrogen91, Nitrogen93 and Nitrogen95 all use AHAB, and EZ BSP configures them almost identically. Everything in this guide applies to all three except the parameters below.

ParameterN91N93N95
SPSDK_FAMILYmimx9131mimx9352mimx9596
ATF_PLATFORMimx91imx93imx95
AHAB container generationas documented hereas documented hereAHAB V2 — the U-Boot recipe applies an extra imx8image signature-block-version patch
Extra image in the boot container——m33_image.bin (M33 System Manager firmware)
Rescue image CONFIG_FASTBOOT_BUF_ADDR0x804000000x804000000x90400000

Identical on all three, and therefore safe to follow as written:

What does not transfer. The container table in the Confirm both OEM containers are signed section — the offsets, header lengths and image counts — was measured on an N93 secure build. The N95 differs in both container generation and contents, so re-run the steps in Verify the containers and Confirm both OEM containers are signed against your own image rather than assuming those numbers. Treat the N91 numbers as unverified too; the configuration matches, but nobody has measured it.

For the N95 there is also existing published material worth reading alongside this guide: the User Guide - EZ BSP for Nitrogen95, “Signed Images” section.

External references

Compiled against EZ BSP 7.2 (nitrogen_7.2.0.5.xml) for the Nitrogen93 SMARC SOM, MACHINE imx93-nitrogen-smarc, U-Boot 2026.07-14.25.0.3, kernel 6.18.20. Where this guide states what the BSP does, it was read from the BSP sources and from built artifacts. No fuse was programmed and no device lifecycle was changed in the preparation of this document.