Introduction
AHAB secure boot on the Nitrogen93 SMARC SOM
| Product | Nitrogen93 SMARC SOM (NXP i.MX93 · SPSDK target mimx9352) |
| BSP | EZ BSP 7.2 — Yocto Walnascar · U-Boot 2026.07-14.25.0.3 · kernel 6.18.20 |
| Manifest | Ezurio/EZ-BSP-Yocto-Release-Packages, branch wrynose-summit-7.2.0.x, nitrogen_7.2.0.5.xml |
| Security | AHAB + EdgeLock secure enclave (ELE) · SPSDK signing · dm-verity · signed SWUpdate |
| Version | 2.0 · August 2026 |
| Owner | Jon 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.
What each link protects
| Link | Artifact | Mechanism | Active by default? |
|---|---|---|---|
| ROM → ELE | mx93a1-ahab-container.img at flash.bin+0x0 | NXP signature | Always |
| ROM/ELE → SPL | SPL + LPDDR4 training firmware | AHAB container, your SRK — ROM authenticates the SPL | No — needs Appendix B — How EZ BSP signing is wired |
| SPL → runtime | bl31.bin (ATF), tee.bin (OP-TEE), u-boot.bin | AHAB container, your SRK — the SPL authenticates U-Boot | No — needs Appendix B — How EZ BSP signing is wired |
| U-Boot → kernel | kernel.itb — kernel + DTB/overlays + boot script | FIT signature, sha256,rsa2048, PSS, required = "conf" | Yes |
| kernel → rootfs | *.squashfs-zst.verity | dm-verity; root hash carried inside the signed FIT | Yes |
| field update | *.swu | SWUpdate CMS signature | Only on a -secure DISTRO |
| runtime secrets | OP-TEE secure storage | HUK-derived encryption | Yes |
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 zstdsudo 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 zstdFor 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-compilernxpimage 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 --pruneBoot switches
| Target | Carrier Switches | SOM SW1 Switch |
|---|---|---|
| SD card | OFF | ON |
| onboard eMMC | OFF | OFF |
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 keyThree separate trust roots live in that one tree, and it is worth being explicit about which is which:
| Key | Signs | Verified by |
|---|---|---|
| SRK<n> (ECC) | the AHAB boot container | the ELE, against the SRK hash in fuses |
| dev (RSA-2048) | the kernel FIT | U-Boot, against the key in its control DTB |
| update_signing (RSA-2048) | the .swu package | SWUpdate 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.crtopenssl genrsa -out update_signing.key 2048
openssl req -batch -new -x509 -days 7300 -key update_signing.key -out update_signing.crtUse 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.pemNote 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-westonImage and DISTRO choices:
| DISTRO | Image | Notes |
|---|---|---|
| summitsom-cmd-secure | image-summitsom-cmd | headless |
| summitsom-wayland-secure | image-summitsom-weston | graphical (Weston) |
| summitsom-xwayland-secure | image-summitsom-weston | graphical + 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, abortingCause: 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.
| Artifact | What it is |
|---|---|
| signed-flash.bin | the AHAB-signed boot container — SPL, DDR firmware, ATF, OP-TEE, U-Boot |
| flash.bin | symlink to signed-flash.bin |
| unsigned-flash.bin | the pre-signing image — a symlink to flash-<machine>-<ver>.img. This is your recovery image; copy it with cp -L |
| flash.bin.tagged | signed image plus a size tag, for tooling that needs it |
| kernel.itb | the signed kernel FIT — kernel, DTB, 17 overlays, boot script |
| *.squashfs-zst.verity | the read-only rootfs with its dm-verity hash tree appended |
| *.squashfs-zst.verity.env | root hash, salt, block counts — build-side bookkeeping |
| *.swu | the CMS-signed SWUpdate package |
| *.wic.bz2, *.wic.bmap | full-disk SD image |
| mksdcard.sh | SD 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
| Item | Value |
|---|---|
| AHAB container | SPSDK nxpimage ahab sign, family mimx9352, srk_set: oem |
| SRK keys | ECC secp256r1, SHA-256, four slots, flag_ca: false |
| Kernel FIT | sha256,rsa2048, PSS padding, key dev, configuration-level signatures |
| FIT enforcement | required = "conf" in U-Boot’s control DTB |
| rootfs | squashfs-zst + dm-verity, SHA-256 |
| .swu | CMS (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.taggedA 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.imgIf 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 mimx9352The 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 signedPass 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 verifiedThat 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:
| Container | Offset | Unsigned | Signed | Images |
|---|---|---|---|---|
| ELE firmware (NXP) | 0x000000 | nxp, len 544 | nxp, len 544 — identical | 1 |
| SPL + DDR firmware | 0x000400 | none, len 160 | oem, len 544 | 1 |
| ATF + U-Boot + OP-TEE | 0x04ac00 | none, len 416 | oem, len 800 | 3 |
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-hintOn a stock build this returns:
conf
sha256,rsa2048
devtogether 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-devConfirm the FIT is signed
mkimage -l kernel.itb | head -40Every 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.swuA 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 6220664A 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/nullCMS 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
| Check | Pass condition |
|---|---|
| signed-flash.bin exists, flash.bin points at it | Confirm 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 image | Verify the containers |
| Both OEM containers = oem, SRK table and signature present | Confirm both OEM containers are signed |
| Used SRK key ID matches used_srk_id in your YAML | Verify the containers |
| /signature/key-dev present with required = "conf" | Confirm the FIT verification key is in U-Boot |
| FIT configurations signed, sign-images includes script | Confirm the FIT is signed |
| .swu contains sw-description.sig | Confirm 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:
| Medium | Container location | Set by |
|---|---|---|
| SD card / eMMC user area | 32 KiB (0x8000) | IMX_BOOT_SEEK = "32" |
| eMMC hardware boot partition (mmcblk0boot0/boot1) | offset 0 | EMMC_BOOT_OFFSET = "0" |
| U-Boot environment | 0x3F0000 within the same boot partition | UBOOT_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/sdXOptions: -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/sdXOn 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.swuPull — 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.swuFor a first-time eMMC provision, fw_update -m complete lays down the full GPT A/B layout:
| Partition | Label | Size | Contents |
|---|---|---|---|
| p1 | kernel_a | 64 M | signed kernel FIT (A) |
| p2 | rootfs_a | 512 M | verity rootfs (A) |
| p3 | rootfs_data_a | 256 M | writable data (A) |
| p4 | kernel_b | 64 M | signed kernel FIT (B) |
| p5 | rootfs_b | 512 M | verity rootfs (B) |
| p6 | rootfs_data_b | 256 M | writable data (B) |
| p7 | perm | 5 G | persistent 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 swupdateThen 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-mainPut the board in USB serial-download mode and run:
sudo uuu -v imx-rescue.uuuThe 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: doneOnce 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, RegDomainenv/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 fightBuild 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 programmedThat is a virgin part: open, nothing fused, as shipped. Interpretation:
| Output | Meaning |
|---|---|
| lifecycle 0x8, no SRK fuse | As-shipped. Open. Signed and unsigned both boot. |
| lifecycle 0x8, SRK fused, no events | Your signed image authenticates. This is the green light for Closing the device. |
| lifecycle 0x8, SRK fused, events listed | Signature is failing. Still boots because the part is open — diagnose and iterate here. |
| lifecycle 0x80 | OEM 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:
- 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.
- 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 furtherNote 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:
- 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.
- 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.
- 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:
- the SRK hash is programmed and reads back correctly
- your signed image is flashed
- it boots
- ahab_status reports no events (Getting a console, and reading ahab_status)
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 closedresponse 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
| Lifecycle | Meaning | Your position |
|---|---|---|
| 0x8 | NXP closed | Normal as-shipped state. NXP finished provisioning; the part is fully OEM-open. Safe. Signed and unsigned images both boot. |
| 0x80 | OEM closed | The enforcing state. The ROM requires a valid signature. One-way door. |
| — | OEM locked | Enforcing, 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
| Region | Protection |
|---|---|
| mmcblk0boot0 / boot1 | AHAB signature over the container (Appendix B — How EZ BSP signing is wired) |
| kernel_a / kernel_b | FIT signature, verified by U-Boot |
| rootfs_a / rootfs_b | dm-verity, read-only; root hash authenticated via the FIT |
| rootfs_data_a / _b | writable overlay; not integrity-protected |
| perm | persistent data; hosts OP-TEE secure storage |
| RPMB | hardware 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:
| Component | Status |
|---|---|
| libckteec.so.0, libckteec.so.0.1.0 | Present at /usr/lib/ (shipped inside the optee-client package, which is why it does not appear as its own manifest entry) |
| pkcs11-provider | Present at /usr/lib/ossl-modules/pkcs11.so |
| p11-kit | Present (/usr/lib/pkcs11/p11-kit-*.so) |
| OP-TEE PKCS#11 TA | Built 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-providerCorrection 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:
| Symptom | Cause |
|---|---|
| ckteec_invoke_init: TEEC init context failed / C_Initialize … CKR_DEVICE_ERROR | OP-TEE not running, or the TA is not in /lib/optee_armtz |
| pkcs11-tool: command not found | opensc not installed |
| dmesg \| grep optee empty, no /dev/tee0 | the 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.swuA 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:
| Stage | Recovery |
|---|---|
| Before fusing | Anything. Reflash SD or eMMC freely; boot either medium via the switches. |
| SRK fused, device open | Unsigned images still boot. Reflash unsigned-flash.bin if needed. |
| OEM closed | Only 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:
- ☐ SIG_DATA_PATH points at your keys, not meta-summit-som/keys/nitrogen/ahab
- ☐ Kernel signing key and update signing key are different keys
- ☐ No private keys in Git, and none on a shared build server
- ☐ SECURE_BOOT=1 and a -secure DISTRO in your release build (Why both are required)
- ☐ All six host-side checks in Verification checklist pass
- ☐ A signed image has booted with a clean ahab_status on an open, SRK-fused device (The safe sequence)
- ☐ unsigned-flash.bin archived for open-device recovery; a signed recovery image exists for closed devices
- ☐ Keys, certificates and SRK_*_fuse.bin archived with the release
- ☐ Rotation plan across SRK1→SRK4 documented (Appendix D — Design note: SGK versus SRK signing)
- ☐ No rescue image on production units (The rescue image — a prompt with the ahab_* commands)
- ☐ Threat model reviewed against the RPMB gap (RPMB is off by design — you are expected to enable it) and the PKCS#11 change (PKCS#11 on the target)
- ☐ perm and rootfs_data partition sizes sized for the product, not left at defaults
Appendix A — Coming from the old CST flow
| Old procedure | On 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 prompt | Still 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 container | Set two variables. nxpimage ahab sign runs automatically in do_deploy and signs every OEM container. |
| binman ls fails, use nxpimage to inspect | Reversed — binman builds the image here. Use nxpimage ahab parse to inspect (still true). |
| Keep a copy of flash.bin as your recovery image | The build keeps it for you, as unsigned-flash.bin. |
| Composite .wic, split-WIC over UUU, raw dd of the whole disk | mksdcard.sh for SD; signed .swu through SWUpdate for eMMC. UUU only RAM-boots a rescue image. |
| Container header at 0x400 / MBR at LBA 0 | flash.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 prompt | The installed U-Boot has no reachable prompt. Fuses are programmed with SPSDK or by Ezurio. |
| pkcs11-tool --module /usr/lib/libckteec.so.0 | libckteec is not built. On-target PKCS#11 is the OpenSSL pkcs11-provider. |
| RPMB secure storage, Found working RPMB device as a factory gate | RPMB 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.
| Control | Set by | What it turns on | Default |
|---|---|---|---|
| UBOOT_SIGN_ENABLE = "1" | the machine config, unconditionally | Kernel FIT signing; insertion of the public key into U-Boot’s control DTB | Already on |
| DISTRO = summitsom-*-secure | you, when choosing a DISTRO | Read-only rootfs, CMS-signed .swu, /data split — and it makes the AHAB include visible | Off |
| SECURE_BOOT = "1" | you, through the environment | AHAB signing of the boot container | Off |
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:
- 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.
- 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.
- Prepends ahab_status; to the built-in bootcmd, so every boot logs its AHAB state.
Then do_deploy runs the signing step, which:
- 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/;
- runs the signing command against the binman-produced image:
- nxpimage ahab sign -c spsdk_ahab_sign.yaml -b flash.bin -o signed-flash.bin --force
- 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:
- generating a real PKI tree and keeping the private keys off the build host (see Key material)
- programming the SRK hash into the device’s fuses (see Provisioning the root of trust (POINT OF NO RETURN))
- transitioning the device to OEM-closed (see Provisioning the root of trust (POINT OF NO RETURN))
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) | |
|---|---|---|
| Scheme | AHAB | HABv4 |
| Crypto arbiter | EdgeLock secure enclave (ELE) | CAAM |
| Image format | Multiple independently signed containers | Single image with an appended CSF |
| Signing tool | NXP SPSDK (nxpimage ahab sign) | NXP CST |
| When signing happens | After image generation, as a post-processing step | During image generation, inside binman |
| SPSDK target | mimx9352 (N93); mimx9131 (N91), mimx9596 (N95) | n/a |
| Fuse programming | ELE-mediated | OCOTP |
| U-Boot status command | ahab_status | hab_status |
| EZ BSP switch | SECURE_BOOT=1 + an ahab machine | SECURE_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
| Purpose | Command |
|---|---|
| Fetch the BSP | repo init -u …/EZ-BSP-Yocto-Release-Packages -b wrynose-summit-7.2.0.x -m nitrogen_7.2.0.5.xml |
| Configure a secure build | export SECURE_BOOT=1 SIG_DATA_PATH=… then MACHINE=imx93-nitrogen-smarc DISTRO=summitsom-wayland-secure . setup-environment-nxp build |
| Build | bitbake image-summitsom-weston |
| Verify a container | nxpimage ahab verify -b signed-flash.bin -f mimx9352 |
| Inspect a container | nxpimage ahab parse -b signed-flash.bin -f mimx9352 -o parse_out |
| Sign a container by hand | nxpimage ahab sign -c <yaml> -b flash.bin -o signed-flash.bin |
| Check the FIT key in U-Boot | fdtget u-boot-imx93-nitrogen-smarc.dtb /signature/key-dev required algo |
| List FIT contents | mkimage -l kernel.itb |
| Check .swu signature | cpio -itv < image-*.swu → look for sw-description.sig |
| Write an SD card | sudo ./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 provision | fw_update -m complete <url> |
| USB recovery | sudo uuu -v imx-rescue.uuu |
| Check secure-boot state | ahab_status (rescue U-Boot, or the boot log of a signed build) |
| Close the device | ahab_close — irreversible, see Closing the device |
Artifact index
| File | Role |
|---|---|
| signed-flash.bin | AHAB-signed boot container (SPL, DDR fw, ATF, OP-TEE, U-Boot) |
| unsigned-flash.bin | pre-signing image — open-device recovery |
| flash.bin | symlink to whichever of the above this build produced |
| kernel.itb | signed kernel FIT: kernel, DTB, overlays, verity boot script |
| *.squashfs-zst.verity | read-only rootfs with appended dm-verity hash tree |
| *.verity.env | root hash / salt / block counts (build-side only) |
| *.swu | CMS-signed SWUpdate package |
| uboot.env | U-Boot environment blob (whitelist-protected on import) |
| SRK_1_2_3_4_table.bin | SRK table embedded in the container |
| SRK_1_2_3_4_fuse.bin | SRK hash — the value programmed into fuses |
| mx93a1-ahab-container.img | NXP-signed ELE firmware container |
Offsets and key variables
| Item | Value |
|---|---|
| Container on SD / eMMC user area | 32 KiB (IMX_BOOT_SEEK = "32") |
| Container in eMMC boot partition | offset 0 (EMMC_BOOT_OFFSET = "0") |
| U-Boot environment | 0x3F0000, 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 family | mimx9352 |
| FIT algorithms | sha256, rsa2048, PSS padding, key dev |
| Serial console | 115200 8N1, ttyLP0 |
Where the behaviour is defined
For anyone who needs to confirm a claim in this document against the BSP:
| Topic | File |
|---|---|
| All signing variables for the machine | meta-summit-som/conf/machine/include/imx93-nitrogen.inc |
| AHAB signing step | meta-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 passthrough | meta-summit-som/tools/setup-environment-nxp |
| Demo keys and SPSDK config | meta-summit-som/keys/nitrogen/ahab/ |
| Rescue U-Boot config fragment | …/u-boot-imx/files/mx93-generic-bsp/uboot-initramfs.cfg |
| dm-verity generation | meta-summit-som/classes-recipe/dm-verity.bbclass |
| FIT generation and signing | meta-summit-som/classes-recipe/summit-kernel-fitimage.bbclass |
| .swu signing | meta-summit-som/classes-recipe/image-summitsom-swu-gen.bbclass |
| eMMC layout and write plan | meta-summit-som/recipes-summit/images/files/imx-generic-bsp/sw-description |
| SD layout | meta-summit-som/files/wic/summit-imx-uboot-spl-bootpart.wks.in |
| OP-TEE configuration | meta-summit-som/recipes-security/optee/optee-os_%.bbappend, optee-client_%.bbappend |
| Environment whitelist | U-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.
| Parameter | N91 | N93 | N95 |
|---|---|---|---|
| SPSDK_FAMILY | mimx9131 | mimx9352 | mimx9596 |
| ATF_PLATFORM | imx91 | imx93 | imx95 |
| AHAB container generation | as documented here | as documented here | AHAB 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_ADDR | 0x80400000 | 0x80400000 | 0x90400000 |
Identical on all three, and therefore safe to follow as written:
- the two-switch gate — a -secure DISTRO and SECURE_BOOT=1 (Building a signed image)
- UBOOT_SIGN_ENABLE = "1", and the same SIG_DATA_PATH default key tree (Key material)
- FIT signing as rsa2048 + sha256 + PSS under key dev, with the key inserted into U-Boot’s control DTB (Confirm the FIT verification key is in U-Boot, Confirm the FIT is signed)
- IMAGE_CLASSES += "dm-verity" and the root-hash-inside-the-signed-FIT mechanism (How the dm-verity root hash is authenticated)
- UBOOT_PROVIDES_BOOT_CONTAINER = "1" — binman builds flash.bin, nxpimage ahab sign signs it
- every offset in Offsets and key variables: IMX_BOOT_SEEK = "32", EMMC_BOOT_OFFSET = "0", UBOOT_ENV_OFFSET = "4128768", EMMC_DEVICE = "0", SD_DEVICE = "1"
- the sysroot collision in the Known issue: the secure build fails on a sysroot collision section— it comes from a shared python3-spsdk-native dependency, so it affects all three
- the rescue-image route to a console in The rescue image — a prompt with the ahab_* commands, flag for flag apart from the buffer address above
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
- NXP AN13575 — Secure Boot on i.MX9 Family Using AHAB (authoritative for the fuse map and lifecycle): https://www.nxp.com/docs/en/application-note/AN13575.pdf
- NXP SPSDK documentation: https://spsdk.readthedocs.io/
- NXP CST download (account and licence required): https://www.nxp.com/webapp/Download?colCode=IMX_CST_TOOL_NEW
- NXP AN13975 — RPMB / secure storage: https://www.nxp.com/docs/en/application-note/AN13975.pdf
- NXP AN4581 — i.MX Secure Boot on HABv4 (for the N8MP, not the N93): https://www.nxp.com/docs/en/application-note/AN4581.pdf
- SWUpdate documentation: https://sbabic.github.io/swupdate
- OP-TEE documentation: https://optee.readthedocs.io/
- Ezurio — User Guide - EZ BSP for Nitrogen95, “Signed Images”: https://www.ezurio.com/documentation/user-guide-ez-bsp-for-nitrogen95#signed-images
- Ezurio — User Guide - Nitrogen93 SMARC: https://www.ezurio.com/documentation/user-guide-nitrogen93-smarc-som
- Ezurio — Recovering i.MX Platforms using UUU: https://www.ezurio.com/documentation/recovering-i-mx-platforms-using-uuu
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.