prpl
FEAT-29 · prplWare 5.1.0

Secure Boot & Secure Upgrade

How the 2024 prpl reference platforms verify every stage — silicon → XBL → U-Boot → Linux — and update themselves safely.

Qualcomm ipq95xx · “Freedom” (WNC) MaxLinear x86-LGM · “OSPv2” (Gemtek)

SMD · standard flash layout · FIT signing · HKDF key derivation · A/B banks · SWUpdate. Sources: prplos!1964 · Confluence architecture page · real serial-console & gensmd artifacts.

The one idea

A chain of trust, anchored in silicon

If an attacker can rewrite flash, they can run their code below your OS — persistently, as root, invisible to it. Secure boot answers this: each stage cryptographically verifies the next before handing over control.

Immutable silicon ROM + fuses XBL U-Boot Linux verifiesverifiesverifies Trust flows one way — from what cannot be changed root of trust

Signing ≠ encryption

Signature = authenticity + integrity (“this came from us, unmodified”) — public-key, anyone can verify. Encryption = confidentiality (“only we can read it”) — symmetric key. FEAT-29 uses both: images are signed; secrets in the SMD are also encrypted.

Why anchor in silicon

A signature is only as trustworthy as the key that checks it. Put that key (or its hash) in one-time-programmable fuses the attacker cannot rewrite — and the whole chain inherits that immutability.

Toolbox

Five primitives — everything else is built from these

① Hash

SHA-256. A fixed fingerprint of any data; changing one bit changes it completely. Used to detect tampering.

② Public-key signature

RSA-4096 / PSS for SMD & FIT pre-load; ECDSA P-256 for the device cert. Sign with private, verify with public.

③ Symmetric cipher

AES-256-CBC. One secret key both encrypts and decrypts. Protects the secret fields inside the SMD.

④ Key derivation

HKDF-SHA256. Stretches one root secret into many purpose-specific keys + IVs, deterministically.

⑤ Immutable / protected storage

Fuses / OTP hold the silicon root key (write-once). The Linux KRS (kernel keyring) holds derived keys at runtime.

Read the tags

vendor = inherited from the SoC / Irdeto.
feat-29 = new prpl work in MR 1964.
scope = deferred to a later epic.

Keep these five in mind — the rest of the deck is just which primitive protects what, and who holds the key.

The whole chain · one picture

Silicon → early stage → U-Boot → Linux

Every stage loads the next from flash, verifies it, then hands over. Two SoCs, the same shape — only the vendor early stage differs.

Silicon ROM Early stage U-Boot Kernel FIT initramfs rootfs on-die mask ROM "BL2" — bank select+ HW watchdog BL33 — SMD + GPT+ load kernel FIT kernel · dtb· initramfs auth rootfs · keys→ switch_root squashfs verifiesverifiesverifiesverifiesauthenticates Freedom: PBL → XBL/SBL1 OSPv2: ROM → RBE (SPL) U-Boot 2025.04-prpl U-Boot 2022.01-MXL What is actually enforced today: the U-Boot pre-load RSA-4096/PSS signature on SMD + kernel/rootfs FIT, with per-node SHA-256 root of trust ↓
Qualcomm "Freedom" MaxLinear "OSPv2" green arrow = "verifies before running"
where the trust is really anchored
Deep dive · the honest root of trust

The silicon anchor exists — but isn't blown on these boards yet

Both SoCs can anchor trust in one-time-programmable fuses (a hash of the OEM signing key in QFPROM / SoC OTP). On the 2024 FastTrack / migration boards that fuse is deliberately not blown — so the ROM/XBL hash-checks run but aren't yet anchored to an immutable OEM key. vendor

What FEAT-29 does enforce, unconditionally, is the prpl pre-load signature: U-Boot will not use the SMD or boot a kernel/rootfs FIT whose RSA-4096/PSS signature doesn't verify against the public key baked into U-Boot's control DTB. feat-29

Blowing the OEM fuse (and signing U-Boot itself) is the next hardening step — out of scope of FEAT-29's migration phase. roadmap

Freedom — Qualcomm PBL/XBL hash-verify (boot ROM) B - 34168 - elf_segs_hash_verify_entry, Start B - 75204 - auth_xbl_sec_hash_seg_entry, Start B - 83649 - xbl_sec_segs_hash_verify_entry, Start D - 6192 - Auth Metadata D - 12383 - Segments hash check D - 51332 - QSEE Image Loaded ... S - Secure Boot: Off ← OEM fuse not blown (dev board)

freedom-uboot-only-prpl-flash…log · freedom-usb-edl…dump.txt

BL33 · where prpl takes over

U-Boot: authenticate the SMD, then the kernel FIT

U-Boot is the first stage fully owned by prpl. In order, it: ① authenticates the SMD and pulls the MAC from it; ② verifies/recovers the GPT; ③ loads the kernel FIT from the selected bank and checks its pre-load signature + every node's SHA-256.

  1. SMD pre-load sig verified → set ethaddr from the SMD's MAC field feat-29
  2. GPT verified (and a corrupt copy auto-recovered from the backup GPT)
  3. Bank chosen by the early stage is read from the DT (booted-bank)
  4. Kernel FIT loaded → pre-load RSA-4096/PSS signature checked
  5. Each sub-image (kernel, fdt, initrd) SHA-256 verified
  6. root=PARTLABEL=rootfs-active handed to the kernel cmdline
## U-Boot (BL33) — real boot, Freedom INFO: signature check has succeed ← SMD authenticated Set ethaddr to ac:91:9b:ff:f1:ac Verify GPT: success! Loading raw FIT from partition kernel-active Booting FIT image at 0x50000000 INFO: signature check has succeed ← FIT pre-load sig ## Loading kernel (any) from FIT Image ... Hash algo: sha256 Verifying Hash Integrity ... sha256+ OK Trying 'fdt-1' fdt subimage … sha256+ OK Trying 'initrd-1' ramdisk … sha256+ OK

freedom-post-flash-linux-boot…log

The handoff into Linux

A pivoting initramfs authenticates the rootfs, then switches into it

The kernel FIT carries a tiny purpose-built initramfs. It runs eight ordered scripts (S00–S08): pick the bank, authenticate the rootfs FIT, map it, mount it, derive keys, and switch_root.

StepScriptDoes
S03rootfs-selectionbank ← root=PARTLABEL=
S04rootfs-sig-preloadauthenticate rootfs FIT (mandatory)
S05rootfs-fitdm linear map to squashfs
S06rootfs-mountmount the rootfs
S08key_derivationHKDF K_UK+IV → kernel keyring
**** initramfs — real boot **** Selected rootfs partition: rootfs-active **** Starting S04rootfs-sig-preload.sh **** (auth OK) **** Starting S05rootfs-fit.sh **** **** Starting S06rootfs-mount.sh **** **** Starting S08key_derivation.sh **** Switching to rootfs

freedom-post-flash-linux-boot…log

dm-verity is intentionally descoped. Integrity is the whole-image RSA-4096 signature checked once at S04 — the dm device is linear, not verity (no per-block runtime checking yet). roadmap

why "authenticate once" ≠ dm-verity
Deep dive · integrity model

Whole-image authentication vs per-block verification

FEAT-29 today — whole-image

The entire rootfs FIT is covered by one RSA-4096/PSS/SHA-256 pre-load signature, verified once in initramfs S04 before the squashfs is mounted (mandatory: …ROOTFS_SIG_PRELOAD_MANDATORY=y — boot aborts if it fails or is absent). Tamper anywhere in the image ⇒ signature fails ⇒ no boot.

Block device = device-mapper linear onto the squashfs payload inside the FIT.

dm-verity — per-block (future)

dm-verity hashes every block in a Merkle tree and re-checks on each read at runtime — catching corruption or tampering that happens after mount. Stronger, but heavier; explicitly out of scope of FEAT-29 and listed as future hardening. roadmap

No in-scope source commits to a date — shown here as the natural next step, not a promise.

The board's signed identity

SMD — Secure Manufacturing Data

A per-board blob written once at the factory into the mfgdata partition. It is a device tree (DTB): the whole thing is signed; the secret fields are also encrypted.

one RSA-4096/PSS signature covers everything ↓ CLEAR (readable by U-Boot) SERIAL_NUMBER BASE_MAC_ADDRESS MANUFACTURER · MODEL_NAME HARDWARE_VERSION WLAN_SSID · WLAN_REGDOMAIN KDF_CTX (per-board KDF context) ENCRYPTED (AES-256-CBC) USERFS_KEY · WLAN_PASSPHRASE · DEVICE_CERT → K_UK DEVICE_CERT_PRIVATE → K_UT

Signed → authentic

The DTB is wrapped in a U-Boot pre-load header (magic UBSH) carrying an RSA-4096/PSS/SHA-256 signature over the whole tree. Nothing is trusted until that verifies.

Encrypted → confidential

Keys, the WiFi passphrase, and the device certificate's private key are individually AES-256-CBC encrypted on top of the signature, so they're never in the clear on flash.

Format: prpl Secure Manufacturing Data Standard v1.2. Read it back with readmfg.

the real fields & the UBSH header
Deep dive · a real decoded SMD

OSPv2 A8C2463D425C — fields & signature header

# dd bs=4096 skip=1 <smd> | dtc -I dtb -O dts - MFG_DATA { MFG_DATA_version = "1.2"; SERIAL_NUMBER = "ospv2_A8C2463D425C"; BASE_MAC_ADDRESS = [a8 c2 46 3d 42 5c]; MANUFACTURER = "Gemtek"; MODEL_NAME = "Gemtek-OSPv2"; WLAN_SSID = "prpl-3D425C"; USERFS_KEY { cipher { key_name="mfg:Kuk"; format="aes-256-cbc"; }; }; DEVICE_CERT_PRIVATE { cipher { key_name="mfg:Kut"; format="aes-256-cbc"; }; }; KDF_CTX = "KTYYFuMhxRY6cUwvlRm0pyWTzEzg3WKD"; };

smd/ospv2-A8C2463D425C/ospv2_A8C2463D425C.dtb

# the 4096-byte UBSH pre-load header 55 42 53 48 "UBSH" magic 00 00 00 01 version 1 00 00 10 00 header_size = 4096 00 00 0d 44 image_size = DTB length 00 00 02 40 ofs_img_sig = 576 ... [ 32 B ] sha256(image signature) [512 B ] RSA-4096/PSS sig over the 64-B header [512 B ] RSA-4096/PSS sig over the DTB [ pad ] → 4096, then the DTB itself

U-Boot verifies via image_pre_load() against the /mfg/sig public key in its control DTB: algo sha256,rsa4096, padding pss, mandatory="yes".

Provisioning · gensmd + Irdeto

How an SMD is born: a CSR to Irdeto, then sign & seal

INPUTS MAC, modelkey_type = ECC_P256metadata.yaml:root_key (secret)mfg:Kuk / mfg:Kutsmd_priv.pem (RSA-4096)input_data.csv (1 row/board) Irdeto PKI Certificate-Authority-as-a-service ① OAuth2 token② POST CSR → signs leafreturns leaf + chain vendor · holds the CA keys prpl_certificate.py local keygen → CSR → write key+chain gen_smd.py HKDF → K_UK, K_UT, IVbuild MFG_DATA treeAES-256-CBC encrypt secretsRSA-4096/PSS pre-load sign feat-29 tooling <serial>.dtb UBSH header + DTB → flashed to mfgdata

The device private key is generated locally and never leaves the host — only a CSR goes to Irdeto, which signs it under the prpl Device CA.

the prpl certificate chain
Deep dive · the prpl device certificate

A three-tier ECDSA trust chain, minted by Irdeto

prpl Foundation Root CA TEST G1 P-384 · ECDSA-SHA384 · (not in SMD) prpl Foundation Device CA TEST G1 P-384 · intermediate · in chain Device leaf · CN = <MAC> EC P-256 · ECDSA-SHA256 OU = WNC-Freedom · EKU = TLS client/server
# the two Irdeto calls (OAuth2 client_credentials) POST api.pki.qa.key-central.irdeto.com /pki/v1/prpl/auth/token → access_token POST /pki/v1/prpl/certificate { profile: "prpl_platform_ECC_P256", subject_vars: { base-mac, model }, csr: "<CSR PEM>" } → { certificate (leaf), chain, certificate_id }

note The standard lists the device key as RSA-4096, but the implemented FastTrack reality is EC P-256, and the endpoint is Irdeto's QA PKI — both worth calling out as "test/dev today".

Runtime · three actors

Authenticate first — then, and only then, decrypt

No field is read until the global signature verifies. Responsibility is split across the boot stages by what each one is allowed to see.

① U-Boot

Authenticates the SMD (pre-load sig). Reads only clear fields — chiefly BASE_MAC_ADDRESSethaddr. Never decrypts.

② initramfs

Reads clear KDF_CTX, derives K_UK + IV (HKDF), and provisions K_UK into the Linux KRS (kernel keyring) as logon: mfg:Kuk.

③ Linux userspace

libmfg/readmfg authenticate + parse, then decrypt the AES-256-CBC fields via libkcapi using the keyring-held K_UK.

# negative test — a tampered SMD is rejected outright ERROR: header signature check has failed (err=-22) ERROR: Failed to verify global header signature of mfg data parse_dtb: Failed to read the MFG DTB

Corrupt one byte of the SMD and U-Boot refuses to read any of it — MAC included. The signature is the gate; encryption is a second wall behind it. ospv2-manual-uboot…log

One secret, many keys

Key derivation: a global secret → per-board keys

FEAT-29 never stores the AES keys. It derives them — at generation time and again at boot — from one root secret plus the board's clear-text KDF_CTX, using HKDF-SHA256.

root_key global secret · 32 B KDF_CTX per-board · from SMD (clear) HKDF HMAC-SHA256 salt=∅ · info=KDF_CTX‖id K_UK info = KDF_CTX + "mfg:Kuk" · 32 B K_UT info = KDF_CTX + "mfg:Kut" · 32 B IV info = KDF_CTX + "IV" · 16 B Linux KRS (kernel keyring) logon: mfg:Kuk ← K_UKKuk-aes-iv.hex ← IV → libkcapi AES-256-CBC decrypt DEVICE_CERT_PRIVATEWLAN_PASSPHRASEUSERFS_KEY → RW partitions

Same function at the factory and at boot ⇒ reproducible keys, no key ever written to flash. openssl kdf -kdfopt digest:SHA2-256 -kdfopt hexkey:<root> -kdfopt info:"<KDF_CTX>mfg:Kuk" HKDF

exactly what's secret — and what isn't yet
Deep dive · the honest crypto detail

Where the security really rests — and the dev placeholder

HKDF, precisely

RFC-5869 HKDF, HMAC-SHA256. salt = none; the per-board value goes into info (KDF_CTX ∥ key-id), not the salt — the prose word "salt" is misleading. Output length sets the key (32 B) or IV (16 B). Both ciphers share root_key + KDF_CTX, so they derive the same IV.

K_UK = "Key-Unique-Kernel" (non-secure world). K_UT = "Key-Unique-Tee" (secure world) — no TEE in FEAT-29, so it lands in the keyring too.

The root secret is a placeholder today

On dev/FastTrack material root_key is the well-known 0011…EEFF and lives inside the initramfs. That's fine for migration but means the whole derivation is only as secret as that constant.

Hardening (out of scope): move the root secret into SoC OTP / an encrypted image, so it's bound to the silicon — the same gap as the un-blown secure-boot fuse. roadmap

Closing the loop

Who signs what — and where the verify key lives

Three things are signed, all with the same primitive (RSA-4096 / PSS / SHA-256 U-Boot pre-load). Images are built and signed by binman; the matching public keys are baked into the verifier so the chain has nowhere to slip.

ArtifactSigned with (private)Verified byPublic key lives in
SMD (mfgdata DTB)smd_priv.pemU-Boot (BL33)U-Boot control DTB /mfg/sig
Kernel FITKsoft-sign-priv.pemU-Boot (BL33)U-Boot control DTB
rootfs FITKsoft-sign-priv.peminitramfs (mandatory)/security/smd_pub.pem in initramfs
.swu upgradeSWUpdate signing keySWUpdate (Linux)/security/public.pem

FIT = one signed container

pre-load{ algo="sha256,rsa4096"; padding="pss"; header-size=4096 } wraps the whole FIT; inside, each node (kernel, fdt, ramdisk, rootfs) carries its own hash{ sha256 }.

Verified by image_pre_load()

Upstream U-Boot mechanism (≥ v2022.07). The public key in the control DTB is immutable to anything U-Boot loads afterward — so the verifier can't be swapped by the thing it verifies.

The honest caveat

This RSA-4096 layer is what's enforced. The link below it — U-Boot itself, and the OEM-key fuse — isn't anchored on these migration boards yet. The root of trust is cryptographic today, silicon-bound tomorrow.

Why signed images exist · resilience

Two banks, selected by name — no "active" pointer to corrupt

Every updatable image exists twice: *-active and *-inactive. The early stage boots by partition name, so there's no stored bank index that can rot. Switching banks = atomically renaming the GPT labels.

ACTIVE bank INACTIVE bank u-boot-active kernel-active rootfs-active u-boot-inactive kernel-inactive rootfs-inactive gptswap → rename labels atomically 2 GPTs (primary + backup) make the rename safe

State is split across three stores — deliberately keeping bootcount out of flash, so a freshly-flashed bad U-Boot can't disable its own failover:

StateLives inInterface
bootcountSoC register / RAM/sys/class/registers/bootcount
force-inactiveSoC register (one-shot)/sys/class/registers/force-inactive
failoverU-Boot env (flash)fw_printenv failover
booted-bankdevice tree (runtime)/proc/.../u-boot,booted-bank
# bootcount is the register, NOT a U-Boot env var $ fw_printenv bootcount → "bootcount" not defined $ cat /sys/class/registers/bootcount → 0

freedom-bootcount-late-probe.log

Self-healing boot

Bootcount failover: try active, fall back to inactive

The early stage arms a hardware watchdog, increments bootcount each attempt, and — while failover is armed — falls back to the inactive bank once the active bank fails to boot bootlimit times.

Power-on (BL2)read count · failover Boot ACTIVEdefault Boot INACTIVEfailover INACTIVE (forced)one-shot Linux upreset bootcount=0 End of Worldcount > 2×limit · LED Commitfailover = 0 defaultcount≥limitforce-inactive good boot → count=0 1 good boot

Temporary failover (the prpl default) is armed during upgrade and disabled on commit. Permanent failover = a compile flag, or an OTP fuse when the early stage is a vendor binary.

the limit of BL2 failover — a real brick
Deep dive · what failover can't catch

A real post-upgrade brick — and why BL2 didn't save it

After a real SWUpdate + gptswap, the freshly-written kernel-active ext4 partition hit a controller DMA error and couldn't be read — inside the initramfs, past BL2's bank selection. So BL2's bootcount failover, which only governs the early stage, never engaged; the box dropped to a rescue shell instead.

Recovery was a U-Boot re-flash of U-Boot+SPL over TFTP. The lesson on the slide: BL2 failover covers load failures it can see — a later-stage mount failure needs a different safety net (the deferred Active-Bank-Recovery / commit machinery). scope

# the recovery, from U-Boot run update_uboot Updating uboot at partition uboot_a / uboot_b Saving Environment to MMC... OK
# post-gptswap boot — kernel-active = mmcblk0p12 (ext4) [ 8.230495] mmc0: cqhci: unable to map sg lists, -12 [ 8.236770] mmc0: cqhci: failed to setup tx desc: -12 [ 8.253754] EXT4-fs error (mmcblk0p12): unable to read itable block mount: mounting /dev/disk/by-partlabel/kernel-active on /mnt/kernel_part failed: Invalid argument Failed to mount kernel-active — Startup failed Dropping to rescue shell for debug

2026-05-30-…-swupdate-brick-serial-console.log

Secure upgrade

SWUpdate: verify, write the inactive bank, swap, arm failover

The whole point of A/B + signed images: update the bank you're not running, prove it's authentic, then flip to it — with a fallback if the new image won't boot.

  1. .swu built by SWUGenerator; its sw-description is signed
  2. SWUpdate verifies the signature against /security/public.pem — wrong key ⇒ refused
  3. Images stream-installed into the inactive partitions (gptpart handler)
  4. gptswap renames active↔inactive labels atomically
  5. Post-install prpl-enable-failover.lua sets failover=1
  6. Reboot into the new bank → one good boot → commit (failover=0)
# real install, OSPv2 — swupdate -e active,full [swupdate_verify_file] : Verified OK [_parse_scripts] : Found Script: prpl-enable-failover.lua [_parse_scripts] : Found Script: prpl-backup.lua [gpt_swap_partition] : swap kernel-inactive ↔ kernel-active [gpt_swap_partition] : swap u-boot-inactive ↔ u-boot-active Enabling the temporary failover [INFO ] : SWUPDATE successful !

2026-05-30-…-swupdate-brick-serial-console.log

scope FEAT-29 ships the command-line upgrade. The upgrade service (data model, commit API, restore, failure hooks) is a later epic.

Two boards, one design — diverging layout

Separate raw FIT partitions vs one shared ext4

The prpl flash-layout spec wants each FIT in its own raw partition (Freedom does this). OSPv2 got a sanctioned exception: kernel and rootfs FITs share one ext4 filesystem.

Qualcomm · Freedom ipq95xx · eMMC

6 dedicated raw FIT partitions — kernel and rootfs are separate.

# mmc part (post-migration) 16 "u-boot-active" 4 MiB raw 17 "u-boot-inactive" 23 "kernel-active" 32 MiB raw FIT 24 "kernel-inactive" 25 "rootfs-active" 256 MiB raw FIT 26 "rootfs-inactive" 30 "mfgdata" SMD

cram 212 checks 3 labels: u-boot, kernel, rootfs.

MaxLinear · OSPv2 x86-LGM · eMMC

2 ext4 kernel partitions, each holding both FITs as files. No rootfs partition.

9 "u-boot-inactive" raw 10 "u-boot-active" 11 "kernel-inactive" ext4 ~112 MiB 12 "kernel-active" ext4: kernel.itb + rootfs.itb 13 "mfgdata" SMD (no rootfs-* partition) rootfs → /dev/mapper/rootfs-fit

cram 212 checks 2 labels: u-boot, kernel.

# the one-line proof — OSPv2 loads the FIT as a FILE from a filesystem Loading FIT /kernel.itb from ext4 partition kernel-active ← OSPv2 Loading raw FIT from partition kernel-active ← Freedom
boot stages, env & migration
Deep dive · everything else that differs

Early stage, env redundancy, and the migration path

DimensionQualcomm · FreedomMaxLinear · OSPv2
ROM → early stagePBL → XBL/SBL1 (proprietary binary)ROM → RBE / SPL (u-boot-spl-emmc.bin)
U-Boot2025.04-prpl (from u-boot-active FIT)2022.01-MXL · OPEN_BOOT
Permanent failoverOTP fuse (early stage is a binary)compile flag (SPL is U-Boot-based)
U-Boot envsingle 0:APPSBLENVredundant env_a + env_b
Securestore / cal0:ART, 0:ETHPHYFW, securestorecalibration_a/b, securestore_a/b, tep-*
Migration3 U-Boot .scr scripts (backup→temp U-Boot→new GPT)update_script.itb + run update_prpl
Kernel artifactseparate kernel.itb + rootfs.itbone *-ext4.img → both kernel banks

Same prpl contract underneath both: boot-by-name, bootcount + temporary failover, booted-bank in the DT, root=PARTLABEL=rootfs-active, 2 GPTs for atomic swap, SMD in mfgdata, RSA-4096-signed FITs. Only the vendor-shaped bits differ.

Everything, on one slide

The whole FEAT-29 picture

TRUST Silicon ROMfuse (un-blown) Early stageXBL / RBE U-Boot (BL33)RSA-4096 verify Kernel FITsig + sha256 initramfsauth rootfs rootfssquashfs verify key inU-Boot control DTB IDENTITY & SECRETS SMD (mfgdata)signed DTB · Irdeto certenc: K_UK / K_UT KDF_CTX + root→ HKDF-SHA256 K_UK · IV→ Linux KRS libkcapi decryptcert key · userfs · wifi MAC → ethaddr · KDF_CTX → initramfs RESILIENCE A/B banksboot by name · 2 GPTs bootcount failoverregister · watchdog SWUpdateverify → write inactive gptswap → commitfailover=0 ↻ next update targets the new inactive bank enforced today ✓ RSA-4096 sig: SMD + FITs✓ SHA-256 per FIT node✓ AES-256 SMD secrets✓ A/B + bootcount failover deferred … OEM fuse + signed U-Boot… dm-verity · TEE · commit API
Where this came from

Honest scope & provenance

What FEAT-29 delivers

A cryptographically enforced boot chain (RSA-4096/PSS-signed SMD + FITs, SHA-256 nodes), a signed & partly-encrypted manufacturing identity with an Irdeto-minted device cert, HKDF-derived keys provisioned to the kernel keyring, and a resilient A/B + bootcount-failover + SWUpdate upgrade — on two different SoCs.

What's deliberately not done yet

OEM secure-boot fuse un-blown & U-Boot unsigned (migration phase); root_key is a dev placeholder; cert PKI is Irdeto QA; dm-verity, a TEE, and the upgrade service (commit API, restore, failure hooks) are future epics. The deck flags each where it appears.

Code & spec

prplos!1964 · Confluence SMD / Flash Layout / Secure Upgrade · gensmd + prpl_certificate.py

Real evidence

Every console snippet, decoded SMD, cert chain & partition table is from real feat-29-tools serial-console & device artifacts (Rigol + serial harness).

Boards

Qualcomm ipq95xx Freedom (WNC) · MaxLinear x86-LGM OSPv2 (Gemtek). eMMC; NAND out of scope.

One design, two silicons: verify before you run, derive instead of store, update the bank you're not on.