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
6 changes: 6 additions & 0 deletions conf/layer.conf
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ BBFILES += "${LAYERDIR}/recipes-wolfssl/wolfssl/*.bb \
${LAYERDIR}/recipes-wolfssl/wolfprovider/wolfssl*.bbappend \
${LAYERDIR}/recipes-wolfssl/wolfengine/wolfengine*.bb \
${LAYERDIR}/recipes-wolfssl/wolfengine/wolfssl*.bbappend \
${LAYERDIR}/recipes-wolfssl/wolfboot/*.bb \
${LAYERDIR}/recipes-wolfssl/wolfboot/*.bbappend \
${LAYERDIR}/recipes-examples/wolfcrypt/wolfcryptbenchmark/*.bb \
${LAYERDIR}/recipes-examples/wolfcrypt/wolfcryptbenchmark/*.bbappend \
${LAYERDIR}/recipes-examples/wolfcrypt/wolfcrypttest/*.bb \
Expand All @@ -45,6 +47,9 @@ BBFILES += "${LAYERDIR}/recipes-wolfssl/wolfssl/*.bb \
${LAYERDIR}/recipes-support/gnutls/*.bbappend \
${LAYERDIR}/recipes-support/gnutls/wolfssl-gnutls-wrapper_git.bb"

# xilinx-bootbin bbappend requires meta-xilinx-tools layer
BBFILES_DYNAMIC += "xilinx-tools:${LAYERDIR}/recipes-bsp/bootbin/*.bbappend"

# Uncomment if building bind with wolfSSL.
#BBFILES += "${LAYERDIR}/recipes-connectivity/bind/*.bbappend"

Expand Down Expand Up @@ -99,6 +104,7 @@ PREFERRED_PROVIDER_wolfssl-py ??= "wolfssl-py"
PREFERRED_PROVIDER_wolfcrypt-py ??= "wolfcrypt-py"
PREFERRED_PROVIDER_wolfprovider ??= "wolfprovider"
PREFERRED_PROVIDER_wolfengine ??= "wolfengine"
PREFERRED_PROVIDER_wolfboot ??= "wolfboot"

BBFILES += "${@bb.utils.contains('WOLFSSL_TYPE', \
'fips', \
Expand Down
42 changes: 42 additions & 0 deletions recipes-bsp/bootbin/xilinx-bootbin_%.bbappend
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Replace U-Boot with wolfBoot as second-stage bootloader in BOOT.BIN
#
# Only activates when WOLFBOOT_ENABLE = "1" is set in configuration (e.g.
# local.conf or a machine .conf). Without that flag this bbappend is a
# no-op and xilinx-bootbin keeps using U-Boot.
#
# A dedicated variable is used here instead of inspecting EXTRA_IMAGEDEPENDS:
# EXTRA_IMAGEDEPENDS is frequently set inside image recipes, which are
# parsed in a different context than xilinx-bootbin, so the anonymous
# python() below would see an empty value and silently stay inert.
Comment thread
dgarske marked this conversation as resolved.
#
# Boot chain with wolfBoot:
# FSBL -> PMU FW -> (bitstream) -> ATF (EL3) -> wolfBoot (EL2) -> signed FIT
#
# Default ZynqMP BIF order (machine-xilinx-zynqmp.inc):
# FSBL -> PMU FW -> bitstream (optional) -> ATF -> DTB -> U-Boot
#
# wolfBoot BIF order:
# FSBL -> PMU FW -> bitstream (optional) -> ATF -> wolfBoot
# (DTB is inside the signed FIT image, loaded by wolfBoot at runtime.)

python () {
if (d.getVar('WOLFBOOT_ENABLE') or '') != '1':
# wolfBoot not requested -- this bbappend is inert.
return
# Apply wolfBoot BIF overrides below.
Comment thread
dgarske marked this conversation as resolved.
d.setVar('BIF_SSBL_ATTR', 'wolfboot')
d.setVarFlag('BIF_PARTITION_ATTR', 'wolfboot',
'destination_cpu=a53-0,exception_level=el-2')
d.setVarFlag('BIF_PARTITION_IMAGE', 'wolfboot',
d.expand('${RECIPE_SYSROOT}/boot/wolfboot.elf'))
d.setVar('BIF_DEVICETREE_ATTR', '')
# BIF_PARTITION_IMAGE[wolfboot] points at ${RECIPE_SYSROOT}/boot/wolfboot.elf,
# which is populated by do_populate_sysroot, not do_deploy. Depend on
# the right task so xilinx-bootbin blocks until wolfboot.elf is staged.
d.appendVar('DEPENDS', ' wolfboot')
existing = d.getVarFlag('do_compile', 'depends') or ''
d.setVarFlag('do_compile', 'depends', existing + ' wolfboot:do_populate_sysroot')
}

# Only meaningful on ZynqMP (Versal has its own PLM-based flow; not handled here)
COMPATIBLE_MACHINE:append = "|xilinx-zynqmp.*"
181 changes: 181 additions & 0 deletions recipes-wolfssl/wolfboot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# wolfBoot (Yocto/OE recipes)

Recipes that build [wolfBoot](https://github.com/wolfssl/wolfBoot), wolfSSL's
portable secure bootloader, as part of a Yocto / OpenEmbedded image.

## Recipes

| Recipe | Purpose |
|---|---|
| `wolfboot-keytools-native_git.bb` | Host-side `wolfboot-keygen` and `wolfboot-sign` utilities (RSA4096 key generation, RSA4096+SHA3-384 image signing). |
| `wolfboot_git.bb` | Cross-compiles `wolfboot.elf` (bare-metal, AArch64 / AArch32 / RISC-V). Picks a config from `${S}/config/examples/` per the `WOLFBOOT_CONFIG` variable. Consumes a user-supplied signing key via `WOLFBOOT_SIGNING_KEY` (see "Signing-key provisioning" below). |
| `wolfboot-signed-image.bb` | Signs the kernel FIT image (default `fitImage`) with RSA4096+SHA3-384 and emits `image_v${WOLFBOOT_IMAGE_VERSION}_signed.bin` into `DEPLOY_DIR_IMAGE`. |

A companion `recipes-bsp/bootbin/xilinx-bootbin_%.bbappend` overrides the
AMD/Xilinx `xilinx-bootbin` recipe to swap U-Boot for `wolfboot.elf` in
`BOOT.BIN` on ZynqMP. It only activates when `WOLFBOOT_ENABLE = "1"` is
set in configuration (`local.conf` or a machine `.conf`).

## Signing-key provisioning (required)

`wolfboot_git.bb` and `wolfboot-signed-image.bb` both require you to
supply a pre-generated RSA4096 private key via `WOLFBOOT_SIGNING_KEY`.
The recipes will **not** auto-generate one — that would leak the
private half through sstate and DEPLOY_DIR_IMAGE.

Generate the key pair once on your workstation:

```sh
# One-time host-side build of the keytools (outside Yocto):
make -C /path/to/wolfBoot/tools/keytools keygen sign

# Generate the RSA4096 key pair:
/path/to/wolfBoot/tools/keytools/keygen --rsa4096 \
-g /secure/path/wolfboot_signing_private_key.der
```

Store `wolfboot_signing_private_key.der` **outside** the build tree — a
secrets manager, an encrypted volume, or at minimum a `.gitignore`d
directory. Back it up: losing the private key means you can never sign
another A/B update image that your deployed `wolfboot.elf` will accept.

## Quick start (AMD/Xilinx ZynqMP, SD-card boot)

Set the following in `local.conf` (or a machine `.conf`). These must live
in configuration, not inside an image recipe: the `xilinx-bootbin`
bbappend reads `WOLFBOOT_ENABLE` at parse time, which runs before any
image recipe is evaluated.

```bitbake
# Activate the xilinx-bootbin bbappend (swap U-Boot for wolfBoot in BOOT.BIN)
WOLFBOOT_ENABLE = "1"

# Ensure the signed FIT image is built alongside the image.
EXTRA_IMAGEDEPENDS:append = " wolfboot-signed-image"

# REQUIRED: absolute path to the pre-generated signing private key.
WOLFBOOT_SIGNING_KEY = "/secure/path/wolfboot_signing_private_key.der"

# Pick a config template from wolfBoot/config/examples/
WOLFBOOT_CONFIG = "zynqmp_sdcard.config"

# Override the Linux rootfs partition in the DTB bootargs fixup.
# Default in the ZynqMP SD-card example is /dev/mmcblk0p4.
WOLFBOOT_LINUX_BOOTARGS_ROOT = "/dev/mmcblk0p4"

# Bump to "2" for A/B update images, etc.
WOLFBOOT_IMAGE_VERSION = "1"
```

Build:

```
bitbake <your-image>
```

Artifacts deployed to `tmp/deploy/images/<MACHINE>/`:

- `BOOT.BIN` - FSBL + PMUFW + ATF + wolfBoot.elf
- `wolfboot.elf` - bare-metal bootloader
- `wolfboot_signing_public_key.der` - verifying key (safe to publish; only if `WOLFBOOT_PUBLIC_KEY` is set)
- `image_v1_signed.bin` - signed FIT image (for OFP_A partition)

Note: the private signing key is **not** deployed — it stays on the
workstation / secrets store you pointed `WOLFBOOT_SIGNING_KEY` at.

## SD card layout (wolfBoot A/B scheme)

| Partition | Size | Type | Contents |
|---|---|---|---|
| p1 | 128 MB | FAT32 | `BOOT.BIN` |
| p2 | 200 MB | raw | `image_v1_signed.bin` (OFP_A = primary) |
| p3 | 200 MB | raw | update slot (OFP_B) |
| p4 | rest | ext4 | Linux rootfs |

See `wolfBoot/tools/scripts/` for helper scripts to create the SD card
and flash update images (once upstreamed).

## Key management

The signing key pair is **user-supplied**, not generated inside the build.
`WOLFBOOT_SIGNING_KEY` must point at a pre-generated RSA4096 private key
file (DER format) — see the "Signing-key provisioning" section at the top
of this README. The recipe:

- Stages the private key at `${S}/wolfboot_signing_private_key.der` so the
wolfBoot Makefile can derive the public half and compile it into
`wolfboot.elf` via `src/keystore.c`.
- Does **not** deploy the private key to `DEPLOY_DIR_IMAGE`. Only
Comment thread
dgarske marked this conversation as resolved.
`wolfboot.elf` (and optionally the public key, if `WOLFBOOT_PUBLIC_KEY`
is set) is deployed.
- `wolfboot-signed-image.bb` reads the same `WOLFBOOT_SIGNING_KEY`
directly to sign the FIT, so there is no on-disk hand-off of the
private key between recipes.

To rotate keys:

1. Generate a new RSA4096 key pair with `wolfboot-keygen --rsa4096 -g …`.
Prefer a **new filename/path** for each rotation (e.g.
`wolfboot_signing_private_key_v2.der`) rather than overwriting the
existing `.der` in place — see the caveat below.
2. Update `WOLFBOOT_SIGNING_KEY` to the new private key path.
3. Build: `wolfboot.elf` embeds the new public key, and
`image_v<N>_signed.bin` is signed with the new private key.
4. Flash the new `BOOT.BIN` (contains new `wolfboot.elf`) to the device,
followed by a signed update image signed with the new key.

Comment thread
dgarske marked this conversation as resolved.
**sstate caveat.** `WOLFBOOT_SIGNING_KEY` points at an absolute path
outside the recipe tree, so BitBake does not checksum the key file's
contents into the task hash — only the path string participates. If you
overwrite the same `.der` file in place with new key material, sstate
and stamps may reuse the previously-built `wolfboot.elf` and signed
image, leaving you with a mismatched public/private pair. To stay safe:

- **Preferred**: rotate to a *new filename/path*. Changing the value of
`WOLFBOOT_SIGNING_KEY` invalidates the task hash naturally.
- **Fallback** (if you must reuse the same path): force a rebuild with
`bitbake -c cleansstate wolfboot wolfboot-signed-image` before the
next `bitbake <your-image>`.

## Swapping U-Boot for wolfBoot

The `recipes-bsp/bootbin/xilinx-bootbin_%.bbappend` rewrites the BIF so
`BOOT.BIN` contains `wolfboot.elf` instead of `u-boot.elf` at EL2, and
drops the standalone DTB partition (wolfBoot loads the DTB out of the
signed FIT image). It activates only when `WOLFBOOT_ENABLE = "1"` is set
in configuration (`local.conf` or a machine `.conf`). Setting it inside
an image recipe is **not** sufficient: the bbappend's anonymous python
runs at parse time, before image recipes are evaluated.

## ZynqMP caveats

See the comments in `wolfBoot/config/examples/zynqmp_sdcard.config` for
two non-obvious points:

1. **Root device**: when the XSA disables `sdhci0` (common on ZCU102
boards where only the external SD slot is populated), SD1 enumerates
as `/dev/mmcblk0`, not `/dev/mmcblk1`. The template default is
`mmcblk0p4`; flip to `mmcblk1p4` only if both sdhci0 + sdhci1 are
enabled. Controlled via `WOLFBOOT_LINUX_BOOTARGS_ROOT`.

2. **BOOT_EL1**: `BOOT_EL1?=1` in the example is currently inert on
ZynqMP because `EL2_HYPERVISOR` is defined in `hal/zynq.h` (included
by `src/boot_aarch64_start.S`) but not in `src/boot_aarch64.c`. wolfBoot
stays at EL2 before jumping to Linux, which matches standard PetaLinux
U-Boot behavior. See the comments in the template.

## Future work

- **`PROVIDES += "virtual/bootloader"`** — On vanilla Yocto where U-Boot
is the sole bootloader, `PREFERRED_PROVIDER_virtual/bootloader = "wolfboot"`
would be a cleaner selector than `WOLFBOOT_ENABLE = "1"` plus the
`xilinx-bootbin` bbappend. We deliberately do not declare that PROVIDES
yet, because on AMD/Xilinx ZynqMP — the only target this layer has been
validated against — it does not actually simplify the integration:
`xilinx-bootbin` references U-Boot by PN (`u-boot-xlnx`) inside its BIF
generation rather than going through `virtual/bootloader`, and
`IMAGE_BOOT_FILES` lists U-Boot artifacts by literal name. The current
bbappend rewrites the BIF entry surgically; that swap would still be
required regardless of who provides `virtual/bootloader`. The right
time to add this PROVIDES is when a non-Xilinx BSP integration lands,
so the contract can be tested end-to-end.
27 changes: 27 additions & 0 deletions recipes-wolfssl/wolfboot/wolfboot-keytools-native_git.bb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
SUMMARY = "wolfBoot signing and key generation tools (native)"
DESCRIPTION = "Host-side keygen and sign utilities for wolfBoot secure-boot \
image signing. Builds RSA4096 signing keys and signs firmware images with \
SHA3-384 hashes. Uses wolfBoot's bundled wolfCrypt (under lib/wolfssl) -- \
no external wolfSSL dependency."

require wolfboot.inc

inherit native

do_configure[noexec] = "1"

do_compile() {
# Build the keytools (host-side signing/keygen utilities).
oe_runmake -C tools/keytools \
CC="${CC}" \
LD="${CC}" \
WOLFBOOTDIR=${S} \
WOLFBOOT_LIB_WOLFSSL=${S}/lib/wolfssl \
V=1
}

do_install() {
install -d ${D}${bindir}
install -m 0755 ${S}/tools/keytools/sign ${D}${bindir}/wolfboot-sign
install -m 0755 ${S}/tools/keytools/keygen ${D}${bindir}/wolfboot-keygen
}
94 changes: 94 additions & 0 deletions recipes-wolfssl/wolfboot/wolfboot-signed-image.bb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
SUMMARY = "wolfBoot signed kernel FIT image"
DESCRIPTION = "Signs the kernel FIT image (fitImage / image.ub) with \
wolfBoot RSA4096+SHA3-384 for verified secure boot. The signed output is \
placed in DEPLOY_DIR_IMAGE as image_v<version>_signed.bin, ready for \
flashing to the OFP_A / OFP_B partitions of an A/B-enabled SD card or QSPI."

HOMEPAGE = "https://github.com/wolfssl/wolfBoot"
SECTION = "bootloaders"
LICENSE = "GPL-3.0-only"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL-3.0-only;md5=c79ff39f19dfec6d293b95dea7b07891"

inherit deploy

DEPENDS = "wolfboot-keytools-native"

# REQUIRED: absolute path to the same wolfBoot signing private key used by
# wolfboot_git.bb to embed the public key into wolfboot.elf. User-supplied
# out-of-band (never sourced from DEPLOY_DIR_IMAGE or sstate).
WOLFBOOT_SIGNING_KEY ?= ""

# Kernel recipe name -- default matches linux-xlnx on ZynqMP / Versal.
# Override in local.conf for non-Xilinx kernels (e.g. KERNEL_PN = "linux-yocto").
KERNEL_PN ?= "linux-xlnx"

# Only depend on the kernel being deployed. The signing key is supplied
# directly by the user, NOT pulled from wolfboot:do_deploy, so there is no
# wolfboot dep here.
do_compile[depends] += "${KERNEL_PN}:do_deploy"

do_configure[noexec] = "1"

# Version stamped into the signed image header. Increment for A/B updates
# (e.g. set to "2" for the next A/B update image).
WOLFBOOT_IMAGE_VERSION ?= "1"

# Name of the FIT image produced by the kernel recipe. Defaults match
# linux-xlnx on ZynqMP / Versal.
WOLFBOOT_FIT_IMAGE ?= "fitImage"

# Validate WOLFBOOT_SIGNING_KEY only when this recipe actually builds
# (see wolfboot_git.bb for the rationale re: parse-time vs task-time).
python check_wolfboot_signing_key() {
key = d.getVar('WOLFBOOT_SIGNING_KEY') or ''
if not key:
bb.fatal("WOLFBOOT_SIGNING_KEY is not set. Generate a signing key "
"with 'wolfboot-keygen --rsa4096 -g <path>.der' and point "
"WOLFBOOT_SIGNING_KEY at it (absolute path). The SAME key "
"must also be set in wolfboot_git.bb so public/private "
"halves match. See recipes-wolfssl/wolfboot/README.md.")
import os
if not os.path.isfile(key):
bb.fatal("WOLFBOOT_SIGNING_KEY='%s' does not exist or is not a "
"regular file." % key)
}
do_compile[prefuncs] += "check_wolfboot_signing_key"

do_compile() {
install -d ${B}

# Validate the upstream FIT image exists before invoking the signing
# tool, so a missing/renamed artifact fails with an actionable message
# instead of a cryptic wolfboot-sign error.
fit_image="${DEPLOY_DIR_IMAGE}/${WOLFBOOT_FIT_IMAGE}"
if [ ! -f "$fit_image" ]; then
bbfatal "FIT image '$fit_image' not found. Check WOLFBOOT_FIT_IMAGE and that ${KERNEL_PN}:do_deploy produced it."
fi

# Sign a local copy under ${B} instead of operating directly on the
# shared DEPLOY_DIR_IMAGE. wolfboot-sign writes its output next to the
# input; keeping both input and output under ${WORKDIR} avoids races
# with the kernel's do_deploy (or parallel multiconfig builds) and
# leaves publishing to do_deploy below.
cp "$fit_image" ${B}/${WOLFBOOT_FIT_IMAGE}

# Sign the FIT image with RSA4096 + SHA3-384 using the user-supplied
# signing key. wolfboot-sign emits the output NEXT TO the input file,
# naming it <input>_v<version>_signed.bin. Run from ${B} with a
# relative path so the output lands inside ${B} predictably.
cd ${B}
wolfboot-sign --rsa4096 --sha3 \
${WOLFBOOT_FIT_IMAGE} \
${WOLFBOOT_SIGNING_KEY} \
${WOLFBOOT_IMAGE_VERSION}
Comment thread
dgarske marked this conversation as resolved.
}

do_install[noexec] = "1"

do_deploy() {
install -d ${DEPLOYDIR}
install -m 0644 ${B}/${WOLFBOOT_FIT_IMAGE}_v${WOLFBOOT_IMAGE_VERSION}_signed.bin \
${DEPLOYDIR}/image_v${WOLFBOOT_IMAGE_VERSION}_signed.bin
}

addtask deploy before do_build after do_compile
Loading