Commit Graph

11 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
496268fa39 feat(cli): numbered selection menus for distributions and versions
Some checks failed
tests / lint (push) Has been cancelled
tests / pytest (push) Has been cancelled
tests / tor-network-e2e (push) Has been cancelled
tests / qemu-e2e-debian (push) Has been cancelled
Replace the free-text distribution/version prompts with numbered menus via a
shared _choose helper. Name-first resolution keeps typing the name working, so
numeric catalog keys (e.g. arch "4") still select by name, not by position.
Applied to the distribution, arch, manjaro, retropie and raspios choosers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:57 +02:00
Kevin Veen-Birkenbach
919651277e fix(image): keep the Tor onion reachable on RTC-less boards
An RTC-less Pi boots at 1970; Tor then rejects the consensus and the unlock
onion never publishes. Bake a clock floor (build epoch) into the initramfs and
jump the clock forward to it before Tor starts, and pass a numeric tor_ntp= in
the cmdline so busybox ntpd syncs without DNS. Proven on hardware: the onion
unlock succeeded on a Pi 3 after this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:57 +02:00
Kevin Veen-Birkenbach
0c621f60ae fix(session): mount the boot FAT at /boot/firmware on Bookworm
Raspberry Pi OS Bookworm keeps the FAT boot partition at /boot/firmware, where
the raspi-firmware post-update hook syncs the initramfs. Binding it at /boot in
the chroot sent update-initramfs's output to the ext4 root, so the firmware
booted a stock initramfs without cryptsetup -> (initramfs) emergency shell, no
LUKS prompt. boot_bind_target() detects the layout and binds (and unmounts) the
FAT at the right place; legacy single-FAT layouts still use /boot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:56 +02:00
Kevin Veen-Birkenbach
30ee1101ce feat(cli): guided encrypted-image wizard (default) + remote-unlock
Add a guided setup: one interactive command that asks everything up front then
builds an encrypted, Tor-remote-unlockable image unattended (distribution,
target device, hostname, login user + key, password), creating or renaming the
login user and installing the SSH key for both unlock and post-boot login.

- wizard.py: _collect (all prompts) + _execute (autonomous build); renames a
  stock pi/alarm user or creates one, grants sudo, installs the login key.
- unlock.py + `lim --type remote-unlock`: reach the initramfs over Tor (onion,
  torsocks) or plain SSH (host/IP), run cryptroot-unlock or the passphrase
  prompt; the wizard persists a target record under ~/.config/lim/unlocks.
- cli.py: register guided (default --type) and remote-unlock; drop the
  deprecated --extra argument.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:56 +02:00
Kevin Veen-Birkenbach
14094e9436 feat(image): auto-enable cross-arch chroot via qemu binfmt
Building a foreign-arch image (e.g. arm64 Raspberry Pi OS on an x86 host) needs
a qemu binfmt handler, or the chroot fails with "Exec format error". crossarch
detects this before the target is erased and, on confirmation, installs
qemu-user-static via the host package manager (apt-get/dnf/zypper/pacman) and
registers binfmt; otherwise it aborts with per-distro instructions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:56 +02:00
Kevin Veen-Birkenbach
5bef6177ac feat(image): flash full disk images into LUKS; add Raspberry Pi OS
Support distributions that ship a full .img (Raspberry Pi OS, moode, RetroPie,
Manjaro ARM) rather than a rootfs tarball, by loop-mounting the image and
copying its boot + root partitions into a fresh LUKS container.

- distributions.yml/catalog.py/choosers.py: raspios catalog (lite64/desktop64/
  lite32 via the stable _latest redirects) + choose_raspios.
- plan.py: source_url override so a _latest redirect downloads under an .img.xz
  name that decompress_command recognises.
- transfer.py: transfer_disk_image (loop-mount -> repartition -> LUKS -> rsync
  copy with progress, cp fallback -> fix boot fstab); transfer_image gains
  interactive= and routes encrypted non-arch images here; download_image gains
  force_prompt.
- loopimg.py: losetup attach/detach/partition helper.
- fsutil.drop_fstab_mount + register_encrypted_root: replace a stock image's
  existing / fstab line instead of colliding with it.
- encryption.configure_encryption returns the onion address.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:56 +02:00
Kevin Veen-Birkenbach
68ea39b784 chore(session): harden chroot package installs (PATH + apt-get update)
- chroot_bash exports a Debian-safe PATH so /usr/sbin tools (update-initramfs,
  useradd, chpasswd, ...) resolve inside the chroot instead of failing with
  code 127.
- install_packages runs apt-get update before install (a stock image ships
  stale lists whose superseded .deb URLs 404) and non-interactive
  apt-get install -y; pacman gains -Sy for the same index-refresh reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:56 +02:00
Kevin Veen-Birkenbach
c3a41db796 style: apply ruff format across the tree; refresh moved-module doc refs
Bring 14 files that predated the ruff-format run into line with the configured
formatter (line-length 100); provably formatting-only (ruff format of HEAD ==
working tree, and ruff format is semantics-preserving). Also refresh two
lim.image.tor._KEYGEN_SCRIPT doc references in tor_harness.py to
lim.image.initramfs.keygen after the module moved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:55 +02:00
Kevin Veen-Birkenbach
9b7a34989d feat(image): distro-agnostic remote unlock via initramfs backends + Debian support
Split the mkinitcpio-only remote-LUKS-unlock path into an InitramfsBackend
ABC with a get_backend() dispatch, and add the initramfs-tools backend for
Debian / Raspberry Pi OS.

- base.py: six-step backend contract; encryption.py becomes a thin,
  distro-neutral sequencer (get_backend by distribution).
- initramfs_tools.py: crypttab `none luks,initramfs`, cmdline rewritten to
  root=/dev/mapper + ip=::::host:eth0:dhcp, dropbear-initramfs
  authorized_keys, update-initramfs -k all (no build-host uname leak).
- shipped hooks (configuration/initramfs-tools/*): single-hop non-anonymous
  onion, libnss DNS baking, sed-not-source DHCP, kill-tor-before-pivot.
- shared offline onion keygen in keygen.py; tor.py removed (logic moved to
  mkinitcpio.py).
- raspios added to the apt distro family (session.py, raspberry.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:54 +02:00
Kevin Veen-Birkenbach
1e711f5c96 feat(image): remote LUKS unlock via a Tor onion service in the initramfs
Encrypted image setups can now bake a Tor onion service into the initramfs
so the dropbear unlock shell stays reachable behind NAT or a dynamic IP.
When the user opts in, configure_encryption installs tor + busybox, drops
the mkinitcpio hooks (ordered `netconf tor dropbear encryptssh`), generates
the v3 onion keys offline in the image chroot, and prints the stable
.onion address. Unlock with `torsocks ssh root@<onion-address>`.

The runtime hook syncs the clock via NTP first (RTC-less boards boot at
1970, which Tor's consensus checks reject) and starts the onion service
pointing at dropbear on 127.0.0.1:22.

Hardening baked in from an adversarial review of the shipped path:
- cmdline.txt boot path (RPi4-class firmware boot) now sets the same
  ip=::::<host>:eth0:dhcp net.ifnames=0 params as the boot.txt path, so
  the initramfs actually gets a network and the onion can publish.
- the initramfs bakes in libnss_dns.so.2 so the NTP hostname resolves.
- the hook extracts DHCP DNS with sed instead of sourcing the lease files,
  which would run attacker-controlled DHCP option strings as root pre-boot.
- NTP is attempted unconditionally (bounded), not gated on DHCP-provided DNS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:12:35 +02:00
Kevin Veen-Birkenbach
ccdef065df refactor!: port shell scripts to Python package
Bash scripts were untestable and duplicated device/LUKS/mount logic;
the lim/ package centralizes it behind one subprocess wrapper and a
YAML image catalog (single point of truth).

BREAKING CHANGE: scripts/*.sh removed. Use `lim --type <cmd>`; new
types mount/umount/single-boot/raid1-boot/lock/unlock/import/export
replace direct script calls. --extra is deprecated and ignored.

- distributions.yml + lim/catalog.py hold the image catalog (PyYAML)
- pytest suite: 102 tests with mocked subprocess (tests/unit) and a
  250-line max file-length guard (tests/lint)
- ruff strict (select ALL), GitHub Actions CI, Dependabot; Travis gone
- Makefile: install (symlink ~/.local/bin/lim) and test targets
- fixes over bash: SUDO_USER-aware chown, mmcblk/nvme partition paths,
  sha512 checksum support, whole-pipeline failure detection, blkid
  UUID fallback for pre-mounted images, conditional fstab seeding for
  PARTUUID/LABEL images, clean errors for missing binaries
2026-07-14 11:37:19 +02:00