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 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..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..6952e96 --- /dev/null +++ b/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt @@ -0,0 +1,25 @@ +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) + +# 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/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..c670344 --- /dev/null +++ b/imx95-pqc-demo/m7/zephyr-app/src/main.c @@ -0,0 +1,564 @@ +/* + * 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. */ +/* 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) --- */ +#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"); + /* 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"); + + 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; +}