Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add make-boot-image service #11

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
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]>'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent formatting - maybe get this through a asciidoc preview tool?


== 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a core dep, or does it require an additional package?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's part of the Python standard library. It's provided by libpython3-stdlib in Debian, but this is a hard-dep of the python3 package. Even if we were to use python3-minimal, we would still get email.utils as part of libpython3.11-minimal

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be expanded to include the vendor fields?

- 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}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the run-around, but I would prefer this was named 'kernel_v8' or similar. Not a blocker.

>&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