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>
158 lines
7.8 KiB
Markdown
158 lines
7.8 KiB
Markdown
# 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
|
|
|
|
```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 |
|