Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions imx95-pqc-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Zephyr build output (m7/build.sh writes here)
m7/build/
# wolfSSL source export and cross-build trees (container/build-aarch64.sh)
container/wolfssl-src/
container/build/
173 changes: 173 additions & 0 deletions imx95-pqc-demo/README.md
Original file line number Diff line number Diff line change
@@ -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@<board-ip> 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
Binary file added imx95-pqc-demo/charts/imx95-m7-code-size.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imx95-pqc-demo/charts/imx95-m7-verify-time.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 68 additions & 0 deletions imx95-pqc-demo/container/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
98 changes: 98 additions & 0 deletions imx95-pqc-demo/container/build-aarch64.sh
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions imx95-pqc-demo/container/build-image.sh
Original file line number Diff line number Diff line change
@@ -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}"
Loading