From fe4467c5c79847014f963c7bcb304b07a9196885 Mon Sep 17 00:00:00 2001 From: Richard Oliver Date: Mon, 5 Aug 2024 11:16:12 +0100 Subject: [PATCH] Add make-boot-image service Signed-off-by: Richard Oliver --- README.adoc | 14 ++ config/validator.py | 24 ++ host-support/bootimg_preinst | 6 + host-support/config | 2 + host-support/ramdisk_cmdline.txt | 1 + host-support/ramdisk_internal_config.txt | 13 ++ host-support/terminal-functions.sh | 26 ++- make-boot-image/README.md | 35 +++ make-boot-image/make-boot-image-from-kernel | 230 ++++++++++++++++++++ make-boot-image/make-boot-image.service | 10 + nfpm.yaml | 17 ++ 11 files changed, 377 insertions(+), 1 deletion(-) create mode 100755 host-support/bootimg_preinst create mode 100644 host-support/ramdisk_cmdline.txt create mode 100644 host-support/ramdisk_internal_config.txt create mode 100644 make-boot-image/README.md create mode 100755 make-boot-image/make-boot-image-from-kernel create mode 100644 make-boot-image/make-boot-image.service diff --git a/README.adoc b/README.adoc index 4456d1a..9581bc1 100644 --- a/README.adoc +++ b/README.adoc @@ -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 ' + == 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. diff --git a/config/validator.py b/config/validator.py index 3d995b3..13a615d 100644 --- a/config/validator.py +++ b/config/validator.py @@ -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]: @@ -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 \"") diff --git a/host-support/bootimg_preinst b/host-support/bootimg_preinst new file mode 100755 index 0000000..be770b3 --- /dev/null +++ b/host-support/bootimg_preinst @@ -0,0 +1,6 @@ +#!/bin/sh + +if [ "$1" = install ] +then + rm -f /boot/firmware/config.txt +fi diff --git a/host-support/config b/host-support/config index d3e67c0..65e8c9a 100644 --- a/host-support/config +++ b/host-support/config @@ -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= diff --git a/host-support/ramdisk_cmdline.txt b/host-support/ramdisk_cmdline.txt new file mode 100644 index 0000000..6314b8c --- /dev/null +++ b/host-support/ramdisk_cmdline.txt @@ -0,0 +1 @@ +rootwait console=tty0 console=serial0,115200 root=/dev/ram0 diff --git a/host-support/ramdisk_internal_config.txt b/host-support/ramdisk_internal_config.txt new file mode 100644 index 0000000..f77af30 --- /dev/null +++ b/host-support/ramdisk_internal_config.txt @@ -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] diff --git a/host-support/terminal-functions.sh b/host-support/terminal-functions.sh index 1e76379..5c3234e 100644 --- a/host-support/terminal-functions.sh +++ b/host-support/terminal-functions.sh @@ -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" @@ -148,4 +164,12 @@ get_signing_directives() { exit 1 fi fi -} \ No newline at end of file +} + +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 +} diff --git a/make-boot-image/README.md b/make-boot-image/README.md new file mode 100644 index 0000000..596a0f8 --- /dev/null +++ b/make-boot-image/README.md @@ -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/make-boot-image@6.6.31\x2brpt\x2drpi\x2dv8.service/boot-image--6.6.31+rpt-rpi-v8_6.6.31-1+rpt1_arm64.deb +``` diff --git a/make-boot-image/make-boot-image-from-kernel b/make-boot-image/make-boot-image-from-kernel new file mode 100755 index 0000000..1c4b89d --- /dev/null +++ b/make-boot-image/make-boot-image-from-kernel @@ -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}" diff --git a/make-boot-image/make-boot-image.service b/make-boot-image/make-boot-image.service new file mode 100644 index 0000000..09d1cb4 --- /dev/null +++ b/make-boot-image/make-boot-image.service @@ -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 diff --git a/nfpm.yaml b/nfpm.yaml index fa3ed2b..b0b904e 100755 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -146,6 +146,8 @@ depends: - libengine-pkcs11-openssl - libp11-kit-dev - gnutls-bin + - dpkg + - zstd # Recommended packages. (overridable) # This will expand any env var you set in the field, e.g. ${RECOMMENDS_BLA} @@ -216,9 +218,18 @@ contents: - src: host-support/fastboot-gadget.img dst: /var/lib/rpi-sb-provisioner/fastboot-gadget.img + - src: host-support/ramdisk_internal_config.txt + dst: /var/lib/rpi-sb-provisioner/ramdisk_internal_config.txt + + - src: host-support/ramdisk_cmdline.txt + dst: /var/lib/rpi-sb-provisioner/ramdisk_cmdline.txt + - src: host-support/make-boot-image dst: /usr/local/bin/make-boot-image + - src: host-support/bootimg_preinst + dst: /var/lib/rpi-sb-provisioner/bootimg_preinst + - src: key-writer/rpi-sb-keywriter.service dst: /usr/local/lib/systemd/system/rpi-sb-keywriter@.service @@ -240,6 +251,12 @@ contents: - src: rpi-package-download/rpi-package-download dst: /usr/local/bin/rpi-package-download + - src: make-boot-image/make-boot-image.service + dst: /usr/local/lib/systemd/system/make-boot-image@.service + + - src: make-boot-image/make-boot-image-from-kernel + dst: /usr/local/bin/make-boot-image-from-kernel + - src: monitor/monitor.sh dst: /usr/local/bin/monitor.sh