diff --git a/make-boot-image/README.md b/make-boot-image/README.md new file mode 100644 index 0000000..c33d98a --- /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 2712 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-2712 +``` +systemctl start make-boot-image@$(systemd-escape 6.6.31+rpt-rpi-2712).service +``` + +To determine the latest 2712 linux image (in order to run the service as +suggested above) +``` +META_PKG=linux-image-rpi-2712 +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\x2d2712.service/boot-image--6.6.31+rpi-rpi-2712_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..b59f73d --- /dev/null +++ b/make-boot-image/make-boot-image-from-kernel @@ -0,0 +1,222 @@ +#!/usr/bin/env bash + +set -e + +# deps: +# - dpkg (dpkg-deb) +# - openssl +# - zstd +# - cpio + +# TODO: Currently, we just assume we're Pi5/2712. +# There is currently no functionality to insert firmware for Pi4 and earlier. + +TMPDIR="${TMPDIR:=/tmp}" + +if [[ -z "${1}" ]]; then + >&2 echo "No linux image specified" + exit 1 +fi + +if [[ -z "${VENDOR}" ]]; then + >&2 echo "'VENDOR' not specified" + exit 1 +fi + +if [[ -z "${OPENSSL}" || ! -f "${OPENSSL}" ]]; then + >&2 echo "'OPENSSL' not set or binary does not exist" + exit 1 +fi + +if [[ -z "${CUSTOMER_KEY_FILE_PEM}" || ! -f "${CUSTOMER_KEY_FILE_PEM}" ]]; then + >&2 echo "'CUSTOMER_KEY_FILE_PEM' not set or file 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}}" +RUNTUME_DIRECTORY="${RUNTIME_DIRECTORY:=/run/${SERVICE_NAME}}" + +# TODO: Might be interesting to start rpi-package-download with --no-block to +# allow multiple simultaneous downloads. +function download_package() { + systemctl start \ + --wait \ + rpi-package-download@$(systemd-escape ${1}).service +} + +KERNEL_2712="linux-image-${LINUX_IMAGE}" +>&2 echo "Downloading ${KERNEL_2712}" +download_package "$KERNEL_2712" + +PACKAGE_NAME="boot-image-${VENDOR}-${LINUX_IMAGE}" + +# Temp directory cleanup +TEMP_DIRS=() +function remove_temp_dirs() { + >&2 echo "Removing temporary directories" + for dir in "${TEMP_DIRS[@]}" + do + rm -rf "$dir" + done +} +trap remove_temp_dirs EXIT + +>&2 echo -n "Creating filesystem hierarchy for deb package: " +DEB_HIER="$(mktemp --directory --tmpdir debhier.XXX)" +TEMP_DIRS+=("${DEB_HIER}") +>&2 echo "${DEB_HIER}" + +>&2 echo -n "Create rootfs working directory: " +WORK_DIR="$(mktemp --directory --tmpdir boot-image-rootfs.XXX)" +TEMP_DIRS+=("${WORK_DIR}") +>&2 echo "${WORK_DIR}" + +function 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_2712)/package.deb" "${WORK_DIR}" + +function get_dctrl_field() { + grep-dctrl \ + --field=Package \ + --exact-match "${2}" \ + --no-field-names \ + --show-field="${3}" \ + "${1}" +} + +# Determine package version for later reuse +KERNEL_2712_VERSION="$(get_dctrl_field ${WORK_DIR}/DEBIAN/control ${KERNEL_2712} Version)" +>&2 echo "Extracted ${KERNEL_2712}, version ${KERNEL_2712_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 echo -n "Create ramdisk working directory: " +BFS_DIR="$(mktemp --directory --tmpdir boot-image-bootfs.XXX)" +TEMP_DIRS+=("${BFS_DIR}") +>&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_2712}/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_2712}/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)" +TEMP_DIRS+=("${INITRAMFS_EXTRACT}") +zstd -q -d /usr/share/misc/cryptroot_initramfs -o "${INITRAMFS_EXTRACT}/initramfs.cpio" +mkdir -p "${INITRAMFS_EXTRACT}/initramfs" +pushd "${INITRAMFS_EXTRACT}/initramfs" > /dev/null +cpio --quiet -id < ../initramfs.cpio > /dev/null +rm ../initramfs.cpio +pushd "${WORK_DIR}" > /dev/null +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/" \; +popd > /dev/null +find . -print0 | cpio --quiet --null -ov --format=newc > ../initramfs.cpio 2> /dev/null +popd > /dev/null +zstd -q -6 "${INITRAMFS_EXTRACT}/initramfs.cpio" -o "${BFS_DIR}/rootfs.cpio.zst" + +# cmdline.txt +>&2 echo "Add cmdline.txt to ramdisk" +# TODO: Needs to be user-modifiable +echo "rootwait console=tty0 console=serial0,115200 root=/dev/ram0" > "${BFS_DIR}/cmdline.txt" + +# Inner config.txt +>&2 echo "Add config.txt to ramdisk" +# TODO: Needs to be user-modifiable +echo \ +'[all] +kernel=zImage +initramfs rootfs.cpio.zst +enable_uart=1 +uart_2ndstage=1 +disable_overscan=1 +cmdline=cmdline.txt + +[cm4] +dtoverlay=dwc2,dr_mode=host + +[none] +' > "${BFS_DIR}/config.txt" + +# Invoke make-boot-image +>&2 echo "Finalise ramdisk in deb package (boot.img)" +mkdir -p "${DEB_HIER}/boot/firmware" +# TODO: Assuming pi5 here +make-boot-image \ + -b pi5 \ + -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 /usr/share/misc/boot_ramdisk_config.txt "${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" +echo -n "rsa2048: " >> "${DEB_HIER}/boot/firmware/boot.sig" +${OPENSSL} dgst \ + -sign "${CUSTOMER_KEY_FILE_PEM}" \ + -keyform PEM \ + -sha256 \ + "${DEB_HIER}/boot/firmware/boot.img" \ + | xxd -c 4096 -p >> "${DEB_HIER}/boot/firmware/boot.sig" + +# Insert control file +# TODO: Needs to be user modifiable +# TODO: depends on kmod, linux-base, etc? +# TODO: breaks for fwupdate, initramfs-tools, etc? +mkdir "${DEB_HIER}/DEBIAN" +echo \ +"Package: ${PACKAGE_NAME} +Source: linux +Version: ${KERNEL_2712_VERSION} +Architecture: arm64 +Maintainer: John Smith +Section: kernel +Priority: optional +Homepage: https://github.com/raspberrypi/linux/ +Provides: ${KERNEL_2712} +Conflicts: ${KERNEL_2712} +Replaces: ${KERNEL_2712} +Description: TODO: Provide a better description" \ +> "${DEB_HIER}/DEBIAN/control" + +# 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..d08b524 --- /dev/null +++ b/make-boot-image/make-boot-image.service @@ -0,0 +1,11 @@ +[Unit] +Description=Creates a signed boot image using a Raspberry Pi OS kernel / bootloader + +[Service] +Type=oneshot +EnvironmentFile=/etc/rpi-sb-provisioner/config +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 66d238b..d82a20d 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -143,6 +143,8 @@ depends: - dctrl-tools - diffutils - findutils + - dpkg + - zstd # Recommended packages. (overridable) # This will expand any env var you set in the field, e.g. ${RECOMMENDS_BLA} @@ -228,6 +230,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 + # This will add all files in some/directory or in subdirectories at the # same level under the directory /etc. This means the tree structure in # some/directory will not be replicated.