Files
mattlqxandClaude Code cbdbe75f00 feat: add ssh-image/ — key-only Dropbear SSH injection for Spike 2 SD images
enable-ssh.sh turns a factory raw SD image (King Kong LE 0.97) into an
SSH-enabled one: injects the static dropbear/dropbearkey/sftp-server
binaries, the SysV init script (/etc/init.d/sshd), the S30sshd boot
symlink and the caller-supplied public key into the ext3 rootfs
partition with debugfs (no mounting). The input image is never
modified, the output path must be explicit and distinct, and every
byte outside p2 is verified identical afterwards. The public key is an
explicit -k argument — nothing is baked in and private keys are
rejected. --dry-run supported; --force for non-standard p2 geometry.

bin/ is byte-identical to the deployed king_kong_le-0_97_0.ssh image.
validate-image.sh functionally proves a result (boots the image's own
init script under qemu, real ssh pubkey login, password auth refused,
sftp round-trip); build-binaries.sh rebuilds bin/ in Docker.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 09:02:32 -04:00
..

ssh-image — key-only Dropbear SSH for a Stern Spike 2 raw SD image

Turns a factory Spike 2 raw SD image (King Kong LE 0.97.0, 16 GiB) into an SSH-enabled image, reproducing the working setup documented in /Users/matt/src/pinball-assets/README.md sections 1 and 3. The original image is only ever read; the result is a new image file you choose.

enable-ssh.sh -i <factory>.raw -o <factory>.ssh.raw -k <your>.pub

No password is ever set or needed — Dropbear is compiled without password auth. The public key is an explicit input (-k); no private key is read, stored, or embedded anywhere by this tool.

What gets injected (into the ext3 rootfs partition, p2, only)

Path on the machine Source here Mode Owner
/usr/local/bin/dropbear bin/dropbear 0755 0:0
/usr/local/bin/dropbearkey bin/dropbearkey 0755 0:0
/usr/libexec/sftp-server bin/sftp-server 0755 0:0
/etc/init.d/sshd files/sshd 0755 0:0
/etc/rc5.d/S30sshd symlink → ../init.d/sshd 0777 0:0
/home/root/.ssh new directory 0700 0:0
/home/root/.ssh/authorized_keys your -k file 0600 0:0

Byte-identical to the deployed king_kong_le-0_97_0.ssh.16G.sdcard.raw (verified against it with debugfs dump + sha256 for all three binaries).

  • dropbear / dropbearkey — 2025.88, static musl armhf. Password auth compiled out (DROPBEAR_SVR_PASSWORD_AUTH 0), DROPBEAR_REEXEC=0 (the Spike 2 kernel is 3.14 and predates execveat).
  • sftp-server — static Go binary (pkg/sftp). dropbear dropped its built-in scp upstream; this makes scp/rsync work. It lives at /usr/libexec/sftp-server because that is dropbear's compile-time SFTPSERVER_PATH default — no -s flag in the init script is needed.
  • /etc/init.d/sshd — SysV init script: on start it generates Dropbear host keys (rsa/ecdsa/ed25519) into /data/dropbear if absent (host keys are per-machine, so they are generated on first boot rather than baked in), then runs dropbear on port 22.
  • /etc/rc5.d/S30sshd — boot hook; S30 runs before the game's S95game.

Everything else in the image — p1 (boot, FAT), the game partition, and every data/dump byte — is untouched and verified byte-identical after the run (cmp over both regions outside p2).

Requirements

  • macOS (Homebrew). The image work uses cp, dd, cmp, shasum — all stock.
  • brew install e2fsprogs — provides debugfs and e2fsck (1.4x).
  • python3 (system) — for MBR parsing and path resolution.
  • For validate-image.sh only: Docker (any linux container) — it runs privileged (bind mounts for /dev, /proc) and uses qemu-user-static to boot the ARM rootfs. build-binaries.sh also needs Docker plus network access (musl.cc, GitHub, codeload).

Usage

# 1. make sure you have a public key (the private half never enters this repo)
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/spike2      # if you don't have one
#    use ~/.ssh/spike2.pub as the -k input

# 2. dry run: parses the MBR, inspects p2, prints the exact plan, writes nothing
./enable-ssh.sh -i ~/images/king_kong_le-0_97_0.Release.16G.sdcard.raw \
    -o /tmp/king_kong_le-0_97_0.ssh.16G.sdcard.raw \
    -k ~/.ssh/spike2.pub --dry-run

# 3. real run (APFS clone is instant; a full 16 GiB copy on other volumes takes a while)
./enable-ssh.sh -i ~/images/king_kong_le-0_97_0.Release.16G.sdcard.raw \
    -o ~/images/king_kong_le-0_97_0.ssh.16G.sdcard.raw \
    -k ~/.ssh/spike2.pub

# 4. optional deep check before flashing (Docker, qemu-arm, real ssh login)
./validate-image.sh -i ~/images/king_kong_le-0_97_0.ssh.16G.sdcard.raw \
    -k ~/.ssh/spike2.pub

Options: -b DIR / -f DIR point at alternative binary / init-script directories (defaults are ./bin and ./files beside the script); --force accepts a p2 geometry other than the known King Kong layout (12 MiB offset, 336 MiB size — the script parses the MBR, it does not assume); --overwrite replaces an existing output file (refused by default); --dry-run writes nothing.

The script refuses to run if: input/output are the same file, the output already exists, the key file contains a private key or unrecognised lines, the image has no MBR / no ext3 rootfs partition, or the rootfs fails a read-only e2fsck -fn before or after injection.

Safety model

  • The input image is opened read-only and is never written; its size is re-checked at the end.
  • The output is created fresh (cp -c APFS clone when possible, else full copy) and must be a different path from the input.
  • Only one region is ever written back: p2, exactly skip/count in 512-byte sectors. Afterwards cmp proves every byte before and after p2 equals the input — including wherever the data/dump partitions live.
  • Host keys are generated on the machine at first boot (/data/dropbear), never baked into the image.
  • Before flashing over a card that is in use: back up its data partition (scores/settings/NVM). The image's /data is factory-empty; flashing it over a live card erases that data. (The original setup lost it once — restored from a pre-SSH backup via tar-over-ssh.)
  • Flash to a full-size SD card in the machine's mmcblk0 slot. Never flash to the machine's live SD while it is the boot device.

Validation

enable-ssh.sh self-verifies every run (filesystem health before/after, presence + mode + owner of all injected objects, sha256 of the three binaries inside the result, and the cmp preservation proof).

validate-image.sh goes further: it extracts the output image's p2 into a scratch rootfs tree (debugfs rdump, no loop device needed) in a privileged Docker container, starts dropbear via the image's own /etc/init.d/sshd (first-boot path: the image's dropbearkey generates the host keys into /data/dropbear), and then from the container (a) logs in as root over ssh with a throwaway ed25519 key generated inside the container (your private key is never read or stored), (b) confirms password auth is refused, and (c) runs sftp put/get to prove the sftp-server wiring. It also statically checks that the deployed authorized_keys in the image equals the -k file you gave enable-ssh.sh. The image file is only ever read; all test mutations happen on a scratch copy of p2.

Building the binaries (provenance and rebuild)

bin/dropbear, bin/dropbearkey and bin/sftp-server were built once, on 2026-10-11, in the pinball-assets/.ssh-build/ tree:

binary build notes
dropbear, dropbearkey dropbear 2025.88, ./configure --host=arm-linux-musleabihf --disable-zlib --enable-static, localoptions.h: DROPBEAR_SVR_PASSWORD_AUTH 0, DROPBEAR_CLI_PASSWORD_AUTH 0, DROPBEAR_SVR_PUBKEY_AUTH 1, DROPBEAR_REEXEC 0 musl.cc arm-linux-musleabihf cross toolchain; Debian 12 container
sftp-server Go 1.19+ in GOPATH mode from codeload tarballs (pkg/sftp v1.13.2, kr/fs v0.1.0, golang.org/x/crypto v0.7.0, x/sys v0.7.0), GOARCH=arm GOARM=7 CGO_ENABLED=0, go build -trimpath -ldflags="-s -w" static, stripped

To rebuild them from this directory (requires Docker):

./build-binaries.sh     # rewrites bin/ and prints file(1) + sha256 for each

The originals live on at pinball-assets/.ssh-build/out-musl/{dropbear,dropbearkey} and pinball-assets/.ssh-build/out-sftp/sftp-server with the build scripts build.sh (dropbear) and build-sftp-go.sh (sftp).

Files

Path What
enable-ssh.sh the injection tool (input image + public key → output image)
validate-image.sh Docker/qemu functional test of an output image (optional)
build-binaries.sh rebuilds bin/ in a Docker container (optional)
files/sshd the SysV init script injected to /etc/init.d/sshd
bin/dropbear, bin/dropbearkey, bin/sftp-server the static armhf binaries injected into the image