Skip to content
Open
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
2 changes: 1 addition & 1 deletion wolfBoot
Submodule wolfBoot updated 1139 files
1 change: 1 addition & 0 deletions zynqmp-zcu102-wolfip/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
out/
115 changes: 115 additions & 0 deletions zynqmp-zcu102-wolfip/README.md
Original file line number Diff line number Diff line change
@@ -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=<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 <leased-ip> 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 |
11 changes: 11 additions & 0 deletions zynqmp-zcu102-wolfip/boot.bif.in
Original file line number Diff line number Diff line change
@@ -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
}
77 changes: 77 additions & 0 deletions zynqmp-zcu102-wolfip/build.sh
Original file line number Diff line number Diff line change
@@ -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=<ip> ./update.sh to update to v$UPDATE_VERSION"
58 changes: 58 additions & 0 deletions zynqmp-zcu102-wolfip/partition-sd.sh
Original file line number Diff line number Diff line change
@@ -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"
61 changes: 61 additions & 0 deletions zynqmp-zcu102-wolfip/program-sd.sh
Original file line number Diff line number Diff line change
@@ -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."
38 changes: 38 additions & 0 deletions zynqmp-zcu102-wolfip/update.sh
Original file line number Diff line number Diff line change
@@ -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=<the board IP shown on its console>}"
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."