diff --git a/wolfBoot b/wolfBoot index 75a7e57..61fd552 160000 --- a/wolfBoot +++ b/wolfBoot @@ -1 +1 @@ -Subproject commit 75a7e57279b1c70ca741111705e98aff6307c333 +Subproject commit 61fd55252627595cfff261c4572593286212a10e diff --git a/zynqmp-zcu102-wolfip/.gitignore b/zynqmp-zcu102-wolfip/.gitignore new file mode 100644 index 0000000..89f9ac0 --- /dev/null +++ b/zynqmp-zcu102-wolfip/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/zynqmp-zcu102-wolfip/README.md b/zynqmp-zcu102-wolfip/README.md new file mode 100644 index 0000000..c95dc62 --- /dev/null +++ b/zynqmp-zcu102-wolfip/README.md @@ -0,0 +1,115 @@ +# wolfBoot + wolfIP on AMD/Xilinx ZynqMP (ZCU102) + +Secure boot of a bare-metal **wolfIP** TCP/IP application with **wolfBoot** on the ZCU102, booting from SD card. wolfBoot verifies the application's RSA-4096 / SHA3 signature before every boot, and the running application fetches a signed firmware update over the network and applies it to the SD card itself. + +## Boot chain + +``` +BootROM -> FSBL -> PMUFW -> BL31 (ATF, EL3) -> wolfBoot (EL2) -> wolfIP app (EL2) + | + +-- verify RSA-4096 / SHA3 signature, then load +``` + +`BOOT.BIN` (on the FAT boot partition) carries FSBL + PMUFW + BL31 + wolfBoot. The wolfIP application is a **separate signed image** on the `OFP_A` SD partition; wolfBoot authenticates it and loads it to DDR `0x10000000` (matching the app's `LAYOUT=ddr` link address), then hands off at EL2. The app runs DHCP, a UDP echo/control service, and the network update logic. + +## Prerequisites + +- **wolfBoot** - the `../wolfBoot` submodule: `git submodule update --init --recursive wolfBoot` +- **wolfIP** - a sibling clone of [wolfSSL/wolfip](https://github.com/wolfSSL/wolfip) (the AMD/Xilinx ports + the ZCU102 OTA support): `git clone https://github.com/wolfSSL/wolfip ../../wolfip` (override with `WOLFIP=/path/to/wolfip`). +- **FSBL / PMUFW / BL31** - build these for the ZCU102 yourself; they are board- and tool-specific. FSBL + PMUFW come from Vitis/PetaLinux for the ZCU102; BL31 from Arm Trusted Firmware (`make PLAT=zynqmp RESET_TO_BL31=1`). Put `zynqmp_fsbl.elf`, `pmufw.elf`, `bl31.elf` in one directory and pass `FW=/that/dir`. +- **Toolchain + tools** - the `aarch64-none-elf-` (bare-metal newlib) GCC and `bootgen` (Vitis) on `PATH`. + +## Build + +``` +./build.sh +``` + +This builds wolfBoot for the ZynqMP SD config (`zynqmp_sdcard.config`, RSA-4096 / SHA3, generating a signing key), builds and signs the `EL=2 LAYOUT=ddr` wolfIP app at **both v1 and v2**, and assembles `out/BOOT.BIN`. Outputs: + +| File | Purpose | +|------|---------| +| `out/BOOT.BIN` | bootloader chain (FSBL+PMUFW+BL31+wolfBoot) | +| `out/wolfip_app_v1_signed.bin` | the app signed v1 - goes to `OFP_A` | +| `out/wolfip_app_v2_signed.bin` / `out/wolfip_update.bin` | signed v2 - the network update | + +## Program the SD card + +A stock PetaLinux ZCU102 SD card (MBR: boot / OFP_A / OFP_B / rootfs) works as-is. For a blank or repurposed card, create that layout first (this ERASES the disk): + +``` +SD=/dev/sdX ./partition-sd.sh +``` + +Then write the bootloader + app: + +``` +SD=/dev/sdX ./program-sd.sh +``` + +This copies `BOOT.BIN` into the FAT boot partition and `dd`s the signed v1 app to the raw `OFP_A` partition (needs root). Add `WIPE_OFP_B=1` to also clear `OFP_B` so the board boots `A:v1` fresh (handy for demoing the update). Then put the card in the ZCU102, set boot-mode `SW6 = SD`, and power on. + +## Run + +On the serial console (PS-UART0, 115200 8N1) you should see FSBL -> wolfBoot (which verifies the signature) -> the wolfIP banner, DHCP bind, and `Ready`. A modified or unsigned `OFP_A` image fails wolfBoot's check and is not booted. + +## Signed firmware update (over the network) + +The running app fetches a newer signed image over **TFTP**, writes it to the `OFP_B` SD partition, and resets. The config is version-selecting (`WOLFBOOT_NO_PARTITIONS=1`, "boot the higher version"), so no update flag is needed: wolfBoot verifies both `OFP_A` (v1) and `OFP_B` (v2), boots the higher version, and rolls back to `OFP_A` if `OFP_B` ever fails to verify. + +What makes this notable is *how* the app reaches the SD card: it re-uses **wolfBoot's own SD-host and disk drivers** (`$WOLFBOOT/src/sdhci.c`, `disk.c`, `gpt.c`) by compiling that same source straight into the application (the `OTA=1` path in the app `Makefile`), behind a small EL2 platform shim (`boards/zcu102/sdhci_shim.c`: register access, timer, SDMA cache maintenance). One driver, two consumers, no runtime hand-off. + +Run it once the app is at `Ready` (the app fetches from the sender's host, so run this on a machine on the board's subnet that has a TFTP server serving `TFTP_ROOT`): + +``` +BOARD_IP= ./update.sh +``` + +`update.sh` stages `out/wolfip_update.bin` into the TFTP root (default `/srv/tftp`, override `TFTP_ROOT=`) and sends the `UPDATE` trigger to the board's port 7. + +### Expected console + +``` +Versions, A:1 B:0 +Attempting boot from P:A +Verifying image signature...done +Firmware Valid. +=== wolfIP ZCU102 (UltraScale+ A53-0 EL2) === +DHCP bound: + IP: 10.0.4.140 +Ready. Try: nc -u 7 + +UDP echo: 6 bytes from 10.0.4.24 +OTA: init SD card... +OTA: SD ready, reading MBR... +OTA: requesting 'wolfip_update.bin' from 10.0.4.24 +OTA: staging update to RAM +...... +OTA: writing 110208 bytes to OFP_B (part 2)... +OTA: update staged to OFP_B +OTA: transfer complete - resetting to apply update <- intentional reset, not a crash + +[board resets] +Versions, A:1 B:2 +Attempting boot from P:B <- now boots the v2 update +Verifying image signature...done +Firmware Valid. +``` + +The reset after "update staged" is intentional - the app reboots so wolfBoot re-evaluates and picks the higher version. A tampered or unsigned download simply fails wolfBoot's signature check on the next boot and the board stays on v1. + +### Notes + +- **Security model** - the `UPDATE` trigger itself is unauthenticated, but every image is RSA-4096 / SHA3 verified by wolfBoot before it ever boots, so the trust boundary is the signature, not the trigger. The worst a rogue trigger can do is cause a download that wolfBoot then rejects. +- **TFTP options** - the client uses 512-byte blocks and windowsize 1: the most compatible settings, which also keep large UDP bursts off the app's poll-driven receive path. + +## Layout + +| File | Purpose | +|------|---------| +| `build.sh` | Build wolfBoot + sign the app (v1 + v2) + assemble `BOOT.BIN` | +| `program-sd.sh` | Write `BOOT.BIN` + signed app to an SD card (`WIPE_OFP_B=1` for a clean slate) | +| `partition-sd.sh` | Create the demo MBR layout on a blank card | +| `update.sh` | Stage the update image + trigger it over the network | +| `boot.bif.in` | bootgen template (FSBL/PMUFW/BL31/wolfBoot) | +| `out/` | Build output | diff --git a/zynqmp-zcu102-wolfip/boot.bif.in b/zynqmp-zcu102-wolfip/boot.bif.in new file mode 100644 index 0000000..42bb1b6 --- /dev/null +++ b/zynqmp-zcu102-wolfip/boot.bif.in @@ -0,0 +1,11 @@ +// bootgen image for the ZCU102 wolfBoot + wolfIP demo. +// FSBL -> PMUFW -> BL31 (EL3) -> wolfBoot (EL2). The signed wolfIP app is NOT +// inside BOOT.BIN - it lives on the SD OFP_A partition and wolfBoot loads it. +// @FW@ and @WOLFBOOT@ are substituted by build.sh. +the_ROM_image: +{ + [bootloader, destination_cpu=a53-0] @FW@/zynqmp_fsbl.elf + [destination_cpu=pmu] @FW@/pmufw.elf + [destination_cpu=a53-0, exception_level=el-3, trustzone] @FW@/bl31.elf + [destination_cpu=a53-0, exception_level=el-2] @WOLFBOOT@/wolfboot.elf +} diff --git a/zynqmp-zcu102-wolfip/build.sh b/zynqmp-zcu102-wolfip/build.sh new file mode 100755 index 0000000..9a99929 --- /dev/null +++ b/zynqmp-zcu102-wolfip/build.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# +# Build the ZCU102 wolfBoot + wolfIP secure-boot demo end to end: +# FSBL -> PMUFW -> BL31 (EL3) -> wolfBoot (EL2) -> signed wolfIP app (EL2). +# +# Produces, in out/: +# BOOT.BIN - bootloader chain (FSBL+PMUFW+BL31+wolfBoot) +# wolfip_app_v1_signed.bin - the app signed v1 (programmed to OFP_A) +# wolfip_app_v2_signed.bin - the same app signed v2 (the network update) +# wolfip_update.bin - a copy of the v2 image (the name update.sh serves) +# +# Dependencies (see README.md): +# - wolfBoot: the ../wolfBoot submodule +# (git submodule update --init --recursive) +# - wolfIP: a sibling clone of wolfSSL/wolfip (default ../../wolfip) +# - FSBL/PMUFW/BL31: build for the ZCU102 with Vitis/PetaLinux; point FW= at them +# - aarch64-none-elf toolchain and bootgen (Vitis) on PATH +# Override any path with the matching env var. +# +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +WOLFBOOT="${WOLFBOOT:-$HERE/../wolfBoot}" +WOLFIP="${WOLFIP:-$HERE/../../wolfip}" +FW="${FW:-$HOME/GitHub/soc-prebuilt-firmware/zcu102-zynqmp}" # zynqmp_fsbl.elf, pmufw.elf, bl31.elf +CROSS="${CROSS_COMPILE:-aarch64-none-elf-}" +UPDATE_VERSION="${UPDATE_VERSION:-2}" +MAC5="${MAC5:-0x33}" +OUT="$HERE/out" +APPDIR="$WOLFIP/src/port/amd/boards/zcu102" +mkdir -p "$OUT" + +# Fail fast on missing dependencies (clearer than a build error halfway through). +[ -d "$WOLFBOOT/src" ] || { echo "ERROR: wolfBoot not at $WOLFBOOT - run: git submodule update --init --recursive" >&2; exit 1; } +[ -d "$APPDIR" ] || { echo "ERROR: wolfIP port not at $APPDIR - clone wolfSSL/wolfip as a sibling or set WOLFIP=" >&2; exit 1; } +[ -f "$FW/zynqmp_fsbl.elf" ] || { echo "ERROR: FSBL/PMUFW/BL31 not in $FW - build them (see README) and set FW=" >&2; exit 1; } + +echo "== 1/3 wolfBoot (ZynqMP SD, RSA4096/SHA3) ==" +cp "$WOLFBOOT/config/examples/zynqmp_sdcard.config" "$WOLFBOOT/.config" +# Build inside $WOLFBOOT (its Makefile resolves the sign tool via $(PWD)). The +# build generates the signing key, reused so a v2 update verifies against the +# same wolfBoot. If a stale key with a different algorithm is present, run +# 'make keysclean' in wolfBoot once. +( cd "$WOLFBOOT" && make keytools >/dev/null ) +( cd "$WOLFBOOT" && make clean >/dev/null 2>&1 || true ) +( cd "$WOLFBOOT" && make CROSS_COMPILE="$CROSS" wolfboot.elf ) +cp "$WOLFBOOT/wolfboot.elf" "$OUT/" + +echo "== 2/3 wolfIP app (EL2, DDR, OTA) + sign v1 + v$UPDATE_VERSION ==" +# OTA=1 compiles wolfBoot's own SD/disk drivers ($WOLFBOOT/src/{sdhci,disk,gpt}.c) +# straight into the app so the running image can fetch a signed update over TFTP +# and stage it to OFP_B itself - no runtime hand-off from wolfBoot. +make -C "$APPDIR" clean >/dev/null 2>&1 || true +make -C "$APPDIR" CROSS_COMPILE="$CROSS" EL=2 LAYOUT=ddr OTA=1 WOLFBOOT="$WOLFBOOT" \ + CFLAGS_EXTRA="-DWOLFIP_MAC_5=$MAC5" +"${CROSS}objcopy" -O binary "$APPDIR/app.elf" "$OUT/wolfip_app.bin" +# Sign the same image at v1 (boots from OFP_A) and at the update version (served +# over the network). wolfBoot boots the higher version, so v1 updates to v2. +KEY="$WOLFBOOT/wolfboot_signing_private_key.der" +"$WOLFBOOT/tools/keytools/sign" --rsa4096 --sha3 "$OUT/wolfip_app.bin" "$KEY" 1 +"$WOLFBOOT/tools/keytools/sign" --rsa4096 --sha3 "$OUT/wolfip_app.bin" "$KEY" "$UPDATE_VERSION" +cp "$OUT/wolfip_app_v${UPDATE_VERSION}_signed.bin" "$OUT/wolfip_update.bin" + +echo "== 3/3 BOOT.BIN (FSBL+PMUFW+BL31+wolfBoot) ==" +# Escape the replacement strings: '\', '&' and the '|' delimiter are special to +# sed's RHS, so a path containing them would otherwise corrupt boot.bif. +sed_escape() { printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'; } +sed -e "s|@FW@|$(sed_escape "$FW")|g" \ + -e "s|@WOLFBOOT@|$(sed_escape "$OUT")|g" \ + "$HERE/boot.bif.in" > "$OUT/boot.bif" +bootgen -arch zynqmp -image "$OUT/boot.bif" -w on -o "$OUT/BOOT.BIN" + +echo +echo "Done. Artifacts in $OUT:" +ls -la "$OUT"/BOOT.BIN "$OUT"/wolfip_app_v1_signed.bin "$OUT"/wolfip_update.bin +echo "Next: SD=/dev/sdX ./program-sd.sh (writes BOOT.BIN + the v1 app)" +echo " boot ZCU102 (SW6=SD), then BOARD_IP= ./update.sh to update to v$UPDATE_VERSION" diff --git a/zynqmp-zcu102-wolfip/partition-sd.sh b/zynqmp-zcu102-wolfip/partition-sd.sh new file mode 100755 index 0000000..9e8ad95 --- /dev/null +++ b/zynqmp-zcu102-wolfip/partition-sd.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# +# Partition a BLANK SD card for the ZCU102 wolfBoot + wolfIP demo, matching the +# zynqmp_sdcard.config MBR layout. A stock PetaLinux ZCU102 card already has +# this layout - use this script only when starting from a blank/repurposed card: +# p1 boot 128M FAT32, bootable <- BOOT.BIN +# p2 OFP_A 200M raw <- signed app (primary) +# p3 OFP_B 200M raw <- update slot +# p4 rootfs rest (unused by this bare-metal demo) +# +# DESTRUCTIVE: erases the entire target disk. Double-check with lsblk! +# +# Usage: SD=/dev/sdX ./partition-sd.sh +# +set -euo pipefail + +SD="${SD:?set SD=/dev/sdX (your card reader - NOT a board, NOT your system disk!)}" +[ -b "$SD" ] || { echo "$SD is not a block device" >&2; exit 1; } + +# Partition node suffix: /dev/sdX -> sdX1 ; /dev/mmcblkN|nvmeN|loopN -> ...p1 +case "$SD" in + *[0-9]) P="p" ;; + *) P="" ;; +esac + +# Refuse if anything on the device is mounted. +if lsblk -nro MOUNTPOINT "$SD" | grep -q .; then + echo "ERROR: $SD has mounted partitions - unmount them first:" >&2 + lsblk -o NAME,SIZE,TYPE,MOUNTPOINT "$SD" >&2 + exit 1 +fi + +echo "Target $SD:"; lsblk -o NAME,SIZE,TYPE,LABEL,FSTYPE "$SD" +echo +echo "This ERASES ALL DATA on $SD and writes the ZCU102 demo MBR layout." +read -r -p "Type the device path ($SD) to confirm: " a +[ "$a" = "$SD" ] || { echo "aborted"; exit 1; } + +echo "== writing MBR partition table ==" +# 1 MiB align; 128M boot (FAT32, bootable), 200M OFP_A, 200M OFP_B, rest rootfs. +sudo sfdisk "$SD" <<'EOF' +label: dos +unit: sectors +start=2048, size=262144, type=c, bootable +size=409600, type=83 +size=409600, type=83 +type=83 +EOF + +sudo partprobe "$SD" 2>/dev/null || true +sync; sleep 1 + +echo "== formatting ${SD}${P}1 as FAT32 (boot) ==" +sudo mkfs.vfat -F 32 -n BOOT "${SD}${P}1" >/dev/null + +sync +echo "Done. $SD now has boot / OFP_A / OFP_B / rootfs." +echo "Next: SD=$SD ./program-sd.sh" diff --git a/zynqmp-zcu102-wolfip/program-sd.sh b/zynqmp-zcu102-wolfip/program-sd.sh new file mode 100755 index 0000000..61713d1 --- /dev/null +++ b/zynqmp-zcu102-wolfip/program-sd.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# +# Program an SD card for the ZCU102 wolfBoot + wolfIP demo. +# +# Expects an MBR card laid out like zynqmp_sdcard.config (a stock PetaLinux +# ZCU102 SD card works): +# p1 boot FAT32, bootable <- BOOT.BIN (FSBL+PMUFW+BL31+wolfBoot) +# p2 OFP_A raw <- signed app (wolfBoot's primary image) +# p3 OFP_B raw (update slot - written over the network at run time) +# p4 rootfs (unused by this bare-metal demo) +# +# wolfBoot reads the *raw* OFP_A partition (WOLFBOOT_NO_PARTITIONS, BOOT_PART_A=1), +# so the signed image is dd'd to the start of p2 (this needs root). Copying +# BOOT.BIN into the FAT p1 does not. +# +# Usage: SD=/dev/sdX ./program-sd.sh (X = your card reader, NOT a board) +# WIPE_OFP_B=1 SD=/dev/sdX ./program-sd.sh also zero OFP_B so the board +# boots A:v1 fresh (for a clean A->B update demo) +# +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="$HERE/out" +VERSION="${VERSION:-1}" +SIGNED="$OUT/wolfip_app_v${VERSION}_signed.bin" +SD="${SD:?set SD=/dev/sdX (your SD card reader block device - double-check with lsblk!)}" + +[ -f "$OUT/BOOT.BIN" ] || { echo "missing $OUT/BOOT.BIN - run ./build.sh first" >&2; exit 1; } +[ -f "$SIGNED" ] || { echo "missing $SIGNED - run ./build.sh first" >&2; exit 1; } +[ -b "$SD" ] || { echo "$SD is not a block device" >&2; exit 1; } + +# Partition node suffix: /dev/sdX -> sdX1 ; /dev/mmcblkN|nvmeN|loopN -> ...p1 +case "$SD" in + *[0-9]) P="p" ;; + *) P="" ;; +esac + +echo "Target $SD:"; lsblk -o NAME,SIZE,TYPE,LABEL,FSTYPE "$SD" +read -r -p "Write BOOT.BIN to ${SD}${P}1 (FAT) and the signed app to ${SD}${P}2 (raw)? [y/N] " a +[ "$a" = y ] || { echo "aborted"; exit 1; } + +echo "== BOOT.BIN -> ${SD}${P}1 (FAT boot partition) ==" +MNT="$(mktemp -d)" +# Ensure the partition is unmounted and the temp dir removed even if a step +# below fails under 'set -e' or the script is interrupted. +trap 'sudo umount "$MNT" 2>/dev/null || true; rmdir "$MNT" 2>/dev/null || true' EXIT +sudo mount "${SD}${P}1" "$MNT" +sudo cp "$OUT/BOOT.BIN" "$MNT/BOOT.BIN" +sync + +echo "== signed app -> ${SD}${P}2 (OFP_A, raw) ==" +sudo dd if="$SIGNED" of="${SD}${P}2" bs=1M conv=fsync status=progress + +# Clean slate: zero the start of OFP_B so wolfBoot sees no valid update there +# (version 0) and boots A:v1, ready for a fresh A->B update demo. +if [ "${WIPE_OFP_B:-0}" = 1 ]; then + echo "== wiping OFP_B header -> ${SD}${P}3 ==" + sudo dd if=/dev/zero of="${SD}${P}3" bs=1M count=1 conv=fsync status=none +fi + +sync +echo "Done. Put the card in the ZCU102, set SW6=SD, and power on." diff --git a/zynqmp-zcu102-wolfip/update.sh b/zynqmp-zcu102-wolfip/update.sh new file mode 100755 index 0000000..71461ff --- /dev/null +++ b/zynqmp-zcu102-wolfip/update.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# +# Trigger the network firmware update for the ZCU102 wolfBoot + wolfIP demo. +# +# Stages the signed update image (out/wolfip_update.bin, from build.sh) into a +# TFTP root, then sends the "UPDATE" trigger to the running app. The app fetches +# the image from the *sender's* host over TFTP, writes it to OFP_B, and resets; +# wolfBoot then verifies and boots the higher version. +# +# Assumes a TFTP server is already running on THIS host and serving $TFTP_ROOT +# (e.g. tftpd-hpa at /srv/tftp), reachable from the board's subnet. (The app +# fetches from the sender's IP, so the TFTP server must be on the machine that +# runs this script.) +# +# Usage: BOARD_IP=10.0.4.140 [TFTP_ROOT=/srv/tftp] ./update.sh +# +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +BOARD_IP="${BOARD_IP:?set BOARD_IP=}" +TFTP_ROOT="${TFTP_ROOT:-/srv/tftp}" +IMG="${IMG:-$HERE/out/wolfip_update.bin}" +PORT="${PORT:-7}" # the app's UDP echo/control port + +[ -f "$IMG" ] || { echo "ERROR: $IMG not found - run ./build.sh first" >&2; exit 1; } +[ -d "$TFTP_ROOT" ] || { echo "ERROR: TFTP_ROOT $TFTP_ROOT is not a directory - start a TFTP server or set TFTP_ROOT=" >&2; exit 1; } +[ -w "$TFTP_ROOT" ] || { echo "ERROR: TFTP_ROOT $TFTP_ROOT not writable by $(id -un) - fix perms or set TFTP_ROOT= to a writable dir" >&2; exit 1; } +command -v nc >/dev/null || { echo "ERROR: 'nc' (netcat) not found" >&2; exit 1; } + +echo "Staging $(basename "$IMG") -> $TFTP_ROOT/wolfip_update.bin" +cp "$IMG" "$TFTP_ROOT/wolfip_update.bin" + +echo "Triggering update on $BOARD_IP:$PORT ..." +printf 'UPDATE' | nc -u -w1 "$BOARD_IP" "$PORT" + +echo +echo "Watch the board console: it fetches wolfip_update.bin over TFTP, writes" +echo "OFP_B, resets (intentional), and wolfBoot then boots the higher version."