Skip to content

Commit fe4467c

Browse files
committed
Add make-boot-image service
Signed-off-by: Richard Oliver <[email protected]>
1 parent 22276dc commit fe4467c

File tree

11 files changed

+377
-1
lines changed

11 files changed

+377
-1
lines changed

README.adoc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,20 @@ Set to `1` to allow the service to run without actually writing keys or OS image
182182

183183
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/`
184184

185+
=== BOOT_IMAGE_VENDOR
186+
*Mandatory* for make-boot-image
187+
188+
Lower-case single-word representation of your organisation name. Used by
189+
make-boot-image service. e.g. `acme` would be appropriate for "Acme
190+
Corporation".
191+
192+
=== BOOT_IMAGE_MAINTAINER
193+
*Mandatory* for make-boot-image
194+
195+
A display name and email address in RFC 5322 mailbox format of the individual /
196+
team responsible for creating your boot-image packages. e.g.
197+
`Packaging Team <[email protected]>'
198+
185199
== Using rpi-sb-provisioner
186200
`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.
187201

config/validator.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## Format of return will be [Happy: bool, error: str]
22
from os import path
3+
from email.utils import parseaddr, formataddr
34
import subprocess
45

56
def validate_CUSTOMER_KEY_FILE_PEM(text) -> tuple[bool, str]:
@@ -69,3 +70,26 @@ def validate_RPI_SB_WORKDIR(text) -> tuple[bool, str]:
6970
else:
7071
return (False, "Please specify absolute path, beginning with /")
7172
return (True, "")
73+
74+
def validate_BOOT_IMAGE_VENDOR(text) -> tuple[bool, str]:
75+
if len(text) > 0:
76+
if text.isalpha() and text.islower():
77+
return (True, "")
78+
else:
79+
return (False, "BOOT_IMAGE_VENDOR must contain only lowercase letters")
80+
else:
81+
return (False, "Please specify a boot image vendor, e.g. \"acme\"")
82+
83+
def validate_BOOT_IMAGE_MAINTAINER(text) -> tuple[bool, str]:
84+
# TODO: parseaddr/formataddr is now a legacy API.
85+
# Switch to python3-email-validator once v2.2.0 is available in Debian.
86+
#
87+
# parseaddr supports many formats but formataddr always uses RFC 5322
88+
# mailbox.
89+
# Ensure that both display name and addr-spec address enclosed in angle
90+
# brackets are present.
91+
maint_addr = parseaddr(text)
92+
if all(maint_addr) and formataddr(maint_addr) == text:
93+
return (True, "")
94+
else:
95+
return (False, "BOOT_IMAGE_MAINTAINER must be an RFC 5322 mailbox, e.g. \"Able Maintainer <[email protected]>\"")

host-support/bootimg_preinst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/sh
2+
3+
if [ "$1" = install ]
4+
then
5+
rm -f /boot/firmware/config.txt
6+
fi

host-support/config

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ RPI_DEVICE_EEPROM_WP_SET=
99
DEVICE_SERIAL_STORE=/usr/local/etc/rpi-sb-provisioner/seen
1010
DEMO_MODE_ONLY=
1111
RPI_SB_WORKDIR=
12+
BOOT_IMAGE_VENDOR=
13+
BOOT_IMAGE_MAINTAINER=

host-support/ramdisk_cmdline.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootwait console=tty0 console=serial0,115200 root=/dev/ram0
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[all]
2+
kernel=zImage
3+
arm_64bit=1
4+
initramfs rootfs.cpio.zst
5+
enable_uart=1
6+
uart_2ndstage=1
7+
disable_overscan=1
8+
cmdline=cmdline.txt
9+
10+
[cm4]
11+
dtoverlay=dwc2,dr_mode=host
12+
13+
[none]

host-support/terminal-functions.sh

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,22 @@ get_fastboot_config_file() {
132132
fi
133133
}
134134

135+
get_internal_config_file() {
136+
if [ -f /etc/rpi-sb-provisioner/ramdisk_internal_config.txt ]; then
137+
echo "/etc/rpi-sb-provisioner/ramdisk_internal_config.txt"
138+
else
139+
echo "/var/lib/rpi-sb-provisioner/ramdisk_internal_config.txt"
140+
fi
141+
}
142+
143+
get_ramdisk_cmdline_file() {
144+
if [ -f /etc/rpi-sb-provisioner/ramdisk_cmdline.txt ]; then
145+
echo "/etc/rpi-sb-provisioner/ramdisk_cmdline.txt"
146+
else
147+
echo "/var/lib/rpi-sb-provisioner/ramdisk_cmdline.txt"
148+
fi
149+
}
150+
135151
get_signing_directives() {
136152
if [ -n "${CUSTOMER_KEY_PKCS11_NAME}" ]; then
137153
echo "${CUSTOMER_KEY_PKCS11_NAME} -engine pkcs11 -keyform engine"
@@ -148,4 +164,12 @@ get_signing_directives() {
148164
exit 1
149165
fi
150166
fi
151-
}
167+
}
168+
169+
get_bootimg_preinst_file() {
170+
if [ -f /etc/rpi-sb-provisioner/bootimg_preinst ]; then
171+
echo "/etc/rpi-sb-provisioner/bootimg_preinst"
172+
else
173+
echo "/var/lib/rpi-sb-provisioner/bootimg_preinst"
174+
fi
175+
}

make-boot-image/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# make-boot-image
2+
A oneshot service to download the specified Raspberry Pi linux-image- and
3+
create a replacement boot-image- package. This replacement package contains a
4+
signed boot.img with a cryptroot-enabled initramfs. The kernel modules are
5+
retained in the replacement package. Necessary firmware file are inserted into
6+
the signed boot.img where appropriate (via raspi-firmware package).
7+
8+
> [!CAUTION]
9+
> Support only exists for v8 kernels at this time.
10+
11+
## Configuration
12+
- VENDOR
13+
- OPENSSL
14+
- CUSTOMER\_KEY\_FILE\_PEM
15+
16+
## Usage
17+
To create a replacement boot-image- package for linux-image-6.6.31+rpt-rpi-v8
18+
```
19+
systemctl start make-boot-image@$(systemd-escape 6.6.31+rpt-rpi-v8).service
20+
```
21+
22+
To determine the latest v8 linux image (in order to run the service as
23+
suggested above)
24+
```
25+
META_PKG=linux-image-rpi-v8
26+
SRV=rpi-package-download@$(systemd-escape $META_PKG).service
27+
systemctl start --wait $SRV \
28+
&& grep-dctrl -F Package -X $META_PKG -n -s Depends /var/cache/$SRV/latest/Packages \
29+
| grep -o '^[[:graph:]]*'
30+
```
31+
32+
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:
33+
```
34+
/var/cache/[email protected]\x2brpt\x2drpi\x2dv8.service/boot-image-<vendor>-6.6.31+rpt-rpi-v8_6.6.31-1+rpt1_arm64.deb
35+
```
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#!/bin/sh
2+
3+
set -e
4+
5+
# deps:
6+
# - dpkg (dpkg-deb)
7+
# - openssl
8+
# - zstd
9+
# - cpio
10+
# - findutils (xargs)
11+
12+
. /usr/local/bin/terminal-functions.sh
13+
14+
read_config
15+
16+
TMPDIR="${TMPDIR:=/tmp}"
17+
18+
if [ -z "${1}" ]; then
19+
>&2 echo "No linux image specified"
20+
exit 1
21+
fi
22+
23+
if [ -z "${RPI_DEVICE_FAMILY}" ]; then
24+
>&2 echo "'RPI_DEVICE_FAMILY' not specified"
25+
exit 1
26+
fi
27+
28+
if [ -z "${BOOT_IMAGE_VENDOR}" ]; then
29+
>&2 echo "'BOOT_IMAGE_VENDOR' not specified"
30+
exit 1
31+
fi
32+
33+
if [ -z "${BOOT_IMAGE_MAINTAINER}" ]; then
34+
>&2 echo "'BOOT_IMAGE_MAINTAINER' not specified"
35+
exit 1
36+
fi
37+
38+
if [ -z "${OPENSSL}" ] || [ ! -f "${OPENSSL}" ]; then
39+
>&2 echo "'OPENSSL' not set or binary does not exist"
40+
exit 1
41+
fi
42+
43+
LINUX_IMAGE="${1}"
44+
45+
# Should be set by systemd
46+
SERVICE_NAME="make-boot-image@$(systemd-escape "$LINUX_IMAGE").service"
47+
CACHE_DIRECTORY="${CACHE_DIRECTORY:=/var/cache/${SERVICE_NAME}}"
48+
49+
# TODO: Might be interesting to start rpi-package-download with --no-block to
50+
# allow multiple simultaneous downloads.
51+
download_package() {
52+
systemctl start \
53+
--wait \
54+
rpi-package-download@"$(systemd-escape "${1}")".service
55+
}
56+
57+
KERNEL_2711="linux-image-${LINUX_IMAGE}"
58+
>&2 echo "Downloading ${KERNEL_2711}"
59+
download_package "$KERNEL_2711"
60+
61+
PACKAGE_NAME="boot-image-${BOOT_IMAGE_VENDOR}-${LINUX_IMAGE}"
62+
63+
# Temp directory cleanup
64+
TEMP_DIRS_LIST=$(mktemp make_boot_image_temp_dirs_list.XXX)
65+
:> "${TEMP_DIRS_LIST}"
66+
remove_temp_dirs() {
67+
>&2 echo "Removing temporary directories"
68+
xargs --null rm -rf < "${TEMP_DIRS_LIST}"
69+
rm -f "${TEMP_DIRS_LIST}"
70+
}
71+
trap remove_temp_dirs EXIT
72+
73+
>&2 printf "Creating filesystem hierarchy for deb package: "
74+
DEB_HIER="$(mktemp --directory --tmpdir debhier.XXX)"
75+
printf "%s\0" "${DEB_HIER}" >> "${TEMP_DIRS_LIST}"
76+
>&2 echo "${DEB_HIER}"
77+
78+
>&2 printf "Create rootfs working directory: "
79+
WORK_DIR="$(mktemp --directory --tmpdir boot-image-rootfs.XXX)"
80+
printf "%s\0" "${WORK_DIR}" >> "${TEMP_DIRS_LIST}"
81+
>&2 echo "${WORK_DIR}"
82+
83+
latest_pkg_dir() {
84+
echo /var/cache/rpi-package-download@"$(systemd-escape "${1}")".service/latest
85+
}
86+
87+
>&2 echo "Extracting package contents"
88+
dpkg-deb --raw-extract "$(latest_pkg_dir "$KERNEL_2711")/package.deb" "${WORK_DIR}"
89+
90+
get_dctrl_field() {
91+
grep-dctrl \
92+
--field=Package \
93+
--exact-match "${2}" \
94+
--no-field-names \
95+
--show-field="${3}" \
96+
"${1}"
97+
}
98+
99+
# Determine package version for later reuse
100+
KERNEL_2711_VERSION="$(get_dctrl_field "${WORK_DIR}/DEBIAN/control" "${KERNEL_2711}" Version)"
101+
>&2 echo "Extracted ${KERNEL_2711}, version ${KERNEL_2711_VERSION}"
102+
103+
# rootfs kernel modules
104+
>&2 echo "Copy kernel modules into deb package"
105+
mkdir -p "${DEB_HIER}/lib/modules"
106+
rsync -crt "${WORK_DIR}/lib/modules/"* "${DEB_HIER}/lib/modules"
107+
108+
>&2 printf "Create ramdisk working directory: "
109+
BFS_DIR="$(mktemp --directory --tmpdir boot-image-bootfs.XXX)"
110+
printf "%s\0" "${BFS_DIR}" >> "${TEMP_DIRS_LIST}"
111+
>&2 echo "${BFS_DIR}"
112+
113+
# Kernel Images
114+
>&2 echo "Copy kernel to ramdisk"
115+
cp "${WORK_DIR}/boot/vmlinuz-${LINUX_IMAGE}" "${BFS_DIR}/zImage"
116+
117+
# Overlays
118+
>&2 echo "Copy overlays to ramdisk"
119+
OVERLAY_PATH="${WORK_DIR}/usr/lib/${KERNEL_2711}/overlays"
120+
rsync -crt "${OVERLAY_PATH}"/*.dtb* "${OVERLAY_PATH}/README" "${BFS_DIR}/overlays"
121+
122+
# DTBs
123+
>&2 echo "Copy DTBs to ramdisk"
124+
DTB_PATH="${WORK_DIR}/usr/lib/${KERNEL_2711}/broadcom"
125+
rsync -crt "${DTB_PATH}"/bcm27*.dtb "${BFS_DIR}"
126+
127+
# Insert an initramfs
128+
>&2 echo "Add cryptoot initramfs to ramdisk (with necessary kernel modules)"
129+
INITRAMFS_EXTRACT="$(mktemp --directory --tmpdir initramfs-extract.XXX)"
130+
printf "%s\0" "${INITRAMFS_EXTRACT}" >> "${TEMP_DIRS_LIST}"
131+
zstd -q -d "$(get_cryptroot)" -o "${INITRAMFS_EXTRACT}/initramfs.cpio"
132+
mkdir -p "${INITRAMFS_EXTRACT}/initramfs"
133+
cd "${INITRAMFS_EXTRACT}/initramfs"
134+
RETURN_DIR="${OLDPWD}"
135+
cpio --quiet -id < ../initramfs.cpio > /dev/null
136+
rm ../initramfs.cpio
137+
cd "${WORK_DIR}"
138+
find lib/modules \
139+
\( \
140+
-name 'dm-mod.*' \
141+
-o \
142+
-name 'dm-crypt.*' \
143+
-o \
144+
-name 'af_alg.*' \
145+
-o \
146+
-name 'algif_skcipher.*' \
147+
-o \
148+
-name 'libaes.*' \
149+
-o \
150+
-name 'aes_generic.*' \
151+
-o \
152+
-name 'aes-arm64.*' \
153+
\) \
154+
-exec cp -r --parents "{}" "${INITRAMFS_EXTRACT}/initramfs/usr/" \;
155+
cd -
156+
find . -print0 | cpio --quiet --null -ov --format=newc > ../initramfs.cpio 2> /dev/null
157+
cd "${RETURN_DIR}"
158+
unset RETURN_DIR
159+
zstd -q -6 "${INITRAMFS_EXTRACT}/initramfs.cpio" -o "${BFS_DIR}/rootfs.cpio.zst"
160+
161+
# raspi-firmware
162+
>&2 echo "Downloading raspi-firmware"
163+
download_package raspi-firmware
164+
165+
>&2 printf "Create temp directory to extract firmware: "
166+
FW_EXTRACT_DIR="$(mktemp --directory --tmpdir boot-image-firmware.XXX)"
167+
printf "%s\0" "${FW_EXTRACT_DIR}" >> "${TEMP_DIRS_LIST}"
168+
>&2 echo "${FW_EXTRACT_DIR}"
169+
170+
>&2 echo "Extracting firmware package contents"
171+
dpkg-deb --raw-extract "$(latest_pkg_dir raspi-firmware)/package.deb" "${FW_EXTRACT_DIR}"
172+
173+
>&2 echo "Add firmware to ramdisk"
174+
rsync -v -crt "${FW_EXTRACT_DIR}/usr/lib/raspi-firmware/" "${BFS_DIR}"
175+
176+
# cmdline.txt
177+
>&2 echo "Add cmdline.txt to ramdisk"
178+
cp "$(get_ramdisk_cmdline_file)" "${BFS_DIR}/cmdline.txt"
179+
180+
# Inner config.txt
181+
>&2 echo "Add config.txt to ramdisk"
182+
cp "$(get_internal_config_file)" "${BFS_DIR}/config.txt"
183+
184+
# Invoke make-boot-image
185+
>&2 echo "Finalise ramdisk in deb package (boot.img)"
186+
mkdir -p "${DEB_HIER}/boot/firmware"
187+
make-boot-image \
188+
-b "pi${RPI_DEVICE_FAMILY}" \
189+
-d "${BFS_DIR}" \
190+
-o "${DEB_HIER}/boot/firmware/boot.img" > /dev/null
191+
192+
# Outer config.txt
193+
>&2 echo "Add config.txt to deb package (ensure boot.img is used)"
194+
cp "$(get_fastboot_config_file)" "${DEB_HIER}/boot/firmware/config.txt"
195+
196+
# boot.sig generation
197+
>&2 echo "Signing ramdisk image"
198+
sha256sum "${DEB_HIER}/boot/firmware/boot.img" | awk '{print $1}' > "${DEB_HIER}/boot/firmware/boot.sig"
199+
printf "rsa2048: " >> "${DEB_HIER}/boot/firmware/boot.sig"
200+
# shellcheck disable=SC2046
201+
${OPENSSL} dgst \
202+
-sign $(get_signing_directives) \
203+
-keyform PEM \
204+
-sha256 \
205+
"${DEB_HIER}/boot/firmware/boot.img" \
206+
| xxd -c 4096 -p >> "${DEB_HIER}/boot/firmware/boot.sig"
207+
208+
# Insert control file
209+
mkdir "${DEB_HIER}/DEBIAN"
210+
echo \
211+
"Package: ${PACKAGE_NAME}
212+
Source: linux
213+
Version: ${KERNEL_2711_VERSION}
214+
Architecture: arm64
215+
Maintainer: ${BOOT_IMAGE_MAINTAINER}
216+
Section: kernel
217+
Priority: optional
218+
Homepage: https://github.com/raspberrypi/linux/
219+
Provides: ${KERNEL_2711}
220+
Conflicts: ${KERNEL_2711}
221+
Replaces: ${KERNEL_2711}
222+
Description: Repackaged ${KERNEL_2711} for signed/cryptroot boot" \
223+
> "${DEB_HIER}/DEBIAN/control"
224+
225+
# Insert preinst script to remove /boot/firmware/config.txt (otherwise dpkg
226+
# attempt to create a ".dpkg-tmp" hardlink.
227+
cp "$(get_bootimg_preinst_file)" "${DEB_HIER}/DEBIAN/preinst"
228+
229+
# Create Debian package
230+
dpkg-deb --build "${DEB_HIER}" "${CACHE_DIRECTORY}"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[Unit]
2+
Description=Creates a signed boot image using a Raspberry Pi OS kernel / bootloader
3+
4+
[Service]
5+
Type=oneshot
6+
ExecStart=/usr/local/bin/make-boot-image-from-kernel "%I"
7+
CacheDirectory=%n
8+
9+
[Install]
10+
WantedBy=multi-user.target

0 commit comments

Comments
 (0)