diff --git a/rpi/mass-storage-gadget64/.gitignore b/rpi/mass-storage-gadget64/.gitignore new file mode 100644 index 0000000..54e00b0 --- /dev/null +++ b/rpi/mass-storage-gadget64/.gitignore @@ -0,0 +1,3 @@ +*.h +boot.sig +bootfiles.original.bin diff --git a/rpi/mass-storage-gadget64/README.md b/rpi/mass-storage-gadget64/README.md new file mode 100644 index 0000000..b58f5f6 --- /dev/null +++ b/rpi/mass-storage-gadget64/README.md @@ -0,0 +1,58 @@ +# USB mass-storage gadget for BCM2711 and BCM2712 + +This directory provides a bootloader image that loads a Linux +initramfs that exports common block devices (EMMC, NVMe) as +USB mass storage devices using the Linux gadget-fs drivers. + +This allows Raspberry Pi Imager to be run on the host computer +and write OS images to the Raspberry Pi or Compute Module block devices. + +## Running +To run load the USB MSD device drivers via RPIBOOT run +```bash +rpiboot -d mass-storage-gadget64 + +``` + +### Debug +The mass-storage-gadget image automatically enables a UART console for debugging (user `root` empty password). + +## Secure boot +Once secure-boot has been enable the OS `boot.img` file must be signed with the customer private key. +On Pi5 firmware must also be counter-signed with this key. + +The `sign.sh` script wraps the command do this on Pi4 and Pi5. +```bash +KEY_FILE=$HOME/private.pem +./sign.sh ${KEY_FILE} +``` +or as follows if using a HSM wrapper script. +```bash +./sign.sh -H hsm-wrapper public.pem +``` + +WARNING: The signed images will not be bootable on a Pi5 without secure-boot enabled. Run `./reset.sh` to reset the signed images to the default unsigned state. + +## Source code +The buildroot configuration and supporting patches is available on +the [mass-storage-gadget64](https://github.com/raspberrypi/buildroot/tree/mass-storage-gadget64) +branch of the Raspberry Pi [buildroot](https://github.com/raspberrypi/buildroot) repo. + +### Building + +In order to build directly on a Linux host that has the needed dependencies, run: +```bash +git clone --branch mass-storage-gadget64 git@github.com:raspberrypi/buildroot.git +cd buildroot +make raspberrypi64-mass-storage-gadget_defconfig +make +``` + +The output is written to `output/images/sdcard.img` and can be copied to `boot.img` + +Alternatively, if you have docker installed and would like to use the upstream buildroot CI docker image for a build environment, use its `utils/docker-run` helper script: +```bash +$ git clone --branch mass-storage-gadget64 git@github.com:raspberrypi/buildroot.git +$ cd buildroot +$ ./utils/docker-run /bin/bash -c "make raspberrypi64-mass-storage-gadget_defconfig && make" +``` diff --git a/rpi/mass-storage-gadget64/boot.img b/rpi/mass-storage-gadget64/boot.img new file mode 100644 index 0000000..efaeea6 Binary files /dev/null and b/rpi/mass-storage-gadget64/boot.img differ diff --git a/rpi/mass-storage-gadget64/bootfiles.bin b/rpi/mass-storage-gadget64/bootfiles.bin new file mode 100644 index 0000000..7035a17 Binary files /dev/null and b/rpi/mass-storage-gadget64/bootfiles.bin differ diff --git a/rpi/mass-storage-gadget64/config.txt b/rpi/mass-storage-gadget64/config.txt new file mode 100644 index 0000000..1089df3 --- /dev/null +++ b/rpi/mass-storage-gadget64/config.txt @@ -0,0 +1,5 @@ +# Load boot.img containing the Linux initramfs for the mass-storage-gadget +# In signed-boot or secure-boot mode the bootloader checks the +# RSA signature of the ramdisk. The signature is located in boot.sig +boot_ramdisk=1 +uart_2ndstage=1 diff --git a/rpi/mass-storage-gadget64/reset.sh b/rpi/mass-storage-gadget64/reset.sh new file mode 100755 index 0000000..f18e629 --- /dev/null +++ b/rpi/mass-storage-gadget64/reset.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +rm -f boot.sig bootfiles.bin bootfiles.original.bin +ln -sf ../firmware/bootfiles.bin . diff --git a/rpi/mass-storage-gadget64/sign.sh b/rpi/mass-storage-gadget64/sign.sh new file mode 100755 index 0000000..cc948b2 --- /dev/null +++ b/rpi/mass-storage-gadget64/sign.sh @@ -0,0 +1,71 @@ +#!/bin/sh + +set -e +set -u +script_dir="$(cd "$(dirname "$0")" && pwd)" + +TMP_DIR="" +SIGN_ARGS="" +PUBLIC_KEY="" + +die() { + echo "$@" >&2 + exit 1 +} + +cleanup() { + if [ -d "${TMP_DIR}" ]; then rm -rf "${TMP_DIR}"; fi +} + +sign_firmware_blob() { + echo "Signing firmware in ${1} using $(which rpi-sign-bootcode)" + rpi-sign-bootcode \ + -c 2712 \ + -i "${1}" \ + -o "${2}" \ + -n 16 \ + -v 0 \ + ${SIGN_ARGS} ${PUBLIC_KEY} +} + +sign_bootfiles() { + echo "Signing OS image ${1}" + input="${1}" + output="${2}" + ( + cd "${TMP_DIR}" + tar -xf "${input}" + echo "Signing 2712/bootcode5.bin" + sign_firmware_blob 2712/bootcode5.bin 2712/bootcode5.bin.signed || die "Failed to sign bootcode5.bin" + mv -f "2712/bootcode5.bin.signed" "2712/bootcode5.bin" + tar -cf "${output}" * + find . + ) +} + +trap cleanup EXIT + +if [ "${1}" = "-H" ]; then + HSM_WRAPPER="${2}" + PUBLIC_KEY="${3}" + [ -f "${PUBLIC_KEY}" ] || die "HSM requires a public key file in PEM format. Public key \"${PUBLIC_KEY}\" not found." + PUBLIC_KEY="-p ${3}" + if ! command -v "${HSM_WRAPPER}"; then + die "HSM wrapper script \"${HSM_WRAPPER}\" not found" + fi + SIGN_ARGS="-H ${HSM_WRAPPER}" +else + KEY_FILE="${1}" + [ -f "${KEY_FILE}" ] || die "KEY_FILE: ${KEY_FILE} not found" + SIGN_ARGS="-k ${KEY_FILE}" +fi + +PATH="${script_dir}/../tools:${PATH}" +KEY_FILE="${1}" +TMP_DIR="$(mktemp -d)" +rm -f bootfiles.bin +ln -sf ../firmware/bootfiles.bin bootfiles.original.bin +sign_bootfiles "$(pwd)/bootfiles.original.bin" "$(pwd)/bootfiles.bin" + +echo "Signing boot.img with ${SIGN_ARGS}" +rpi-eeprom-digest -i boot.img -o boot.sig ${SIGN_ARGS} diff --git a/rpi/rpiboot b/rpi/rpiboot new file mode 100755 index 0000000..819c101 Binary files /dev/null and b/rpi/rpiboot differ diff --git a/rpi/write_to b/rpi/write_from_mac similarity index 100% rename from rpi/write_to rename to rpi/write_from_mac diff --git a/rpi/write_from_rpi b/rpi/write_from_rpi new file mode 100755 index 0000000..cf94d68 --- /dev/null +++ b/rpi/write_from_rpi @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +import sys +import os +import time +import re +from select import select +from glob import glob +from subprocess import check_output, STDOUT, PIPE, Popen, TimeoutExpired + +IMAGES_HOST = 'l_samenv@linse-c' +IMAGES_DIR = 'boxes/rpi_images' +IMAGES_PAT = '*.lz4' + +extdisks = {} + + +def prt(text): + print(time.strftime('%H:%M:%S'), text) + + +def cmd(command, verbose=False): + if verbose: + print(f'> {command}') + return check_output(command.split(), stderr=STDOUT).decode('latin-1') + + +def get_disks(): + return [line for line in cmd('lsblk -d').split('\n') if line] + + +def rpiboot(): + lines = [] + with Popen(['sudo', './rpiboot', '-d', 'mass-storage-gadget64'], stdout=PIPE) as p: + tmo = 1 + t = 0 + while select([p.stdout], [], [], tmo)[0]: + rawline = p.stdout.readline() + if not rawline: + return tmo != 1 + line = rawline.decode('latin-1').strip() + print(line) + if lines is None: + pass # print(line) + elif 'Waiting' in line: + tmo = 10 + lines.append(line) + t = time.time() + elif tmo == 1: + lines.append(line) + else: + print('\n'.join(lines)) + print(line) + lines = None + else: + p.terminate() + return False + + +def list_external(): + print('--- external disks:') + for disk, info in extdisks.items(): + print(disk, info) + + +rescue = """ +Remark: not all cables might work. +Example on Markus Mac: +- does not work: black USB C to USB C cable +- works: white USB A to USB C cable plugged into docking station. + +Please unpower the box with compute module, +start this script again and put power immediately.""" + + +def wait_disks(prevset, delay): + for i in range(int(delay)): + disks = get_disks() + diskset = set(disks) + if diskset != prevset: + print('\n'.join(disks)) + if i: + print('') + return diskset - prevset + print('.', end='') + time.sleep(1) + return None + + +def find_raspi(kind='emmc'): + input('make sure device is not connected [confirm]') + prevset = set(get_disks()) + input('now connect [confirm]') + disks = wait_disks(prevset, 5) + if not disks: + print('can not yet detect disk, try to run rpiboot') + print('if is blocked at "Waiting for BCM...", turn off and on power of target rpi') + # cmd('sudo ./rpiboot -d mass-storage-gadget64') + rpiboot() + disks = wait_disks(prevset, 15) + if not disks: + raise RuntimeError('raspi disk does not appear') + return disks + + +def write_image(kind): + proposed = [v.split()[0] for v in find_raspi(kind)] + list_external() + if len(proposed) == 1: + dev = proposed[0] + elif len(proposed) > 1: + if kind == 'emmc': + print('several potential devices:', proposed) + return + dev = input('select device to write to {" ".join(proposed)}: ') + else: + print('not found') + return + images = check_output(['sudo', '-u', 'l_samenv', 'ssh', IMAGES_HOST, f'cd {IMAGES_DIR} ; ls {IMAGES_PAT}']).decode('latin-1').split('\n') + for file in sorted(images): + print(file) + print('') + image = input(f'select image to write to {dev} [{file}]: ') or file + prt(f'unmount {dev} ...') + print('you may be prompted for your account password for sudo') + os.system(f'sudo umount /dev/{dev}') + print('') + prt('get, uncompress and write... (takes about 15 min)') + os.system(f'ssh {IMAGES_HOST} "dd if={IMAGES_DIR}/{image} bs=512k" | lz4 -d | sudo dd of=/dev/{dev} bs=512k') + prt('unmount bootfs') + for _ in range(3): + if os.path.exists('/Volumes/bootfs'): + prt('unmount bootfs') + os.system('sudo umount bootfs') + break + time.sleep(1.0) + prt('done') + os.system(f'sudo umount /dev/{dev}') + +if len(sys.argv) == 2: + write_image(sys.argv[1]) +else: + print(""" +Usage: + +> write_to emmc # write to compute module using PoE board" +> write_to sd # write to sd card + +""")