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..7bd2981 --- /dev/null +++ b/imx95-pqc-demo/.gitignore @@ -0,0 +1,5 @@ +# 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/ 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 0000000..5db15d3 Binary files /dev/null and b/imx95-pqc-demo/charts/imx95-m7-code-size.png differ 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 0000000..622f8f5 Binary files /dev/null and b/imx95-pqc-demo/charts/imx95-m7-verify-time.png differ diff --git a/imx95-pqc-demo/container/Dockerfile b/imx95-pqc-demo/container/Dockerfile new file mode 100644 index 0000000..afde6a9 --- /dev/null +++ b/imx95-pqc-demo/container/Dockerfile @@ -0,0 +1,68 @@ +# wolfCrypt PQC benchmark container for Toradex SMARC i.MX95 / Torizon OS. +# +# The builder stage runs on the NATIVE build platform and cross-compiles for +# aarch64, rather than compiling arm64 under qemu emulation. Same artifacts, +# minutes instead of the better part of an hour. +# +# Both wolfSSL configurations are shipped in the image so the SHA-3 A/B can be +# run on the board without a rebuild, and so the entrypoint can pick the right +# one at runtime from /proc/cpuinfo. + +# ---------------------------------------------------------------- builder --- +FROM --platform=$BUILDPLATFORM debian:bookworm AS builder + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + autoconf automake libtool make gcc \ + gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ + libc6-dev-arm64-cross \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY wolfssl-src /src/wolfssl +WORKDIR /src/wolfssl + +# --enable-mldsa is REQUIRED: it is off by default, and without it the +# benchmark silently emits no ML-DSA rows rather than failing. +ENV WOLF_COMMON="--host=aarch64-linux-gnu --enable-mlkem --enable-mldsa \ +--enable-sp --enable-sp-asm --enable-keygen --enable-sha3 \ +--enable-curve25519 --enable-ed25519 --disable-shared --enable-static" + +# baseline: NEON only, runs on any Cortex-A55. +RUN mkdir -p /build/baseline && cd /build/baseline \ + && /src/wolfssl/configure $WOLF_COMMON --enable-armasm \ + && make -j"$(nproc)" + +# sha3-crypto: ARMv8.2 SHA3 instructions. Requires FEAT_SHA3 on the silicon; +# the entrypoint checks /proc/cpuinfo before selecting this one. +RUN mkdir -p /build/sha3-crypto && cd /build/sha3-crypto \ + && /src/wolfssl/configure $WOLF_COMMON --enable-armasm=sha3-crypto \ + && make -j"$(nproc)" + +RUN set -eux; \ + for b in baseline sha3-crypto; do \ + mkdir -p /out/$b/bin; \ + cp /build/$b/wolfcrypt/benchmark/benchmark /out/$b/bin/; \ + cp /build/$b/wolfcrypt/test/testwolfcrypt /out/$b/bin/; \ + cp /build/$b/examples/client/client /out/$b/bin/ 2>/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..b70e590 --- /dev/null +++ b/imx95-pqc-demo/container/docker-compose.yml @@ -0,0 +1,42 @@ +# 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: + image: wolfssl/wolfcrypt-pqc:latest + container_name: wolfcrypt-pqc + restart: 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: "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..12c0fc5 --- /dev/null +++ b/imx95-pqc-demo/demo/demo-run.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Two-pane i.MX95 demo. Runs ON THE BOARD as root. +# +# left - wolfCrypt PQC benchmarks in a container on the Cortex-A55 cluster +# 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 HDMI console +# +# 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 + # A getty owns tty1; stop it first or the two fight over the console. + systemctl stop getty@"$(basename "$TARGET")" 2>/dev/null || true + exec python3 "$DEMO/twopane.py" > "$TARGET" 2>/dev/null +else + exec python3 "$DEMO/twopane.py" +fi diff --git a/imx95-pqc-demo/demo/install-autostart.sh b/imx95-pqc-demo/demo/install-autostart.sh new file mode 100755 index 0000000..d09a0fd --- /dev/null +++ b/imx95-pqc-demo/demo/install-autostart.sh @@ -0,0 +1,26 @@ +#!/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 +HERE="$(cd "$(dirname "$0")" && pwd)" + +if [ "${1:-}" = "--off" ]; then + systemctl disable --now wolfssl-demo.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" +systemctl daemon-reload +systemctl enable wolfssl-demo.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" +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..131f5d4 --- /dev/null +++ b/imx95-pqc-demo/demo/stage.sh @@ -0,0 +1,41 @@ +#!/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/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" "$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..f3d1c3a --- /dev/null +++ b/imx95-pqc-demo/demo/twopane.py @@ -0,0 +1,177 @@ +#!/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") +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. +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 = " A55 x6: wolfCrypt PQC " +RIGHT_TITLE = " 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 "" + + +def clean(text, width, drop_cycles=False): + """Strip ANSI so column widths are computed on what is actually shown.""" + out = [] + for ln in text.splitlines(): + ln = ANSI.sub("", ln).expandtabs(4).rstrip() + if drop_cycles: + ln = CYCLES.sub("", ln) + ln = CPB.sub("", ln) + ln = condense(ln.rstrip().rstrip(","), width) + out.append(ln) + return out + + +def fit(lines, width, height, wrap=False): + """Last `height` lines, fitted to `width`. + + The benchmark pane truncates: its lines are already condensed to fit, and a + wrapped one would cost extra rows and break alignment. The wolfBoot pane + wraps: its output is short, it is the point of the demo, and losing the end + of "wc_MlDsaKey_Verify returned OK" would defeat the purpose. + """ + 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(): + while True: + cols, rows = shutil.get_terminal_size((100, 30)) + half = (cols - 3) // 2 + body = rows - 4 + + left = fit(clean(run(f"docker logs --tail 400 {CONTAINER} 2>&1"), half, drop_cycles=True), half, body) + 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)) + sys.stdout.write("\n".join(painted) + "\x1b[J") + sys.stdout.flush() + time.sleep(INTERVAL) + + +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.service b/imx95-pqc-demo/demo/wolfssl-demo.service new file mode 100644 index 0000000..956d68a --- /dev/null +++ b/imx95-pqc-demo/demo/wolfssl-demo.service @@ -0,0 +1,30 @@ +[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 +# 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/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..3d678f4 --- /dev/null +++ b/imx95-pqc-demo/m7/build.sh @@ -0,0 +1,36 @@ +#!/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" + +west build -p always -b "$BOARD" -d "$BUILD" "$HERE/zephyr-app" \ + -- -DDTC_OVERLAY_FILE="$HERE/imx95_wolfboot.overlay;$SAMPLE_OVERLAY" + +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..238cfda --- /dev/null +++ b/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt @@ -0,0 +1,4 @@ +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) 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..13da979 --- /dev/null +++ b/imx95-pqc-demo/m7/zephyr-app/prj.conf @@ -0,0 +1,35 @@ +CONFIG_KERNEL_BIN_NAME="wolfboot_openamp" +CONFIG_MAIN_STACK_SIZE=4096 +CONFIG_HEAP_MEM_POOL_SIZE=8192 + +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; logs go to a RAM buffer read with +# memtool, and the interesting output is relayed over RPMsg instead. +CONFIG_UART_CONSOLE=n +CONFIG_RAM_CONSOLE=y +CONFIG_RAM_CONSOLE_BUFFER_SIZE=8192 +CONFIG_PRINTK=y +CONFIG_LOG=y +CONFIG_LOG_MODE_MINIMAL=y +CONFIG_LOG_DEFAULT_LEVEL=3 + +# 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..ffdcb48 --- /dev/null +++ b/imx95-pqc-demo/m7/zephyr-app/src/main.c @@ -0,0 +1,426 @@ +/* + * 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 +LOG_MODULE_REGISTER(wolfboot_openamp, LOG_LEVEL_INF); + +/* --- 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; +}; + +/* 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; + + 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); + + 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); + + 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; +}