Skip to content

Commit

Permalink
Add make-boot-image service
Browse files Browse the repository at this point in the history
Signed-off-by: Richard Oliver <[email protected]>
  • Loading branch information
roliver-rpi committed Aug 20, 2024
1 parent 22276dc commit fe4467c
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 1 deletion.
14 changes: 14 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,20 @@ Set to `1` to allow the service to run without actually writing keys or OS image

WARNING: Setting `DEMO_MODE_ONLY` will cause your seen-devices storage location to change to a subdirectory of the one specified by `RPI_DEVICE_SERIAL_STORE`, `demo/`

=== BOOT_IMAGE_VENDOR
*Mandatory* for make-boot-image

Lower-case single-word representation of your organisation name. Used by
make-boot-image service. e.g. `acme` would be appropriate for "Acme
Corporation".

=== BOOT_IMAGE_MAINTAINER
*Mandatory* for make-boot-image

A display name and email address in RFC 5322 mailbox format of the individual /
team responsible for creating your boot-image packages. e.g.
`Packaging Team <[email protected]>'

== Using rpi-sb-provisioner
`rpi-sb-provisioner` is composed of three `systemd` services that are triggered by the connection of a device in RPIBoot mode to a USB port. With `rpi-sb-provisioner` configured to your requirements, all that is therefore required is to connect your target Raspberry Pi device in RPIBoot mode.

Expand Down
24 changes: 24 additions & 0 deletions config/validator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Format of return will be [Happy: bool, error: str]
from os import path
from email.utils import parseaddr, formataddr
import subprocess

def validate_CUSTOMER_KEY_FILE_PEM(text) -> tuple[bool, str]:
Expand Down Expand Up @@ -69,3 +70,26 @@ def validate_RPI_SB_WORKDIR(text) -> tuple[bool, str]:
else:
return (False, "Please specify absolute path, beginning with /")
return (True, "")

def validate_BOOT_IMAGE_VENDOR(text) -> tuple[bool, str]:
if len(text) > 0:
if text.isalpha() and text.islower():
return (True, "")
else:
return (False, "BOOT_IMAGE_VENDOR must contain only lowercase letters")
else:
return (False, "Please specify a boot image vendor, e.g. \"acme\"")

def validate_BOOT_IMAGE_MAINTAINER(text) -> tuple[bool, str]:
# TODO: parseaddr/formataddr is now a legacy API.
# Switch to python3-email-validator once v2.2.0 is available in Debian.
#
# parseaddr supports many formats but formataddr always uses RFC 5322
# mailbox.
# Ensure that both display name and addr-spec address enclosed in angle
# brackets are present.
maint_addr = parseaddr(text)
if all(maint_addr) and formataddr(maint_addr) == text:
return (True, "")
else:
return (False, "BOOT_IMAGE_MAINTAINER must be an RFC 5322 mailbox, e.g. \"Able Maintainer <[email protected]>\"")
6 changes: 6 additions & 0 deletions host-support/bootimg_preinst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh

if [ "$1" = install ]
then
rm -f /boot/firmware/config.txt
fi
2 changes: 2 additions & 0 deletions host-support/config
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ RPI_DEVICE_EEPROM_WP_SET=
DEVICE_SERIAL_STORE=/usr/local/etc/rpi-sb-provisioner/seen
DEMO_MODE_ONLY=
RPI_SB_WORKDIR=
BOOT_IMAGE_VENDOR=
BOOT_IMAGE_MAINTAINER=
1 change: 1 addition & 0 deletions host-support/ramdisk_cmdline.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootwait console=tty0 console=serial0,115200 root=/dev/ram0
13 changes: 13 additions & 0 deletions host-support/ramdisk_internal_config.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[all]
kernel=zImage
arm_64bit=1
initramfs rootfs.cpio.zst
enable_uart=1
uart_2ndstage=1
disable_overscan=1
cmdline=cmdline.txt

[cm4]
dtoverlay=dwc2,dr_mode=host

[none]
26 changes: 25 additions & 1 deletion host-support/terminal-functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ get_fastboot_config_file() {
fi
}

get_internal_config_file() {
if [ -f /etc/rpi-sb-provisioner/ramdisk_internal_config.txt ]; then
echo "/etc/rpi-sb-provisioner/ramdisk_internal_config.txt"
else
echo "/var/lib/rpi-sb-provisioner/ramdisk_internal_config.txt"
fi
}

get_ramdisk_cmdline_file() {
if [ -f /etc/rpi-sb-provisioner/ramdisk_cmdline.txt ]; then
echo "/etc/rpi-sb-provisioner/ramdisk_cmdline.txt"
else
echo "/var/lib/rpi-sb-provisioner/ramdisk_cmdline.txt"
fi
}

get_signing_directives() {
if [ -n "${CUSTOMER_KEY_PKCS11_NAME}" ]; then
echo "${CUSTOMER_KEY_PKCS11_NAME} -engine pkcs11 -keyform engine"
Expand All @@ -148,4 +164,12 @@ get_signing_directives() {
exit 1
fi
fi
}
}

get_bootimg_preinst_file() {
if [ -f /etc/rpi-sb-provisioner/bootimg_preinst ]; then
echo "/etc/rpi-sb-provisioner/bootimg_preinst"
else
echo "/var/lib/rpi-sb-provisioner/bootimg_preinst"
fi
}
35 changes: 35 additions & 0 deletions make-boot-image/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# make-boot-image
A oneshot service to download the specified Raspberry Pi linux-image- and
create a replacement boot-image- package. This replacement package contains a
signed boot.img with a cryptroot-enabled initramfs. The kernel modules are
retained in the replacement package. Necessary firmware file are inserted into
the signed boot.img where appropriate (via raspi-firmware package).

> [!CAUTION]
> Support only exists for v8 kernels at this time.
## Configuration
- VENDOR
- OPENSSL
- CUSTOMER\_KEY\_FILE\_PEM

## Usage
To create a replacement boot-image- package for linux-image-6.6.31+rpt-rpi-v8
```
systemctl start make-boot-image@$(systemd-escape 6.6.31+rpt-rpi-v8).service
```

To determine the latest v8 linux image (in order to run the service as
suggested above)
```
META_PKG=linux-image-rpi-v8
SRV=rpi-package-download@$(systemd-escape $META_PKG).service
systemctl start --wait $SRV \
&& grep-dctrl -F Package -X $META_PKG -n -s Depends /var/cache/$SRV/latest/Packages \
| grep -o '^[[:graph:]]*'
```

The service makes use of systemd's CacheDirectory during execution. The boot-image- package created by the example given above would typically be found at:
```
/var/cache/[email protected]\x2brpt\x2drpi\x2dv8.service/boot-image-<vendor>-6.6.31+rpt-rpi-v8_6.6.31-1+rpt1_arm64.deb
```
230 changes: 230 additions & 0 deletions make-boot-image/make-boot-image-from-kernel
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
#!/bin/sh

set -e

# deps:
# - dpkg (dpkg-deb)
# - openssl
# - zstd
# - cpio
# - findutils (xargs)

. /usr/local/bin/terminal-functions.sh

read_config

TMPDIR="${TMPDIR:=/tmp}"

if [ -z "${1}" ]; then
>&2 echo "No linux image specified"
exit 1
fi

if [ -z "${RPI_DEVICE_FAMILY}" ]; then
>&2 echo "'RPI_DEVICE_FAMILY' not specified"
exit 1
fi

if [ -z "${BOOT_IMAGE_VENDOR}" ]; then
>&2 echo "'BOOT_IMAGE_VENDOR' not specified"
exit 1
fi

if [ -z "${BOOT_IMAGE_MAINTAINER}" ]; then
>&2 echo "'BOOT_IMAGE_MAINTAINER' not specified"
exit 1
fi

if [ -z "${OPENSSL}" ] || [ ! -f "${OPENSSL}" ]; then
>&2 echo "'OPENSSL' not set or binary does not exist"
exit 1
fi

LINUX_IMAGE="${1}"

# Should be set by systemd
SERVICE_NAME="make-boot-image@$(systemd-escape "$LINUX_IMAGE").service"
CACHE_DIRECTORY="${CACHE_DIRECTORY:=/var/cache/${SERVICE_NAME}}"

# TODO: Might be interesting to start rpi-package-download with --no-block to
# allow multiple simultaneous downloads.
download_package() {
systemctl start \
--wait \
rpi-package-download@"$(systemd-escape "${1}")".service
}

KERNEL_2711="linux-image-${LINUX_IMAGE}"
>&2 echo "Downloading ${KERNEL_2711}"
download_package "$KERNEL_2711"

PACKAGE_NAME="boot-image-${BOOT_IMAGE_VENDOR}-${LINUX_IMAGE}"

# Temp directory cleanup
TEMP_DIRS_LIST=$(mktemp make_boot_image_temp_dirs_list.XXX)
:> "${TEMP_DIRS_LIST}"
remove_temp_dirs() {
>&2 echo "Removing temporary directories"
xargs --null rm -rf < "${TEMP_DIRS_LIST}"
rm -f "${TEMP_DIRS_LIST}"
}
trap remove_temp_dirs EXIT

>&2 printf "Creating filesystem hierarchy for deb package: "
DEB_HIER="$(mktemp --directory --tmpdir debhier.XXX)"
printf "%s\0" "${DEB_HIER}" >> "${TEMP_DIRS_LIST}"
>&2 echo "${DEB_HIER}"

>&2 printf "Create rootfs working directory: "
WORK_DIR="$(mktemp --directory --tmpdir boot-image-rootfs.XXX)"
printf "%s\0" "${WORK_DIR}" >> "${TEMP_DIRS_LIST}"
>&2 echo "${WORK_DIR}"

latest_pkg_dir() {
echo /var/cache/rpi-package-download@"$(systemd-escape "${1}")".service/latest
}

>&2 echo "Extracting package contents"
dpkg-deb --raw-extract "$(latest_pkg_dir "$KERNEL_2711")/package.deb" "${WORK_DIR}"

get_dctrl_field() {
grep-dctrl \
--field=Package \
--exact-match "${2}" \
--no-field-names \
--show-field="${3}" \
"${1}"
}

# Determine package version for later reuse
KERNEL_2711_VERSION="$(get_dctrl_field "${WORK_DIR}/DEBIAN/control" "${KERNEL_2711}" Version)"
>&2 echo "Extracted ${KERNEL_2711}, version ${KERNEL_2711_VERSION}"

# rootfs kernel modules
>&2 echo "Copy kernel modules into deb package"
mkdir -p "${DEB_HIER}/lib/modules"
rsync -crt "${WORK_DIR}/lib/modules/"* "${DEB_HIER}/lib/modules"

>&2 printf "Create ramdisk working directory: "
BFS_DIR="$(mktemp --directory --tmpdir boot-image-bootfs.XXX)"
printf "%s\0" "${BFS_DIR}" >> "${TEMP_DIRS_LIST}"
>&2 echo "${BFS_DIR}"

# Kernel Images
>&2 echo "Copy kernel to ramdisk"
cp "${WORK_DIR}/boot/vmlinuz-${LINUX_IMAGE}" "${BFS_DIR}/zImage"

# Overlays
>&2 echo "Copy overlays to ramdisk"
OVERLAY_PATH="${WORK_DIR}/usr/lib/${KERNEL_2711}/overlays"
rsync -crt "${OVERLAY_PATH}"/*.dtb* "${OVERLAY_PATH}/README" "${BFS_DIR}/overlays"

# DTBs
>&2 echo "Copy DTBs to ramdisk"
DTB_PATH="${WORK_DIR}/usr/lib/${KERNEL_2711}/broadcom"
rsync -crt "${DTB_PATH}"/bcm27*.dtb "${BFS_DIR}"

# Insert an initramfs
>&2 echo "Add cryptoot initramfs to ramdisk (with necessary kernel modules)"
INITRAMFS_EXTRACT="$(mktemp --directory --tmpdir initramfs-extract.XXX)"
printf "%s\0" "${INITRAMFS_EXTRACT}" >> "${TEMP_DIRS_LIST}"
zstd -q -d "$(get_cryptroot)" -o "${INITRAMFS_EXTRACT}/initramfs.cpio"
mkdir -p "${INITRAMFS_EXTRACT}/initramfs"
cd "${INITRAMFS_EXTRACT}/initramfs"
RETURN_DIR="${OLDPWD}"
cpio --quiet -id < ../initramfs.cpio > /dev/null
rm ../initramfs.cpio
cd "${WORK_DIR}"
find lib/modules \
\( \
-name 'dm-mod.*' \
-o \
-name 'dm-crypt.*' \
-o \
-name 'af_alg.*' \
-o \
-name 'algif_skcipher.*' \
-o \
-name 'libaes.*' \
-o \
-name 'aes_generic.*' \
-o \
-name 'aes-arm64.*' \
\) \
-exec cp -r --parents "{}" "${INITRAMFS_EXTRACT}/initramfs/usr/" \;
cd -
find . -print0 | cpio --quiet --null -ov --format=newc > ../initramfs.cpio 2> /dev/null
cd "${RETURN_DIR}"
unset RETURN_DIR
zstd -q -6 "${INITRAMFS_EXTRACT}/initramfs.cpio" -o "${BFS_DIR}/rootfs.cpio.zst"

# raspi-firmware
>&2 echo "Downloading raspi-firmware"
download_package raspi-firmware

>&2 printf "Create temp directory to extract firmware: "
FW_EXTRACT_DIR="$(mktemp --directory --tmpdir boot-image-firmware.XXX)"
printf "%s\0" "${FW_EXTRACT_DIR}" >> "${TEMP_DIRS_LIST}"
>&2 echo "${FW_EXTRACT_DIR}"

>&2 echo "Extracting firmware package contents"
dpkg-deb --raw-extract "$(latest_pkg_dir raspi-firmware)/package.deb" "${FW_EXTRACT_DIR}"

>&2 echo "Add firmware to ramdisk"
rsync -v -crt "${FW_EXTRACT_DIR}/usr/lib/raspi-firmware/" "${BFS_DIR}"

# cmdline.txt
>&2 echo "Add cmdline.txt to ramdisk"
cp "$(get_ramdisk_cmdline_file)" "${BFS_DIR}/cmdline.txt"

# Inner config.txt
>&2 echo "Add config.txt to ramdisk"
cp "$(get_internal_config_file)" "${BFS_DIR}/config.txt"

# Invoke make-boot-image
>&2 echo "Finalise ramdisk in deb package (boot.img)"
mkdir -p "${DEB_HIER}/boot/firmware"
make-boot-image \
-b "pi${RPI_DEVICE_FAMILY}" \
-d "${BFS_DIR}" \
-o "${DEB_HIER}/boot/firmware/boot.img" > /dev/null

# Outer config.txt
>&2 echo "Add config.txt to deb package (ensure boot.img is used)"
cp "$(get_fastboot_config_file)" "${DEB_HIER}/boot/firmware/config.txt"

# boot.sig generation
>&2 echo "Signing ramdisk image"
sha256sum "${DEB_HIER}/boot/firmware/boot.img" | awk '{print $1}' > "${DEB_HIER}/boot/firmware/boot.sig"
printf "rsa2048: " >> "${DEB_HIER}/boot/firmware/boot.sig"
# shellcheck disable=SC2046
${OPENSSL} dgst \
-sign $(get_signing_directives) \
-keyform PEM \
-sha256 \
"${DEB_HIER}/boot/firmware/boot.img" \
| xxd -c 4096 -p >> "${DEB_HIER}/boot/firmware/boot.sig"

# Insert control file
mkdir "${DEB_HIER}/DEBIAN"
echo \
"Package: ${PACKAGE_NAME}
Source: linux
Version: ${KERNEL_2711_VERSION}
Architecture: arm64
Maintainer: ${BOOT_IMAGE_MAINTAINER}
Section: kernel
Priority: optional
Homepage: https://github.com/raspberrypi/linux/
Provides: ${KERNEL_2711}
Conflicts: ${KERNEL_2711}
Replaces: ${KERNEL_2711}
Description: Repackaged ${KERNEL_2711} for signed/cryptroot boot" \
> "${DEB_HIER}/DEBIAN/control"

# Insert preinst script to remove /boot/firmware/config.txt (otherwise dpkg
# attempt to create a ".dpkg-tmp" hardlink.
cp "$(get_bootimg_preinst_file)" "${DEB_HIER}/DEBIAN/preinst"

# Create Debian package
dpkg-deb --build "${DEB_HIER}" "${CACHE_DIRECTORY}"
10 changes: 10 additions & 0 deletions make-boot-image/make-boot-image.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=Creates a signed boot image using a Raspberry Pi OS kernel / bootloader

[Service]
Type=oneshot
ExecStart=/usr/local/bin/make-boot-image-from-kernel "%I"
CacheDirectory=%n

[Install]
WantedBy=multi-user.target
Loading

0 comments on commit fe4467c

Please sign in to comment.