From 6a1ee76731e62fcd123f0345a88f768e812d8974 Mon Sep 17 00:00:00 2001 From: Shayne Legault <8741557+shaynelegault@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:14:34 -0600 Subject: [PATCH 1/3] Compose private ARMv7 P105 recovery roots --- README.md | 4 + p105/Containerfile | 11 +++ p105/README.md | 26 ++++++ p105/build-base.py | 33 +++++++ p105/prepare-recovery.py | 182 ++++++++++++++++++++++++++++++++++++ tests/test_p105_recovery.py | 56 +++++++++++ 6 files changed, 312 insertions(+) create mode 100644 p105/Containerfile create mode 100644 p105/README.md create mode 100644 p105/build-base.py create mode 100644 p105/prepare-recovery.py create mode 100644 tests/test_p105_recovery.py diff --git a/README.md b/README.md index dc7b9f4..dbfe7de 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ A small Alpine-based Linux image for Apple devices, demonstrating the Orchard re Currently supports iPad 6 Wi-Fi (`iPad7,5`, J71b), experimentally. Boot is tethered and requires a host computer. This is a development/reference image, not a general-purpose tablet distribution. +The [P105 recovery composer](p105/README.md) adds a private ARMv7 RAM environment +for the original iPad mini Wi-Fi. It works with hardware 0.1.11 and the native +Orchard CLI adapter for tethered boot and handoff to an existing NFS desktop. + ## Build and validate Follow [BUILD.md](BUILD.md) to set up a repository-local environment, build public components, assemble a coherent root/recovery release and run isolated tests. [Boot presentation](boot/README.md) describes the Alpine artwork and loader producers. diff --git a/p105/Containerfile b/p105/Containerfile new file mode 100644 index 0000000..41c94b1 --- /dev/null +++ b/p105/Containerfile @@ -0,0 +1,11 @@ +# Native ARM64 apk verifies ARMv7 packages with the official ARMv7 signing keys. +FROM docker.io/library/alpine@sha256:6db0aa37b0f1ba86c31102b0972cb082a1d22ed1390440de84f1076d78d4d399 AS armkeys +FROM docker.io/library/alpine@sha256:e311ae748be7f56ae5cba304bc488f0786df68ee507513f654a5cc8597cffb42 +COPY --from=armkeys /etc/apk/keys /armroot/etc/apk/keys +COPY --from=armkeys /etc/apk/keys /etc/apk/keys +RUN apk --root /armroot --arch armv7 --initdb --no-scripts \ + --keys-dir /etc/apk/keys --repositories-file /etc/apk/repositories \ + add busybox musl python3 dropbear nfs-utils \ + && mkdir -p /armroot/bin \ + && cp /etc/apk/repositories /armroot/etc/apk/repositories \ + && cp -a /etc/apk/keys /armroot/etc/apk/keys diff --git a/p105/README.md b/p105/README.md new file mode 100644 index 0000000..77bd498 --- /dev/null +++ b/p105/README.md @@ -0,0 +1,26 @@ +# P105 ARMv7 RAM recovery + +`build-base.py` obtains an Alpine ARMv7 base through the signed package indexes. +Its pinned ARM64 container runs the package manager; the official ARMv7 image +supplies the target signing keys. Target programs are extracted without running +package scripts. The receipt records the resulting image and archive hashes. + +`prepare-recovery.py` composes a private root from that archive, an explicit +owner initramfs and its matching MtCl capture. Supply the verified target JSON +with `udid` and `ecid`. The result records the selected ECID for Orchard's USB +identity and the device protocol. Firmware and calibration remain private. + +The composer clears inherited SSH keys. Recovery generates its diagnostic key +after a host provisions access through the physical serial console. A small +closure of the ARMv7 SSH executable and its libraries can travel in RAM through +the NFS pivot, preserving its pinned key across userspaces. + +Pass the composed root to the hardware package's `kernel/p105/build` as +`--private-initramfs`, with the separate `--calibration` input. That producer +installs the recovery init and selects its kernel USB settings while retaining +the P105 flash fence. The companion CLI's `docs/p105.md` describes private +manifest assembly and the native boot workflow. + +The NFS root needs the hardware package's P105 systemd runtime and Python 3.9 +or later. It can contain the preserved Arch / Omarchy A desktop during this +phase. The ARMv7 desktop release builder remains a later contribution. diff --git a/p105/build-base.py b/p105/build-base.py new file mode 100644 index 0000000..a63c999 --- /dev/null +++ b/p105/build-base.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Export signed Alpine ARMv7 packages for the P105 RAM recovery.""" +import argparse +import hashlib +import json +from pathlib import Path +import subprocess + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + output = args.output.absolute() + receipt = output.with_suffix('.json') + if output.exists() or output.is_symlink() or receipt.exists(): + parser.error('Choose a fresh base output') + output.parent.mkdir(parents=True, exist_ok=True) + recipe = Path(__file__).with_name('Containerfile') + tag = 'localhost/orchard-p105-recovery-base:alpine' + subprocess.run(['podman', 'build', '-t', tag, '-f', str(recipe), str(recipe.parent)], check=True) + image = subprocess.check_output(['podman', 'image', 'inspect', tag, '--format', '{{.Id}}'], text=True).strip() + with output.open('xb') as stream: + subprocess.run(['podman', 'run', '--rm', '--network', 'none', image, + 'tar', '-C', '/armroot', '-cf', '-', '.'], stdout=stream, check=True) + receipt.write_text(json.dumps({'schema': 1, 'architecture': 'armv7h', + 'signed_apk': True, 'image': image, 'bytes': output.stat().st_size, + 'sha256': hashlib.sha256(output.read_bytes()).hexdigest(), + 'recipe_sha256': hashlib.sha256(recipe.read_bytes()).hexdigest()}, indent=2)+'\n') + + +if __name__ == '__main__': + main() diff --git a/p105/prepare-recovery.py b/p105/prepare-recovery.py new file mode 100644 index 0000000..9feba79 --- /dev/null +++ b/p105/prepare-recovery.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Compose an owner-bound ARMv7 recovery root from signed Alpine packages.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import struct +import tarfile + +import orchard_hardware + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def extract(archive, root): + """Normalize guest links, and refuse archive writes through outside paths.""" + with tarfile.open(archive) as stream: + for member in stream: + name = member.name.removeprefix('./') + if name in ('', '.'): + continue + if name.startswith('/') or '..' in Path(name).parts: + raise ValueError('Unsafe base archive path') + path = root/name + if not path.parent.resolve().is_relative_to(root.resolve()): + raise ValueError('Base archive parent escapes its root') + path.parent.mkdir(parents=True, exist_ok=True) + if member.isdir(): + path.mkdir(exist_ok=True) + elif member.issym(): + target = root/member.linkname.lstrip('/') if member.linkname.startswith('/') else path.parent/member.linkname + if not target.resolve().is_relative_to(root.resolve()): + raise ValueError('Base archive link escapes its root') + if path.is_dir() and not path.is_symlink(): + raise ValueError('Base archive would replace a directory with a link') + path.unlink(missing_ok=True) + path.symlink_to(os.path.relpath(target, path.parent)) + elif member.isfile(): + if path.is_symlink(): + path.unlink() + with stream.extractfile(member) as source, path.open('wb') as destination: + shutil.copyfileobj(source, destination) + path.chmod(member.mode & 0o777) + elif member.islnk(): + target = root/member.linkname.removeprefix('./') + if not target.resolve().is_relative_to(root.resolve()) or not target.is_file(): + raise ValueError('Unsafe base archive hard link') + path.unlink(missing_ok=True) + shutil.copyfile(target, path) + else: + raise ValueError('Special file in recovery base') + + +def needed32(data): + if data[:7] != b'\x7fELF\x01\x01\x01' or data[18:20] != b'\x28\x00': + raise ValueError('Expected ARM32 ELF') + offset = struct.unpack_from(' Date: Tue, 27 Oct 2026 20:11:18 -0600 Subject: [PATCH 2/3] Pair the P105 recovery workspace with its native CLI and hardware --- cli | 2 +- hardware | 2 +- uv.lock | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli b/cli index 4f96e58..47e6c79 160000 --- a/cli +++ b/cli @@ -1 +1 @@ -Subproject commit 4f96e5805975d00ec4b89fe4064e8c1acbb1bcc4 +Subproject commit 47e6c79a26ed77a1e6860be448d5a7d25acf3e16 diff --git a/hardware b/hardware index d60da6b..867b87b 160000 --- a/hardware +++ b/hardware @@ -1 +1 @@ -Subproject commit d60da6b31a07f84a76fe24df27fcd398e60363ee +Subproject commit 867b87beab50b88ade85194321b7c7e0b1156b1c diff --git a/uv.lock b/uv.lock index e3bfe8f..7a6ce97 100644 --- a/uv.lock +++ b/uv.lock @@ -151,7 +151,7 @@ requires-dist = [ [[package]] name = "orchard-cli" -version = "0.1.10" +version = "0.1.11" source = { editable = "cli" } dependencies = [ { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -177,7 +177,7 @@ dev = [{ name = "mypy", specifier = "==1.18.2" }] [[package]] name = "orchard-hardware" -version = "0.1.10" +version = "0.1.11" source = { editable = "hardware" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, From d936a9b55b174e5926f78672eb17d64f724247ad Mon Sep 17 00:00:00 2001 From: Shayne Legault <8741557+shaynelegault@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:29:55 -0600 Subject: [PATCH 3/3] Pin P105 hardware with preserved upstream README text --- hardware | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardware b/hardware index 867b87b..407c910 160000 --- a/hardware +++ b/hardware @@ -1 +1 @@ -Subproject commit 867b87beab50b88ade85194321b7c7e0b1156b1c +Subproject commit 407c9101c0066ecdfa1a1ad2750ef91947450141