diff --git a/README.md b/README.md index 4caf77f..02f560f 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. + ## Local development CLI and hardware sources are Git submodules in `cli/` and `hardware/`. 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..407c910 160000 --- a/hardware +++ b/hardware @@ -1 +1 @@ -Subproject commit d60da6b31a07f84a76fe24df27fcd398e60363ee +Subproject commit 407c9101c0066ecdfa1a1ad2750ef91947450141 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('