# 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 .raw -o .ssh.raw -k .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 ```sh # 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): ```sh ./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 |