From 237c44701ce1dccd13213311ec865cbd3085e3a7 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 17 Sep 2026 15:54:47 -0700 Subject: [PATCH 01/14] Add NXP i.MX95 post-quantum demo for the A55 cluster and Cortex-M7 --- README.md | 1 + imx95-pqc-demo/.gitignore | 7 + imx95-pqc-demo/README.md | 173 ++++++ imx95-pqc-demo/charts/imx95-m7-code-size.png | Bin 0 -> 29653 bytes .../charts/imx95-m7-verify-time.png | Bin 0 -> 23308 bytes imx95-pqc-demo/container/Dockerfile | 68 +++ imx95-pqc-demo/container/build-aarch64.sh | 98 +++ imx95-pqc-demo/container/build-image.sh | 43 ++ imx95-pqc-demo/container/docker-compose.yml | 46 ++ imx95-pqc-demo/container/entrypoint.sh | 133 +++++ imx95-pqc-demo/demo/demo-run.sh | 54 ++ imx95-pqc-demo/demo/demo-uart.sh | 213 +++++++ imx95-pqc-demo/demo/drmfb.py | 541 +++++++++++++++++ imx95-pqc-demo/demo/font8x8.py | 104 ++++ imx95-pqc-demo/demo/install-autostart.sh | 29 + imx95-pqc-demo/demo/m7-console-tail.sh | 49 ++ imx95-pqc-demo/demo/m7-rpmsg-log.sh | 40 ++ imx95-pqc-demo/demo/m7-start.sh | 27 + imx95-pqc-demo/demo/stage.sh | 44 ++ imx95-pqc-demo/demo/twopane.py | 281 +++++++++ imx95-pqc-demo/demo/wolfssl-demo-uart.service | 29 + imx95-pqc-demo/demo/wolfssl-demo.service | 33 ++ imx95-pqc-demo/demo/wolfssl-m7.service | 18 + imx95-pqc-demo/gallery/README.md | 116 ++++ imx95-pqc-demo/gallery/docker-compose.yml | 48 ++ imx95-pqc-demo/gallery/publish.sh | 52 ++ imx95-pqc-demo/m7/build.sh | 48 ++ imx95-pqc-demo/m7/imx95_wolfboot.overlay | 67 +++ imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt | 19 + imx95-pqc-demo/m7/zephyr-app/prj.conf | 76 +++ imx95-pqc-demo/m7/zephyr-app/src/main.c | 556 ++++++++++++++++++ imx95-pqc-demo/tools/memtool.c | 259 ++++++++ 32 files changed, 3272 insertions(+) create mode 100644 imx95-pqc-demo/.gitignore create mode 100644 imx95-pqc-demo/README.md create mode 100644 imx95-pqc-demo/charts/imx95-m7-code-size.png create mode 100644 imx95-pqc-demo/charts/imx95-m7-verify-time.png create mode 100644 imx95-pqc-demo/container/Dockerfile create mode 100755 imx95-pqc-demo/container/build-aarch64.sh create mode 100755 imx95-pqc-demo/container/build-image.sh create mode 100644 imx95-pqc-demo/container/docker-compose.yml create mode 100755 imx95-pqc-demo/container/entrypoint.sh create mode 100755 imx95-pqc-demo/demo/demo-run.sh create mode 100755 imx95-pqc-demo/demo/demo-uart.sh create mode 100644 imx95-pqc-demo/demo/drmfb.py create mode 100644 imx95-pqc-demo/demo/font8x8.py create mode 100755 imx95-pqc-demo/demo/install-autostart.sh create mode 100755 imx95-pqc-demo/demo/m7-console-tail.sh create mode 100755 imx95-pqc-demo/demo/m7-rpmsg-log.sh create mode 100755 imx95-pqc-demo/demo/m7-start.sh create mode 100755 imx95-pqc-demo/demo/stage.sh create mode 100644 imx95-pqc-demo/demo/twopane.py create mode 100644 imx95-pqc-demo/demo/wolfssl-demo-uart.service create mode 100644 imx95-pqc-demo/demo/wolfssl-demo.service create mode 100644 imx95-pqc-demo/demo/wolfssl-m7.service create mode 100644 imx95-pqc-demo/gallery/README.md create mode 100644 imx95-pqc-demo/gallery/docker-compose.yml create mode 100755 imx95-pqc-demo/gallery/publish.sh create mode 100755 imx95-pqc-demo/m7/build.sh create mode 100644 imx95-pqc-demo/m7/imx95_wolfboot.overlay create mode 100644 imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt create mode 100644 imx95-pqc-demo/m7/zephyr-app/prj.conf create mode 100644 imx95-pqc-demo/m7/zephyr-app/src/main.c create mode 100644 imx95-pqc-demo/tools/memtool.c diff --git a/README.md b/README.md index 608a0d1..33ce7c0 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This repository contains example applications using [wolfBoot](https://github.co * wolfSSH SCP transfer firmware update mechanism, with [freeRTOS on Freescale K64F](freeRTOS-Freescale-K64F-scp) * BLE-GATT FOTA service using [RIOT-OS and Nimble on Nordic nRF52](riotOS-nrf52840dk-ble) * Measured boot demo using [wolfTPM on STM32F4](test-app-STM32F4-measured-boot) + * Post-quantum demo on both clusters of the [NXP i.MX95](imx95-pqc-demo): ML-DSA-87 verified boot of the Cortex-M7 alongside wolfCrypt ML-KEM/ML-DSA benchmarks on the Cortex-A55 cluster ## License diff --git a/imx95-pqc-demo/.gitignore b/imx95-pqc-demo/.gitignore new file mode 100644 index 0000000..c8a103f --- /dev/null +++ b/imx95-pqc-demo/.gitignore @@ -0,0 +1,7 @@ +# Zephyr build output (m7/build.sh writes here) +m7/build/ +# wolfSSL source export and cross-build trees (container/build-aarch64.sh) +container/wolfssl-src/ +container/build/ +# Python bytecode from the demo renderer +demo/__pycache__/ diff --git a/imx95-pqc-demo/README.md b/imx95-pqc-demo/README.md new file mode 100644 index 0000000..709ede7 --- /dev/null +++ b/imx95-pqc-demo/README.md @@ -0,0 +1,173 @@ +# Post-quantum on both clusters of the NXP i.MX95 + +A two-pane demo on a single screen, running entirely on a Toradex SMARC iMX95: + +| pane | what it shows | +|---|---| +| **left** | wolfCrypt ML-KEM / ML-DSA benchmarks in a container on the six Cortex-A55 cores, under Torizon OS | +| **right** | wolfBoot performing **ML-DSA-87 verified boot** of the Cortex-M7, with its console relayed to Linux | + +The point of the pairing: the same post-quantum algorithms, at the same security +level, on two very different cores of one SoC - a Linux application cluster and +a bare-metal real-time core. + +## Measured on hardware + +Cortex-M7 at 800 MHz, DWT cycle counter, caches enabled: + +| | wolfBoot text | verify + boot | at 800 MHz | +|---|---|---|---| +| ECDSA P-256 | 22,548 B | 1,741,288 cycles | 2.177 ms | +| **ML-DSA-87** | **21,608 B** | **4,201,179 cycles** | **5.251 ms** | +| ML-DSA-87, Zephyr payload | 21,608 B | 5,916,540 cycles | 7.396 ms | + +Two results worth calling out. **Post-quantum verified boot costs 5.25 ms** - +2.4x the cycles of ECDSA P-256 and irrelevant against any real boot time. And +the **ML-DSA-87 bootloader is 940 bytes smaller than the ECDSA one**: ML-DSA +verification is SHAKE plus polynomial arithmetic and never pulls in the +big-integer math P-256 needs, so for a verify-only workload post-quantum can +cost *less* flash. + +Verification also scales far better than payload size suggests - 1,004 B to +54,080 B is **54x the payload for 1.41x the verify**, because the lattice +signature check is a fixed cost and only the hash grows. + +> The M7's I- and D-caches are **disabled out of reset**. Any i.MX95 M7 +> benchmark taken without enabling them is wrong by one to two orders of +> magnitude - we measured SHA-256 at 1.1 MiB/s before enabling them and +> 29.2 MiB/s after. + +## Layout + +``` +container/ wolfCrypt PQC benchmark container for the A55 cluster (left pane) +m7/ Zephyr RPMsg payload wolfBoot verifies and boots (right pane) +demo/ board-side orchestration and the two-pane renderer +gallery/ Torizon Demo Gallery submission: compose file and partner metadata +tools/ memtool - mmap-based /dev/mem access for the M7 console +charts/ the measured figures, as images +``` + +## Requirements + +- Toradex SMARC iMX95 (or another i.MX95 board) running Torizon OS +- wolfBoot with the `imx95_m7` target +- A Zephyr workspace (4.4.0 or newer) and an `arm-none-eabi` toolchain +- Docker on the board (Torizon ships it) + +## Building + +**1. wolfBoot for the M7**, signing with ML-DSA-87: + +```sh +cp config/examples/imx95-m7.config .config +make SIGN=ML_DSA ML_DSA_LEVEL=5 IMAGE_SIGNATURE_SIZE=4627 DEBUG_UART=1 +``` + +`DEBUG_UART=1` is what makes wolfBoot write its verification log to the +shared-memory console the right pane reads. + +**2. The Zephyr payload:** + +```sh +cd m7 && ZEPHYR_BASE=~/zephyrproject/zephyr ./build.sh +``` + +Then sign it with the same key wolfBoot was built with: + +```sh +IMAGE_HEADER_SIZE=12288 ML_DSA_LEVEL=5 ./tools/keytools/sign --ml_dsa --sha256 \ + m7/build/zephyr/payload.bin wolfboot_signing_private_key.der 1 +``` + +**3. The benchmark container:** + +```sh +cd container && WOLFSSL_REPO=/path/to/wolfssl ./build-aarch64.sh all && ./build-image.sh +``` + +## Running + +```sh +BOARD=torizon@ WOLFBOOT=/path/to/wolfboot ./demo/stage.sh +``` + +then on the board: + +```sh +sudo bash ~/demo/demo-run.sh /dev/tty1 # /dev/tty1 is the HDMI console +``` + +Omit the argument to render on the current terminal instead. + +### Starting it automatically + +Because replaying the demo means power-cycling the board (see below), running it +by hand also means logging back in afterwards, over a network link that does not +always come up. Installing the unit makes cutting and restoring power the entire +replay: + +```sh +sudo bash ~/demo/install-autostart.sh # enable +sudo bash ~/demo/install-autostart.sh --off # disable, restore the getty +``` + +It takes tty1 from `getty@tty1`, so the HDMI console is the demo rather than a +login prompt. + +### Reading the verify log over RPMsg + +The right pane does not need this, but the same log can be read as an ordinary +Linux tty: + +```sh +sudo bash ~/demo/m7-rpmsg-log.sh +``` + +Order matters. The payload relays the whole console ring as soon as its endpoint +has a destination address, which the host supplies while binding the channel - +strictly before `/dev/ttyRPMSG*` exists. No reader can be attached for that first +pass, and the log it sends is discarded by a tty nobody has open. The payload +therefore treats any byte written to the tty as a request to rewind and send the +log again, so the script attaches a reader first and then pokes. + +## Notes that will save you time + +**The M7 starts once per Linux boot.** `echo stop > .../state` fails on this BSP +with "Interrupted system call" and the core stays running, so replaying the boot +means a real power cycle of the board - not a restart. Plan the demo around +that. + +**The payload is not the upstream `openamp_rsc_table` sample, deliberately.** +That sample assumes Linux loaded *its* ELF: the remote declares vrings with +`da = FW_RSC_ADDR_ANY` and Linux writes the resolved addresses and the virtio +status back into the resource table it loaded. Here Linux loads **wolfBoot**, so +those values land in wolfBoot's table while the sample polls its own - and it +waits forever in `rproc_virtio_wait_remote_ready()`. This payload uses fixed +vring addresses matching wolfBoot's table and skips the wait, because there is +nothing to wait for: Linux registers virtio0 before it releases the core. + +**The right pane reads the console ring directly, not `rpmsg_tty`.** wolfBoot +writes its log before any RPMsg endpoint exists, so the ring is the only source +that contains the verification output - and reading it needs no driver, no +endpoint binding and no module load. + +**Nothing may sit unflushed in the page cache.** The replay beat is a hard power +cut, so a file written seconds earlier is simply gone after the next cycle - a +staged payload, or the autostart unit, silently reverts to what was there +before. `stage.sh` and `install-autostart.sh` both `sync`; verify a hand-copied +payload with `md5sum` rather than a timestamp. + +**Torizon has no tmux**, and its rootfs is read-only OSTree, so there is nothing +to install. `demo/twopane.py` renders two fixed columns with a full redraw +instead. + +**Do not quote the benchmark's "Cycles per byte" or "Cycles/op" columns on an +A55.** They derive from the 24 MHz generic timer rather than the 1.8 GHz core +clock and are wrong by roughly 75x. Use ops/sec and MB/s, or pass +`-freq 1800000000`. The demo renderer strips those columns for this reason. + +## Support + +wolfSSL is dual licensed under GPLv3 or a commercial license. Questions: +support@wolfssl.com diff --git a/imx95-pqc-demo/charts/imx95-m7-code-size.png b/imx95-pqc-demo/charts/imx95-m7-code-size.png new file mode 100644 index 0000000000000000000000000000000000000000..5db15d37b11e75d7190994d2790e1088c350f9b4 GIT binary patch literal 29653 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

Xp+bZCo4Ib`d3*i)}wPQ9@4 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ⅈ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 I4Y%0;0iWavX#fBK literal 0 HcmV?d00001 diff --git a/imx95-pqc-demo/charts/imx95-m7-verify-time.png b/imx95-pqc-demo/charts/imx95-m7-verify-time.png new file mode 100644 index 0000000000000000000000000000000000000000..622f8f5b9ba8266da6bcae27e5f52b482685432e GIT binary patch literal 23308 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/dev/null || true; \ + cp /build/$b/examples/server/server /out/$b/bin/ 2>/dev/null || true; \ + cp /build/$b/wolfssl/options.h /out/$b/; \ + done; \ + cp /src/wolfssl/README.md /out/ 2>/dev/null || true + +# ---------------------------------------------------------------- runtime --- +FROM debian:bookworm-slim + +LABEL org.opencontainers.image.title="wolfCrypt PQC benchmark (i.MX95)" \ + org.opencontainers.image.description="Live ML-KEM / ML-DSA benchmarks on the Cortex-A55 cluster" \ + org.opencontainers.image.vendor="wolfSSL Inc." + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + util-linux procps ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /out/ /opt/wolfssl/ +COPY entrypoint.sh /opt/wolfssl/ +RUN chmod +x /opt/wolfssl/entrypoint.sh + +WORKDIR /opt/wolfssl +ENTRYPOINT ["/opt/wolfssl/entrypoint.sh"] diff --git a/imx95-pqc-demo/container/build-aarch64.sh b/imx95-pqc-demo/container/build-aarch64.sh new file mode 100755 index 0000000..310eb12 --- /dev/null +++ b/imx95-pqc-demo/container/build-aarch64.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Cross-build wolfSSL for the Toradex SMARC i.MX95 (6x Cortex-A55, aarch64). +# +# Builds two configurations out-of-tree so the wolfSSL source checkout stays clean: +# baseline -- --enable-armasm (NEON; works on any A55) +# sha3-crypto -- --enable-armasm=sha3-crypto (ARMv8.2 SHA3 instructions, FEAT_SHA3) +# +# The pair exists because ML-DSA has NO ARM assembly in wolfSSL: on aarch64 its +# speed comes entirely from the SHA-3/SHAKE backend, so this is the only lever +# that moves ML-DSA numbers. FEAT_SHA3 is optional on Cortex-A55, so the +# sha3-crypto build may not be runnable on the actual silicon -- check +# "grep Features /proc/cpuinfo" for 'sha3' on the board before shipping it. +# +# Static libraries only, so the binaries run under qemu-aarch64 with no +# LD_LIBRARY_PATH juggling. +# +# Usage: ./build-aarch64.sh [baseline|sha3-crypto|all] + +set -euo pipefail + +# Point this at your wolfSSL checkout: +# WOLFSSL_REPO=/path/to/wolfssl ./build-aarch64.sh all +WOLFSSL_REPO="${WOLFSSL_REPO:?set WOLFSSL_REPO to your wolfSSL checkout}" +WOLFSSL_REF="${WOLFSSL_REF:-HEAD}" +DEMO_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)" +# Pristine export of the repo. The developer's checkout is already configured +# in-tree, and autotools refuses an out-of-tree build against a configured +# source dir. Exporting rather than running "make distclean" over there keeps +# their working tree and build state untouched. +WOLFSSL_SRC="${DEMO_DIR}/wolfssl-src" +HOST_TRIPLE=aarch64-linux-gnu +JOBS="$(nproc)" + +export_src() { + if [ -x "${WOLFSSL_SRC}/configure" ]; then + echo "=== source export already present: ${WOLFSSL_SRC} ===" + return + fi + echo "=== exporting ${WOLFSSL_REPO} @ ${WOLFSSL_REF} -> ${WOLFSSL_SRC} ===" + rm -rf "${WOLFSSL_SRC}" + mkdir -p "${WOLFSSL_SRC}" + git -C "${WOLFSSL_REPO}" archive "${WOLFSSL_REF}" | tar -x -C "${WOLFSSL_SRC}" + echo "=== autogen.sh ===" + (cd "${WOLFSSL_SRC}" && ./autogen.sh > autogen.log 2>&1) \ + || { tail -40 "${WOLFSSL_SRC}/autogen.log"; exit 1; } +} + +# Shared across both configurations. --enable-mldsa is REQUIRED: it is off by +# default, and without it the benchmark silently emits no ML-DSA rows at all +# rather than failing. +COMMON_OPTS=( + "--host=${HOST_TRIPLE}" + --enable-mlkem + --enable-mldsa + --enable-sp + --enable-sp-asm + --enable-keygen + --enable-sha3 + --enable-curve25519 + --enable-ed25519 + --disable-shared + --enable-static +) + +build_one() { + local name="$1" armasm="$2" + local builddir="${DEMO_DIR}/build/${name}" + + echo "=== [${name}] configure (${armasm}) ===" + rm -rf "${builddir}" + mkdir -p "${builddir}" + ( + cd "${builddir}" + "${WOLFSSL_SRC}/configure" \ + "${COMMON_OPTS[@]}" \ + "${armasm}" \ + > configure.log 2>&1 || { tail -40 configure.log; exit 1; } + ) + + echo "=== [${name}] make -j${JOBS} ===" + make -C "${builddir}" -j"${JOBS}" > "${builddir}/build.log" 2>&1 \ + || { tail -60 "${builddir}/build.log"; exit 1; } + + echo "=== [${name}] OK ===" + file "${builddir}/wolfcrypt/benchmark/benchmark" || true +} + +target="${1:-all}" +export_src +case "${target}" in + baseline) build_one baseline "--enable-armasm" ;; + sha3-crypto) build_one sha3-crypto "--enable-armasm=sha3-crypto" ;; + all) + build_one baseline "--enable-armasm" + build_one sha3-crypto "--enable-armasm=sha3-crypto" + ;; + *) echo "usage: $0 [baseline|sha3-crypto|all]" >&2; exit 2 ;; +esac diff --git a/imx95-pqc-demo/container/build-image.sh b/imx95-pqc-demo/container/build-image.sh new file mode 100755 index 0000000..7c09512 --- /dev/null +++ b/imx95-pqc-demo/container/build-image.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Build the arm64 demo container. +# +# Cross-compiles in the builder stage on the native build platform, so this is +# fast even though the output image is linux/arm64. Validate the result under +# binfmt on the x86 bench before it ever touches the board. +# +# Publishing is deliberately opt-in: pass --push (and set REGISTRY) only when +# you actually intend to make the image public. +# +# Usage: +# ./build-image.sh # build + load locally +# ./build-image.sh --push # build + push to $REGISTRY (requires login) + +set -euo pipefail + +DEMO_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)" +REGISTRY="${REGISTRY:-ghcr.io/wolfssl}" +IMAGE="${IMAGE:-wolfcrypt-pqc-imx95}" +TAG="${TAG:-latest}" +PLATFORM="${PLATFORM:-linux/arm64}" +REF="${REGISTRY}/${IMAGE}:${TAG}" + +if [ ! -d "${DEMO_DIR}/wolfssl-src" ]; then + echo "wolfssl-src/ missing -- run ./build-aarch64.sh first to export it" >&2 + exit 1 +fi + +OUTPUT=(--load) +if [ "${1:-}" = "--push" ]; then + OUTPUT=(--push) + echo "=== PUBLISHING to ${REF} ===" + echo "=== this makes the image publicly pullable; Ctrl-C within 5s to abort ===" + sleep 5 +fi + +set -x +docker buildx build \ + --platform "${PLATFORM}" \ + -t "${REF}" \ + -f "${DEMO_DIR}/Dockerfile" \ + "${OUTPUT[@]}" \ + "${DEMO_DIR}" diff --git a/imx95-pqc-demo/container/docker-compose.yml b/imx95-pqc-demo/container/docker-compose.yml new file mode 100644 index 0000000..57132ef --- /dev/null +++ b/imx95-pqc-demo/container/docker-compose.yml @@ -0,0 +1,46 @@ +# Torizon OS deployment unit for the wolfCrypt PQC benchmark pane. +# +# Deploy with: +# docker compose up (on the board, or via Torizon Cloud) +# +# The container writes to stdout; the demo host captures that into the left +# tmux pane on the HDMI console. Part 2's Cortex-M7 wolfBoot output arrives +# over RPMsg (rpmsg_tty) into the right pane of the same tmux session. + +services: + wolfcrypt-pqc: + # Overridable so a board can run a locally built or differently published + # image without editing this file. + image: "${WOLFCRYPT_PQC_IMAGE:-ghcr.io/wolfssl/wolfcrypt-pqc-imx95:latest}" + container_name: wolfcrypt-pqc + # The serial demo runs a single pass and stops, so it sets RESTART_POLICY=no + # to keep Docker from relaunching the container the moment it exits. + restart: "${RESTART_POLICY:-unless-stopped}" + + # Live console demo: keep the TTY so the ANSI banner renders and output is + # not block-buffered behind a pipe. + tty: true + stdin_open: true + + environment: + # auto -- pick sha3-crypto only if the CPU advertises FEAT_SHA3 + # ab -- alternate builds each cycle to show the SHA-3 lever live + MODE: "${MODE:-auto}" + LOOP: "${LOOP:-1}" + PAUSE: "${PAUSE:-5}" + + # The entrypoint reads /proc/cpuinfo to decide which build is safe to run, + # so the host's real CPU feature list must be visible. + volumes: + - /proc/cpuinfo:/proc/cpuinfo:ro + - ./results:/results + + # Benchmarks are CPU-bound and want the whole A55 cluster. Leave unset to + # use all cores; pin here if the M7-side demo needs headroom. + # cpuset: "0-5" + + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/imx95-pqc-demo/container/entrypoint.sh b/imx95-pqc-demo/container/entrypoint.sh new file mode 100755 index 0000000..06876c8 --- /dev/null +++ b/imx95-pqc-demo/container/entrypoint.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Left-pane demo loop: live wolfCrypt PQC benchmarks on the i.MX95 A55 cluster. +# +# Environment: +# MODE=auto|baseline|sha3-crypto|ab which wolfSSL build to run (default auto) +# LOOP=1|0 run continuously (default 1) +# PAUSE= pause between cycles (default 5) +# ONCE_ARGS="..." override the algorithm set + +set -uo pipefail + +WOLF_ROOT=/opt/wolfssl +# Named at runtime rather than baked in: the image is plain aarch64 and runs on +# any Arm64 Torizon module, so naming one SoC in the banner would be wrong on +# the others. A container does not normally see /proc/device-tree, so the model +# is only available when the host exposes it; brace the redirect so a missing +# file is silent rather than a shell error on stderr. +soc_model() { { tr -d '\0' < /proc/device-tree/model; } 2>/dev/null; } +SOC_LABEL="${SOC_LABEL:-$(soc_model)}" +SOC_LABEL="${SOC_LABEL:-Arm64 module}" +MODE="${MODE:-auto}" +LOOP="${LOOP:-1}" +PAUSE="${PAUSE:-5}" + +B=$'\033[1m'; DIM=$'\033[2m'; CYAN=$'\033[36m'; GREEN=$'\033[32m' +YELLOW=$'\033[33m'; RED=$'\033[31m'; R=$'\033[0m' +[ -t 1 ] || { B=""; DIM=""; CYAN=""; GREEN=""; YELLOW=""; RED=""; R=""; } + +have_sha3() { grep -m1 '^Features' /proc/cpuinfo 2>/dev/null | grep -qw sha3; } + +select_build() { + case "${MODE}" in + baseline|sha3-crypto) echo "${MODE}" ;; + auto) + # FEAT_SHA3 is optional on Cortex-A55. Running the sha3-crypto + # binary without it faults, so this check is not cosmetic. + if have_sha3; then echo sha3-crypto; else echo baseline; fi ;; + *) echo baseline ;; + esac +} + +banner() { + local build="$1" feats ver + feats="$(grep -m1 '^Features' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | sed 's/^ //')" + ver="$("${WOLF_ROOT}/${build}/bin/benchmark" -? 2>&1 | grep -m1 -o 'wolfSSL version [0-9.]*')" + + echo "${CYAN}${B}" + echo " wolfSSL / wolfCrypt post-quantum benchmarks" + echo " ${SOC_LABEL} -- Torizon OS" + echo "${R}${DIM} ------------------------------------------------------------${R}" + printf " %-14s %s\n" "Library:" "${ver:-unknown}" + printf " %-14s %s\n" "Build:" "${build}" + printf " %-14s %s\n" "Cores:" "$(nproc)" + printf " %-14s %s\n" "Kernel:" "$(uname -r)" + printf " %-14s %s\n" "CPU flags:" "${feats:-unavailable}" + + if [ "${build}" = "sha3-crypto" ]; then + printf " %-14s ${GREEN}%s${R}\n" "Keccak:" \ + "ARMv8.2 SHA3 instructions (EOR3/RAX1/XAR/BCAX)" + elif have_sha3; then + printf " %-14s ${YELLOW}%s${R}\n" "Keccak:" \ + "NEON only -- CPU has FEAT_SHA3, build does not use it" + else + printf " %-14s ${YELLOW}%s${R}\n" "Keccak:" \ + "NEON only -- this CPU has no FEAT_SHA3" + fi + # ML-DSA has no ARM assembly in wolfSSL, so its numbers ride entirely on + # the Keccak backend named above. Say so, rather than let a reader assume + # a hand-tuned ML-DSA path exists. + printf " ${DIM}%s${R}\n" "ML-KEM: NEON NTT + 3-way NEON Keccak. ML-DSA: no ARM asm; Keccak-bound." + echo "${DIM} ------------------------------------------------------------${R}" + echo +} + +run_cycle() { + local build="$1" bin="${WOLF_ROOT}/$1/bin/benchmark" + if [ ! -x "${bin}" ]; then + echo "${RED}missing benchmark binary: ${bin}${R}" >&2 + return 1 + fi + if [ -n "${ONCE_ARGS:-}" ]; then + # shellcheck disable=SC2086 + "${bin}" ${ONCE_ARGS} + else + "${bin}" -ml-kem-512 -ml-kem-768 -ml-kem-1024 \ + -ml-dsa-44 -ml-dsa-65 -ml-dsa-87 \ + -ecc -rsa -sha3-256 -shake256 + fi +} + +main() { + local build cycle=0 status= + build="$(select_build)" + + if [ "${MODE}" = auto ] && [ "${build}" = baseline ] && have_sha3; then + : # unreachable, kept for clarity + fi + if [ "${MODE}" = sha3-crypto ] && ! have_sha3; then + echo "${RED}WARNING: MODE=sha3-crypto but this CPU does not advertise FEAT_SHA3." >&2 + echo " The binary is likely to fault with SIGILL.${R}" >&2 + fi + + while :; do + cycle=$((cycle + 1)) + if [ "${MODE}" = ab ]; then + # Alternate builds so the SHA-3 lever is visible live, side by side + # across cycles, without restarting the container. + if [ $((cycle % 2)) -eq 1 ]; then build=baseline + elif have_sha3; then build=sha3-crypto + else build=baseline + fi + fi + + banner "${build}" + printf " ${GREEN}${B}[RUNNING]${R} benchmark cycle %d, started %s\n\n" \ + "${cycle}" "$(date -u '+%H:%M:%SZ')" + + if run_cycle "${build}"; then + status="${GREEN}${B}[OK]${R}" + else + status="${RED}${B}[FAILED]${R}" + fi + + [ "${LOOP}" = "1" ] || break + echo + printf " %b cycle %d complete, next run in %ss\n" \ + "${status}" "${cycle}" "${PAUSE}" + sleep "${PAUSE}" + echo + done +} + +main "$@" diff --git a/imx95-pqc-demo/demo/demo-run.sh b/imx95-pqc-demo/demo/demo-run.sh new file mode 100755 index 0000000..75944c8 --- /dev/null +++ b/imx95-pqc-demo/demo/demo-run.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Two-pane i.MX95 demo. Runs ON THE BOARD as root. +# +# left - wolfBoot's own ML-DSA-87 verified boot of Linux, replaced by the +# wolfCrypt PQC benchmarks once the container has results +# right - wolfBoot ML-DSA-87 verified boot of the Cortex-M7 +# +# Everything is local to the board, so once this is running nothing depends on +# a host being connected. +# +# sudo bash demo-run.sh # render on this terminal +# sudo bash demo-run.sh /dev/tty1 # render on the DisplayPort panel +# +# The M7 can only be started ONCE per Linux boot, so re-running the demo needs a +# full board power cycle. That is the "reboot beat" - and it has to come from +# the host relay or a person, since the board cannot power-cycle itself. +set -uo pipefail + +DEMO=/home/torizon/demo +TARGET=${1:-} + +echo "== starting the benchmark container ==" +# "down" first, deliberately. A container created by an earlier run survives a +# reboot while its compose network does not, so a bare "up -d" fails with +# "network not found" and the left pane silently stays empty. Since the +# demo's replay beat IS a power cycle, that is exactly when it would bite. +( cd "$DEMO" && docker compose down --remove-orphans >/dev/null 2>&1 || true ) +( cd "$DEMO" && docker compose up -d ) || exit 1 + +echo "== releasing the Cortex-M7 ==" +bash "$DEMO/m7-start.sh" || echo " (M7 already running - power cycle to replay the boot)" + +echo "== rendering ==" +if [ -n "$TARGET" ]; then + # Torizon's kernel has no fbdev emulation, so text written to a VT reaches + # no display: the renderer has to own the DisplayPort through KMS. Take the + # screen away from everything else that wants it first - the getty on the + # console, the framebuffer console redrawing over the KMS output, and + # Torizon's pairing overlay - then render as the sole DRM master. + systemctl stop getty@"$(basename "$TARGET")" 2>/dev/null || true + for v in /sys/class/vtconsole/vtcon*; do + if grep -qi 'frame buffer' "$v/name" 2>/dev/null; then + echo 0 > "$v/bind" 2>/dev/null || true + fi + done + docker stop torizon-easy-pairing-bash-1 >/dev/null 2>&1 || true + # stderr is left alone on purpose: the DRM path is the most failure-prone + # part of the demo (hotplug, EDID, DRM-master contention), and discarding + # it turns a black screen into a silent restart loop with nothing in the + # journal to explain it. + exec env RENDER=drm python3 "$DEMO/twopane.py" +else + exec python3 "$DEMO/twopane.py" +fi diff --git a/imx95-pqc-demo/demo/demo-uart.sh b/imx95-pqc-demo/demo/demo-uart.sh new file mode 100755 index 0000000..c1c1f93 --- /dev/null +++ b/imx95-pqc-demo/demo/demo-uart.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# Single-stream demo on the serial console. Runs ON THE BOARD as root. +# +# The DisplayPort renderer can only start once Linux is up, which is long after +# the interesting part: by then wolfBoot has already verified and booted the +# kernel, and the pane can only replay that from the DDR ring. On the UART there +# is nothing to replay - wolfBoot's own ML-DSA-87 verification prints live, +# seconds after power-on, and everything below simply continues the same stream: +# +# wolfBoot (A55) verified boot of Linux - printed by wolfBoot itself +# [M7 ] wolfBoot ML-DSA-87 -> Zephyr on the Cortex-M7 +# [A55] wolfCrypt ML-KEM / ML-DSA benchmarks, in a container +# +# Each source is prefixed so one scrolling stream stays readable, and output +# goes to the console the kernel already owns, so a plain `screen`/`picocom` on +# the host records the whole demo with no display, no KMS and no X. +set -uo pipefail + +DEMO=${DEMO:-/home/torizon/demo} +MEMTOOL=${MEMTOOL:-/home/torizon/bin/memtool} +CONSOLE_ADDR=${CONSOLE_ADDR:-0x80F00000} +STATUS_ADDR=${STATUS_ADDR:-0x80F10000} +M7_CORE_MHZ=${M7_CORE_MHZ:-800} +CONTAINER=${CONTAINER:-wolfcrypt-pqc} +INTERVAL=${INTERVAL:-0.5} +SETTLE=${SETTLE:-3} + +# systemd writes its status lines as "[ OK ] ...". Demo output +# that lands between those two codes inherits the green, so every line below +# starts with an explicit reset rather than trusting the console's state. +R=$'\033[0m' +C_M7=$'\033[1;36m' +C_A55=$'\033[1;33m' + +# Stop the kernel from writing to this console for the rest of the demo. Its +# late-boot chatter (USB, Bluetooth, wlan scans) otherwise interleaves with the +# output and there is nothing in it the demo wants to show. The messages still +# reach the journal, so nothing is lost - dmesg -n 8 restores them. +dmesg -n 1 2>/dev/null || true + +# Let the last of systemd's own status lines drain before the banner, so the +# demo starts on a clean console instead of halfway through someone else's line. +sleep "$SETTLE" + +printf '%s\n' "$R" +echo "==================================================================" +echo " NXP i.MX95 - post-quantum verified boot on both clusters" +echo " Cortex-A55 wolfBoot ML-DSA-87 -> Linux (printed above)" +echo " Cortex-M7 wolfBoot ML-DSA-87 -> Zephyr [M7 ]" +echo " Cortex-A55 wolfCrypt ML-KEM / ML-DSA [A55]" +echo "==================================================================" +echo + +# The compose network does not survive a power cut while a container created by +# an earlier run does, so "up -d" alone fails with "network not found" and the +# benchmark half of the stream stays empty. The demo's replay beat IS a power +# cut, so tear down first. +( cd "$DEMO" && docker compose down --remove-orphans >/dev/null 2>&1 || true ) +# One pass, not a loop: the demo's beat is a board reset, so the stream should +# end rather than scroll benchmark cycles forever. LOOP=0 makes the container's +# entrypoint break after a single cycle and exit, which is also what lets the +# reader below know the A55 half is finished. +( cd "$DEMO" && LOOP=0 RESTART_POLICY=no docker compose up -d >/dev/null 2>&1 ) \ + || echo "[A55] container failed to start" + +# The M7 is started by wolfssl-m7.service; only release it here if that unit is +# not in use. It can only be started once per Linux boot either way. +if [ "$(cat /sys/class/remoteproc/remoteproc1/state 2>/dev/null)" != "running" ]; then + bash "$DEMO/m7-start.sh" >/dev/null 2>&1 || true +fi + +# memtool dumps the whole ring each call, so track what has already been shown +# and print only what is new, splitting on lines so the prefix lands correctly. +m7_stream() { + local shown=0 out len chunk complete idle=0 + while true; do + # Stop once the payload says its benchmark is done, so this half of the + # stream ends instead of relaying heartbeats indefinitely. + if [ -n "${M7_DONE:-}" ]; then + return 0 + fi + out=$("$MEMTOOL" con "$CONSOLE_ADDR" 2>/dev/null) || { sleep "$INTERVAL"; continue; } + len=${#out} + if [ "$len" -gt "$shown" ]; then + chunk=${out:$shown} + case "$chunk" in + *$'\n'*) + # Everything up to the final newline is complete; whatever + # follows is a partial line still being written, so leave it + # for the next poll rather than printing half of it. + complete=${chunk%$'\n'*} + shown=$(( shown + ${#complete} + 1 )) + idle=0 + ;; + *) + # No newline yet. Hold the tail, but do not hold it forever: + # the last line of a run never gets one. + idle=$(( idle + 1 )) + if [ "$idle" -ge 6 ]; then + complete=$chunk + shown=$len + idle=0 + else + complete="" + fi + ;; + esac + if [ -n "$complete" ]; then + printf '%s\n' "$complete" | while IFS= read -r line; do + case "$line" in + *[![:space:]]*) printf '%s%s[M7 ]%s %s\n' "$R" "$C_M7" "$R" "$line" ;; + esac + done + case "$complete" in + *"end of Cortex-M7 benchmark"*) M7_DONE=1 ;; + esac + fi + elif [ "$len" -lt "$shown" ]; then + printf '%s%s[M7 ]%s --- ring restarted ---\n' "$R" "$C_M7" "$R" + shown=0 + idle=0 + fi + sleep "$INTERVAL" + done +} + +# Only result lines. The banner, the CPU flags and the dashed rules are noise on +# a demo screen, and the Cycles/op columns derive from the 24 MHz generic timer +# rather than the core clock, so they are wrong by roughly 75x and must never be +# shown - ops/sec and MB/s are the correct numbers. +# The raw line is about 95 characters, which wraps on an 80-column console and +# breaks the column the eye is actually following. Condense to +# " " and drop the redundant parameter fields, so +# a recording stays readable at any width. +bench_stream() { + docker logs -f --tail 0 "$CONTAINER" 2>&1 | awk -v R="$R" -v C="$C_A55" ' + function shorten(s) { + gsub(/\[ *SECP256R1\]/, "P-256", s) + gsub(/P-256 +256/, "P-256", s) + gsub(/ML-KEM +512 +128/, "ML-KEM-512", s) + gsub(/ML-KEM +768 +192/, "ML-KEM-768", s) + gsub(/ML-KEM +1024 +256/, "ML-KEM-1024", s) + gsub(/ML-DSA +44/, "ML-DSA-44", s) + gsub(/ML-DSA +65/, "ML-DSA-65", s) + gsub(/ML-DSA +87/, "ML-DSA-87", s) + gsub(/RSA +2048/, "RSA-2048", s) + gsub(/ +/, " ", s) + sub(/^ +/, "", s); sub(/ +$/, "", s) + return s + } + # Match the wanted figure directly. The line also carries trailing + # "N cycles / X Cycles/op" columns, and taking the last comma-separated + # field would print those - they derive from the 24 MHz generic timer + # rather than the core clock, so they are wrong by roughly 75x. + /ops took/ && match($0, /[0-9.]+ ops\/sec/) { + rate = substr($0, RSTART, RLENGTH); sub(/ ops\/sec/, "", rate) + name = $0; sub(/ +[0-9]+ ops took.*/, "", name) + printf "%s%s[A55]%s %-30s %12s ops/sec\n", R, C, R, shorten(name), rate + fflush(); next + } + /iB took/ && match($0, /[0-9.]+ [KMG]iB\/s/) { + rate = substr($0, RSTART, RLENGTH) + name = $0; sub(/ +[0-9.]+ [KMG]iB took.*/, "", name) + printf "%s%s[A55]%s %-30s %12s\n", R, C, R, shorten(name), rate + fflush(); next + }' +} + +# wolfBoot on the M7 stamps the cycle counter into its status block at hal_init +# and again just before it hands off, so the difference is the cost of the +# whole verified boot on that core. Report it once it is available: the boot +# itself is milliseconds, which is why the log appears in one burst rather than +# scrolling. +m7_boot_time() { + local out w2 w3 shown=0 + while [ "$shown" -eq 0 ]; do + # memtool leads with a blank line, so match the address line itself + # rather than trusting a line number. + out=$("$MEMTOOL" r "$STATUS_ADDR" 4 2>/dev/null | awk -F: '/:/{print $2; exit}') + w2=$(echo "$out" | awk '{print $3}') + w3=$(echo "$out" | awk '{print $4}') + if [ -n "$w2" ] && [ -n "$w3" ] && [ "$w3" != "00000000" ]; then + printf '%s%s[M7 ]%s %s\n' "$R" "$C_M7" "$R" \ + "$(awk -v a="0x$w2" -v b="0x$w3" -v mhz="$M7_CORE_MHZ" \ + 'BEGIN { c = strtonum(b) - strtonum(a); + printf "wolfBoot verified boot: %.2f ms (%d cycles at %d MHz)", + c / (mhz * 1000), c, mhz }')" + shown=1 + fi + sleep "$INTERVAL" + done +} + +m7_stream & +m7_pid=$! +m7_boot_time & +time_pid=$! +bench_stream & +bench_pid=$! + +# Both halves run at once - the [M7 ] and [A55] prefixes say which core each +# line came from - and the demo ends when both have finished their single pass. +wait "$m7_pid" 2>/dev/null || true +wait "$bench_pid" 2>/dev/null || true +kill "$time_pid" 2>/dev/null || true + +printf '%s\n' "$R" +echo "==================================================================" +echo " Demo complete. Both cores booted firmware verified with ML-DSA-87:" +echo " Cortex-A55 wolfBoot -> Linux (above, before this stream)" +echo " Cortex-M7 wolfBoot -> Zephyr [M7 ]" +echo " Power-cycle the board to run it again." +echo "==================================================================" diff --git a/imx95-pqc-demo/demo/drmfb.py b/imx95-pqc-demo/demo/drmfb.py new file mode 100644 index 0000000..cbd8680 --- /dev/null +++ b/imx95-pqc-demo/demo/drmfb.py @@ -0,0 +1,541 @@ +"""Minimal DRM dumb-buffer text renderer. + +Torizon's kernel has no fbdev emulation (CONFIG_DRM_FBDEV_EMULATION is off), +so /dev/fb0 never exists and nothing written to a VT can reach the display. +Containers are expected to own the screen through KMS. This module is the +smallest possible version of that: open /dev/dri/card0, pick the connected +connector's preferred mode, create one dumb buffer, set the CRTC once, and +draw text into the mapped pixels with an embedded 8x8 font. + +Pure ctypes - no libdrm, no PIL, no new packages on the device. Must run as +root and be the only DRM master (no weston container running). + +ioctl numbers are computed from the struct sizes, the same way the kernel's +_IOWR macro does, so a wrong struct definition fails loudly at open time +rather than corrupting memory. +""" + +import ctypes +import fcntl +import mmap +import os + +from font8x8 import FONT + +DRM_IOCTL_BASE = ord('d') + + +def _IOWR(nr, struct_type): + return (3 << 30) | (ctypes.sizeof(struct_type) << 16) | \ + (DRM_IOCTL_BASE << 8) | nr + + +class DrmModeRes(ctypes.Structure): + _fields_ = [ + ("fb_id_ptr", ctypes.c_uint64), + ("crtc_id_ptr", ctypes.c_uint64), + ("connector_id_ptr", ctypes.c_uint64), + ("encoder_id_ptr", ctypes.c_uint64), + ("count_fbs", ctypes.c_uint32), + ("count_crtcs", ctypes.c_uint32), + ("count_connectors", ctypes.c_uint32), + ("count_encoders", ctypes.c_uint32), + ("min_width", ctypes.c_uint32), + ("max_width", ctypes.c_uint32), + ("min_height", ctypes.c_uint32), + ("max_height", ctypes.c_uint32), + ] + + +class DrmModeModeinfo(ctypes.Structure): + _fields_ = [ + ("clock", ctypes.c_uint32), + ("hdisplay", ctypes.c_uint16), ("hsync_start", ctypes.c_uint16), + ("hsync_end", ctypes.c_uint16), ("htotal", ctypes.c_uint16), + ("hskew", ctypes.c_uint16), + ("vdisplay", ctypes.c_uint16), ("vsync_start", ctypes.c_uint16), + ("vsync_end", ctypes.c_uint16), ("vtotal", ctypes.c_uint16), + ("vscan", ctypes.c_uint16), + ("vrefresh", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("type", ctypes.c_uint32), + ("name", ctypes.c_char * 32), + ] + + +class DrmModeGetConnector(ctypes.Structure): + _fields_ = [ + ("encoders_ptr", ctypes.c_uint64), + ("modes_ptr", ctypes.c_uint64), + ("props_ptr", ctypes.c_uint64), + ("prop_values_ptr", ctypes.c_uint64), + ("count_modes", ctypes.c_uint32), + ("count_props", ctypes.c_uint32), + ("count_encoders", ctypes.c_uint32), + ("encoder_id", ctypes.c_uint32), + ("connector_id", ctypes.c_uint32), + ("connector_type", ctypes.c_uint32), + ("connector_type_id", ctypes.c_uint32), + ("connection", ctypes.c_uint32), + ("mm_width", ctypes.c_uint32), + ("mm_height", ctypes.c_uint32), + ("subpixel", ctypes.c_uint32), + ("pad", ctypes.c_uint32), + ] + + +class DrmModeGetEncoder(ctypes.Structure): + _fields_ = [ + ("encoder_id", ctypes.c_uint32), + ("encoder_type", ctypes.c_uint32), + ("crtc_id", ctypes.c_uint32), + ("possible_crtcs", ctypes.c_uint32), + ("possible_clones", ctypes.c_uint32), + ] + + +class DrmModeCreateDumb(ctypes.Structure): + _fields_ = [ + ("height", ctypes.c_uint32), + ("width", ctypes.c_uint32), + ("bpp", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("handle", ctypes.c_uint32), + ("pitch", ctypes.c_uint32), + ("size", ctypes.c_uint64), + ] + + +class DrmModeFbCmd(ctypes.Structure): + _fields_ = [ + ("fb_id", ctypes.c_uint32), + ("width", ctypes.c_uint32), + ("height", ctypes.c_uint32), + ("pitch", ctypes.c_uint32), + ("bpp", ctypes.c_uint32), + ("depth", ctypes.c_uint32), + ("handle", ctypes.c_uint32), + ] + + +class DrmModeMapDumb(ctypes.Structure): + _fields_ = [ + ("handle", ctypes.c_uint32), + ("pad", ctypes.c_uint32), + ("offset", ctypes.c_uint64), + ] + + +class DrmModeDestroyDumb(ctypes.Structure): + _fields_ = [ + ("handle", ctypes.c_uint32), + ] + + +class DrmModeCrtc(ctypes.Structure): + _fields_ = [ + ("set_connectors_ptr", ctypes.c_uint64), + ("count_connectors", ctypes.c_uint32), + ("crtc_id", ctypes.c_uint32), + ("fb_id", ctypes.c_uint32), + ("x", ctypes.c_uint32), + ("y", ctypes.c_uint32), + ("gamma_size", ctypes.c_uint32), + ("mode_valid", ctypes.c_uint32), + ("mode", DrmModeModeinfo), + ] + + +GETRESOURCES = _IOWR(0xA0, DrmModeRes) +SETCRTC = _IOWR(0xA2, DrmModeCrtc) +GETENCODER = _IOWR(0xA6, DrmModeGetEncoder) +GETCONNECTOR = _IOWR(0xA7, DrmModeGetConnector) +ADDFB = _IOWR(0xAE, DrmModeFbCmd) +CREATE_DUMB = _IOWR(0xB2, DrmModeCreateDumb) +MAP_DUMB = _IOWR(0xB3, DrmModeMapDumb) +DESTROY_DUMB = _IOWR(0xB4, DrmModeDestroyDumb) +RMFB = _IOWR(0xAF, ctypes.c_uint32) + +DRM_MODE_CONNECTED = 1 + +# CEA-861 modelines to fall back on when a connected connector reports zero +# modes - the LT8912B DSI-to-HDMI bridge on the Toradex DSI adapter often +# fails the EDID read, leaving the connector "connected" but modeless. +# (clock kHz, hd, hss, hse, ht, vd, vss, vse, vt, refresh, flags PHSYNC|PVSYNC) +FALLBACK_MODES = ( + ("1920x1080", 148500, 1920, 2008, 2052, 2200, 1080, 1084, 1089, 1125, 60), + ("1280x720", 74250, 1280, 1390, 1430, 1650, 720, 725, 730, 750, 60), +) + + +def _make_mode(name, clock, hd, hss, hse, ht, vd, vss, vse, vt, hz): + m = DrmModeModeinfo() + m.clock = clock + m.hdisplay, m.hsync_start, m.hsync_end, m.htotal = hd, hss, hse, ht + m.vdisplay, m.vsync_start, m.vsync_end, m.vtotal = vd, vss, vse, vt + m.vrefresh = hz + m.flags = 0x5 # PHSYNC | PVSYNC + m.type = 1 << 3 # DRM_MODE_TYPE_PREFERRED + m.name = name.encode() + return m + +# The few SGR colour codes twopane.py emits, as XRGB8888. +COLORS = { + "0": 0x00CCCCCC, # reset -> light grey + "1;36": 0x0000E5E5, # bold cyan (title) + "1;33": 0x00E5C000, # bold yellow (pane headers) + "1;30": 0x00555555, # dim (separator) +} +DEFAULT = COLORS["0"] + + +def _arr(count): + return (ctypes.c_uint32 * max(1, count))() + + +def _release_fbcon(): + """Drop the kernel framebuffer console from the DRM device. When Linux is + booted by wolfBoot the console lands on the DP framebuffer, so fbcon holds + DRM master and SETCRTC fails with EACCES. Unbinding it (root, best effort) + lets this renderer master the CRTC. Harmless if fbcon is not on the fb.""" + import glob + for name in glob.glob("/sys/class/vtconsole/vtcon*/name"): + try: + with open(name) as f: + if "frame buffer device" not in f.read(): + continue + with open(name.replace("name", "bind"), "w") as f: + f.write("0") + except OSError: + pass + + +class DrmFramebuffer: + def __init__(self, path="/dev/dri/card0", scale=2): + _release_fbcon() + self.fd = os.open(path, os.O_RDWR) + self.scale = scale + + res = DrmModeRes() + fcntl.ioctl(self.fd, GETRESOURCES, res) + # Arrays for every category the kernel reported: leaving any pointer + # NULL while its count is nonzero makes the kernel write to 0 (EFAULT). + fbs = _arr(res.count_fbs) + crtcs = _arr(res.count_crtcs) + conns = _arr(res.count_connectors) + encs_r = _arr(res.count_encoders) + res.fb_id_ptr = ctypes.addressof(fbs) + res.crtc_id_ptr = ctypes.addressof(crtcs) + res.connector_id_ptr = ctypes.addressof(conns) + res.encoder_id_ptr = ctypes.addressof(encs_r) + fcntl.ioctl(self.fd, GETRESOURCES, res) + + # Find a connected connector and its preferred (first) mode + self.mode = None + self.conn_id = None + for i in range(res.count_connectors): + conn = DrmModeGetConnector() + conn.connector_id = conns[i] + fcntl.ioctl(self.fd, GETCONNECTOR, conn) + n_modes = conn.count_modes + modes = (DrmModeModeinfo * max(1, n_modes))() + encs = _arr(conn.count_encoders) + props = _arr(conn.count_props) + propv = (ctypes.c_uint64 * max(1, conn.count_props))() + conn.modes_ptr = ctypes.addressof(modes) + conn.encoders_ptr = ctypes.addressof(encs) + conn.props_ptr = ctypes.addressof(props) + conn.prop_values_ptr = ctypes.addressof(propv) + fcntl.ioctl(self.fd, GETCONNECTOR, conn) + if conn.connection != DRM_MODE_CONNECTED and \ + not os.environ.get("DRM_FORCE"): + # DRM_FORCE=1 accepts a disconnected connector: the LT8912B + # adapter's hotplug detect is unreliable, and the CEA fallback + # modes need no EDID, so a forced modeset can still light a + # display HPD never reported. + continue + self.conn_id = conn.connector_id + if conn.count_modes > 0: + self.mode = DrmModeModeinfo() + ctypes.memmove(ctypes.addressof(self.mode), + ctypes.addressof(modes[0]), + ctypes.sizeof(DrmModeModeinfo)) + else: + # Connected but modeless (EDID failed through the bridge): + # fall back to a standard CEA mode. SETCRTC below validates + # it; __init__ retries the next fallback on failure. + self.mode = None + enc = DrmModeGetEncoder() + enc.encoder_id = conn.encoder_id if conn.encoder_id else encs[0] + fcntl.ioctl(self.fd, GETENCODER, enc) + self.crtc_id = enc.crtc_id if enc.crtc_id else crtcs[0] + break + if self.conn_id is None: + raise RuntimeError("no connected DRM connector") + + candidates = [self.mode] if self.mode is not None else [_make_mode(*fm) for fm in FALLBACK_MODES] + + err = None + for mode in candidates: + try: + self._setup(mode) + return + except OSError as e: + err = e + self._release_buffers() + raise RuntimeError("no mode accepted by SETCRTC: %s" % err) + + def _release_buffers(self): + """Drop every framebuffer and dumb handle allocated by _setup(). + + SETCRTC rejects a mode often enough that the fallback list exists, and + each rejected attempt would otherwise strand its buffers in the kernel + for the lifetime of the fd - the mmap alone is not enough, since the + GEM handle outlives it.""" + for mm in getattr(self, "_maps", []): + try: + mm.close() + except Exception: + pass + for fb_id in getattr(self, "_fb_ids", []): + try: + fcntl.ioctl(self.fd, RMFB, (ctypes.c_uint32 * 1)(fb_id)) + except OSError: + pass + for handle in getattr(self, "_handles", []): + try: + req = DrmModeDestroyDumb() + req.handle = handle + fcntl.ioctl(self.fd, DESTROY_DUMB, req) + except OSError: + pass + self._maps = [] + self._fb_ids = [] + self._handles = [] + + def _create_dumb(self): + """Allocate one dumb buffer + framebuffer, return (fb_id, mmap).""" + dumb = DrmModeCreateDumb() + dumb.width = self.width + dumb.height = self.height + dumb.bpp = 32 + fcntl.ioctl(self.fd, CREATE_DUMB, dumb) + # Record the handle before anything else can fail: ADDFB, MAP_DUMB and + # mmap below all raise OSError on a mode the CRTC rejects, and the + # fallback loop catches that - without this the buffer would be + # stranded in the kernel for the lifetime of the fd. + self._handles.append(dumb.handle) + self.pitch = dumb.pitch + + fb = DrmModeFbCmd() + fb.width, fb.height = self.width, self.height + fb.pitch, fb.bpp, fb.depth = dumb.pitch, 32, 24 + fb.handle = dumb.handle + fcntl.ioctl(self.fd, ADDFB, fb) + + mreq = DrmModeMapDumb() + mreq.handle = dumb.handle + fcntl.ioctl(self.fd, MAP_DUMB, mreq) + mm = mmap.mmap(self.fd, dumb.size, mmap.MAP_SHARED, + mmap.PROT_READ | mmap.PROT_WRITE, offset=mreq.offset) + return fb.fb_id, mm + + def _setcrtc(self, fb_id): + crtc = DrmModeCrtc() + conn_arr = (ctypes.c_uint32 * 1)(self.conn_id) + crtc.set_connectors_ptr = ctypes.addressof(conn_arr) + crtc.count_connectors = 1 + crtc.crtc_id = self.crtc_id + crtc.fb_id = fb_id + crtc.mode_valid = 1 + ctypes.memmove(ctypes.addressof(crtc.mode), + ctypes.addressof(self.mode), + ctypes.sizeof(DrmModeModeinfo)) + fcntl.ioctl(self.fd, SETCRTC, crtc) + + def _setup(self, mode): + self.mode = mode + self.width = self.mode.hdisplay + self.height = self.mode.vdisplay + self._handles = [] + + # Double-buffered: render the whole frame into the off-screen back + # buffer, then SETCRTC-flip the scanout to it. The display only ever + # shows a complete frame, so the repaint is never visible - the fix + # for "you can watch it redraw" (single-buffering paints the live + # scanned-out buffer in place). + self.fb_id, self.map = self._create_dumb() + self._fb_ids = [self.fb_id] + self._maps = [self.map] + self._front = 0 + try: + fb1, mm1 = self._create_dumb() + self._fb_ids.append(fb1) + self._maps.append(mm1) + except OSError: + pass # single-buffered fallback if a second buffer won't allocate + self._setcrtc(self.fb_id) + + # Prefer a crisp TrueType renderer (Pillow) when a font is present; + # fall back to the built-in 8x8 bitmap otherwise. The bitmap path + # scaled 2x is blocky and slow; Pillow draws antialiased glyphs and + # blits the whole frame in one memcpy. + self.pil = None + font_px = int(os.environ.get("DRM_FONT_PX", "18")) + font_path = os.environ.get("DRM_FONT", + os.path.join(os.path.dirname(__file__), + "font.ttf")) + try: + from PIL import Image, ImageDraw, ImageFont + if os.path.exists(font_path): + self._pil_mod = (Image, ImageDraw, ImageFont) + self._font = ImageFont.truetype(font_path, font_px) + self._img = Image.new("RGB", (self.width, self.height), + (16, 16, 16)) + self._draw = ImageDraw.Draw(self._img) + # Monospace cell metrics from the font itself. + self.cell_w = int(round(self._font.getlength("M"))) or font_px + asc, desc = self._font.getmetrics() + self.cell_h = asc + desc + self.cols = self.width // self.cell_w + self.rows = self.height // self.cell_h + self.pil = True + except Exception as e: + self.pil = None + if os.path.exists(font_path): + import sys + sys.stderr.write("drmfb: font present but PIL path " + "unavailable (%s); using blocky bitmap " + "fallback\n" % e) + sys.stderr.flush() + + if not self.pil: + # Built-in 8x8 bitmap fallback. + self.cell_w = 8 * self.scale + self.cell_h = 8 * self.scale + self.cols = self.width // self.cell_w + self.rows = self.height // self.cell_h + + def _put_glyph(self, cx, cy, ch, color): + glyph = FONT.get(ord(ch)) + if glyph is None: + glyph = FONT[ord('?')] + s = self.scale + x0 = cx * self.cell_w + y0 = cy * self.cell_h + pitch = self.pitch + for gy in range(8): + bits = glyph[gy] + rowbytes = bytearray() + for gx in range(8): + v = color if (bits >> gx) & 1 else 0x00101010 + rowbytes += int(v).to_bytes(4, "little") * s + for sy in range(s): + off = (y0 + gy * s + sy) * pitch + x0 * 4 + self.map[off:off + len(rowbytes)] = rowbytes + + def draw_lines(self, lines, ansi_re): + """Render a list of strings. Understands only the SGR codes in COLORS; + everything else is stripped by ansi_re before drawing. + + Only cells that changed since the last frame are redrawn. Pushing the + whole 1920x1080 buffer from Python every frame is what made the refresh + crawl; between frames almost every cell is identical (only the + benchmark lines scroll), so the per-cell (char,color) cache turns a + full repaint into a few dozen glyph writes. """ + if self.pil: + self._draw_lines_pil(lines, ansi_re) + return + if not hasattr(self, "_cache"): + self._cache = {} + for cy in range(self.rows): + raw = lines[cy] if cy < len(lines) else "" + color = DEFAULT + cx = 0 + i = 0 + while cx < self.cols: + ch = " " + if i < len(raw): + if raw[i] == "\x1b": + m = ansi_re.match(raw, i) + if m: + if m.group(0).endswith("m"): + color = COLORS.get(m.group(0)[2:-1], DEFAULT) + i = m.end() + continue + i += 1 + continue + ch = raw[i] + i += 1 + key = (cx, cy) + cell = (ch, color) + if self._cache.get(key) != cell: + self._put_glyph(cx, cy, ch, color) + self._cache[key] = cell + cx += 1 + + @staticmethod + def _rgb(color): + return ((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF) + + def _draw_lines_pil(self, lines, ansi_re): + """Crisp path: render the whole frame into a PIL image with a real + monospace font, then blit it to the off-screen back buffer and flip. + Runs a segment at a time so per-run SGR colors are honored.""" + # Skip the whole frame when nothing changed - most ticks only the + # left benchmark pane scrolls, and an unchanged frame need not repaint + # or flip at all. + sig = tuple(lines) + if getattr(self, "_last_sig", None) == sig: + return + self._last_sig = sig + draw = self._draw + draw.rectangle((0, 0, self.width, self.height), fill=(16, 16, 16)) + font = self._font + cw, ch = self.cell_w, self.cell_h + for cy in range(self.rows): + raw = lines[cy] if cy < len(lines) else "" + color = DEFAULT + cx = 0 + i = 0 + seg = [] + seg_x = 0 + while i < len(raw) and cx < self.cols: + if raw[i] == "\x1b": + m = ansi_re.match(raw, i) + if m: + if m.group(0).endswith("m"): + if seg: + draw.text((seg_x * cw, cy * ch), "".join(seg), + font=font, fill=self._rgb(color)) + seg = [] + color = COLORS.get(m.group(0)[2:-1], DEFAULT) + seg_x = cx + i = m.end() + continue + i += 1 + continue + if not seg: + seg_x = cx + seg.append(raw[i]) + cx += 1 + i += 1 + if seg: + draw.text((seg_x * cw, cy * ch), "".join(seg), + font=font, fill=self._rgb(color)) + # Blit into the back buffer: PIL RGB -> XRGB8888 little-endian + # (memory bytes B,G,R,X) = PIL "BGRX". + back = 1 - self._front if len(self._maps) > 1 else 0 + bmap = self._maps[back] + raw = self._img.tobytes("raw", "BGRX") + if self.pitch == self.width * 4: + bmap[0:len(raw)] = raw + else: + rb = self.width * 4 + for y in range(self.height): + bmap[y * self.pitch:y * self.pitch + rb] = \ + raw[y * rb:y * rb + rb] + # Flip the scanout to the freshly rendered buffer (atomic frame swap). + if len(self._maps) > 1: + self._setcrtc(self._fb_ids[back]) + self._front = back diff --git a/imx95-pqc-demo/demo/font8x8.py b/imx95-pqc-demo/demo/font8x8.py new file mode 100644 index 0000000..650c7ac --- /dev/null +++ b/imx95-pqc-demo/demo/font8x8.py @@ -0,0 +1,104 @@ +"""8x8 bitmap font for the DRM renderer. + +Derived from font8x8_basic (https://github.com/dhepper/font8x8), +public domain. Each glyph is 8 rows of 8 bits, LSB = leftmost +pixel, indexed by ASCII codepoint. Printable ASCII only. +""" + +FONT = { + 32: [0, 0, 0, 0, 0, 0, 0, 0], + 33: [24, 60, 60, 24, 24, 0, 24, 0], + 34: [54, 54, 0, 0, 0, 0, 0, 0], + 35: [54, 54, 127, 54, 127, 54, 54, 0], + 36: [12, 62, 3, 30, 48, 31, 12, 0], + 37: [0, 99, 51, 24, 12, 102, 99, 0], + 38: [28, 54, 28, 110, 59, 51, 110, 0], + 39: [6, 6, 3, 0, 0, 0, 0, 0], + 40: [24, 12, 6, 6, 6, 12, 24, 0], + 41: [6, 12, 24, 24, 24, 12, 6, 0], + 42: [0, 102, 60, 255, 60, 102, 0, 0], + 43: [0, 12, 12, 63, 12, 12, 0, 0], + 44: [0, 0, 0, 0, 0, 12, 12, 6], + 45: [0, 0, 0, 63, 0, 0, 0, 0], + 46: [0, 0, 0, 0, 0, 12, 12, 0], + 47: [96, 48, 24, 12, 6, 3, 1, 0], + 48: [62, 99, 115, 123, 111, 103, 62, 0], + 49: [12, 14, 12, 12, 12, 12, 63, 0], + 50: [30, 51, 48, 28, 6, 51, 63, 0], + 51: [30, 51, 48, 28, 48, 51, 30, 0], + 52: [56, 60, 54, 51, 127, 48, 120, 0], + 53: [63, 3, 31, 48, 48, 51, 30, 0], + 54: [28, 6, 3, 31, 51, 51, 30, 0], + 55: [63, 51, 48, 24, 12, 12, 12, 0], + 56: [30, 51, 51, 30, 51, 51, 30, 0], + 57: [30, 51, 51, 62, 48, 24, 14, 0], + 58: [0, 12, 12, 0, 0, 12, 12, 0], + 59: [0, 12, 12, 0, 0, 12, 12, 6], + 60: [24, 12, 6, 3, 6, 12, 24, 0], + 61: [0, 0, 63, 0, 0, 63, 0, 0], + 62: [6, 12, 24, 48, 24, 12, 6, 0], + 63: [30, 51, 48, 24, 12, 0, 12, 0], + 64: [62, 99, 123, 123, 123, 3, 30, 0], + 65: [12, 30, 51, 51, 63, 51, 51, 0], + 66: [63, 102, 102, 62, 102, 102, 63, 0], + 67: [60, 102, 3, 3, 3, 102, 60, 0], + 68: [31, 54, 102, 102, 102, 54, 31, 0], + 69: [127, 70, 22, 30, 22, 70, 127, 0], + 70: [127, 70, 22, 30, 22, 6, 15, 0], + 71: [60, 102, 3, 3, 115, 102, 124, 0], + 72: [51, 51, 51, 63, 51, 51, 51, 0], + 73: [30, 12, 12, 12, 12, 12, 30, 0], + 74: [120, 48, 48, 48, 51, 51, 30, 0], + 75: [103, 102, 54, 30, 54, 102, 103, 0], + 76: [15, 6, 6, 6, 70, 102, 127, 0], + 77: [99, 119, 127, 127, 107, 99, 99, 0], + 78: [99, 103, 111, 123, 115, 99, 99, 0], + 79: [28, 54, 99, 99, 99, 54, 28, 0], + 80: [63, 102, 102, 62, 6, 6, 15, 0], + 81: [30, 51, 51, 51, 59, 30, 56, 0], + 82: [63, 102, 102, 62, 54, 102, 103, 0], + 83: [30, 51, 7, 14, 56, 51, 30, 0], + 84: [63, 45, 12, 12, 12, 12, 30, 0], + 85: [51, 51, 51, 51, 51, 51, 63, 0], + 86: [51, 51, 51, 51, 51, 30, 12, 0], + 87: [99, 99, 99, 107, 127, 119, 99, 0], + 88: [99, 99, 54, 28, 28, 54, 99, 0], + 89: [51, 51, 51, 30, 12, 12, 30, 0], + 90: [127, 99, 49, 24, 76, 102, 127, 0], + 91: [30, 6, 6, 6, 6, 6, 30, 0], + 92: [3, 6, 12, 24, 48, 96, 64, 0], + 93: [30, 24, 24, 24, 24, 24, 30, 0], + 94: [8, 28, 54, 99, 0, 0, 0, 0], + 95: [0, 0, 0, 0, 0, 0, 0, 255], + 96: [12, 12, 24, 0, 0, 0, 0, 0], + 97: [0, 0, 30, 48, 62, 51, 110, 0], + 98: [7, 6, 6, 62, 102, 102, 59, 0], + 99: [0, 0, 30, 51, 3, 51, 30, 0], + 100: [56, 48, 48, 62, 51, 51, 110, 0], + 101: [0, 0, 30, 51, 63, 3, 30, 0], + 102: [28, 54, 6, 15, 6, 6, 15, 0], + 103: [0, 0, 110, 51, 51, 62, 48, 31], + 104: [7, 6, 54, 110, 102, 102, 103, 0], + 105: [12, 0, 14, 12, 12, 12, 30, 0], + 106: [48, 0, 48, 48, 48, 51, 51, 30], + 107: [7, 6, 102, 54, 30, 54, 103, 0], + 108: [14, 12, 12, 12, 12, 12, 30, 0], + 109: [0, 0, 51, 127, 127, 107, 99, 0], + 110: [0, 0, 31, 51, 51, 51, 51, 0], + 111: [0, 0, 30, 51, 51, 51, 30, 0], + 112: [0, 0, 59, 102, 102, 62, 6, 15], + 113: [0, 0, 110, 51, 51, 62, 48, 120], + 114: [0, 0, 59, 110, 102, 6, 15, 0], + 115: [0, 0, 62, 3, 30, 48, 31, 0], + 116: [8, 12, 62, 12, 12, 44, 24, 0], + 117: [0, 0, 51, 51, 51, 51, 110, 0], + 118: [0, 0, 51, 51, 51, 30, 12, 0], + 119: [0, 0, 99, 107, 127, 127, 54, 0], + 120: [0, 0, 99, 54, 28, 54, 99, 0], + 121: [0, 0, 51, 51, 51, 62, 48, 31], + 122: [0, 0, 63, 25, 12, 38, 63, 0], + 123: [56, 12, 12, 7, 12, 12, 56, 0], + 124: [24, 24, 24, 0, 24, 24, 24, 0], + 125: [7, 12, 12, 56, 12, 12, 7, 0], + 126: [110, 59, 0, 0, 0, 0, 0, 0], +} diff --git a/imx95-pqc-demo/demo/install-autostart.sh b/imx95-pqc-demo/demo/install-autostart.sh new file mode 100755 index 0000000..131eddf --- /dev/null +++ b/imx95-pqc-demo/demo/install-autostart.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Install and enable the boot-time demo unit. Run ON THE BOARD as root. +# +# sudo bash install-autostart.sh # install + enable +# sudo bash install-autostart.sh --off # disable, restore the getty +set -euo pipefail +UNIT=/etc/systemd/system/wolfssl-demo.service +M7_UNIT=/etc/systemd/system/wolfssl-m7.service +HERE="$(cd "$(dirname "$0")" && pwd)" + +if [ "${1:-}" = "--off" ]; then + systemctl disable --now wolfssl-demo.service 2>/dev/null || true + systemctl disable --now wolfssl-m7.service 2>/dev/null || true + systemctl start getty@tty1.service 2>/dev/null || true + sync + echo "demo autostart disabled, getty on tty1 restored" + exit 0 +fi + +install -m 0644 "$HERE/wolfssl-demo.service" "$UNIT" +install -m 0644 "$HERE/wolfssl-m7.service" "$M7_UNIT" +systemctl daemon-reload +systemctl enable wolfssl-demo.service wolfssl-m7.service +# The demo's replay beat is a hard power cut, so nothing may sit in the page +# cache: an unsynced unit file is simply gone after the next cycle. +sync +echo "installed and enabled: $UNIT and $M7_UNIT" +echo "the demo now starts on boot; power cycle is the whole replay" +echo "disable with: sudo bash $HERE/install-autostart.sh --off" diff --git a/imx95-pqc-demo/demo/m7-console-tail.sh b/imx95-pqc-demo/demo/m7-console-tail.sh new file mode 100755 index 0000000..f66e8d0 --- /dev/null +++ b/imx95-pqc-demo/demo/m7-console-tail.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Right demo pane: live view of the Cortex-M7 console. +# +# Runs ON THE BOARD (the HDMI output is the board's own), so no host connection +# is involved once the demo is running. +# +# wolfBoot writes its log to a shared-memory ring at 0x80F00000 before any RPMsg +# endpoint exists, so this reads the ring directly rather than going through +# rpmsg_tty. That is deliberate: the ring is the only source that contains +# wolfBoot's own PQC verification output, and it needs no driver, no endpoint +# binding and no module load. +# +# memtool dumps the whole ring each call, so track how much has already been +# shown and print only what is new. +# +# Must run as root (/dev/mem). The demo launcher starts the whole tmux session +# under sudo so no password prompt can appear mid-demo. +set -uo pipefail + +MEMTOOL=${MEMTOOL:-/home/torizon/bin/memtool} +ADDR=${ADDR:-0x80F00000} +INTERVAL=${INTERVAL:-0.5} + +printf '\033[1;36m' +cat <<'BANNER' ++--------------------------------------------------------------+ +| NXP i.MX95 Cortex-M7 - wolfBoot secure boot | +| ML-DSA-87 (NIST level 5) post-quantum verified boot | ++--------------------------------------------------------------+ +BANNER +printf '\033[0m\n' +echo "waiting for the M7 to boot ..." +echo + +shown=0 +while true; do + out=$("$MEMTOOL" con "$ADDR" 2>/dev/null) || { sleep "$INTERVAL"; continue; } + len=${#out} + if [ "$len" -gt "$shown" ]; then + printf '%s' "${out:$shown}" + shown=$len + elif [ "$len" -lt "$shown" ]; then + # Ring was re-initialised (the M7 restarted): start over. + printf '\n\033[1;33m--- M7 restarted ---\033[0m\n' + printf '%s' "$out" + shown=$len + fi + sleep "$INTERVAL" +done diff --git a/imx95-pqc-demo/demo/m7-rpmsg-log.sh b/imx95-pqc-demo/demo/m7-rpmsg-log.sh new file mode 100755 index 0000000..6d5d79d --- /dev/null +++ b/imx95-pqc-demo/demo/m7-rpmsg-log.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Read wolfBoot's verify log from the M7 over RPMsg. Runs ON THE BOARD as root. +# +# Order matters, and not in the obvious way. The payload relays the whole +# console ring as soon as its endpoint has a destination address, which the +# host supplies while binding the channel. That is strictly before +# /dev/ttyRPMSG* exists, so a reader can never be attached in time for the +# first pass, and the log it sends is discarded by a tty nobody has open. +# +# The payload therefore treats any byte written to the tty as a request to +# rewind and send the log again. So: attach the reader, then poke. +# +# The demo's right pane does NOT use this path - it reads the shared-memory +# ring directly, which needs no driver, no bind and no module load. This script +# is for showing the same log arriving as a normal Linux tty. +set -uo pipefail + +SECS=${SECS:-8} +modprobe imx_rpmsg_tty 2>/dev/null || true + +TTY="" +for _ in $(seq 1 30); do + TTY=$(ls /dev/ttyRPMSG* 2>/dev/null | head -1) + [ -n "$TTY" ] && break + sleep 1 +done +if [ -z "$TTY" ]; then + echo "no /dev/ttyRPMSG* - is the M7 running? (cat /sys/class/remoteproc/remoteproc1/state)" >&2 + exit 1 +fi + +# raw so the log is not line-edited on its way through the line discipline +stty -F "$TTY" raw -echo clocal + +timeout "$SECS" cat "$TTY" & +reader=$! +sleep 1 +printf '\n' > "$TTY" # request the replay +wait $reader 2>/dev/null +exit 0 diff --git a/imx95-pqc-demo/demo/m7-start.sh b/imx95-pqc-demo/demo/m7-start.sh new file mode 100755 index 0000000..f0ef7a0 --- /dev/null +++ b/imx95-pqc-demo/demo/m7-start.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Load wolfBoot + the signed payload onto the Cortex-M7 and release the core. +# Runs ON THE BOARD as root, so the demo needs no host connection. +# +# The M7 can only be started ONCE per Linux boot - "echo stop" fails on this BSP +# with "Interrupted system call" and the core stays running. So a second run of +# the demo needs a full power cycle, not a restart. That is why the demo's +# "reboot beat" is a real power cycle. +set -euo pipefail + +FW=/home/torizon/demo/fw +M=/home/torizon/bin/memtool +RP=/sys/class/remoteproc/remoteproc1 +BOOT_ADDR=0x80100000 +STATUS=0x80F10000 + +if [ "$(cat $RP/state)" = "running" ]; then + echo "M7 is already running - power cycle the board to run the demo again" >&2 + exit 1 +fi + +$M fill $STATUS 32 0 +$M load $BOOT_ADDR "$FW/payload.bin" +echo "$FW" > /sys/module/firmware_class/parameters/path +echo start > $RP/state +sleep 1 +echo "M7 state: $(cat $RP/state)" diff --git a/imx95-pqc-demo/demo/stage.sh b/imx95-pqc-demo/demo/stage.sh new file mode 100755 index 0000000..b687998 --- /dev/null +++ b/imx95-pqc-demo/demo/stage.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Copy the demo onto the board. Uses ssh keys - no passwords here. +# +# BOARD=torizon@ WOLFBOOT=/path/to/wolfboot ./stage.sh +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +DEMO="$(cd "$HERE/.." && pwd)" +BOARD="${BOARD:?set BOARD, e.g. BOARD=torizon@192.168.1.50}" +WOLFBOOT="${WOLFBOOT:?set WOLFBOOT to your wolfBoot build directory}" +PAYLOAD="${PAYLOAD:-$DEMO/m7/build/zephyr/payload_v1_signed.bin}" + +[ -f "$WOLFBOOT/wolfboot.elf" ] || { echo "no wolfboot.elf in $WOLFBOOT" >&2; exit 1; } +[ -f "$PAYLOAD" ] || { echo "no signed payload at $PAYLOAD" >&2; exit 1; } + +ssh "$BOARD" 'mkdir -p ~/demo/fw ~/demo/results ~/bin' + +scp -q "$DEMO/demo/twopane.py" "$DEMO/demo/drmfb.py" "$DEMO/demo/font8x8.py" \ + "$DEMO/demo/demo-run.sh" \ + "$DEMO/demo/m7-start.sh" "$DEMO/demo/m7-console-tail.sh" \ + "$DEMO/demo/m7-rpmsg-log.sh" "$DEMO/demo/install-autostart.sh" \ + "$DEMO/demo/wolfssl-demo.service" "$DEMO/demo/wolfssl-m7.service" \ + "$DEMO/demo/demo-uart.sh" "$DEMO/demo/wolfssl-demo-uart.service" \ + "$BOARD:~/demo/" +scp -q "$DEMO/container/docker-compose.yml" "$BOARD:~/demo/" +scp -q "$WOLFBOOT/wolfboot.elf" "$BOARD:~/demo/fw/rproc-imx-rproc-fw" +scp -q "$PAYLOAD" "$BOARD:~/demo/fw/payload.bin" + +# memtool is how both the demo and the M7 console reader reach /dev/mem. +aarch64-linux-gnu-gcc -O2 -o /tmp/memtool "$DEMO/tools/memtool.c" +scp -q /tmp/memtool "$BOARD:~/bin/memtool" + +# The demo is replayed by cutting power, so anything still in the page cache is +# lost on the next cycle and the board silently runs the previous payload. +# Flush, then verify the payload by content rather than by timestamp. +ssh "$BOARD" 'sync' +want=$(md5sum "$PAYLOAD" | cut -d' ' -f1) +got=$(ssh "$BOARD" 'md5sum ~/demo/fw/payload.bin' | cut -d' ' -f1) +if [ "$want" != "$got" ]; then + echo "payload mismatch after staging: local $want, board $got" >&2 + exit 1 +fi + +echo "staged and verified. On the board: sudo bash ~/demo/demo-run.sh /dev/tty1" diff --git a/imx95-pqc-demo/demo/twopane.py b/imx95-pqc-demo/demo/twopane.py new file mode 100644 index 0000000..09c5315 --- /dev/null +++ b/imx95-pqc-demo/demo/twopane.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +"""Two-pane i.MX95 demo renderer. + +Torizon OS ships no tmux, screen or dtach, and its rootfs is read-only OSTree, +so there is nothing to install. For a demo that is exactly two fixed panes of +append-only text, a full redraw is simpler and more predictable than shipping a +static multiplexer: no incremental cursor management, and a resize or a stray +escape sequence cannot corrupt the layout permanently. + + left - wolfCrypt PQC benchmarks, in a container on the Cortex-A55 cluster + right - wolfBoot ML-DSA-87 verified boot of the Cortex-M7 + +Run as root (the right pane reads /dev/mem via memtool), on the console you +want it displayed on: + + sudo python3 twopane.py # current terminal + sudo python3 twopane.py > /dev/tty1 # the HDMI console +""" + +import os +import re +import shutil +import subprocess +import sys +import time + +CONTAINER = os.environ.get("CONTAINER", "wolfcrypt-pqc") +MEMTOOL = os.environ.get("MEMTOOL", "/home/torizon/bin/memtool") +CONSOLE_ADDR = os.environ.get("CONSOLE_ADDR", "0x80F00000") +A55_CONSOLE_ADDR = os.environ.get("A55_CONSOLE_ADDR", "0x80F20000") +INTERVAL = float(os.environ.get("INTERVAL", "1.0")) + +ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") + +# The benchmark's cycle columns derive from the 24 MHz generic timer, not the +# 1.8 GHz core clock, so they are wrong by roughly 75x. They must never appear +# on a demo screen someone might photograph. Strip them; ops/sec and ms are the +# numbers that are actually correct. +CYCLES = re.compile(r",?\s*\d+\s+cycles\s+[\d.]+\s+Cycles/op\s*$") +CPB = re.compile(r"\s*Cycles per byte\s*=\s*[\d.]+\s*$") + +# The raw benchmark line is far too wide for half a console, and truncating it +# cuts off ops/sec - the one number worth showing. Condense to +# " " so each result fits on one readable row. +# The A55 boot log is long and most of it is bookkeeping: the handoff register +# dump, the MBR walk, and a line per FIT sub-image copied. Only the chain of +# trust belongs on a demo screen, and the pane holds far fewer rows than the log +# has lines, so without this the verification scrolls off before anyone reads it. +BOOT_KEEP = re.compile( + r"^(wolfBoot:|scmi:|usdhc:|Boot partition:|Booting version|" + r"Checking image integrity|Verifying image signature|info: ML-DSA|" + r"info: using ML-DSA|info: wc_MlDsaKey_Verify|Firmware Valid|Booting at)") + +OPS = re.compile(r"^(.*?)\s+\d+ ops took [\d.]+ sec, avg [\d.]+ ms,\s+([\d.]+) ops/sec") +THRU = re.compile(r"^(\S.*?)\s+[\d.]+ [KMG]iB took [\d.]+ seconds,\s+([\d.]+) ([KMG]iB/s)") + + +# The HDMI console on this board is 80x25, so each pane gets ~38 columns. +# Benchmark labels have to lose their redundant parameter fields to fit. +SHORTEN = ( + ("[ SECP256R1]", "P-256"), + ("[ SECP256R1]", "P-256"), + ("ML-KEM 512 128", "ML-KEM-512"), + ("ML-KEM 768 192", "ML-KEM-768"), + ("ML-KEM 1024 256", "ML-KEM-1024"), + ("ML-DSA 44", "ML-DSA-44"), + ("ML-DSA 65", "ML-DSA-65"), + ("ML-DSA 87", "ML-DSA-87"), + ("RSA 2048", "RSA-2048"), +) + + +def shorten(name): + for a, b in SHORTEN: + name = name.replace(a, b) + return " ".join(name.split()) + + +def condense(ln, width): + m = THRU.match(ln) + if m: + unit = m.group(3) if width >= 46 else m.group(3).replace("iB/s", "B/s") + value = f"{float(m.group(2)):,.1f} {unit}" + else: + m = OPS.match(ln) + if not m: + return ln + unit = "ops/sec" if width >= 46 else "/s" + value = f"{float(m.group(2)):,.0f} {unit}" + + # Pad the label to exactly what is left, so the value always lands flush + # right and nothing is clipped. + wname = max(6, width - len(value) - 1) + return f"{shorten(m.group(1))[:wname]:<{wname}} {value:>{len(value)}}" + +LEFT_TITLE = " Cortex-A55: wolfCrypt PQC (on 1 of 6 cores) " +LEFT_BOOT_TITLE = " Cortex-A55: wolfBoot ML-DSA-87 " +RIGHT_TITLE = " Cortex-M7: wolfBoot ML-DSA-87 " + + +def run(cmd): + try: + p = subprocess.run(cmd, shell=True, capture_output=True, + text=True, timeout=5) + return p.stdout + except Exception: + return "" + + +class LogTail: + """Follow a container's logs in one persistent `docker logs -f` process, + buffering the last N lines. Polling `docker logs` every tick spawned the + CLI several times a second (86% CPU at a 0.2s tick) and beat against the + benchmark's ~1s output, so updates aliased into ragged multi-second jumps. + Streaming keeps the buffer current with one process, so the render loop + reads memory (cheap) and always sees the latest line.""" + + def __init__(self, container, maxlines=400): + import collections + import threading + self.buf = collections.deque(maxlen=maxlines) + self._container = container + self._lock = threading.Lock() + # Set whenever a new line arrives, so the render loop can wake on the + # newline instead of polling on a timer. This is what keeps the display + # in lockstep with the benchmark: one result line -> one redraw. + self.event = threading.Event() + t = threading.Thread(target=self._follow, daemon=True) + t.start() + + def _follow(self): + while True: + try: + p = subprocess.Popen( + ["docker", "logs", "-f", "--tail", "400", + self._container], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1) + for line in p.stdout: + with self._lock: + self.buf.append(line.rstrip("\n")) + self.event.set() + except Exception: + pass + # Container gone/restarting: wait and reattach. + time.sleep(2) + + def text(self): + with self._lock: + return "\n".join(self.buf) + + +def boot_lines(text): + """Keep only the chain-of-trust lines from wolfBoot's own boot log.""" + return [ln for ln in text.splitlines() if BOOT_KEEP.match(ln.strip())] + + +def clean(text, width, drop_cycles=False): + """Strip ANSI so column widths are computed on what is actually shown. + + With drop_cycles (the benchmark pane) this keeps ONLY result lines and + drops the banner/header chatter - the wolfSSL version block, the CPU flags, + the "Keccak: NEON only" / "no FEAT_SHA3" implementation notes, the dashed + separators. The pane title already says what this is; the results are the + point.""" + out = [] + for ln in text.splitlines(): + ln = ANSI.sub("", ln).expandtabs(4).rstrip() + if drop_cycles: + stripped = CPB.sub("", CYCLES.sub("", ln)).rstrip().rstrip(",") + if not (OPS.match(stripped) or THRU.match(stripped)): + continue # not a benchmark result -> drop as noise + out.append(condense(stripped, width)) + else: + out.append(ln) + return out + + +def fit(lines, width, height, wrap=False): + """Last `height` lines, fitted to `width`. + + Both panes wrap. The wolfBoot panes must: their output is the point of the + demo, and losing the end of "wc_MlDsaKey_Verify returned OK" would defeat + it. The benchmark pane is already condensed to the pane width, so wrapping + is normally a no-op there, and it is still what we want in the narrow case + condense() cannot squeeze (a long value clamps the label at six columns and + the line runs over) - an extra row beats a silently cut number. + """ + out = [] + for ln in lines: + if not wrap: + out.append(ln[:width]) + continue + if not ln: + out.append("") + while ln: + out.append(ln[:width]) + ln = ln[width:] + return out[-height:] if len(out) > height else out + [""] * (height - len(out)) + + +def main(): + # Torizon's kernel has no fbdev emulation, so a VT can never reach the + # HDMI. RENDER=drm draws straight into a DRM dumb buffer instead; the + # default remains the terminal for ssh/serial use. + fb = None + if os.environ.get("RENDER", "tty") == "drm": + from drmfb import DrmFramebuffer + fb = DrmFramebuffer(scale=int(os.environ.get("DRM_SCALE", "2"))) + logtail = LogTail(CONTAINER) + while True: + if fb is not None: + cols, rows = fb.cols, fb.rows + else: + cols, rows = shutil.get_terminal_size((100, 30)) + half = (cols - 3) // 2 + body = rows - 4 + + # The left pane tells the A55's story in the order it happened: first + # wolfBoot verifying the kernel it booted (read back from the DDR ring, + # since that output is long gone from the console by the time anything + # can render), then the benchmarks, from the moment the container has a + # result worth showing. + bench = clean(logtail.text(), half, drop_cycles=True) + if bench: + left_title = LEFT_TITLE + left = fit(bench, half, body, wrap=True) + else: + left_title = LEFT_BOOT_TITLE + boot_raw = run(f"{MEMTOOL} con {A55_CONSOLE_ADDR} 2>/dev/null") + boot = boot_lines(ANSI.sub("", boot_raw)) + if not boot: + boot = ["waiting for the benchmark container ..."] + left = fit(boot, half, body, wrap=True) + right_raw = run(f"{MEMTOOL} con {CONSOLE_ADDR} 2>/dev/null") + if not right_raw.strip(): + right_raw = "waiting for the Cortex-M7 to boot ...\n" + right = fit(clean(right_raw, half), half, body, wrap=True) + + buf = ["\x1b[?25l\x1b[H"] # hide cursor, home - no clear, see note above + buf.append("\x1b[1;36m" + "NXP i.MX95 - post-quantum on both clusters".center(cols) + "\x1b[0m") + buf.append("\x1b[1;33m" + left_title.ljust(half) + " | " + + RIGHT_TITLE.ljust(half) + "\x1b[0m") + buf.append("-" * cols) + for i in range(body): + buf.append(left[i].ljust(half) + " \x1b[1;30m|\x1b[0m " + right[i].ljust(half)) + + # Pad to the full width so the previous frame is fully overwritten. + # Pad on VISIBLE length: these lines contain colour escapes, and + # ljust() on the raw string counts those bytes and silently clips real + # characters off the right-hand pane. + painted = [] + for i, ln in enumerate(buf): + if i == 0: + painted.append(ln) + continue + visible = len(ANSI.sub("", ln)) + painted.append(ln + " " * max(0, cols - visible)) + if fb is not None: + # Drop the cursor-control prefix; drmfb parses only SGR colours. + painted[0] = painted[0].replace("\x1b[?25l\x1b[H", "") + fb.draw_lines(painted, ANSI) + else: + sys.stdout.write("\n".join(painted) + "\x1b[J") + sys.stdout.flush() + # Wake immediately on a new benchmark line (event-driven, so the redraw + # tracks the newline exactly); otherwise fall through after INTERVAL to + # refresh the M7 pane. No busy polling either way. + logtail.event.wait(timeout=INTERVAL) + logtail.event.clear() + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + pass + finally: + # Always give the cursor back, or the console is left unusable. + sys.stdout.write("\x1b[?25h\n") + sys.stdout.flush() diff --git a/imx95-pqc-demo/demo/wolfssl-demo-uart.service b/imx95-pqc-demo/demo/wolfssl-demo-uart.service new file mode 100644 index 0000000..a6109a1 --- /dev/null +++ b/imx95-pqc-demo/demo/wolfssl-demo-uart.service @@ -0,0 +1,29 @@ +[Unit] +# Single-stream demo on the serial console, started automatically at boot. +# +# Preferred over the DisplayPort renderer when the demo is being recorded or +# watched over serial: wolfBoot's own verified-boot output is already on this +# console seconds after power-on, and this unit simply continues that stream +# with the Cortex-M7's log and the benchmark results. +Description=wolfSSL i.MX95 post-quantum demo on the serial console +Documentation=https://github.com/wolfSSL/wolfBoot-examples/pull/13 +After=multi-user.target docker.service +Wants=docker.service + +[Service] +Type=simple +ExecStart=/bin/bash /home/torizon/demo/demo-uart.sh +# The kernel console is this port, so writing here puts the demo in the same +# stream the boot messages arrived on. +StandardOutput=tty +StandardError=journal +TTYPath=/dev/console +# A dead stream is worse than a retry on a show floor, and re-running is safe: +# the M7 start is skipped when the core is already running. +Restart=on-failure +RestartSec=5 +StandardInput=null +TimeoutStartSec=0 + +[Install] +WantedBy=multi-user.target diff --git a/imx95-pqc-demo/demo/wolfssl-demo.service b/imx95-pqc-demo/demo/wolfssl-demo.service new file mode 100644 index 0000000..5bd3267 --- /dev/null +++ b/imx95-pqc-demo/demo/wolfssl-demo.service @@ -0,0 +1,33 @@ +[Unit] +# Two-pane i.MX95 post-quantum demo, started automatically at boot. +# +# The point of this unit is the replay beat. The Cortex-M7 can only be started +# once per Linux boot, so re-running the demo needs a full power cycle. Without +# autostart that also means someone logging in and typing a command afterwards, +# over a network link that comes up unreliably. With it, cutting and restoring +# power is the entire replay, and nothing depends on a host being connected. +Description=wolfSSL i.MX95 post-quantum two-pane demo +Documentation=https://github.com/wolfSSL/wolfBoot-examples/pull/13 +After=docker.service +Requires=docker.service +# tty1 is the HDMI console; a getty there fights the renderer for it. +Conflicts=getty@tty1.service + +[Service] +Type=simple +ExecStart=/bin/bash /home/torizon/demo/demo-run.sh /dev/tty1 +# The renderer owns the DisplayPort through KMS (see demo-run.sh); Torizon has +# no fbdev emulation, so there is no console path to the screen. +Environment=RENDER=drm +# A dead screen is worse than a retry on a show floor. Re-running is safe: the +# M7 start is skipped when the core is already running, and the compose project +# is torn down and brought back up on each pass. +Restart=on-failure +RestartSec=5 +StandardInput=null +StandardOutput=journal +StandardError=journal +TimeoutStartSec=0 + +[Install] +WantedBy=multi-user.target diff --git a/imx95-pqc-demo/demo/wolfssl-m7.service b/imx95-pqc-demo/demo/wolfssl-m7.service new file mode 100644 index 0000000..fa15797 --- /dev/null +++ b/imx95-pqc-demo/demo/wolfssl-m7.service @@ -0,0 +1,18 @@ +[Unit] +# Release the Cortex-M7 once Linux is up. Separate from the renderer unit so +# the core still boots (and its log still lands in the shared ring) when the +# display is absent or the renderer is being restarted. +Description=wolfSSL i.MX95 demo - start the Cortex-M7 (wolfBoot -> Zephyr) +Documentation=https://github.com/wolfSSL/wolfBoot-examples/pull/13 +After=multi-user.target + +[Service] +# One shot per Linux boot, because that is the hardware's limit: "echo stop" +# fails on this BSP and the core stays running, so replaying the M7 boot is a +# board power cycle. RemainAfterExit keeps the unit from being re-run. +Type=oneshot +RemainAfterExit=yes +ExecStart=/home/torizon/demo/m7-start.sh + +[Install] +WantedBy=multi-user.target diff --git a/imx95-pqc-demo/gallery/README.md b/imx95-pqc-demo/gallery/README.md new file mode 100644 index 0000000..c2c1a32 --- /dev/null +++ b/imx95-pqc-demo/gallery/README.md @@ -0,0 +1,116 @@ +# wolfCrypt Post-Quantum Benchmarks - Torizon Demo Gallery entry + +Submission package for the [Torizon Demo Gallery](https://www.torizon.io/demo-gallery), +prepared against the [partner guidelines](https://developer.toradex.com/torizon/application-development/demo-gallery/demo-gallery-partner-guidelines/). + +## Demo name + +wolfCrypt Post-Quantum Benchmarks + +## Description + +Live NIST post-quantum cryptography benchmarks on the Arm cores of a Torizon +module, running continuously on stock Torizon OS. The demo measures ML-KEM (FIPS 203) +and ML-DSA (FIPS 204) against the classical ECDSA, ECDHE and RSA they replace, +and prints each result as it completes. + +The headline it demonstrates is counter-intuitive: post-quantum key +establishment is **faster** than the classical cryptography it replaces on this +part. ML-KEM-768 encapsulation runs at 17,112 ops/sec against 2,865 for an +ECDHE P-256 agreement, a 6x speedup, and ML-DSA-44 verifies 1.7x faster than +ECDSA P-256. + +## Value proposition + +Anyone shipping a connected device has to migrate to post-quantum cryptography +for CRA and IEC 62443 timelines. The usual assumption is that this costs +performance on embedded silicon. This demo shows, on real hardware rather than +in a datasheet, where that assumption holds and where it does not, so an +integrator can plan a migration with measured numbers. + +## Expected behavior + +On start the container prints a banner naming the wolfSSL version, the selected +build and the CPU features it detected, then marks the cycle `[RUNNING]` with a +timestamp, runs the benchmark set printing one line per algorithm and operation, +and closes the cycle with `[OK]` or `[FAILED]`. It pauses briefly and repeats, +so the screen is never static and the current state is always legible at a +glance rather than inferred from output scrolling. + +Results are also written to `/results` in a named volume for later collection. + +## Hardware + +| | | +|---|---| +| Verified on | Toradex SMARC iMX95 Hexa 8GB (PN 00961100) on the Toradex SMARC Development Board | +| Expected to run on | any Arm64 Toradex module running Torizon OS | +| Torizon OS | 7.7.0 and newer | +| Architecture | arm64 / aarch64 | +| Peripherals | none required | +| Display | optional; output is a text console, readable over ssh or on HDMI | + +The image contains no i.MX95-specific code. It is plain aarch64 and selects its +wolfSSL build from the CPU features the host reports at runtime, so it should +run unchanged on other Arm64 modules; the numbers will differ with the core and +clock. The i.MX95 is the part we have measured, and the only one we claim. + +No carrier-specific hardware, no peripherals to connect, and no user-specific +configuration are needed. Everything is optional tuning through the environment +variables documented in `docker-compose.yml`, including `SOC_LABEL`, which names +the board in the banner (a container cannot normally read +`/proc/device-tree/model`). + +## Deployment + +```sh +docker compose up -d +``` + +`restart: always` brings it back after provisioning and after every reboot. + +## Container image + +- Registry: Docker Hub, `wolfssl/wolfcrypt-pqc` +- Tags: a pinned version (`1.0.0`) and `latest`, pushed together +- Platform: `linux/arm64` +- Base image: `debian:bookworm-slim` +- Size: about 129 MB +- Built from `../container/`, which cross-compiles wolfSSL for aarch64 on the + native build platform rather than under emulation + +Publish with `./publish.sh` (`VERSION=1.0.0 ./publish.sh` after `docker login`). +Docker Hub repositories are private by default; the Gallery needs this one set +to public once, in the repository's settings, after the first push. + +Two wolfSSL builds ship in the image: a NEON baseline that runs on any +Cortex-A55, and one using the Armv8.2 SHA-3 instructions. The entrypoint reads +`/proc/cpuinfo` and picks the safe one at runtime, because FEAT_SHA3 is optional +in the architecture and running the SHA-3 build without it faults. On the i.MX95 +that resolves to the baseline build. + +## External dependencies + +None beyond the base image. wolfSSL is compiled from source into the image and +linked statically, so there is nothing to install on the host and nothing +pulled at runtime. + +## Licensing + +wolfSSL is dual licensed under **GPLv3** or a commercial license from wolfSSL +Inc. Everything in this image is redistributable and publicly demonstrable +under GPLv3. The Debian base image carries its own upstream licenses. + +## Links + +- wolfSSL: https://www.wolfssl.com +- Source for this demo: https://github.com/wolfSSL/wolfBoot-examples +- Contact: facts@wolfssl.com + +## Note on the Cortex-M7 half + +The full demo pairs these benchmarks with wolfBoot performing ML-DSA-87 verified +boot on the i.MX95's Cortex-M7. That half is deliberately **not** part of this +Gallery entry: it requires wolfBoot flashed to the module and privileged access +to `/dev/mem`, so it cannot be plug-and-play on an unmodified Torizon OS image. +It is documented in the parent directory for anyone who wants to reproduce it. diff --git a/imx95-pqc-demo/gallery/docker-compose.yml b/imx95-pqc-demo/gallery/docker-compose.yml new file mode 100644 index 0000000..f2581e6 --- /dev/null +++ b/imx95-pqc-demo/gallery/docker-compose.yml @@ -0,0 +1,48 @@ +# wolfCrypt post-quantum benchmarks - Torizon Demo Gallery deployment. +# +# Self-contained: pulls one public image and starts on its own after +# provisioning. No host packages, no build step, no configuration required. +# +# docker compose up -d (on the board, or through Torizon Cloud) +# +# This is the Linux half of a larger demo. The Cortex-M7 half needs wolfBoot +# flashed to the module and privileged access to /dev/mem, so it is deliberately +# not part of the Gallery entry - see ../README.md for that. + +services: + wolfcrypt-pqc: + image: wolfssl/wolfcrypt-pqc:latest + container_name: wolfcrypt-pqc + # "always" rather than "unless-stopped": the demo has to come back on its + # own after provisioning and after every reboot. + restart: always + + # Keep a TTY so the banner renders in colour and output is line-buffered + # rather than held in a pipe's block buffer. + tty: true + + environment: + # auto pick the SHA-3 build only if the CPU advertises FEAT_SHA3 + # baseline force the NEON build + # sha3-crypto force the FEAT_SHA3 build (faults on parts without it) + # ab alternate each cycle to show the SHA-3 lever live + # Optional: name the board in the banner. A container cannot normally + # read /proc/device-tree, so without this it reads "Arm64 module". + SOC_LABEL: "${SOC_LABEL:-Toradex SMARC iMX95}" + MODE: "${MODE:-auto}" + LOOP: "1" + PAUSE: "${PAUSE:-5}" + + # Results are kept in a named volume so the demo needs no pre-created host + # directory and survives a container replacement. + volumes: + - wolfcrypt-results:/results + + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +volumes: + wolfcrypt-results: diff --git a/imx95-pqc-demo/gallery/publish.sh b/imx95-pqc-demo/gallery/publish.sh new file mode 100755 index 0000000..21f00fb --- /dev/null +++ b/imx95-pqc-demo/gallery/publish.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Build and publish the Gallery image to Docker Hub. Run on an x86 host with +# docker buildx; the build cross-compiles for aarch64 rather than emulating it. +# +# docker login +# VERSION=1.0.0 ./publish.sh +# +# Pushes : and :latest. The Demo Gallery needs the package +# to be PUBLIC: Docker Hub repositories default to private, so set visibility to +# public once, in the repository's Settings, after the first push. +set -euo pipefail + +REPO="${REPO:-wolfssl/wolfcrypt-pqc}" +VERSION="${VERSION:?set VERSION, e.g. VERSION=1.0.0}" +HERE="$(cd "$(dirname "$0")" && pwd)" +CTX="$HERE/../container" + +# The Docker builder stage compiles wolfSSL itself, so all it needs in the +# context is a pristine source export - not the full cross-build that +# build-aarch64.sh also performs. Export it here when it is missing, so +# publishing is one command rather than two with a non-obvious ordering. +if [ ! -x "$CTX/wolfssl-src/configure" ]; then + if [ -z "${WOLFSSL_REPO:-}" ]; then + echo "wolfssl-src/ missing in $CTX" >&2 + echo "point WOLFSSL_REPO at a wolfSSL checkout and re-run, e.g." >&2 + echo " WOLFSSL_REPO=~/GitHub/wolfssl VERSION=$VERSION $0" >&2 + echo "(or run ../container/build-aarch64.sh, which exports it as a side effect)" >&2 + exit 1 + fi + echo "=== exporting $WOLFSSL_REPO @ ${WOLFSSL_REF:-HEAD} -> $CTX/wolfssl-src ===" + # A pristine export, not a copy: the developer's checkout is configured + # in-tree and autotools refuses to build against one. + rm -rf "$CTX/wolfssl-src" + mkdir -p "$CTX/wolfssl-src" + git -C "$WOLFSSL_REPO" archive "${WOLFSSL_REF:-HEAD}" | tar -x -C "$CTX/wolfssl-src" + ( cd "$CTX/wolfssl-src" && ./autogen.sh >autogen.log 2>&1 ) \ + || { tail -20 "$CTX/wolfssl-src/autogen.log"; exit 1; } +fi + +ver=$(sed -n "s/^PACKAGE_VERSION='\(.*\)'/\1/p" "$CTX/wolfssl-src/configure" | head -1) +echo "=== building $REPO:$VERSION from wolfSSL ${ver:-unknown} ===" + +# linux/arm64 only: every Torizon module this targets is Arm64, and a second +# architecture would double build time for an image nothing would pull. +docker buildx build --platform linux/arm64 \ + -t "$REPO:$VERSION" -t "$REPO:latest" \ + --push "$CTX" + +echo +echo "pushed $REPO:$VERSION and $REPO:latest" +echo "if this was the first push, set the repository to PUBLIC at:" +echo " https://hub.docker.com/r/$REPO/settings" diff --git a/imx95-pqc-demo/m7/build.sh b/imx95-pqc-demo/m7/build.sh new file mode 100755 index 0000000..9d5ad0f --- /dev/null +++ b/imx95-pqc-demo/m7/build.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Build the Zephyr RPMsg payload for the i.MX95 Cortex-M7, linked into +# wolfBoot's boot partition. +# +# Requires a Zephyr workspace and an arm-none-eabi toolchain. The Zephyr SDK's +# arm-zephyr-eabi works too; this defaults to the system toolchain because it is +# usually already present. +# +# ZEPHYR_BASE=~/zephyrproject/zephyr ./build.sh +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +BUILD="${BUILD:-$HERE/build}" +BOARD="${BOARD:-imx95_evk/mimx9596/m7/ddr}" + +: "${ZEPHYR_BASE:?set ZEPHYR_BASE to your Zephyr checkout}" +export ZEPHYR_BASE +export ZEPHYR_TOOLCHAIN_VARIANT="${ZEPHYR_TOOLCHAIN_VARIANT:-gnuarmemb}" +export GNUARMEMB_TOOLCHAIN_PATH="${GNUARMEMB_TOOLCHAIN_PATH:-/usr}" + +# The upstream openamp_rsc_table board overlay supplies the shared-memory and +# mailbox nodes; ours relocates the image and enables the MU and the console +# region. Both are needed. +SAMPLE_OVERLAY="$ZEPHYR_BASE/samples/subsys/ipc/openamp_rsc_table/boards/imx95_evk_mimx9596_m7.overlay" + +# wolfCrypt runs on this core too, so the demo can show the same post-quantum +# algorithms on the M7 and the A55 cluster. wolfSSL ships a Zephyr module +# manifest, so point the build at a checkout rather than adding it to the +# workspace west manifest. +WOLFSSL_ROOT="${WOLFSSL_ROOT:-$HOME/GitHub/wolfssl}" +if [ ! -f "$WOLFSSL_ROOT/zephyr/module.yml" ]; then + echo "no wolfSSL Zephyr module at $WOLFSSL_ROOT" >&2 + echo "set WOLFSSL_ROOT to a wolfSSL checkout" >&2 + exit 1 +fi + +west build -p always -b "$BOARD" -d "$BUILD" "$HERE/zephyr-app" \ + -- -DDTC_OVERLAY_FILE="$HERE/imx95_wolfboot.overlay;$SAMPLE_OVERLAY" \ + -DEXTRA_ZEPHYR_MODULES="$WOLFSSL_ROOT" + +arm-none-eabi-objcopy -O binary \ + "$BUILD/zephyr/wolfboot_openamp.elf" "$BUILD/zephyr/payload.bin" + +echo +echo "payload: $BUILD/zephyr/payload.bin" +echo "sign it with wolfBoot's keytool, e.g.:" +echo " IMAGE_HEADER_SIZE=12288 ML_DSA_LEVEL=5 ./tools/keytools/sign --ml_dsa --sha256 \\" +echo " $BUILD/zephyr/payload.bin wolfboot_signing_private_key.der 1" diff --git a/imx95-pqc-demo/m7/imx95_wolfboot.overlay b/imx95-pqc-demo/m7/imx95_wolfboot.overlay new file mode 100644 index 0000000..6d33de9 --- /dev/null +++ b/imx95-pqc-demo/m7/imx95_wolfboot.overlay @@ -0,0 +1,67 @@ +#include + +/* + * i.MX95 Cortex-M7: place a Zephyr image where wolfBoot boots it, and expose + * the Message Unit that Linux uses to kick RPMsg. + * + * 1. Relocation + * ------------- + * The stock imx95_evk/mimx9596/m7/ddr target links at 0x80000000, which + * collides with wolfBoot's partition map. wolfBoot boots the payload in place + * at BOOT + IMAGE_HEADER_SIZE: + * + * BOOT partition 0x80100000 (4 MiB) + * IMAGE_HEADER_SIZE 0x3000 (12288, sized for ML-DSA-87) + * payload entry 0x80103000 <- must match test-app/ARM-imx95_m7.ld + * + * 2 MiB is carved here, well inside the 4 MiB partition and clear of the + * wolfBoot console ring at 0x80F00000 and the status block at 0x80F10000. + * + * 2. Message Unit + * --------------- + * The board's device tree points remoteproc-cm7 at mailbox@42430000, which the + * MIMX9596 CM7 header names MU7_MUA - the *A55* side of the pair. The M7 owns + * the B side: MU7_MUB at 0x42440000, NVIC vector MU7_B_IRQn = 207. + * + * Zephyr already describes that node upstream as &mu7, disabled by default, so + * all that is needed here is to enable it. Without it Linux's kick has nothing + * to talk to and the kernel logs "imx_rproc_kick: failed (0, err:-62)". + */ + +&ddr { + reg = <0x80103000 0x00200000>; +}; + +/* + * 3. wolfBoot's console ring and status block + * ------------------------------------------- + * The payload reads wolfBoot's shared-memory console at 0x80F00000 to relay it + * over RPMsg. That address is outside this image's linked SRAM, and Zephyr + * enables the ARM MPU on this SoC (CONFIG_ARM_MPU is select'ed, so prj.conf + * cannot turn it off), so the access faults. + * + * The failure is quiet and easy to misread: the payload boots, announces its + * RPMsg channel, faults on the first read of the console header, resets, and + * announces again - roughly every 15 ms. On the Linux side that looks like + * "creating channel ... already exist" repeating forever, and the RAM console + * reads back all zeros because .bss is wiped on every reset. Nothing says + * "fault". + * + * Declaring the region with an MPU attribute gives it a proper entry. 128 KiB + * covers the 64 KiB console page at 0x80F00000 and the status block at + * 0x80F10000. + */ + +&mu7 { + rx-channels = <4>; + status = "okay"; +}; + +/ { + wbconsole: memory@80f00000 { + compatible = "zephyr,memory-region", "mmio-sram"; + reg = <0x80F00000 DT_SIZE_K(128)>; + zephyr,memory-region = "WBCONSOLE"; + zephyr,memory-attr = ; + }; +}; diff --git a/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt b/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt new file mode 100644 index 0000000..8d5b79b --- /dev/null +++ b/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(imx95_m7_wolfboot_openamp) +target_sources(app PRIVATE src/main.c) + +# wolfCrypt's own benchmark, not a hand-rolled one: it is the same program the +# A55 container runs, so the two cores' numbers are directly comparable and in +# the same units, and there is no bespoke timing code here to get wrong. +target_include_directories(app PRIVATE + ${ZEPHYR_WOLFSSL_MODULE_DIR}/wolfcrypt/benchmark) + +target_sources(app PRIVATE + ${ZEPHYR_WOLFSSL_MODULE_DIR}/wolfcrypt/benchmark/benchmark.c) + +# benchmark.c carries its own main() for standalone use; this app has one. +# NOTE: this also compiles out wolfcrypt_benchmark_main() and the option +# tables (benchmark.c lines 18780-19294), so the algorithm selection cannot be +# driven by arguments - see the comment in src/main.c. +zephyr_compile_definitions(NO_MAIN_DRIVER) diff --git a/imx95-pqc-demo/m7/zephyr-app/prj.conf b/imx95-pqc-demo/m7/zephyr-app/prj.conf new file mode 100644 index 0000000..f25fba6 --- /dev/null +++ b/imx95-pqc-demo/m7/zephyr-app/prj.conf @@ -0,0 +1,76 @@ +CONFIG_KERNEL_BIN_NAME="wolfboot_openamp" + +CONFIG_OPENAMP=y +CONFIG_IPM=y +CONFIG_IPM_MBOX=y +CONFIG_MBOX_NXP_IMX_MU=y +CONFIG_MBOX_INIT_PRIORITY=0 +CONFIG_OPENAMP_WITH_DCACHE=y + +# The resource table Linux reads belongs to wolfBoot, not to this payload, so +# the subsystem's own table is deliberately NOT built - it would describe vrings +# that nobody honours. See src/main.c. +CONFIG_OPENAMP_RSC_TABLE=n + +CONFIG_CLOCK_CONTROL=y +CONFIG_ARM_SCMI=y + +# No M7 UART is routed on this carrier, so the console is a printk hook in +# main.c that appends to the shared ring wolfBoot already writes (and that the +# demo already reads). That puts the Zephyr banner, driver init and every LOG +# line in the same stream as wolfBoot's own verified-boot output, on one core's +# timeline. Zephyr's RAM console is off deliberately: it installs a competing +# printk hook and writes to a second buffer nothing reads. +CONFIG_UART_CONSOLE=n +CONFIG_RAM_CONSOLE=n +CONFIG_PRINTK=y +CONFIG_BOOT_BANNER=y +CONFIG_LOG=y +CONFIG_LOG_MODE_MINIMAL=y +# Debug level. It adds about thirty lines over a boot - IRQ registration, +# per-thread stack placement, the scheduler settling - which is detail worth +# showing on a demo of what the second core is doing, and nowhere near enough +# to wrap the 64 KiB ring. +CONFIG_LOG_DEFAULT_LEVEL=4 +# ...except the kernel itself. At debug it logs a scheduler line every couple +# of ticks forever, which says nothing about this demo and would eventually +# wrap wolfBoot's verified-boot output out of the ring. What is worth seeing +# from startup - version, clocks, the vring contract, time to main - the +# payload prints itself. +CONFIG_KERNEL_LOG_LEVEL_INF=y + +# wolfCrypt on the M7, so this core can run the same post-quantum algorithms +# the A55 cluster benchmarks. ML-DSA-87 in particular needs far more stack and +# heap than a plain RPMsg payload: the key and signature alone are several KiB. +CONFIG_WOLFSSL=y +CONFIG_WOLFSSL_BUILTIN=y +CONFIG_WOLFSSL_MLKEM=y +CONFIG_WOLFSSL_MLDSA=y +# Thumb-2 assembly for SHA-2/SHA-3 and friends. ML-KEM and ML-DSA are dominated +# by SHAKE, so this is the single biggest lever on the numbers below, and the +# A55 side is built with its own architecture's assembly for the same reason. +CONFIG_WOLFCRYPT_ARMASM=y +CONFIG_WOLFCRYPT_ARMASM_THUMB2=y +# The benchmark reports MiB/s and ms as floats. Without this Zephyr's printf +# prints "*float*" for every number, which is exactly the part worth reading. +CONFIG_CBPRINTF_FP_SUPPORT=y +CONFIG_FPU=y +CONFIG_MAIN_STACK_SIZE=65536 +CONFIG_HEAP_MEM_POOL_SIZE=262144 +# wolfCrypt's Zephyr port seeds from sys_rand_get(), and this core has no +# entropy peripheral - the TRNG on this SoC sits behind ELE, which only the +# A55 side reaches. A software PRNG is therefore the only option here. It is +# adequate for a timing benchmark, where the numbers do not depend on the +# quality of the input, and is NOT an entropy source: no key generated on this +# core should be treated as secret. +CONFIG_TEST_RANDOM_GENERATOR=y +CONFIG_XOSHIRO_RANDOM_GENERATOR=y +CONFIG_IPM_LOG_LEVEL_DBG=y +CONFIG_MBOX_LOG_LEVEL_DBG=y + +# The payload reads two regions outside its own linked SRAM: wolfBoot's console +# ring at 0x80F00000 and the RPMsg vrings at 0x88000000. Zephyr's MPU config +# does not cover them, and the access faults - which shows up as the payload +# announcing its RPMsg channel and then resetting in a ~15 ms loop, with the +# RAM console reading back all zeros because .bss is wiped on every reset. +# wolfBoot itself runs this target with NO_MPU=1, so match that. diff --git a/imx95-pqc-demo/m7/zephyr-app/src/main.c b/imx95-pqc-demo/m7/zephyr-app/src/main.c new file mode 100644 index 0000000..aad3fb9 --- /dev/null +++ b/imx95-pqc-demo/m7/zephyr-app/src/main.c @@ -0,0 +1,556 @@ +/* + * wolfBoot-aware Zephyr RPMsg payload for the i.MX95 Cortex-M7. + * + * Brings up the M7 side of the RPMsg link that wolfBoot's resource table + * declared to Linux, and relays the M7's shared-memory console - wolfBoot's own + * verify log included - to the A55 as a virtual TTY. + * + * + * Why this is not samples/subsys/ipc/openamp_rsc_table + * --------------------------------------------------- + * That sample assumes Linux loaded *its* ELF. The standard remoteproc contract + * is that the remote declares vrings with da = FW_RSC_ADDR_ANY, Linux allocates + * them, and Linux writes the resolved addresses and the virtio status back into + * the resource table it loaded. The remote then reads its own table. + * + * Here Linux loads wolfBoot, not this payload. So Linux writes the resolved + * values into wolfBoot's .resource_table, while the sample polls the table + * linked into its own image - two different structures. The sample gets as far + * as rproc_virtio_wait_remote_ready() and waits forever for a status nobody + * will ever write. + * + * CONFIG_OPENAMP_COPY_RSC_TABLE looks like the bridge and is not: it memcpy's + * the payload's own unresolved table over the shared location before reading it + * back. + * + * The fix is to stop relying on host writeback. Both sides use FIXED addresses: + * wolfBoot's table hardcodes them, so Linux honours them, and this payload uses + * the same constants. They come from the board's own reserved-memory nodes, so + * the two sides agree by construction rather than by convention: + * + * vdev0vring0 0x88000000 32 KiB + * vdev0vring1 0x88008000 32 KiB + * vdevbuffer 0x88020000 1 MiB + * + * Because the addresses are fixed there is nothing to wait for, and the host is + * ready by construction: Linux registers virtio0 during rproc_start, before it + * releases the core that eventually runs this code. + * + * KEEP IN SYNC with the resource table in wolfBoot's hal/imx95_m7.c. If the + * vring geometry there changes, change it here too - a mismatch shows up as + * silence, not as an error. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +/* wolfCrypt's stock benchmark, the same program the A55 container runs. It + * takes the same argv the command-line version does, so "-pq" restricts it to + * the post-quantum algorithms - the ones this demo is about, and the ones that + * finish in a sensible time on an 800 MHz in-order core. */ +/* Selection by argument is not available here: wolfcrypt/settings.h defines + * MAIN_NO_ARGS for every Zephyr build, and NO_MAIN_DRIVER (needed so this app + * keeps its own main) removes wolfcrypt_benchmark_main() and the option tables + * outright. benchmark_test() ignores what it is passed. So the algorithm set + * is whatever the build enables - which is why prj.conf turns on only ML-KEM + * and ML-DSA. */ +extern int benchmark_test(void *args); +LOG_MODULE_REGISTER(wolfboot_openamp, LOG_LEVEL_DBG); + +/* --- Must match wolfBoot's resource table (hal/imx95_m7.c) --- */ +#define VRING0_DA 0x88000000UL /* host -> remote */ +#define VRING1_DA 0x88008000UL /* remote -> host */ +#define VRING_ALIGN 0x1000U +#define VRING_NUM 256U + +/* vrings plus the 1 MiB vdevbuffer at 0x88020000 */ +#define SHM_BASE 0x88000000UL +#define SHM_SIZE 0x00120000UL + +#define VDEV_ID 0xFFU +#define VRING0_ID 0U +#define VRING1_ID 1U + +/* --- wolfBoot's shared-memory console (hal/imx95_m7.h) --- */ +#define CONSOLE_BASE 0x80F00000UL +#define CONSOLE_HDR_SIZE 16U +#define CONSOLE_MAGIC 0x4E4F4357UL /* "WCON" */ + +/* The Linux imx_rpmsg_tty driver binds this name and creates /dev/ttyRPMSG*. */ +#define TTY_CHANNEL_NAME "rpmsg-virtual-tty-channel" + +/* Progress published to wolfBoot's app status block, read from Linux with + * "memtool r 0x80F10010 4". The RAM console proved unreliable for this - a + * fault can halt the core before anything is flushed - so the state goes to a + * plain shared word instead: + * + * +0x00 magic 'ZOAM' + * +0x04 progress: 1 platform_init, 2 vdev up, 3 endpoint created + * +0x08 heartbeat, increments every loop pass + * +0x0C 1 once the endpoint has a destination (host has replied) + */ +#define APP_STATUS_ADDR 0x80F10010UL +#define APP_STATUS_MAGIC 0x5A4F414DUL /* "ZOAM" */ + +/* The M7 runs with D-cache enabled (wolfBoot turns it on), so a plain store + * lands in cache and the A55 never sees it. wolfBoot's own console code cleans + * the cache after every write for exactly this reason; do the same here. + * Without it the status words read back as zero from Linux - or worse, appear + * intermittently as lines happen to get evicted. */ +static void app_status(uint32_t idx, uint32_t val) +{ + volatile uint32_t *st = (volatile uint32_t *)APP_STATUS_ADDR; + + st[0] = APP_STATUS_MAGIC; + st[idx] = val; + sys_cache_data_flush_range((void *)APP_STATUS_ADDR, 32); +} + +struct console_hdr { + volatile uint32_t magic; + volatile uint32_t wr; /* total bytes ever written, monotonic */ + volatile uint32_t size; + volatile uint32_t rsvd; +}; + +/* Append bytes to wolfBoot's shared-memory console ring. wolfBoot is done + * writing by the time this payload runs, so there is no writer to race; the + * A55 demo only ever reads. Data is cleaned before the header so a reader + * never sees wr ahead of the bytes it covers. + * + * Only the bytes just written are cleaned. Flushing the whole ring instead + * would move 64 KiB per call, which is wasteful for a heartbeat line and + * unusable once this is the console and each character arrives on its own. */ +static void ring_write(const char *buf, size_t len) +{ + struct console_hdr *hdr = (struct console_hdr *)CONSOLE_BASE; + uint8_t *data = (uint8_t *)(CONSOLE_BASE + CONSOLE_HDR_SIZE); + uint32_t size = hdr->size; + uint32_t wr, start, first; + size_t i; + + if (hdr->magic != CONSOLE_MAGIC || size == 0U || + size > (0x10000U - CONSOLE_HDR_SIZE) || len == 0U) { + return; + } + wr = hdr->wr; + start = wr % size; + for (i = 0; i < len; i++) { + data[(wr + i) % size] = (uint8_t)buf[i]; + } + /* One contiguous run, plus the wrapped remainder when it crosses the end. */ + first = size - start; + if ((uint32_t)len <= first) { + sys_cache_data_flush_range((void *)&data[start], len); + } + else { + sys_cache_data_flush_range((void *)&data[start], first); + sys_cache_data_flush_range((void *)data, (uint32_t)len - first); + } + hdr->wr = wr + (uint32_t)len; + sys_cache_data_flush_range((void *)hdr, CONSOLE_HDR_SIZE); +} + +static void ring_puts(const char *str) +{ + ring_write(str, strlen(str)); +} + +/* Send everything Zephyr prints - the boot banner, driver init, LOG output and + * any printk in this app - to the same ring wolfBoot used, so one reader on the + * A55 sees the whole story of this core rather than just the lines this file + * writes by hand. Zephyr's own RAM console is a separate buffer nobody reads. + * + * Installed from PRE_KERNEL_1 so it is in place before the kernel prints its + * banner; ring_puts() only touches DDR and the cache, both of which are usable + * that early. */ +static int ring_char_out(int c) +{ + char ch = (char)c; + + ring_write(&ch, 1); + return c; +} + +static int ring_console_init(void) +{ + __printk_hook_install(ring_char_out); + return 0; +} + +SYS_INIT(ring_console_init, PRE_KERNEL_1, 0); + +/* rproc_virtio_create_vdev() only reads the header fields of this structure; + * the vrings are supplied separately to rproc_virtio_init_vring(). They are + * declared anyway so the layout matches a real resource table entry. + * + * status is pre-set to DRIVER_OK rather than waiting for the host to write it: + * see the file header for why there is no writeback to wait for. */ +struct local_vdev_rsc { + struct fw_rsc_vdev vdev; + struct fw_rsc_vdev_vring vring[2]; +}; + +static struct local_vdev_rsc vdev_rsc = { + .vdev = { + .type = RSC_VDEV, + .id = VIRTIO_ID_RPMSG, + .notifyid = 0, + .dfeatures = 1U << VIRTIO_RPMSG_F_NS, + .gfeatures = 1U << VIRTIO_RPMSG_F_NS, + .config_len = 0, + .status = VIRTIO_CONFIG_STATUS_DRIVER_OK, + .num_of_vrings = 2, + }, + .vring = { + { VRING0_DA, VRING_ALIGN, VRING_NUM, VRING0_ID, 0 }, + { VRING1_DA, VRING_ALIGN, VRING_NUM, VRING1_ID, 0 }, + }, +}; + +static const struct device *const ipm_handle = DEVICE_DT_GET(DT_CHOSEN(zephyr_ipc)); + +/* A fault here halts the core with nothing useful in the RAM console - the log + * backend does not get a chance to flush. Publish the reason and PC to the + * status block instead, which is a plain store and always works: + * + * +0x10 fault reason (Zephyr K_ERR_*) + * +0x14 faulting PC + * +0x18 0xDEADBEEF marker that a fault happened at all + */ +void k_sys_fatal_error_handler(unsigned int reason, const struct arch_esf *esf) +{ + volatile uint32_t *st = (volatile uint32_t *)APP_STATUS_ADDR; + + st[4] = (uint32_t)reason; + st[5] = (esf != NULL) ? (uint32_t)esf->basic.pc : 0xFFFFFFFFU; + st[6] = 0xDEADBEEFU; + sys_cache_data_flush_range((void *)APP_STATUS_ADDR, 32); + + for (;;) { + /* halt - leave the state readable from the A55 */ + } +} + +static metal_phys_addr_t shm_physmap[] = { SHM_BASE }; +static metal_phys_addr_t rsc_physmap[] = { (metal_phys_addr_t)(uintptr_t)&vdev_rsc }; + +static struct metal_io_region shm_io_data; +static struct metal_io_region rsc_io_data; +static struct metal_io_region *shm_io = &shm_io_data; +static struct metal_io_region *rsc_io = &rsc_io_data; + +static struct rpmsg_virtio_device rvdev; +static struct rpmsg_endpoint tty_ept; + +static K_SEM_DEFINE(kick_sem, 0, 1); + +static void ipm_callback(const struct device *dev, void *context, + uint32_t id, volatile void *data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(context); + ARG_UNUSED(id); + ARG_UNUSED(data); + k_sem_give(&kick_sem); +} + +static int mailbox_notify(void *priv, uint32_t id) +{ + ARG_UNUSED(priv); + ipm_send(ipm_handle, 0, id, &id, sizeof(id)); + return 0; +} + +/* Set when the host writes to the TTY, asking for the console log again. + * + * The relay flushes the whole ring the moment the endpoint has a destination + * address, which is before any reader can realistically be attached: the + * /dev/ttyRPMSG node does not exist until the host has bound the channel, and + * by the time a reader opens it the one-shot replay has already been sent and + * dropped. Treating any inbound byte as a replay request makes the log + * retrievable on demand, so "attach a reader, then poke the TTY" works every + * time instead of racing the bind. */ +static volatile bool replay_req; +static uint32_t rx_count; + +static int tty_recv(struct rpmsg_endpoint *ept, void *data, size_t len, + uint32_t src, void *priv) +{ + ARG_UNUSED(ept); + ARG_UNUSED(data); + ARG_UNUSED(src); + ARG_UNUSED(priv); + LOG_DBG("rx %u bytes from the host, replaying console", + (unsigned int)len); + rx_count++; + app_status(10, rx_count); + replay_req = true; + return RPMSG_SUCCESS; +} + +static void ns_bind_cb(struct rpmsg_device *rdev, const char *name, uint32_t src) +{ + ARG_UNUSED(rdev); + ARG_UNUSED(src); + LOG_INF("host announced service '%s'", name); +} + +static int platform_init(void) +{ + struct metal_init_params params = METAL_INIT_DEFAULTS; + int ret; + + ret = metal_init(¶ms); + if (ret != 0) { + LOG_ERR("metal_init failed: %d", ret); + return ret; + } + + metal_io_init(shm_io, (void *)SHM_BASE, shm_physmap, SHM_SIZE, -1, 0, NULL); + metal_io_init(rsc_io, &vdev_rsc, rsc_physmap, sizeof(vdev_rsc), -1, 0, NULL); + + if (!device_is_ready(ipm_handle)) { + LOG_ERR("IPM device not ready"); + return -ENODEV; + } + + ipm_register_callback(ipm_handle, ipm_callback, NULL); + + ret = ipm_set_enabled(ipm_handle, 1); + if (ret != 0) { + LOG_ERR("ipm_set_enabled failed: %d", ret); + return ret; + } + + return 0; +} + +static struct rpmsg_device *rpmsg_start(void) +{ + struct virtio_device *vdev; + int ret; + + /* VIRTIO_DEV_DEVICE: this core is the device/remote, Linux is the driver. */ + vdev = rproc_virtio_create_vdev(VIRTIO_DEV_DEVICE, VDEV_ID, &vdev_rsc.vdev, + rsc_io, NULL, mailbox_notify, NULL); + if (vdev == NULL) { + LOG_ERR("rproc_virtio_create_vdev failed"); + return NULL; + } + + /* No rproc_virtio_wait_remote_ready() here on purpose - the status it polls + * is written into wolfBoot's table, not this one, and the host is already + * up by the time this code runs. */ + + ret = rproc_virtio_init_vring(vdev, 0, VRING0_ID, (void *)VRING0_DA, + shm_io, VRING_NUM, VRING_ALIGN); + if (ret != 0) { + LOG_ERR("init vring 0 failed: %d", ret); + return NULL; + } + + ret = rproc_virtio_init_vring(vdev, 1, VRING1_ID, (void *)VRING1_DA, + shm_io, VRING_NUM, VRING_ALIGN); + if (ret != 0) { + LOG_ERR("init vring 1 failed: %d", ret); + return NULL; + } + + ret = rpmsg_init_vdev(&rvdev, vdev, ns_bind_cb, shm_io, NULL); + if (ret != 0) { + LOG_ERR("rpmsg_init_vdev failed: %d", ret); + return NULL; + } + + return rpmsg_virtio_get_rpmsg_device(&rvdev); +} + +/* Copy whatever is new in wolfBoot's console ring out to the endpoint. + * + * The ring publishes `wr` as a monotonic count of bytes ever written, so the + * reader keeps its own position and copies (wr - pos) from data[pos % size]. + * Starting pos at 0 means the first pass replays everything already in the + * buffer, which is the point: wolfBoot's verify log is written long before any + * RPMsg endpoint exists, and replaying it is the only way the A55 ever sees it. + */ +static void relay_console(struct rpmsg_endpoint *ept, uint32_t *pos) +{ + const struct console_hdr *h = (const struct console_hdr *)CONSOLE_BASE; + const volatile uint8_t *data = + (const volatile uint8_t *)(CONSOLE_BASE + CONSOLE_HDR_SIZE); + static uint32_t sent_total; + uint8_t chunk[256]; + uint32_t wr, size, avail, i; + int ret; + + /* wolfBoot wrote this ring with its own cache cleans, but this core may + * still hold stale lines for it, so invalidate before every read. */ + sys_cache_data_invd_range((void *)CONSOLE_BASE, CONSOLE_HDR_SIZE); + + if (h->magic != CONSOLE_MAGIC) { + return; + } + + size = h->size; + wr = h->wr; + if (size == 0U || wr == *pos) { + return; + } + + avail = wr - *pos; + if (avail > size) { + /* Reader fell behind; report the gap rather than emit corrupt text. */ + static const char msg[] = "\r\n[console overrun - output dropped]\r\n"; + + (void)rpmsg_send(ept, msg, sizeof(msg) - 1U); + *pos = wr - size; + avail = size; + } + + sys_cache_data_invd_range((void *)(CONSOLE_BASE + CONSOLE_HDR_SIZE), size); + + while (avail > 0U) { + uint32_t n = (avail > sizeof(chunk)) ? (uint32_t)sizeof(chunk) : avail; + + for (i = 0; i < n; i++) { + chunk[i] = data[(*pos + i) % size]; + } + + ret = rpmsg_send(ept, chunk, (int)n); + app_status(7, (uint32_t)ret); /* last send result */ + app_status(9, avail); /* bytes still pending */ + if (ret < 0) { + /* Host buffer full: leave the rest for the next pass. */ + return; + } + sent_total += n; + app_status(8, sent_total); + + *pos += n; + avail -= n; + } +} + +int main(void) +{ + struct rpmsg_device *rpdev; + uint32_t pos = 0; + uint32_t beat = 0; + int ret; + + /* Say what this core actually is and what it was handed, rather than + * leaving the demo to infer it. The vring and buffer addresses are the + * contract wolfBoot's resource table declared to Linux, so printing them + * here is what makes a mismatch visible instead of a silent hang. */ + ring_puts("\r\n"); + printk("Zephyr %s on %s, cycle clock %u Hz, %u Hz tick\n", + KERNEL_VERSION_STRING, CONFIG_BOARD, + (unsigned)sys_clock_hw_cycles_per_sec(), + (unsigned)CONFIG_SYS_CLOCK_TICKS_PER_SEC); + printk("payload verified by wolfBoot (ML-DSA-87) and entered at %p\n", + (void *)main); + printk("rpmsg: vring0 0x%08lx vring1 0x%08lx, %u descriptors each\n", + (unsigned long)VRING0_DA, (unsigned long)VRING1_DA, + (unsigned)VRING_NUM); + /* Microseconds: the whole Zephyr startup is well under a millisecond, so + * milliseconds would just print zero. */ + printk("reached main %llu us after Zephyr entry\n", + (unsigned long long)k_cyc_to_us_floor64(k_cycle_get_32())); + + LOG_INF("wolfBoot RPMsg payload starting"); + + ret = platform_init(); + if (ret != 0) { + return ret; + } + app_status(1, 1); + + rpdev = rpmsg_start(); + if (rpdev == NULL) { + return -EIO; + } + app_status(1, 2); + + ret = rpmsg_create_ept(&tty_ept, rpdev, TTY_CHANNEL_NAME, + RPMSG_ADDR_ANY, RPMSG_ADDR_ANY, tty_recv, NULL); + if (ret != 0) { + LOG_ERR("rpmsg_create_ept failed: %d", ret); + return ret; + } + + app_status(1, 3); + LOG_INF("endpoint '%s' announced", TTY_CHANNEL_NAME); + + ring_puts("\r\nZephyr on Cortex-M7: verified payload running, " + "RPMsg tty up\r\n"); + + /* Run wolfCrypt's own benchmark once, so this core reports the same + * algorithms in the same units as the A55 cluster does. It prints through + * printk, which this app has pointed at the shared ring, so it lands in + * the same stream as wolfBoot's verified-boot output above. */ + { + + + ring_puts("\r\n--- wolfCrypt post-quantum benchmark, Cortex-M7 ---\r\n"); + (void)benchmark_test(NULL); + } + ring_puts("--- end of Cortex-M7 benchmark ---\r\n"); + + while (1) { + /* Service the host's kick if one arrived, then push console output. */ + if (k_sem_take(&kick_sem, K_MSEC(50)) == 0) { + rproc_virtio_notified(rvdev.vdev, VRING1_ID); + } + + /* Only relay once the host has bound the endpoint and a destination + * address is known. Sending earlier fails, and the failed send makes + * OpenAMP re-announce the name service - which shows up on the Linux + * side as "creating channel ... already exist" repeating forever and + * stops the tty driver from ever attaching. */ +#ifdef RELAY_DISABLED + (void)pos; +#else + app_status(2, ++beat); + app_status(3, is_rpmsg_ept_ready(&tty_ept) ? 1U : 0U); + app_status(11, tty_ept.dest_addr); + + /* Proof of life on the shared console every ~10 s (the loop ticks at + * ~20 Hz on the 50 ms kick timeout), so the demo pane shows the + * verified payload doing work rather than freezing after boot. */ + if ((beat % 200U) == 0U) { + char hb[64]; + snprintk(hb, sizeof(hb), + "M7 heartbeat %u: verified payload alive\r\n", + beat / 200U); + ring_puts(hb); + } + + if (is_rpmsg_ept_ready(&tty_ept)) { + if (replay_req) { + replay_req = false; + pos = 0; /* rewind; relay_console reports any lost span */ + } + relay_console(&tty_ept, &pos); + } +#endif + } + + return 0; +} diff --git a/imx95-pqc-demo/tools/memtool.c b/imx95-pqc-demo/tools/memtool.c new file mode 100644 index 0000000..eb3e4ba --- /dev/null +++ b/imx95-pqc-demo/tools/memtool.c @@ -0,0 +1,259 @@ +/* memtool.c - peek/poke/load physical memory via /dev/mem on the i.MX95. + * + * Torizon's busybox has no devmem applet, and plain dd on /dev/mem fails with + * EFAULT, so map the page explicitly instead. + * + * Usage: + * memtool r [words] read (default 8 words) + * memtool w write one 32-bit word + * memtool load copy a file to physical memory + * memtool fill + * + * Copyright (C) 2026 wolfSSL Inc. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int map_region(off_t phys, size_t len, void **base, void **ptr, int *fd) +{ + long pagesz = sysconf(_SC_PAGESIZE); + off_t aligned = phys & ~((off_t)pagesz - 1); + size_t offset = (size_t)(phys - aligned); + size_t maplen = len + offset; + + *fd = open("/dev/mem", O_RDWR | O_SYNC); + if (*fd < 0) { + fprintf(stderr, "open /dev/mem: %s\n", strerror(errno)); + return -1; + } + *base = mmap(NULL, maplen, PROT_READ | PROT_WRITE, MAP_SHARED, *fd, aligned); + if (*base == MAP_FAILED) { + fprintf(stderr, "mmap 0x%llx (len %zu): %s\n", + (unsigned long long)aligned, maplen, strerror(errno)); + close(*fd); + return -1; + } + *ptr = (void *)((char *)*base + offset); + return (int)maplen; +} + +int main(int argc, char **argv) +{ + void *base = NULL, *ptr = NULL; + int fd = -1, maplen; + off_t addr; + + if (argc < 3) + goto usage; + + addr = (off_t)strtoull(argv[2], NULL, 16); + + if (strcmp(argv[1], "r") == 0) { + unsigned n = (argc > 3) ? (unsigned)strtoul(argv[3], NULL, 0) : 8; + unsigned i; + + maplen = map_region(addr, n * 4, &base, &ptr, &fd); + if (maplen < 0) + return 1; + for (i = 0; i < n; i++) { + if ((i % 4) == 0) + printf("\n%08llx: ", (unsigned long long)(addr + i * 4)); + printf("%08x ", ((volatile uint32_t *)ptr)[i]); + } + printf("\n"); + } + else if (strcmp(argv[1], "w") == 0) { + uint32_t val; + + if (argc < 4) + goto usage; + val = (uint32_t)strtoul(argv[3], NULL, 16); + maplen = map_region(addr, 4, &base, &ptr, &fd); + if (maplen < 0) + return 1; + *(volatile uint32_t *)ptr = val; + } + else if (strcmp(argv[1], "fill") == 0) { + size_t len; + int byte; + + if (argc < 5) + goto usage; + len = (size_t)strtoul(argv[3], NULL, 0); + byte = (int)strtoul(argv[4], NULL, 16); + maplen = map_region(addr, len, &base, &ptr, &fd); + if (maplen < 0) + return 1; + /* Same device-memory constraint as "load" below: libc's memset emits + * unaligned and multi-register stores, which raise SIGBUS against the + * /dev/mem mapping on this part. Fill in aligned 32-bit words. */ + { + volatile uint32_t *dst = (volatile uint32_t *)ptr; + uint32_t word; + size_t words, i; + + word = (uint32_t)(byte & 0xff); + word |= (word << 8); + word |= (word << 16); + words = (len + 3) / 4; + for (i = 0; i < words; i++) + dst[i] = word; + } + } + else if (strcmp(argv[1], "load") == 0) { + struct stat st; + FILE *f; + size_t got; + + if (argc < 4) + goto usage; + if (stat(argv[3], &st) != 0) { + fprintf(stderr, "stat %s: %s\n", argv[3], strerror(errno)); + return 1; + } + f = fopen(argv[3], "rb"); + if (f == NULL) { + fprintf(stderr, "open %s: %s\n", argv[3], strerror(errno)); + return 1; + } + maplen = map_region(addr, (size_t)st.st_size, &base, &ptr, &fd); + if (maplen < 0) { + fclose(f); + return 1; + } + /* Do NOT fread() straight into the mapping. /dev/mem hands back a + * device-memory mapping on this part, where the unaligned and + * multi-register stores libc's memcpy emits raise SIGBUS. Stage the + * file in normal memory and copy it across in aligned 32-bit words. */ + { + unsigned char *buf = malloc((size_t)st.st_size + 4); + volatile uint32_t *dst = (volatile uint32_t *)ptr; + const uint32_t *src; + size_t words, i; + + if (buf == NULL) { + fprintf(stderr, "malloc %lld failed\n", (long long)st.st_size); + fclose(f); + return 1; + } + memset(buf, 0, (size_t)st.st_size + 4); + got = fread(buf, 1, (size_t)st.st_size, f); + src = (const uint32_t *)buf; + words = ((size_t)st.st_size + 3) / 4; + for (i = 0; i < words; i++) + dst[i] = src[i]; + free(buf); + } + fclose(f); + if (got != (size_t)st.st_size) { + fprintf(stderr, "short read: %zu of %lld\n", + got, (long long)st.st_size); + munmap(base, (size_t)maplen); + close(fd); + return 1; + } + printf("loaded %lld bytes to 0x%llx\n", + (long long)st.st_size, (unsigned long long)addr); + } + else if (strcmp(argv[1], "con") == 0) { + /* Dump the M7 shared-memory console ring buffer (see + * wolfboot hal/uart/uart_drv_imx95_m7.c). Header is + * magic / wr / size / rsvd, followed by the text. */ + volatile uint32_t *hdr; + uint32_t magic, wr, bufsz, i; + unsigned char *out; + + maplen = map_region(addr, 16, &base, &ptr, &fd); + if (maplen < 0) + return 1; + hdr = (volatile uint32_t *)ptr; + magic = hdr[0]; + wr = hdr[1]; + bufsz = hdr[2]; + munmap(base, (size_t)maplen); + close(fd); + base = NULL; fd = -1; + + if (magic != 0x4E4F4357UL) { /* "WCON" */ + fprintf(stderr, "no console at 0x%llx (magic 0x%08x)\n", + (unsigned long long)addr, magic); + return 1; + } + if (bufsz == 0 || bufsz > (64U * 1024U * 1024U)) { + fprintf(stderr, "implausible console size %u\n", bufsz); + return 1; + } + if (wr == 0) { + fprintf(stderr, "console empty\n"); + return 0; + } + if (wr > bufsz) { + fprintf(stderr, + "[console overran: %u bytes written into a %u byte buffer; " + "showing the most recent %u]\n", wr, bufsz, bufsz); + } + + maplen = map_region(addr + 16, bufsz, &base, &ptr, &fd); + if (maplen < 0) + return 1; + /* The copy below moves whole 32-bit words, so round the allocation up + * to the word count rather than bufsz + 1: a ring size that is not a + * multiple of 4 would otherwise be written up to 2 bytes past the end. */ + out = malloc(((size_t)bufsz + 3u) / 4u * 4u + 1u); + if (out == NULL) { + fprintf(stderr, "malloc failed\n"); + munmap(base, (size_t)maplen); + close(fd); + return 1; + } + /* /dev/mem hands back device memory: copy with aligned 32-bit reads, + * never memcpy (which raises SIGBUS with unaligned/vector loads). */ + { + volatile uint32_t *src = (volatile uint32_t *)ptr; + uint32_t words = (bufsz + 3) / 4; + uint32_t *dst = (uint32_t *)out; + + for (i = 0; i < words; i++) + dst[i] = src[i]; + } + + if (wr <= bufsz) { + fwrite(out, 1, wr, stdout); + } + else { + /* Ring wrapped: oldest byte is at wr % bufsz. */ + uint32_t start = wr % bufsz; + fwrite(out + start, 1, bufsz - start, stdout); + fwrite(out, 1, start, stdout); + } + fflush(stdout); + free(out); + } + else { + goto usage; + } + + if (base != NULL && base != MAP_FAILED) + munmap(base, (size_t)maplen); + if (fd >= 0) + close(fd); + return 0; + +usage: + fprintf(stderr, + "usage:\n" + " memtool r [words]\n" + " memtool w \n" + " memtool load \n" + " memtool fill \n" + " memtool con dump M7 shared-memory console\n"); + return 1; +} From a820d6bb259ab41772f77fd3e385292b626a6bdc Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 17 Sep 2026 16:22:44 -0700 Subject: [PATCH 02/14] imx95-pqc-demo: run the same post-quantum algorithm set on both cores --- imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt | 16 +++++++++----- imx95-pqc-demo/m7/zephyr-app/src/main.c | 24 ++++++++++++++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt b/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt index 8d5b79b..6952e96 100644 --- a/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt +++ b/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt @@ -12,8 +12,14 @@ target_include_directories(app PRIVATE target_sources(app PRIVATE ${ZEPHYR_WOLFSSL_MODULE_DIR}/wolfcrypt/benchmark/benchmark.c) -# benchmark.c carries its own main() for standalone use; this app has one. -# NOTE: this also compiles out wolfcrypt_benchmark_main() and the option -# tables (benchmark.c lines 18780-19294), so the algorithm selection cannot be -# driven by arguments - see the comment in src/main.c. -zephyr_compile_definitions(NO_MAIN_DRIVER) +# Select the algorithm set the same way the A55 container does, by argument. +# Two things are needed for that, and NO_MAIN_DRIVER is deliberately not one of +# them: it would remove wolfcrypt_benchmark_main() and the option tables along +# with main(). Rename main() instead, and ask wolfSSL to leave the parsing in - +# its Zephyr block defines MAIN_NO_ARGS by default because most applications +# have no argv to give it. +zephyr_compile_definitions(WOLFSSL_ZEPHYR_MAIN_ARGS) +set_source_files_properties( + ${ZEPHYR_WOLFSSL_MODULE_DIR}/wolfcrypt/benchmark/benchmark.c + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + PROPERTIES COMPILE_DEFINITIONS "main=wolfcrypt_bench_unused_main") diff --git a/imx95-pqc-demo/m7/zephyr-app/src/main.c b/imx95-pqc-demo/m7/zephyr-app/src/main.c index aad3fb9..c670344 100644 --- a/imx95-pqc-demo/m7/zephyr-app/src/main.c +++ b/imx95-pqc-demo/m7/zephyr-app/src/main.c @@ -64,13 +64,9 @@ * takes the same argv the command-line version does, so "-pq" restricts it to * the post-quantum algorithms - the ones this demo is about, and the ones that * finish in a sensible time on an 800 MHz in-order core. */ -/* Selection by argument is not available here: wolfcrypt/settings.h defines - * MAIN_NO_ARGS for every Zephyr build, and NO_MAIN_DRIVER (needed so this app - * keeps its own main) removes wolfcrypt_benchmark_main() and the option tables - * outright. benchmark_test() ignores what it is passed. So the algorithm set - * is whatever the build enables - which is why prj.conf turns on only ML-KEM - * and ML-DSA. */ -extern int benchmark_test(void *args); +/* wolfCrypt's stock benchmark. benchmark_test() ignores what it is passed, so + * the entry point that parses arguments is the one to call. */ +extern int wolfcrypt_benchmark_main(int argc, char **argv); LOG_MODULE_REGISTER(wolfboot_openamp, LOG_LEVEL_DBG); /* --- Must match wolfBoot's resource table (hal/imx95_m7.c) --- */ @@ -509,7 +505,19 @@ int main(void) ring_puts("\r\n--- wolfCrypt post-quantum benchmark, Cortex-M7 ---\r\n"); - (void)benchmark_test(NULL); + /* The post-quantum half of what the A55 container runs + * (container/entrypoint.sh), so the two cores report the same + * algorithms in the same units. The container also covers ECC, RSA and + * the hashes; those are left to it, because on this core they add + * minutes to a demo whose point is the PQC comparison. */ + static char *bench_argv[] = { + "benchmark", + "-ml-kem-512", "-ml-kem-768", "-ml-kem-1024", + "-ml-dsa-44", "-ml-dsa-65", "-ml-dsa-87" + }; + + (void)wolfcrypt_benchmark_main( + (int)(sizeof(bench_argv) / sizeof(bench_argv[0])), bench_argv); } ring_puts("--- end of Cortex-M7 benchmark ---\r\n"); From 54c38746b36e5473cb943e5c1cb9ee4fe0fef37a Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 06:51:35 -0700 Subject: [PATCH 03/14] imx95-pqc-demo: add AHAB packaging and eMMC boot-partition tools --- imx95-pqc-demo/tools/ahab-pack-container2.sh | 49 ++++++++++++ imx95-pqc-demo/tools/ahab-replace-spl.py | 80 ++++++++++++++++++++ imx95-pqc-demo/tools/emmc-select-boot.sh | 31 ++++++++ imx95-pqc-demo/tools/emmc-write-boot.sh | 51 +++++++++++++ 4 files changed, 211 insertions(+) create mode 100755 imx95-pqc-demo/tools/ahab-pack-container2.sh create mode 100755 imx95-pqc-demo/tools/ahab-replace-spl.py create mode 100755 imx95-pqc-demo/tools/emmc-select-boot.sh create mode 100755 imx95-pqc-demo/tools/emmc-write-boot.sh diff --git a/imx95-pqc-demo/tools/ahab-pack-container2.sh b/imx95-pqc-demo/tools/ahab-pack-container2.sh new file mode 100755 index 0000000..db6ea50 --- /dev/null +++ b/imx95-pqc-demo/tools/ahab-pack-container2.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Build AHAB container 2 (BL31 + BL33 + OP-TEE) and splice it into a copy of an +# existing eMMC boot-partition image. +# +# Container 1 - the ELE firmware, the System Manager, the OEI that trains DDR +# and the first A55 image - is copied through from the base image untouched, so +# the component that makes serial-download recovery possible is never rebuilt. +# +# Usage: +# ahab-pack-container2.sh +# +# Requires mkimage_imx8 from NXP's imx-mkimage: +# git clone https://github.com/nxp-imx/imx-mkimage +# cd imx-mkimage && make bin # produces ./mkimage_imx8 +# Point MKIMAGE at it, or leave it on PATH. +set -euo pipefail + +MKIMAGE="${MKIMAGE:-mkimage_imx8}" +CTNR2_OFF=$((0xcf800)) # where the i.MX95 boot ROM's second container sits + +[ $# -eq 5 ] || { sed -n '2,20p' "$0"; exit 1; } +BASE="$1"; BL31="$2"; BL33="$3"; OPTEE="$4"; OUT="$5" + +command -v "$MKIMAGE" >/dev/null || { echo "mkimage_imx8 not found; set MKIMAGE" >&2; exit 1; } +for f in "$BASE" "$BL31" "$BL33" "$OPTEE"; do + [ -f "$f" ] || { echo "missing: $f" >&2; exit 1; } +done + +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT + +# mkimage appends its own commit stamp to the payload it signs over. +"$MKIMAGE" -commit > "$TMP/head.hash" +cat "$BL33" "$TMP/head.hash" > "$TMP/bl33-hash.bin" + +"$MKIMAGE" -soc IMX9 -cntr_version 2 -c \ + -ap "$BL31" a55 0x8A200000 \ + -ap "$TMP/bl33-hash.bin" a55 0x90200000 \ + -ap "$OPTEE" a55 0x8C000000 \ + -out "$TMP/container2.img" > "$TMP/mkimage.log" 2>&1 \ + || { echo "mkimage failed:" >&2; tail -20 "$TMP/mkimage.log" >&2; exit 1; } + +cp "$BASE" "$OUT" +dd if="$TMP/container2.img" of="$OUT" bs=1 seek=$CTNR2_OFF conv=notrunc status=none + +[ "$(stat -c %s "$OUT")" = "$(stat -c %s "$BASE")" ] \ + || { echo "REFUSING: $OUT changed size against the base image" >&2; exit 1; } + +echo "container 2: $(stat -c %s "$TMP/container2.img") bytes at $(printf 0x%x $CTNR2_OFF)" +echo "output : $OUT" diff --git a/imx95-pqc-demo/tools/ahab-replace-spl.py b/imx95-pqc-demo/tools/ahab-replace-spl.py new file mode 100755 index 0000000..1bb898b --- /dev/null +++ b/imx95-pqc-demo/tools/ahab-replace-spl.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Put a wolfBoot stage 1 into AHAB container 0, in place of U-Boot SPL. + +The container's other images - the ELE firmware, the System Manager and the +OEI that trains DDR - are copied through byte for byte, and so is container 1. +Only the SPL slot's payload, size and hash change, and because the slot is +followed by a zero-length end marker at a fixed offset the container's overall +size does not move, so container 1 stays where it was. + + ahab-replace-spl.py + +The base image is a full eMMC boot-partition image (or any image holding an +AHAB container set at 0x8000). The image array records a SHA-384 over each +image, so the replaced slot's hash is recomputed here; the boot ROM rejects +the container otherwise. +""" +import hashlib +import struct +import sys + +CTNR0 = 0x8000 +SPL_DST = 0x20480000 +IMG_ENTRY = 128 +HDR = 16 +ALIGN = 0x400 + + +def main(argv): + if len(argv) != 4: + sys.stderr.write(__doc__) + return 1 + base, stage1_path, out = argv[1], argv[2], argv[3] + + buf = bytearray(open(base, 'rb').read()) + stage1 = open(stage1_path, 'rb').read() + + if buf[CTNR0 + 3] != 0x87: + sys.stderr.write("no container at 0x%x\n" % CTNR0) + return 1 + count = buf[CTNR0 + 11] + + slot = None + for i in range(count): + e = CTNR0 + HDR + i * IMG_ENTRY + dst, = struct.unpack_from(' old_size: + sys.stderr.write("stage 1 is %d bytes, slot holds %d\n" + % (new_size, old_size)) + return 1 + + start = CTNR0 + off + buf[start:start + old_size] = b'\0' * old_size + buf[start:start + len(stage1)] = stage1 + + struct.pack_into('&2; exit 1 ;; +esac + +echo "selecting $LABEL" +echo "-- before --" +ssh "$BOARD" "sudo mmc extcsd read /dev/mmcblk0 2>/dev/null | grep -iA2 'PARTITION_CONFIG'" || true +ssh "$BOARD" "sudo mmc bootpart enable $N 1 /dev/mmcblk0" +echo "-- after --" +ssh "$BOARD" "sudo mmc extcsd read /dev/mmcblk0 2>/dev/null | grep -iA2 'PARTITION_CONFIG'" || true +echo +echo "Power-cycle the board to boot from $LABEL." diff --git a/imx95-pqc-demo/tools/emmc-write-boot.sh b/imx95-pqc-demo/tools/emmc-write-boot.sh new file mode 100755 index 0000000..a0c2172 --- /dev/null +++ b/imx95-pqc-demo/tools/emmc-write-boot.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Write a boot-partition image to the target's eMMC over ssh, and verify it. +# +# The i.MX95 boot ROM reads its AHAB container set from whichever eMMC boot +# partition EXT_CSD selects. Writing the one that is NOT currently selected is +# safe: the board still boots the other one until you switch deliberately with +# emmc-select-boot.sh. +# +# Usage: +# BOARD=user@host emmc-write-boot.sh [0|1] # default 1 = boot1 +# +# sudo on the target must be usable; this deliberately embeds no password. Use +# an ssh key and either passwordless sudo or an askpass helper. +set -euo pipefail + +BOARD="${BOARD:?set BOARD=user@host}" +IMG="${1:?usage: BOARD=user@host $0 [0|1]}" +PART="${2:-1}" +case "$PART" in 0|1) ;; *) echo "partition must be 0 or 1" >&2; exit 1 ;; esac +DEV="/dev/mmcblk0boot$PART" + +[ -f "$IMG" ] || { echo "missing: $IMG" >&2; exit 1; } +LOCAL_MD5=$(md5sum "$IMG" | cut -d' ' -f1) +echo "image : $IMG ($(stat -c %s "$IMG") bytes, md5 $LOCAL_MD5)" +echo "target : $BOARD $DEV" + +echo "== which partition does the ROM boot from now? ==" +ssh "$BOARD" "sudo mmc extcsd read /dev/mmcblk0 2>/dev/null | grep -i 'PARTITION_CONFIG'" || true + +echo "== staging ==" +scp -q "$IMG" "$BOARD:/tmp/boot-image.bin" +ssh "$BOARD" 'sync' + +echo "== writing ==" +ssh "$BOARD" "sudo sh -c ' + set -e + echo 0 > /sys/block/mmcblk0boot$PART/force_ro + dd if=/tmp/boot-image.bin of=$DEV bs=1M conv=fsync status=none + sync + echo 1 > /sys/block/mmcblk0boot$PART/force_ro +'" + +echo "== verifying readback ==" +REMOTE_MD5=$(ssh "$BOARD" "sudo dd if=$DEV bs=1M count=32 2>/dev/null | md5sum | cut -d' ' -f1") +ssh "$BOARD" 'rm -f /tmp/boot-image.bin' +if [ "$LOCAL_MD5" != "$REMOTE_MD5" ]; then + echo "MD5 MISMATCH: local $LOCAL_MD5, board $REMOTE_MD5" >&2 + exit 1 +fi +echo "OK - readback matches ($REMOTE_MD5)" +echo "Nothing about the boot path changed yet; use emmc-select-boot.sh for that." From 1406dc4aaeef5b5e063978a74f94321c7db57fe0 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 06:56:21 -0700 Subject: [PATCH 04/14] imx95-pqc-demo: add AHAB image extractor, build guide and boot walkthrough --- imx95-pqc-demo/README.md | 13 ++ imx95-pqc-demo/docs/BOOT-WALKTHROUGH.md | 166 ++++++++++++++++++++++ imx95-pqc-demo/docs/BUILD.md | 179 ++++++++++++++++++++++++ imx95-pqc-demo/tools/ahab-extract.py | 124 ++++++++++++++++ 4 files changed, 482 insertions(+) create mode 100644 imx95-pqc-demo/docs/BOOT-WALKTHROUGH.md create mode 100644 imx95-pqc-demo/docs/BUILD.md create mode 100755 imx95-pqc-demo/tools/ahab-extract.py diff --git a/imx95-pqc-demo/README.md b/imx95-pqc-demo/README.md index 709ede7..e3b2b73 100644 --- a/imx95-pqc-demo/README.md +++ b/imx95-pqc-demo/README.md @@ -167,6 +167,19 @@ A55.** They derive from the 24 MHz generic timer rather than the 1.8 GHz core clock and are wrong by roughly 75x. Use ops/sec and MB/s, or pass `-freq 1800000000`. The demo renderer strips those columns for this reason. +## Documentation + +- [docs/BUILD.md](docs/BUILD.md) - building the demo from a stock module, end to end +- [docs/BOOT-WALKTHROUGH.md](docs/BOOT-WALKTHROUGH.md) - the boot explained stage by stage, and how to switch between the stock and wolfBoot chains + +## Tools + +- `tools/ahab-extract.py` - list or extract the images in an AHAB container set +- `tools/ahab-pack-container2.sh` - build container 2 (BL31, BL33, OP-TEE) into a boot image +- `tools/ahab-replace-spl.py` - put a wolfBoot stage 1 in the U-Boot SPL slot +- `tools/emmc-write-boot.sh` - write and verify an eMMC boot partition over ssh +- `tools/emmc-select-boot.sh` - choose which boot partition the boot ROM loads + ## Support wolfSSL is dual licensed under GPLv3 or a commercial license. Questions: diff --git a/imx95-pqc-demo/docs/BOOT-WALKTHROUGH.md b/imx95-pqc-demo/docs/BOOT-WALKTHROUGH.md new file mode 100644 index 0000000..bbd7090 --- /dev/null +++ b/imx95-pqc-demo/docs/BOOT-WALKTHROUGH.md @@ -0,0 +1,166 @@ +# i.MX95 demo: a technical walkthrough of the boot + +This follows a single cold boot line by line, explaining what each stage is and what it hands to the next. It is written to be read next to a live console or the captured log. + +The one-line summary: **wolfBoot is the first thing the boot ROM runs.** It occupies the slot U-Boot SPL normally holds, and there is no U-Boot anywhere in this chain. + +## The chain + +``` + power on + | + boot ROM on-die, immutable. Reads the AHAB container set from eMMC. + | + ELE firmware EdgeLock Enclave. The security subsystem. Container 0, image 1. + | + System Manager runs on the Cortex-M33. Owns clocks, power domains, pinmux. + | Container 0, image 2. + OEI Optional Executable Image. Trains DDR. Container 0, image 3. + | + wolfBoot stage 1 <-- replaces U-Boot SPL. Container 0, image 4, OCRAM 0x20480000. + | Reads eMMC, walks the container set, loads the next three. + BL31 ARM Trusted Firmware, secure monitor, EL3. DRAM 0x8A200000. + | + OP-TEE trusted OS, secure EL1. DRAM 0x8C000000. + | + wolfBoot BL33 verifies the Linux FIT with ML-DSA-87. DRAM 0x90200000. + | + Linux +``` + +The Cortex-M7 runs a second, independent wolfBoot that verifies a Zephyr image, started later by Linux. + +**Acronyms.** *AHAB* - Advanced High Assurance Boot, NXP's signed-container boot format. *ELE* - EdgeLock Enclave. *OEI* - Optional Executable Image, code the ROM runs and returns from. *BL31/BL33* - ARM Trusted Firmware boot-loader stages; BL31 is the secure monitor, BL33 the non-secure payload. *OCRAM* - on-chip RAM, available before DDR is trained. *FIT* - Flattened Image Tree, the kernel/DTB/initramfs bundle. *ML-DSA* - the NIST post-quantum signature standard, FIPS 204, formerly Dilithium. + +## Stage 1 - what replaces U-Boot SPL + +``` +wolfBoot stage 1: NXP i.MX95 Cortex-A55 +``` + +The first line on the console. On a stock module this would be `U-Boot SPL 2024.04`. The ROM has authenticated container 0, run the ELE firmware, started the System Manager on the M33, and let the OEI train DDR. It then loaded this image into OCRAM at `0x20480000` and jumped to it at EL3. + +``` +scmi: uSDHC1 clock 200000000 -> 400000000 Hz +scmi: uSDHC1 clock+pinmux up +``` + +The M33 System Manager owns the clock tree, so a stage running on the A55 asks for clocks over **SCMI** (System Control and Management Interface) rather than writing clock registers itself. The ROM leaves the eMMC controller clocked at 200 MHz; the driver's dividers are written against 400 MHz, so stage 1 sets the parent and rate before touching the controller. + +``` +emmc: ready, rca=0x1 part_config=0x50 +stage1: eMMC boot1 +``` + +The eMMC is up. `part_config` is EXT_CSD byte 179: `0x50` means the ROM boots from **boot1**. Stage 1 reads that same field back and points its own reads at the same partition, so it always reads the container set the ROM actually used. + +``` +stage1: image 0 -> 0x8A200000, 38912 bytes +stage1: image 1 -> 0x90200000, 64512 bytes +stage1: image 2 -> 0x8C000000, 593920 bytes +stage1: entering BL31 at 0x8A200000 +``` + +The container walk. Nothing on the medium records where the second container begins - it is derived as the end of the first one, rounded up. Stage 1 parses the image array and copies each image to its destination: BL31, then wolfBoot BL33, then OP-TEE. Then it enters BL31, exactly as U-Boot SPL would. + +## BL31 and OP-TEE + +``` +NOTICE: BL31: v2.10.0 (release):lf-6.6.52-2.2.1 +``` + +ARM Trusted Firmware takes EL3. It starts OP-TEE in the secure world, then drops to non-secure EL2 and enters BL33. These are the SoC vendor's own binaries, unchanged. + +## wolfBoot as BL33 + +``` +wolfBoot: NXP i.MX95 Cortex-A55 (BL33) +i.MX95 A55 handoff recon: + CurrentEL: EL2 + SCTLR_ELx: 0x0000000030C51835 + MMU=1 I$=1 D$=1 +``` + +wolfBoot reports the machine state it was handed: exception level 2, MMU and both caches on. The caches matter - hashing a 56 MB image uncached would be roughly a hundred times slower, and the MMU is what lets wolfBoot use the A55's ARMv8 cryptographic instructions. + +``` +scmi: M7 powered, TCM ECC initialized +scmi: uSDHC2 clock+pinmux+power up +usdhc: high speed (50 MHz) +usdhc: SD card ready, rca=0xAAAA (high capacity) +``` + +wolfBoot powers the Cortex-M7 domain and scrubs its tightly-coupled memory so its ECC is valid, then brings up the second SD controller for the carrier card. Card power and the card-detect line are carrier GPIOs owned by the System Manager, so those go over SCMI too. + +``` +Reading MBR... + MBR part 1: type=0x83, start=0x4000000, size=64MB +Checking primary OS image in 0,1... +Versions, A:1 B:0 +Attempting boot from P:A +``` + +wolfBoot finds its A and B slots on the SD card and picks A, which holds version 1. B is empty. This is the A/B update machinery: a new image goes to B, and wolfBoot falls back if it fails to boot. + +``` +Boot partition: 0x9024C290 (sz 56343747, ver 0x1, type 0xB01) +Loading image from disk...done (4915 ms) +Checking image integrity...done (134 ms) +Verifying image signature... +info: ML-DSA 5 verify_signature: pubkey 2592, sig 4627 +info: wc_MlDsaKey_Verify returned OK +``` + +**This is the point of the demo.** The FIT is read from the SD card, hashed, and its signature checked against the public key compiled into wolfBoot. The algorithm is ML-DSA at security level 5 - a 2592-byte public key and a 4627-byte signature. Only after that does wolfBoot hand off to Linux. + +If the signature does not match, wolfBoot says `Error validating authenticity` and stops. It does not boot the image. + +## Linux, and the second core + +Linux comes up, and systemd starts the demo units. They launch the Cortex-M7 through `remoteproc` and stream both cores to the console. Lines are tagged by origin: + +``` +[M7 ] wolfBoot: NXP i.MX95 Cortex-M7 +[M7 ] info: wc_MlDsaKey_Verify returned OK +[M7 ] *** Booting Zephyr OS build v4.4.0 *** +[A55] ML-DSA-87 verify 1719.904 ops/sec +``` + +The M7 is running its own wolfBoot, verifying a Zephyr image with the same algorithm. Both cores then run wolfCrypt benchmarks - ML-KEM key encapsulation and ML-DSA signatures - so the audience sees post-quantum cryptography both *verifying the boot* and *running as a workload*. + +The run ends with: + +``` + Demo complete. Both cores booted firmware verified with ML-DSA-87: + Cortex-A55 wolfBoot -> Linux + Cortex-M7 wolfBoot -> Zephyr +``` + +## Switching modes + +The board carries two boot chains, one per eMMC boot partition, and you can switch between them to show the same hardware booting with and without wolfBoot in the SPL slot. + +| | `PARTITION_CONFIG` | chain | +|---|---|---| +| **boot0** | `0x48` | stock Toradex: U-Boot SPL -> U-Boot -> Linux | +| **boot1** | `0x50` | wolfBoot stage 1 -> BL31 -> OP-TEE -> wolfBoot -> Linux | + +``` +export BOARD=torizon@ +../tools/emmc-select-boot.sh 0 # stock +../tools/emmc-select-boot.sh 1 # wolfBoot +``` + +Then power-cycle. The setting lives in EXT_CSD and survives power loss, so the board stays on whichever side you selected. + +The tell is the first console line: `U-Boot SPL` on boot0, `wolfBoot stage 1` on boot1. The rootfs is the same eMMC filesystem either way, so userspace is identical and only the boot chain differs. + +**The boot jumper cannot do this.** On this module SD boot is enabled by SoC fuses, not by the carrier's BOOT_SEL strap, so BOOT_SEL cannot select the boot medium. Leave it alone. + +## If something does not come up + +Check the first console line. No output at all usually means the board is not powered or the console cable is on the wrong header - the demo console is SMARC SER1 at 115200 8N1. + +`wolfBoot: PANIC!` after `Error validating authenticity` means the image on the SD card was not signed with the key compiled into this wolfBoot. That is a mismatched pair, not a corrupt card. + +If `boot1` does not start at all, select `boot0` and power-cycle; the stock chain is untouched. If the board will not boot either way, hold the recovery button and power on with a USB-C cable to the recovery port, then run `./uuu uuu-partconf.auto` from the Toradex Easy Installer recovery directory - it resets the boot partition to `boot0` from RAM without writing storage. diff --git a/imx95-pqc-demo/docs/BUILD.md b/imx95-pqc-demo/docs/BUILD.md new file mode 100644 index 0000000..4dffbb9 --- /dev/null +++ b/imx95-pqc-demo/docs/BUILD.md @@ -0,0 +1,179 @@ +# Building the i.MX95 post-quantum boot demo + +This builds a Toradex SMARC i.MX95 that boots with **wolfBoot in place of U-Boot SPL**, verifies Linux with ML-DSA-87 on the Cortex-A55, and runs a second wolfBoot on the Cortex-M7 that verifies a Zephyr image the same way. + +Everything here is a direct command. Where a script is used it lives in a public repository and is named by its path in that repository. + +## What you need + +**Hardware.** A Toradex SMARC iMX95 module on its carrier, a USB serial cable on SMARC SER1, a microSD card, and a way to power-cycle the board. A USB-C cable to the recovery port is worth having before you start - see *Recovery* at the end. + +**Repositories.** + +``` +git clone https://github.com/wolfSSL/wolfBoot +git clone https://github.com/wolfSSL/wolfBoot-examples +git clone https://github.com/nxp-imx/imx-mkimage +``` + +**Toolchains.** `aarch64-none-elf-` (or `aarch64-linux-gnu-`) for the A55, `arm-none-eabi-` for the M7. On Debian or Ubuntu: `apt install gcc-aarch64-linux-gnu gcc-arm-none-eabi`. + +**From the running board, once.** You need the SoC's own boot images - the EdgeLock Enclave (ELE) firmware, the M33 System Manager, and the Optional Executable Image (OEI) that trains DDR. They are not redistributable and not rebuilt here; take them from the module as shipped: + +``` +ssh "sudo dd if=/dev/mmcblk0boot0 bs=1M count=32" > boot0.bin +``` + +`boot0.bin` is your base image and your fallback. Keep it. + +ARM Trusted Firmware (BL31) and OP-TEE are in there too, so you do not need to build or source them separately. Pull everything out of that one file: + +``` +python3 ../tools/ahab-extract.py boot0.bin -o vendor/ +``` + +``` +container 0 at 0x8000, 5 image(s) + [1] 0x1FFC0000 321536 bytes Cortex-M33 image (System Manager or OEI) + [2] 0x1FFC0000 129024 bytes Cortex-M33 image (System Manager or OEI) + [3] 0x20480000 141312 bytes A55 image in OCRAM (U-Boot SPL, or wolfBoot stage 1) +container 1 at 0xcf800, 3 image(s) + [0] 0x8A200000 38912 bytes ARM Trusted Firmware BL31 (secure monitor, EL3) + [1] 0x90200000 968704 bytes BL33 (U-Boot proper, or wolfBoot) + [2] 0x8C000000 593920 bytes OP-TEE (trusted OS, secure EL1) +``` + +That gives you `vendor/bl31.bin` and `vendor/tee.bin`, which is all you need from the vendor side. Images are named by load address, because those are fixed on this SoC. The two M33 images share a load address - the OEI runs and returns before the System Manager is loaded over it - so the second is written with an index suffix. + +## 1. Build mkimage + +``` +cd imx-mkimage +make bin +export MKIMAGE=$PWD/mkimage_imx8 +``` + +## 2. Build wolfBoot for the A55 + +wolfBoot runs twice on the A55: as **BL33**, where ARM Trusted Firmware hands it control and it verifies and boots Linux; and as **stage 1**, in the slot U-Boot SPL normally occupies. + +``` +cd wolfBoot +cp config/examples/imx95-a55.config .config +make # BL33 -> wolfboot.bin +make -C stage1 DISK_EMMC=1 DISK_SDCARD=0 loader_stage1.bin +``` + +The first build also generates a signing key (`wolfboot_signing_private_key.der`) and compiles the matching public key into wolfBoot's keystore. **Copy that key somewhere outside the tree.** `make keysclean` regenerates it, and a keystore that no longer matches your signed images means the board stops booting with an authenticity error. + +``` +cp wolfboot_signing_private_key.der ~/keys/imx95-demo-signing-key.der +``` + +To reuse an existing key instead of generating one, import its public half before building. Note the level must be given in the environment - both key tools default to ML-DSA level 2 and will silently truncate a level 5 key: + +``` +python3 -c "d=open('/path/to/key.der','rb').read(); open('pub.der','wb').write(d[-2592:])" +ML_DSA_LEVEL=5 ./tools/keytools/keygen --ml_dsa -i pub.der --der +``` + +## 3. Sign the Linux FIT + +The payload is a FIT image holding the kernel, device tree and initramfs. Build it however your distribution does - for Torizon it comes out of the BSP - then sign it with the same key wolfBoot was built with: + +``` +ML_DSA_LEVEL=5 IMAGE_HEADER_SIZE=12288 \ + wolfBoot/tools/keytools/sign --ml_dsa --sha256 \ + fitImage wolfBoot/wolfboot_signing_private_key.der 1 +``` + +This writes `fitImage_v1_signed.bin`: the image with a 12288-byte wolfBoot header prepended. The `1` is the version; wolfBoot refuses to roll back to a lower one. + +## 4. Assemble the boot image + +The i.MX95 boot ROM reads a set of **AHAB** (Advanced High Assurance Boot) containers from the boot medium. There are two: + +- **Container 0** at offset `0x8000` - ELE firmware, System Manager, OEI, and one A55 image loaded into OCRAM at `0x20480000`. That last slot is U-Boot SPL on a stock module, and is what wolfBoot takes over. +- **Container 1**, immediately after - BL31, BL33 and OP-TEE. + +Nothing records where container 1 starts. It is the end of container 0 - the furthest of its header, its images and its signature block - rounded up to 1 KiB. On this module that lands at `0xcf800`. + +Build container 1 with wolfBoot as BL33, splicing it into a copy of `boot0.bin`: + +``` +MKIMAGE=$MKIMAGE ../tools/ahab-pack-container2.sh \ + boot0.bin vendor/bl31.bin wolfBoot/wolfboot.bin vendor/tee.bin boot-wolfboot.bin +``` + +Then replace the SPL slot in container 0 with the stage 1: + +``` +python3 ../tools/ahab-replace-spl.py \ + boot-wolfboot.bin wolfBoot/stage1/loader_stage1.bin boot-final.bin +``` + +Each image entry carries a **SHA-384** over its payload, so that script recomputes the hash for the slot it replaces; the ROM rejects the container otherwise. It leaves container 0's other images and all of container 1 untouched, and because the SPL slot is followed by a zero-length end marker at a fixed offset, the container's size does not change and container 1 does not move. + +Both containers this produces are byte-identical to the ones running on our demo board, which is the check that the path above is complete: everything came from that single `dd`, plus wolfBoot. + +## 5. Write the SD card + +wolfBoot BL33 reads the signed FIT from the carrier SD. Partition it once - three primary partitions, the first two being wolfBoot's A and B slots: + +``` +sudo sfdisk /dev/sdX <<'EOF' +label: dos +start=131072, size=131072, type=83 +start=262144, size=262144, type=83 +start=524288, size=262144, type=83 +EOF +sudo dd if=fitImage_v1_signed.bin of=/dev/sdX2 bs=1M conv=fsync +``` + +wolfBoot numbers partitions from zero, so its "partition A" is the second MBR entry, `/dev/sdX2`. + +## 6. Flash the board + +The eMMC has two boot partitions and EXT_CSD selects which one the ROM reads. Write the one you are **not** currently booting from, so the other stays as a way back: + +``` +export BOARD=torizon@ +../tools/emmc-write-boot.sh boot-final.bin 1 +../tools/emmc-select-boot.sh 1 +``` + +Power-cycle. Both scripts verify by reading back and neither embeds a password; use an ssh key and sudo on the target. + +## 7. Build the Cortex-M7 side + +``` +cd wolfBoot +cp config/examples/imx95-m7.config .config +make +``` + +The M7 image is loaded by Linux `remoteproc` rather than by the ROM, so it is not part of the AHAB container set. Staging and start-up are covered by `../demo/m7-start.sh`, and the Zephyr application it verifies is built from `../m7/zephyr-app/`. + +Two things bite when reloading M7 firmware. The core cannot be stopped once wolfBoot's `main()` is spinning - the kernel logs `Not in wfi` and `can't stop rproc: -4` - so a reload silently leaves the previous firmware running; power-cycle instead. And after copying firmware to the board, run `sync` before cutting power, or the write sits in page cache and the board comes up on the old image with a filename and size that look right. + +## 8. Install the demo + +``` +scp -r ../demo :~/demo +ssh "~/demo/install-autostart.sh" +``` + +That installs systemd units which start the M7 and stream both cores' output to the console on every boot. + +## Recovery + +`boot0` is never written by any of the above, so it is always the way back. If an image in `boot1` fails to start, hold the recovery button (**B4**) and power the board on, with a USB-C cable from the recovery port to your host. The boot ROM's serial downloader takes over - it lives in ROM and needs nothing on the eMMC, so replacing SPL does not put recovery at risk. Then, from Toradex's Easy Installer package: + +``` +cd /recovery +./uuu uuu-partconf.auto +``` + +That boots a recovery U-Boot entirely in RAM and sets the boot partition back to `boot0` without writing storage. Release the button and power-cycle. + +The carrier's BOOT_SEL jumper does **not** need changing, and should be left alone: on this module SD boot is enabled by SoC fuses rather than by the strap, so BOOT_SEL cannot select it. diff --git a/imx95-pqc-demo/tools/ahab-extract.py b/imx95-pqc-demo/tools/ahab-extract.py new file mode 100755 index 0000000..0629ff8 --- /dev/null +++ b/imx95-pqc-demo/tools/ahab-extract.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""List and extract the images in an i.MX95 AHAB container set. + +The boot medium holds a set of containers: the first one the boot ROM reads at +0x8000, and further ones immediately after. Nothing records where the next +container starts - it is the end of the previous one (the furthest of its +header, its images and its signature block) rounded up to 1 KiB. + +Use this to recover the SoC's own images from a stock boot image, which is +where BL31 and OP-TEE come from when rebuilding the container set: + + ahab-extract.py boot0.bin # list what is there + ahab-extract.py boot0.bin -o out/ # write each image out + +Images are named by their load address, which is what identifies them: +BL31, OP-TEE and BL33 all sit at fixed addresses on this SoC. +""" +import argparse +import os +import struct +import sys + +MMC_OFFSET = 0x8000 +ALIGN = 1024 +TAG = 0x87 +VERSION = 0x02 +HDR = 16 +ENTRY = 128 + +# Load addresses are stable on i.MX95, so they name the image. +KNOWN = { + 0x8A200000: ("bl31.bin", "ARM Trusted Firmware BL31 (secure monitor, EL3)"), + 0x8C000000: ("tee.bin", "OP-TEE (trusted OS, secure EL1)"), + 0x90200000: ("bl33.bin", "BL33 (U-Boot proper, or wolfBoot)"), + 0x20480000: ("spl.bin", "A55 image in OCRAM (U-Boot SPL, or wolfBoot stage 1)"), + 0x1FFC0000: ("m33.bin", "Cortex-M33 image (System Manager or OEI)"), +} + + +def parse_container(buf, base): + """Return (images, size) for the container at base, or None.""" + if base + HDR > len(buf): + return None + version, length, tag = buf[base], struct.unpack_from(' 8: + return None + sig_off = struct.unpack_from(' 0xFFFFFFFF - off: + return None + end = max(end, off + size) + images.append(dict(index=i, offset=off, size=size, dst=dst, + entry=entry, flags=flags)) + if sig_off: + sig_len = struct.unpack_from(' %s" % out) + found += 1 + base = (base + size + ALIGN - 1) & ~(ALIGN - 1) + n += 1 + + if args.out_dir: + print("\nextracted %d image(s) to %s" % (found, args.out_dir)) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) From 6800e453bb64bbda02d10162b0197e4fde73e1b3 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 06:59:58 -0700 Subject: [PATCH 05/14] imx95-pqc-demo: document building the kernel FIT from the running board --- imx95-pqc-demo/docs/BUILD.md | 57 ++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/imx95-pqc-demo/docs/BUILD.md b/imx95-pqc-demo/docs/BUILD.md index 4dffbb9..cbe8766 100644 --- a/imx95-pqc-demo/docs/BUILD.md +++ b/imx95-pqc-demo/docs/BUILD.md @@ -77,9 +77,62 @@ python3 -c "d=open('/path/to/key.der','rb').read(); open('pub.der','wb').write(d ML_DSA_LEVEL=5 ./tools/keytools/keygen --ml_dsa -i pub.der --der ``` -## 3. Sign the Linux FIT +## 3. Build and sign the Linux FIT -The payload is a FIT image holding the kernel, device tree and initramfs. Build it however your distribution does - for Torizon it comes out of the BSP - then sign it with the same key wolfBoot was built with: +The payload is a FIT (Flattened Image Tree) holding the kernel, device tree and initramfs. Take the three components off the running board - they are whatever the distribution installed - and bundle them: + +``` +scp :/boot/ostree/torizon-*/vmlinuz-* . +scp :/boot/ostree/torizon-*/initramfs-* . +scp :/boot/ostree/torizon-*/dtb/imx95-toradex-smarc-dev.dtb . +gunzip -c vmlinuz-* > Image +``` + +The kernel in `/boot` is gzip-compressed; the FIT wants the flat `Image`. Write `fit.its` describing the three: + +``` +/dts-v1/; +/ { + description = "Kernel FIT for wolfBoot verified boot"; + #address-cells = <1>; + images { + kernel { + data = /incbin/("Image"); + type = "kernel"; arch = "arm64"; os = "linux"; + compression = "none"; + load = <0xB2000000>; entry = <0xB2000000>; + }; + fdt { + data = /incbin/("imx95-toradex-smarc-dev.dtb"); + type = "flat_dt"; arch = "arm64"; compression = "none"; + load = <0xB8000000>; + }; + ramdisk { + data = /incbin/("initramfs-6.6.142-7.7.0.img"); + type = "ramdisk"; arch = "arm64"; os = "linux"; + compression = "none"; + }; + }; + configurations { + default = "conf"; + conf { kernel = "kernel"; fdt = "fdt"; ramdisk = "ramdisk"; }; + }; +}; +``` + +The load addresses put the kernel and device tree in DRAM clear of the regions BL31, OP-TEE and wolfBoot occupy. Then build it with `mkimage` from `u-boot-tools`: + +``` +mkimage -f fit.its fitImage +``` + +The FIT records a build timestamp, so two builds of identical inputs differ in three bytes. Set `SOURCE_DATE_EPOCH` if you want a reproducible result: + +``` +SOURCE_DATE_EPOCH=$(date +%s) mkimage -f fit.its fitImage +``` + +Now sign it with the same key wolfBoot was built with: ``` ML_DSA_LEVEL=5 IMAGE_HEADER_SIZE=12288 \ From 609d90ab6b147f32ca24f3504a2829f5a0df356e Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 07:01:20 -0700 Subject: [PATCH 06/14] imx95-pqc-demo: install the serial-console demo by default, display renderer opt-in --- imx95-pqc-demo/demo/install-autostart.sh | 26 ++++++++++++++++++------ imx95-pqc-demo/docs/BUILD.md | 4 ++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/imx95-pqc-demo/demo/install-autostart.sh b/imx95-pqc-demo/demo/install-autostart.sh index 131eddf..81a9f66 100755 --- a/imx95-pqc-demo/demo/install-autostart.sh +++ b/imx95-pqc-demo/demo/install-autostart.sh @@ -1,14 +1,20 @@ #!/usr/bin/env bash -# Install and enable the boot-time demo unit. Run ON THE BOARD as root. +# Install and enable the boot-time demo units. Run ON THE BOARD as root. # -# sudo bash install-autostart.sh # install + enable -# sudo bash install-autostart.sh --off # disable, restore the getty +# sudo bash install-autostart.sh # serial console (default) +# sudo bash install-autostart.sh --display # two-pane renderer on tty1 +# sudo bash install-autostart.sh --off # disable, restore the getty +# +# The serial-console demo is the default because it needs nothing but the +# console cable; the two-pane renderer additionally needs a DisplayPort screen. set -euo pipefail -UNIT=/etc/systemd/system/wolfssl-demo.service +UART_UNIT=/etc/systemd/system/wolfssl-demo-uart.service +DISPLAY_UNIT=/etc/systemd/system/wolfssl-demo.service M7_UNIT=/etc/systemd/system/wolfssl-m7.service HERE="$(cd "$(dirname "$0")" && pwd)" if [ "${1:-}" = "--off" ]; then + systemctl disable --now wolfssl-demo-uart.service 2>/dev/null || true systemctl disable --now wolfssl-demo.service 2>/dev/null || true systemctl disable --now wolfssl-m7.service 2>/dev/null || true systemctl start getty@tty1.service 2>/dev/null || true @@ -17,10 +23,18 @@ if [ "${1:-}" = "--off" ]; then exit 0 fi -install -m 0644 "$HERE/wolfssl-demo.service" "$UNIT" install -m 0644 "$HERE/wolfssl-m7.service" "$M7_UNIT" +if [ "${1:-}" = "--display" ]; then + install -m 0644 "$HERE/wolfssl-demo.service" "$DISPLAY_UNIT" + UNIT="$DISPLAY_UNIT" + systemctl disable wolfssl-demo-uart.service 2>/dev/null || true +else + install -m 0644 "$HERE/wolfssl-demo-uart.service" "$UART_UNIT" + UNIT="$UART_UNIT" + systemctl disable wolfssl-demo.service 2>/dev/null || true +fi systemctl daemon-reload -systemctl enable wolfssl-demo.service wolfssl-m7.service +systemctl enable "$(basename "$UNIT")" wolfssl-m7.service # The demo's replay beat is a hard power cut, so nothing may sit in the page # cache: an unsynced unit file is simply gone after the next cycle. sync diff --git a/imx95-pqc-demo/docs/BUILD.md b/imx95-pqc-demo/docs/BUILD.md index cbe8766..7615bc9 100644 --- a/imx95-pqc-demo/docs/BUILD.md +++ b/imx95-pqc-demo/docs/BUILD.md @@ -213,10 +213,10 @@ Two things bite when reloading M7 firmware. The core cannot be stopped once wolf ``` scp -r ../demo :~/demo -ssh "~/demo/install-autostart.sh" +ssh "sudo bash ~/demo/install-autostart.sh" ``` -That installs systemd units which start the M7 and stream both cores' output to the console on every boot. +That installs two systemd units: one starts the Cortex-M7 through `remoteproc`, the other streams both cores' output to the serial console on every boot. Pass `--display` instead if you have a DisplayPort screen attached and want the two-pane renderer, or `--off` to disable the demo and get the login prompt back. ## Recovery From 0dfc209388a67f5256c1b4af96b5c77531502f36 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 08:33:42 -0700 Subject: [PATCH 07/14] imx95-pqc-demo: give sudo a terminal in the eMMC helpers so it can prompt --- imx95-pqc-demo/tools/emmc-select-boot.sh | 9 ++++++--- imx95-pqc-demo/tools/emmc-write-boot.sh | 11 ++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/imx95-pqc-demo/tools/emmc-select-boot.sh b/imx95-pqc-demo/tools/emmc-select-boot.sh index ee56e7f..41643ea 100755 --- a/imx95-pqc-demo/tools/emmc-select-boot.sh +++ b/imx95-pqc-demo/tools/emmc-select-boot.sh @@ -11,6 +11,9 @@ # # mmc-utils numbers the boot partitions from 1, so boot0 is "1" and boot1 "2" # in the underlying command. +# +# ssh -t gives sudo a terminal so it can prompt; nothing here embeds a password. +# It also makes the remote echo CRLF, hence the tr on captured output. set -euo pipefail BOARD="${BOARD:?set BOARD=user@host}" @@ -23,9 +26,9 @@ esac echo "selecting $LABEL" echo "-- before --" -ssh "$BOARD" "sudo mmc extcsd read /dev/mmcblk0 2>/dev/null | grep -iA2 'PARTITION_CONFIG'" || true -ssh "$BOARD" "sudo mmc bootpart enable $N 1 /dev/mmcblk0" +ssh -t "$BOARD" "sudo mmc extcsd read /dev/mmcblk0 2>/dev/null | grep -iA2 'PARTITION_CONFIG'" 2>/dev/null | tr -d '\r' || true +ssh -t "$BOARD" "sudo mmc bootpart enable $N 1 /dev/mmcblk0" 2>/dev/null | tr -d '\r' echo "-- after --" -ssh "$BOARD" "sudo mmc extcsd read /dev/mmcblk0 2>/dev/null | grep -iA2 'PARTITION_CONFIG'" || true +ssh -t "$BOARD" "sudo mmc extcsd read /dev/mmcblk0 2>/dev/null | grep -iA2 'PARTITION_CONFIG'" 2>/dev/null | tr -d '\r' || true echo echo "Power-cycle the board to boot from $LABEL." diff --git a/imx95-pqc-demo/tools/emmc-write-boot.sh b/imx95-pqc-demo/tools/emmc-write-boot.sh index a0c2172..d98f907 100755 --- a/imx95-pqc-demo/tools/emmc-write-boot.sh +++ b/imx95-pqc-demo/tools/emmc-write-boot.sh @@ -9,8 +9,9 @@ # Usage: # BOARD=user@host emmc-write-boot.sh [0|1] # default 1 = boot1 # -# sudo on the target must be usable; this deliberately embeds no password. Use -# an ssh key and either passwordless sudo or an askpass helper. +# sudo on the target must be usable; this deliberately embeds no password. +# ssh -t gives sudo a terminal so it can prompt for one. It also makes the +# remote echo CRLF, hence the tr on captured output. set -euo pipefail BOARD="${BOARD:?set BOARD=user@host}" @@ -25,14 +26,14 @@ echo "image : $IMG ($(stat -c %s "$IMG") bytes, md5 $LOCAL_MD5)" echo "target : $BOARD $DEV" echo "== which partition does the ROM boot from now? ==" -ssh "$BOARD" "sudo mmc extcsd read /dev/mmcblk0 2>/dev/null | grep -i 'PARTITION_CONFIG'" || true +ssh -t "$BOARD" "sudo mmc extcsd read /dev/mmcblk0 2>/dev/null | grep -i 'PARTITION_CONFIG'" 2>/dev/null | tr -d '\r' || true echo "== staging ==" scp -q "$IMG" "$BOARD:/tmp/boot-image.bin" ssh "$BOARD" 'sync' echo "== writing ==" -ssh "$BOARD" "sudo sh -c ' +ssh -t "$BOARD" "sudo sh -c ' set -e echo 0 > /sys/block/mmcblk0boot$PART/force_ro dd if=/tmp/boot-image.bin of=$DEV bs=1M conv=fsync status=none @@ -41,7 +42,7 @@ ssh "$BOARD" "sudo sh -c ' '" echo "== verifying readback ==" -REMOTE_MD5=$(ssh "$BOARD" "sudo dd if=$DEV bs=1M count=32 2>/dev/null | md5sum | cut -d' ' -f1") +REMOTE_MD5=$(ssh -t "$BOARD" "sudo dd if=$DEV bs=1M count=32 2>/dev/null | md5sum | cut -d' ' -f1" 2>/dev/null | tr -d '\r\n') ssh "$BOARD" 'rm -f /tmp/boot-image.bin' if [ "$LOCAL_MD5" != "$REMOTE_MD5" ]; then echo "MD5 MISMATCH: local $LOCAL_MD5, board $REMOTE_MD5" >&2 From 0c2c7cfa61ae3519ff84c62e80c4334e1613f4b7 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 09:47:33 -0700 Subject: [PATCH 08/14] imx95-pqc-demo: replay the A55 verified-boot lines where the audience can see them --- imx95-pqc-demo/demo/demo-uart.sh | 37 +++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/imx95-pqc-demo/demo/demo-uart.sh b/imx95-pqc-demo/demo/demo-uart.sh index c1c1f93..02ea0a7 100755 --- a/imx95-pqc-demo/demo/demo-uart.sh +++ b/imx95-pqc-demo/demo/demo-uart.sh @@ -20,6 +20,9 @@ DEMO=${DEMO:-/home/torizon/demo} MEMTOOL=${MEMTOOL:-/home/torizon/bin/memtool} CONSOLE_ADDR=${CONSOLE_ADDR:-0x80F00000} STATUS_ADDR=${STATUS_ADDR:-0x80F10000} +# wolfBoot (BL33) mirrors its console into this DDR ring (IMX95_LOG_RING=1), so +# its verified-boot lines can still be read after the handoff to Linux. +A55_RING_ADDR=${A55_RING_ADDR:-0x80F20000} M7_CORE_MHZ=${M7_CORE_MHZ:-800} CONTAINER=${CONTAINER:-wolfcrypt-pqc} INTERVAL=${INTERVAL:-0.5} @@ -31,6 +34,8 @@ SETTLE=${SETTLE:-3} R=$'\033[0m' C_M7=$'\033[1;36m' C_A55=$'\033[1;33m' +C_OK=$'\033[1;32m' +C_HDR=$'\033[1;37m' # Stop the kernel from writing to this console for the rest of the demo. Its # late-boot chatter (USB, Bluetooth, wlan scans) otherwise interleaves with the @@ -45,12 +50,42 @@ sleep "$SETTLE" printf '%s\n' "$R" echo "==================================================================" echo " NXP i.MX95 - post-quantum verified boot on both clusters" -echo " Cortex-A55 wolfBoot ML-DSA-87 -> Linux (printed above)" +echo " Cortex-A55 wolfBoot ML-DSA-87 -> Linux (replayed below)" echo " Cortex-M7 wolfBoot ML-DSA-87 -> Zephyr [M7 ]" echo " Cortex-A55 wolfCrypt ML-KEM / ML-DSA [A55]" echo "==================================================================" echo +# Replay the A55 verified-boot lines. +# +# They print live, seconds after power-on, but by the time anyone is watching +# they have scrolled away under the kernel's own boot messages. wolfBoot mirrors +# its console into a DDR ring precisely so this moment can be shown again, at +# the point in the demo where someone is actually looking at the screen. +a55_verified_boot() { + local ring + ring=$("$MEMTOOL" con "$A55_RING_ADDR" 2>/dev/null) || return 0 + [ -n "$ring" ] || return 0 + + printf '%s\n' "$R" + echo "${C_HDR}+---------------------------------------------------------------+${R}" + echo "${C_HDR}| Cortex-A55: what wolfBoot did before Linux started |${R}" + echo "${C_HDR}+---------------------------------------------------------------+${R}" + # wolfBoot's console writes CRLF; strip the CR or it prints as ^M here. + printf '%s' "$ring" | tr -d '\r' \ + | grep -aE "Boot partition:|Checking image integrity|Verifying image signature|ML-DSA|MlDsaKey|Firmware Valid" \ + | awk '!seen[$0]++' \ + | while IFS= read -r line; do + case "$line" in + *"returned OK"|*"Firmware Valid"*) printf '%s %s%s\n' "$C_OK" "$line" "$R" ;; + *) printf '%s %s%s\n' "$C_A55" "$line" "$R" ;; + esac + done + echo "${C_HDR}+---------------------------------------------------------------+${R}" + echo +} +a55_verified_boot + # The compose network does not survive a power cut while a container created by # an earlier run does, so "up -d" alone fails with "network not found" and the # benchmark half of the stream stays empty. The demo's replay beat IS a power From 53fd9991a93f21e130172173790ccbac730d7bb6 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 09:58:52 -0700 Subject: [PATCH 09/14] imx95-pqc-demo: stop two starters from loading the M7 payload at once --- imx95-pqc-demo/demo/demo-uart.sh | 13 ++++++++++--- imx95-pqc-demo/demo/m7-start.sh | 8 ++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/imx95-pqc-demo/demo/demo-uart.sh b/imx95-pqc-demo/demo/demo-uart.sh index 02ea0a7..cc4c73d 100755 --- a/imx95-pqc-demo/demo/demo-uart.sh +++ b/imx95-pqc-demo/demo/demo-uart.sh @@ -98,10 +98,17 @@ a55_verified_boot ( cd "$DEMO" && LOOP=0 RESTART_POLICY=no docker compose up -d >/dev/null 2>&1 ) \ || echo "[A55] container failed to start" -# The M7 is started by wolfssl-m7.service; only release it here if that unit is -# not in use. It can only be started once per Linux boot either way. +# The M7 is normally released by wolfssl-m7.service, which systemd starts in +# parallel with this unit. Go through systemd where that unit exists, so the two +# cannot both load the payload: the second load lands in DDR under a core that +# is already executing it, and the app faults on a corrupted heap seconds later. +# m7-start.sh takes a lock as well, for a board without the unit. if [ "$(cat /sys/class/remoteproc/remoteproc1/state 2>/dev/null)" != "running" ]; then - bash "$DEMO/m7-start.sh" >/dev/null 2>&1 || true + if systemctl cat wolfssl-m7.service >/dev/null 2>&1; then + systemctl start wolfssl-m7.service >/dev/null 2>&1 || true + else + bash "$DEMO/m7-start.sh" >/dev/null 2>&1 || true + fi fi # memtool dumps the whole ring each call, so track what has already been shown diff --git a/imx95-pqc-demo/demo/m7-start.sh b/imx95-pqc-demo/demo/m7-start.sh index f0ef7a0..c3e1402 100755 --- a/imx95-pqc-demo/demo/m7-start.sh +++ b/imx95-pqc-demo/demo/m7-start.sh @@ -14,6 +14,14 @@ RP=/sys/class/remoteproc/remoteproc1 BOOT_ADDR=0x80100000 STATUS=0x80F10000 +# wolfssl-m7.service and demo-uart.sh's fallback both release the core, and +# systemd starts them in parallel. Both read the remoteproc state before +# loading, so without a lock the loser rewrites the payload in DDR under a core +# that is already executing it; the app then faults on a corrupted heap a second +# or two later. Serialize check, load and start. +exec 9>/run/wolfssl-m7-start.lock +flock 9 + if [ "$(cat $RP/state)" = "running" ]; then echo "M7 is already running - power cycle the board to run the demo again" >&2 exit 1 From f9df190cc33b8bc13bb1e8aaa496f9f7ca66c013 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 09:58:52 -0700 Subject: [PATCH 10/14] imx95-pqc-demo: add the DisplayPort renderer unit and its font lookup --- imx95-pqc-demo/demo/demo-display-fg.sh | 28 +++++++++++++++++ imx95-pqc-demo/demo/drmfb.py | 27 +++++++++++++++-- imx95-pqc-demo/demo/install-autostart.sh | 30 ++++++++++++------- imx95-pqc-demo/demo/stage.sh | 1 + imx95-pqc-demo/demo/wolfssl-display.service | 33 +++++++++++++++++++++ imx95-pqc-demo/docs/BUILD.md | 18 ++++++++++- 6 files changed, 122 insertions(+), 15 deletions(-) create mode 100755 imx95-pqc-demo/demo/demo-display-fg.sh create mode 100644 imx95-pqc-demo/demo/wolfssl-display.service diff --git a/imx95-pqc-demo/demo/demo-display-fg.sh b/imx95-pqc-demo/demo/demo-display-fg.sh new file mode 100755 index 0000000..b336c6f --- /dev/null +++ b/imx95-pqc-demo/demo/demo-display-fg.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Render the two panes on the DisplayPort panel, in the foreground so systemd +# owns the renderer's lifetime. Runs ON THE BOARD as root. +# +# This is the display half of the demo only. It starts nothing: the benchmark +# container and the Cortex-M7 are brought up by demo-uart.sh (or demo-run.sh), +# and this just follows what they produce. That split is what lets the screen +# be a second view of the serial console rather than a competing demo. +set -uo pipefail + +DEMO="$(cd "$(dirname "$0")" && pwd)" + +# Torizon's kernel has no fbdev emulation, so text written to a VT reaches no +# display: the renderer has to own the DisplayPort through KMS. Take the screen +# away from everything else that wants it - the framebuffer console redrawing +# over the KMS output, and Torizon's pairing overlay - then render as the sole +# DRM master. The getty is handled by the unit's Conflicts= line. +for v in /sys/class/vtconsole/vtcon*; do + if grep -qi 'frame buffer' "$v/name" 2>/dev/null; then + echo 0 > "$v/bind" 2>/dev/null || true + fi +done +docker stop torizon-easy-pairing-bash-1 >/dev/null 2>&1 || true + +# stderr is left alone on purpose: the DRM path is the most failure-prone part +# of the demo (hotplug, EDID, DRM-master contention), and discarding it turns a +# black screen into a silent restart loop with nothing in the journal. +exec env RENDER=drm python3 "$DEMO/twopane.py" diff --git a/imx95-pqc-demo/demo/drmfb.py b/imx95-pqc-demo/demo/drmfb.py index cbd8680..c5ef560 100644 --- a/imx95-pqc-demo/demo/drmfb.py +++ b/imx95-pqc-demo/demo/drmfb.py @@ -353,6 +353,29 @@ def _setcrtc(self, fb_id): ctypes.sizeof(DrmModeModeinfo)) fcntl.ioctl(self.fd, SETCRTC, crtc) + # Monospace TrueType faces shipped by the distributions this has run on. + # No font is vendored here; DRM_FONT or a font.ttf dropped next to this + # file overrides the search, and the 8x8 bitmap covers a board with none. + FONT_SEARCH = ( + "/usr/share/fonts/truetype/noto/NotoSansMono-Regular.ttf", + "/usr/share/fonts/noto/NotoSansMono-Regular.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + "/usr/share/fonts/dejavu/DejaVuSansMono.ttf", + "/usr/share/fonts/liberation/LiberationMono-Regular.ttf", + ) + + def _find_font(self): + env = os.environ.get("DRM_FONT") + if env: + return env + local = os.path.join(os.path.dirname(__file__), "font.ttf") + if os.path.exists(local): + return local + for path in self.FONT_SEARCH: + if os.path.exists(path): + return path + return local + def _setup(self, mode): self.mode = mode self.width = self.mode.hdisplay @@ -382,9 +405,7 @@ def _setup(self, mode): # blits the whole frame in one memcpy. self.pil = None font_px = int(os.environ.get("DRM_FONT_PX", "18")) - font_path = os.environ.get("DRM_FONT", - os.path.join(os.path.dirname(__file__), - "font.ttf")) + font_path = self._find_font() try: from PIL import Image, ImageDraw, ImageFont if os.path.exists(font_path): diff --git a/imx95-pqc-demo/demo/install-autostart.sh b/imx95-pqc-demo/demo/install-autostart.sh index 81a9f66..23dc876 100755 --- a/imx95-pqc-demo/demo/install-autostart.sh +++ b/imx95-pqc-demo/demo/install-autostart.sh @@ -2,19 +2,27 @@ # Install and enable the boot-time demo units. Run ON THE BOARD as root. # # sudo bash install-autostart.sh # serial console (default) -# sudo bash install-autostart.sh --display # two-pane renderer on tty1 +# sudo bash install-autostart.sh --display # serial console + DisplayPort # sudo bash install-autostart.sh --off # disable, restore the getty # # The serial-console demo is the default because it needs nothing but the -# console cable; the two-pane renderer additionally needs a DisplayPort screen. +# console cable. --display adds a renderer that mirrors the same demo onto a +# DisplayPort panel; it is additive because the renderer starts nothing of its +# own, it only follows the container logs and the Cortex-M7 ring that the +# serial unit brings up. +# +# wolfssl-demo.service is the older combined unit, which both renders and +# starts the demo. Running it next to these two gives two renderers fighting +# for the panel and two writers to the same container, so it is disabled here. set -euo pipefail UART_UNIT=/etc/systemd/system/wolfssl-demo-uart.service -DISPLAY_UNIT=/etc/systemd/system/wolfssl-demo.service +DISPLAY_UNIT=/etc/systemd/system/wolfssl-display.service M7_UNIT=/etc/systemd/system/wolfssl-m7.service HERE="$(cd "$(dirname "$0")" && pwd)" if [ "${1:-}" = "--off" ]; then systemctl disable --now wolfssl-demo-uart.service 2>/dev/null || true + systemctl disable --now wolfssl-display.service 2>/dev/null || true systemctl disable --now wolfssl-demo.service 2>/dev/null || true systemctl disable --now wolfssl-m7.service 2>/dev/null || true systemctl start getty@tty1.service 2>/dev/null || true @@ -24,20 +32,20 @@ if [ "${1:-}" = "--off" ]; then fi install -m 0644 "$HERE/wolfssl-m7.service" "$M7_UNIT" +install -m 0644 "$HERE/wolfssl-demo-uart.service" "$UART_UNIT" +UNITS="wolfssl-demo-uart.service wolfssl-m7.service" if [ "${1:-}" = "--display" ]; then - install -m 0644 "$HERE/wolfssl-demo.service" "$DISPLAY_UNIT" - UNIT="$DISPLAY_UNIT" - systemctl disable wolfssl-demo-uart.service 2>/dev/null || true + install -m 0644 "$HERE/wolfssl-display.service" "$DISPLAY_UNIT" + UNITS="$UNITS wolfssl-display.service" else - install -m 0644 "$HERE/wolfssl-demo-uart.service" "$UART_UNIT" - UNIT="$UART_UNIT" - systemctl disable wolfssl-demo.service 2>/dev/null || true + systemctl disable wolfssl-display.service 2>/dev/null || true fi +systemctl disable wolfssl-demo.service 2>/dev/null || true systemctl daemon-reload -systemctl enable "$(basename "$UNIT")" wolfssl-m7.service +systemctl enable $UNITS # The demo's replay beat is a hard power cut, so nothing may sit in the page # cache: an unsynced unit file is simply gone after the next cycle. sync -echo "installed and enabled: $UNIT and $M7_UNIT" +echo "installed and enabled: $UNITS" echo "the demo now starts on boot; power cycle is the whole replay" echo "disable with: sudo bash $HERE/install-autostart.sh --off" diff --git a/imx95-pqc-demo/demo/stage.sh b/imx95-pqc-demo/demo/stage.sh index b687998..ad3065c 100755 --- a/imx95-pqc-demo/demo/stage.sh +++ b/imx95-pqc-demo/demo/stage.sh @@ -21,6 +21,7 @@ scp -q "$DEMO/demo/twopane.py" "$DEMO/demo/drmfb.py" "$DEMO/demo/font8x8.py" \ "$DEMO/demo/m7-rpmsg-log.sh" "$DEMO/demo/install-autostart.sh" \ "$DEMO/demo/wolfssl-demo.service" "$DEMO/demo/wolfssl-m7.service" \ "$DEMO/demo/demo-uart.sh" "$DEMO/demo/wolfssl-demo-uart.service" \ + "$DEMO/demo/demo-display-fg.sh" "$DEMO/demo/wolfssl-display.service" \ "$BOARD:~/demo/" scp -q "$DEMO/container/docker-compose.yml" "$BOARD:~/demo/" scp -q "$WOLFBOOT/wolfboot.elf" "$BOARD:~/demo/fw/rproc-imx-rproc-fw" diff --git a/imx95-pqc-demo/demo/wolfssl-display.service b/imx95-pqc-demo/demo/wolfssl-display.service new file mode 100644 index 0000000..559dc10 --- /dev/null +++ b/imx95-pqc-demo/demo/wolfssl-display.service @@ -0,0 +1,33 @@ +[Unit] +# DisplayPort view of the demo, started automatically at boot. +# +# Enable this alongside wolfssl-demo-uart.service rather than instead of it. +# The serial unit drives the demo (container, Cortex-M7, the stream a recording +# captures); this one only renders what that produces onto a panel. On a show +# floor that makes the screen a backup for the console rather than a second +# demo competing for the same hardware. +Description=wolfSSL i.MX95 PQC demo - DisplayPort renderer +Documentation=https://github.com/wolfSSL/wolfBoot-examples/pull/13 +After=multi-user.target docker.service +Wants=docker.service +# tty1 is the DisplayPort console; a getty there fights the renderer for it. +Conflicts=getty@tty1.service + +[Service] +Type=simple +ExecStart=/home/torizon/demo/demo-display-fg.sh +# Pillow lives in the user site directory on Torizon, and this runs as root. +Environment=PYTHONPATH=/home/torizon/.local/lib/python3.12/site-packages +Environment=INTERVAL=0.5 +Environment=DRM_FONT_PX=20 +# A dead screen is worse than a retry on a show floor, and the renderer starts +# nothing, so restarting it cannot disturb the demo it is displaying. +Restart=always +RestartSec=2 +StandardInput=null +StandardOutput=journal +StandardError=journal +TimeoutStartSec=0 + +[Install] +WantedBy=multi-user.target diff --git a/imx95-pqc-demo/docs/BUILD.md b/imx95-pqc-demo/docs/BUILD.md index 7615bc9..855b5b6 100644 --- a/imx95-pqc-demo/docs/BUILD.md +++ b/imx95-pqc-demo/docs/BUILD.md @@ -216,7 +216,23 @@ scp -r ../demo :~/demo ssh "sudo bash ~/demo/install-autostart.sh" ``` -That installs two systemd units: one starts the Cortex-M7 through `remoteproc`, the other streams both cores' output to the serial console on every boot. Pass `--display` instead if you have a DisplayPort screen attached and want the two-pane renderer, or `--off` to disable the demo and get the login prompt back. +That installs two systemd units: one starts the Cortex-M7 through `remoteproc`, the other streams both cores' output to the serial console on every boot. `--off` disables the demo and gives the login prompt back. + +### DisplayPort as a second view + +``` +ssh "sudo bash ~/demo/install-autostart.sh --display" +``` + +`--display` adds a third unit, `wolfssl-display.service`, which renders the same two panes onto a DisplayPort panel. It is additive rather than an alternative: the renderer starts nothing, it only follows the container logs and the Cortex-M7 console ring that the serial unit brings up, so the screen and the console show the same run. It also disables the older combined `wolfssl-demo.service`, which both renders and starts the demo - running that one alongside these gives two renderers fighting for the panel. + +Torizon has no fbdev emulation, so the renderer owns the panel through KMS as the sole DRM master. The unit takes tty1 from the getty with `Conflicts=`, and `demo-display-fg.sh` unbinds the framebuffer console and stops Torizon's pairing overlay before it starts. + +No font is committed here. `drmfb.py` uses `$DRM_FONT` if set, then a `font.ttf` placed next to it, then the first monospace TrueType face it finds among the usual distribution paths (Noto Sans Mono, DejaVu Sans Mono, Liberation Mono). With none of those and no Pillow it falls back to a built-in 8x8 bitmap, which is legible but blocky. On Torizon: + +``` +ssh "sudo apt-get install -y fonts-noto-mono python3-pil" +``` ## Recovery From 06d1aa48f1293bf19858089738d2dab86bf83548 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 11:05:38 -0700 Subject: [PATCH 11/14] imx95-pqc-demo: correct the font notes for Torizon, which ships neither fonts nor apt --- imx95-pqc-demo/docs/BUILD.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/imx95-pqc-demo/docs/BUILD.md b/imx95-pqc-demo/docs/BUILD.md index 855b5b6..59a92d5 100644 --- a/imx95-pqc-demo/docs/BUILD.md +++ b/imx95-pqc-demo/docs/BUILD.md @@ -228,10 +228,12 @@ ssh "sudo bash ~/demo/install-autostart.sh --display" Torizon has no fbdev emulation, so the renderer owns the panel through KMS as the sole DRM master. The unit takes tty1 from the getty with `Conflicts=`, and `demo-display-fg.sh` unbinds the framebuffer console and stops Torizon's pairing overlay before it starts. -No font is committed here. `drmfb.py` uses `$DRM_FONT` if set, then a `font.ttf` placed next to it, then the first monospace TrueType face it finds among the usual distribution paths (Noto Sans Mono, DejaVu Sans Mono, Liberation Mono). With none of those and no Pillow it falls back to a built-in 8x8 bitmap, which is legible but blocky. On Torizon: +No font is committed here. `drmfb.py` uses `$DRM_FONT` if set, then a `font.ttf` placed next to it, then the first monospace TrueType face it finds among the usual distribution paths (Noto Sans Mono, DejaVu Sans Mono, Liberation Mono). With none of those it falls back to a built-in 8x8 bitmap, which is legible but blocky. + +Torizon OS ships no fonts and has no package manager - the filesystem is an OSTree deployment - so on this board the search finds nothing and a font has to be staged by hand. Pillow is already installed. Any monospace TrueType face works; the demo was shown with Noto Sans Mono, which is under the SIL Open Font License 1.1 and available from : ``` -ssh "sudo apt-get install -y fonts-noto-mono python3-pil" +scp NotoSansMono-Regular.ttf :~/demo/font.ttf ``` ## Recovery From dbd076c77c5174d187b8d8952f09979ef4f54a30 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 11:17:05 -0700 Subject: [PATCH 12/14] imx95-pqc-demo: note the wolfSSL fix the M7 benchmark depends on --- imx95-pqc-demo/docs/BUILD.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/imx95-pqc-demo/docs/BUILD.md b/imx95-pqc-demo/docs/BUILD.md index 59a92d5..d505e2f 100644 --- a/imx95-pqc-demo/docs/BUILD.md +++ b/imx95-pqc-demo/docs/BUILD.md @@ -207,6 +207,8 @@ make The M7 image is loaded by Linux `remoteproc` rather than by the ROM, so it is not part of the AHAB container set. Staging and start-up are covered by `../demo/m7-start.sh`, and the Zephyr application it verifies is built from `../m7/zephyr-app/`. +The Zephyr application links wolfCrypt's own benchmark, which needs a wolfSSL new enough to zero the ML-KEM key objects in `bench_mlkem()`. Without that, `bench_mlkem_keygen()` calls `wc_MlKemKey_Free()` on memory that was only allocated, frees whatever pointers it happened to contain, and the core takes a bus fault a moment after the benchmark header prints. + Two things bite when reloading M7 firmware. The core cannot be stopped once wolfBoot's `main()` is spinning - the kernel logs `Not in wfi` and `can't stop rproc: -4` - so a reload silently leaves the previous firmware running; power-cycle instead. And after copying firmware to the board, run `sync` before cutting power, or the write sits in page cache and the board comes up on the old image with a filename and size that look right. ## 8. Install the demo From bc8f6060759f13a25863057e6e6be38bb96ea392 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 15:44:20 -0700 Subject: [PATCH 13/14] imx95-pqc-demo: correct the build guide against a clean-room run --- imx95-pqc-demo/docs/BUILD.md | 60 ++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/imx95-pqc-demo/docs/BUILD.md b/imx95-pqc-demo/docs/BUILD.md index d505e2f..ad911eb 100644 --- a/imx95-pqc-demo/docs/BUILD.md +++ b/imx95-pqc-demo/docs/BUILD.md @@ -14,6 +14,24 @@ Everything here is a direct command. Where a script is used it lives in a public git clone https://github.com/wolfSSL/wolfBoot git clone https://github.com/wolfSSL/wolfBoot-examples git clone https://github.com/nxp-imx/imx-mkimage +export TOOLS=$PWD/wolfBoot-examples/imx95-pqc-demo/tools +``` + +Every command below runs from that directory - the one holding the three clones - and `$TOOLS` is where this demo's scripts live. + +wolfBoot keeps wolfCrypt in a submodule, and nothing builds without it: + +``` +cd wolfBoot +git submodule update --init lib/wolfssl +``` + +The Cortex-A55 target is not on `master` yet - it is [PR #888](https://github.com/wolfSSL/wolfBoot/pull/888). Until that merges, `config/examples/imx95-a55.config`, `hal/imx95_a55.c` and the stage 1 come from its branch. The Cortex-M7 target in step 7 is already on `master`. + +``` +git fetch origin pull/888/head:imx95_a55_pr +git checkout imx95_a55_pr +git submodule update --init lib/wolfssl ``` **Toolchains.** `aarch64-none-elf-` (or `aarch64-linux-gnu-`) for the A55, `arm-none-eabi-` for the M7. On Debian or Ubuntu: `apt install gcc-aarch64-linux-gnu gcc-arm-none-eabi`. @@ -29,21 +47,29 @@ ssh "sudo dd if=/dev/mmcblk0boot0 bs=1M count=32" > boot0.bin ARM Trusted Firmware (BL31) and OP-TEE are in there too, so you do not need to build or source them separately. Pull everything out of that one file: ``` -python3 ../tools/ahab-extract.py boot0.bin -o vendor/ +python3 $TOOLS/ahab-extract.py boot0.bin -o vendor/ ``` ``` -container 0 at 0x8000, 5 image(s) +container 0 at 0x8000, 5 image(s), 817152 bytes + [0] empty marker at +0x75800 [1] 0x1FFC0000 321536 bytes Cortex-M33 image (System Manager or OEI) + -> vendor/m33.bin [2] 0x1FFC0000 129024 bytes Cortex-M33 image (System Manager or OEI) + -> vendor/m33_c0i2.bin [3] 0x20480000 141312 bytes A55 image in OCRAM (U-Boot SPL, or wolfBoot stage 1) -container 1 at 0xcf800, 3 image(s) + -> vendor/spl.bin + [4] empty marker at +0xc7800 +container 1 at 0xcf800, 3 image(s), 1650688 bytes [0] 0x8A200000 38912 bytes ARM Trusted Firmware BL31 (secure monitor, EL3) + -> vendor/bl31.bin [1] 0x90200000 968704 bytes BL33 (U-Boot proper, or wolfBoot) + -> vendor/bl33.bin [2] 0x8C000000 593920 bytes OP-TEE (trusted OS, secure EL1) + -> vendor/tee.bin ``` -That gives you `vendor/bl31.bin` and `vendor/tee.bin`, which is all you need from the vendor side. Images are named by load address, because those are fixed on this SoC. The two M33 images share a load address - the OEI runs and returns before the System Manager is loaded over it - so the second is written with an index suffix. +Of those, `vendor/bl31.bin` and `vendor/tee.bin` are what the rest of this guide consumes; the others are kept because they document what the container holds. Images are named by load address, because those are fixed on this SoC. The two M33 images share a load address - the OEI runs and returns before the System Manager is loaded over it - so the second is written with an index suffix. The empty markers are zero-length end-of-list entries the ROM expects, and are why replacing the SPL slot does not move container 1. ## 1. Build mkimage @@ -70,13 +96,19 @@ The first build also generates a signing key (`wolfboot_signing_private_key.der` cp wolfboot_signing_private_key.der ~/keys/imx95-demo-signing-key.der ``` -To reuse an existing key instead of generating one, import its public half before building. Note the level must be given in the environment - both key tools default to ML-DSA level 2 and will silently truncate a level 5 key: +To reuse an existing key instead of generating one, point the build at it. `USER_PRIVATE_KEY` and `USER_PUBLIC_KEY` take precedence over the generated key, and the build then never runs the key generator at all: ``` -python3 -c "d=open('/path/to/key.der','rb').read(); open('pub.der','wb').write(d[-2592:])" -ML_DSA_LEVEL=5 ./tools/keytools/keygen --ml_dsa -i pub.der --der +python3 -c "d=open('/path/to/key.der','rb').read(); open('/tmp/pub.der','wb').write(d[-2592:])" +make USER_PRIVATE_KEY=/path/to/key.der USER_PUBLIC_KEY=/tmp/pub.der +make -C stage1 DISK_EMMC=1 DISK_SDCARD=0 loader_stage1.bin \ + USER_PRIVATE_KEY=/path/to/key.der USER_PUBLIC_KEY=/tmp/pub.der ``` +Both must be given together; the build errors out if only one is set, or if either file is missing. The private key is the 7488-byte DER as `keygen` writes it - 4896 bytes of private key followed by the 2592-byte public key, which is where the one-liner above takes the public half from. + +Prefer this over importing with `keytools/keygen -i`. That route works, but it leaves the tree with a keystore and no matching private key, so the next plain `make` tries to generate a fresh key and stops on `Are you sure you want to generate a new key and overwrite the existing key ?`. Answering yes there replaces the keystore your signed images were built against, and the board stops booting with an authenticity error. For the same reason, do not reach for `make keysclean` to get out of that state: it deletes every `*.der` in the tree root, the extracted public key included. + ## 3. Build and sign the Linux FIT The payload is a FIT (Flattened Image Tree) holding the kernel, device tree and initramfs. Take the three components off the running board - they are whatever the distribution installed - and bundle them: @@ -154,20 +186,22 @@ Nothing records where container 1 starts. It is the end of container 0 - the fur Build container 1 with wolfBoot as BL33, splicing it into a copy of `boot0.bin`: ``` -MKIMAGE=$MKIMAGE ../tools/ahab-pack-container2.sh \ +MKIMAGE=$MKIMAGE $TOOLS/ahab-pack-container2.sh \ boot0.bin vendor/bl31.bin wolfBoot/wolfboot.bin vendor/tee.bin boot-wolfboot.bin ``` Then replace the SPL slot in container 0 with the stage 1: ``` -python3 ../tools/ahab-replace-spl.py \ +python3 $TOOLS/ahab-replace-spl.py \ boot-wolfboot.bin wolfBoot/stage1/loader_stage1.bin boot-final.bin ``` Each image entry carries a **SHA-384** over its payload, so that script recomputes the hash for the slot it replaces; the ROM rejects the container otherwise. It leaves container 0's other images and all of container 1 untouched, and because the SPL slot is followed by a zero-length end marker at a fixed offset, the container's size does not change and container 1 does not move. -Both containers this produces are byte-identical to the ones running on our demo board, which is the check that the path above is complete: everything came from that single `dd`, plus wolfBoot. +Every image in both containers comes out byte-identical to the ones running on our demo board - the two M33 images, the stage 1, BL31, BL33 and OP-TEE - which is the check that the path above is complete: everything came from that single `dd`, plus wolfBoot. Extract your result with `ahab-extract.py` and compare the files it writes. + +Compare the extracted images, not the raw assembled file. Splicing a smaller BL33 into the slot a larger U-Boot occupied leaves the tail of that slot holding whatever the base `boot0.bin` had there. Those bytes sit past the declared image length, so the ROM never reads them, but they differ between base images and make a plain `cmp` of the whole file misleading. ## 5. Write the SD card @@ -191,8 +225,8 @@ The eMMC has two boot partitions and EXT_CSD selects which one the ROM reads. Wr ``` export BOARD=torizon@ -../tools/emmc-write-boot.sh boot-final.bin 1 -../tools/emmc-select-boot.sh 1 +$TOOLS/emmc-write-boot.sh boot-final.bin 1 +$TOOLS/emmc-select-boot.sh 1 ``` Power-cycle. Both scripts verify by reading back and neither embeds a password; use an ssh key and sudo on the target. @@ -205,7 +239,7 @@ cp config/examples/imx95-m7.config .config make ``` -The M7 image is loaded by Linux `remoteproc` rather than by the ROM, so it is not part of the AHAB container set. Staging and start-up are covered by `../demo/m7-start.sh`, and the Zephyr application it verifies is built from `../m7/zephyr-app/`. +The M7 image is loaded by Linux `remoteproc` rather than by the ROM, so it is not part of the AHAB container set. Staging and start-up are covered by `wolfBoot-examples/imx95-pqc-demo/demo/m7-start.sh`, and the Zephyr application it verifies is built from `wolfBoot-examples/imx95-pqc-demo/m7/zephyr-app/`. The Zephyr application links wolfCrypt's own benchmark, which needs a wolfSSL new enough to zero the ML-KEM key objects in `bench_mlkem()`. Without that, `bench_mlkem_keygen()` calls `wc_MlKemKey_Free()` on memory that was only allocated, frees whatever pointers it happened to contain, and the core takes a bus fault a moment after the benchmark header prints. From 3aa331196145c860b80e04716e8836373c8ce5c8 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 19 Sep 2026 15:46:12 -0700 Subject: [PATCH 14/14] imx95-pqc-demo: verify the eMMC readback over the image length, not the whole partition --- imx95-pqc-demo/tools/emmc-write-boot.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/imx95-pqc-demo/tools/emmc-write-boot.sh b/imx95-pqc-demo/tools/emmc-write-boot.sh index d98f907..43488e2 100755 --- a/imx95-pqc-demo/tools/emmc-write-boot.sh +++ b/imx95-pqc-demo/tools/emmc-write-boot.sh @@ -42,7 +42,12 @@ ssh -t "$BOARD" "sudo sh -c ' '" echo "== verifying readback ==" -REMOTE_MD5=$(ssh -t "$BOARD" "sudo dd if=$DEV bs=1M count=32 2>/dev/null | md5sum | cut -d' ' -f1" 2>/dev/null | tr -d '\r\n') +# Read back exactly as many bytes as were written. The boot partition is larger +# than the image (31.5 MiB on this module), so hashing the whole partition +# compares the image against the image plus whatever follows it, and reports a +# mismatch on a write that was perfectly good. +IMG_SZ=$(stat -c %s "$IMG") +REMOTE_MD5=$(ssh -t "$BOARD" "sudo dd if=$DEV bs=1M iflag=count_bytes count=$IMG_SZ 2>/dev/null | md5sum | cut -d' ' -f1" 2>/dev/null | tr -d '\r\n') ssh "$BOARD" 'rm -f /tmp/boot-image.bin' if [ "$LOCAL_MD5" != "$REMOTE_MD5" ]; then echo "MD5 MISMATCH: local $LOCAL_MD5, board $REMOTE_MD5" >&2