mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
The documentation site, docs.forgefirm.org, is the one home for the developer documentation. BUILD.md, kas/README.md, and docs/ACCEPTANCE.md are on the site under Developers, and they are deleted here. Every reference points at the site: README.md, BRINGUP.md, UPDATE-SYSTEM.md, the kas configuration comments, release.sh, the CI workflow comments, and the releases, forgetest, and bench READMEs. BRINGUP.md: "Next work" item 16, step timing under CPU contention, is closed; the video work resolved it. The items after it are renumbered. CAMPAIGN-LOG.md records the closure. Documentation and comments only. No layer content changes, so the manifest identity of the next image does not change. No catalog consequence.
290 lines
15 KiB
Markdown
290 lines
15 KiB
Markdown
# ForgeFIRM install, update & recovery system
|
|
|
|
Design and contracts of the ForgeFIRM install/update/recovery system:
|
|
the factory's own A/B slot scheme, signed `.fw` packaging, the GUI
|
|
update manager, factory restore, and the recovery image. The system is
|
|
described in the implementation units ("phases") it is built from. The
|
|
measured ground truth this rests on lives in `BRINGUP.md` (eMMC layout,
|
|
boot0/boot1 maps, saved-env location, factory `.fw`/updater internals —
|
|
"eMMC boot & recovery architecture"), which also carries what is still
|
|
open; the bench record of each phase is in `CAMPAIGN-LOG.md`.
|
|
|
|
## Settled decisions
|
|
|
|
- **Factory partition scheme, unmodified**: ForgeFIRM lives in the two
|
|
200 MiB rootfs slots (`mmcblk2p1`/`p2`); `/data` (p3) keeps its full
|
|
factory size. No repartitioning at install, ever.
|
|
- **Single-OS, not dual-boot**: a machine runs ForgeFIRM *or* factory
|
|
firmware, with a clean migration each way. The factory updater's
|
|
behavior toward foreign slot contents is irrelevant because we never
|
|
operate both long-term.
|
|
- **fwup is the universal package/apply format** (the factory's own
|
|
mechanism): ForgeFIRM upgrades, factory restore, and provisioning all
|
|
use signed `.fw` archives applied to the inactive slot, followed by a
|
|
U-Boot env flip — exactly the factory update flow.
|
|
- **Factory firmware is archived to `/data` before any factory slot is
|
|
overwritten.** Restore-to-factory never depends on Glowforge's
|
|
servers; the cloud path (`GET /update/current`, already implemented
|
|
in gfutilities) is the optional "restore to *latest*" upgrade.
|
|
- **Release artifacts are built and signed locally**, uploaded as
|
|
GitHub releases. The Ed25519 private key never leaves the build
|
|
host, so GitHub is untrusted hosting: machines verify signatures
|
|
before applying. CI does compile checks only, never artifacts.
|
|
- **Reinstalling ForgeFIRM from factory = run the installer again**,
|
|
until the recovery refresh (Phase 5) subsumes it.
|
|
- **Recovery refresh is squashfs-only in v1**: factory U-Boot, DTB and
|
|
the 3.14.28 recovery kernel stay in place; only the recovery
|
|
userspace is replaced.
|
|
|
|
## Invariants (every flash path, every phase)
|
|
|
|
1. Never write the active (running) slot.
|
|
2. Machine idle; one flash operation at a time (lock file); no
|
|
flash/reboot while a job runs.
|
|
3. Archive factory content before the write that would destroy the
|
|
last copy of it (rootfs slots; boot0/boot1 before a recovery
|
|
refresh).
|
|
4. Env flips are atomic: one `fw_setenv -s` transaction setting all
|
|
four of `mmcdev`/`mmchwpart`/`mmcpart`/`mmcroot`.
|
|
5. Automatic paths (release updater, cloud restore) require a valid
|
|
signature — ours or Glowforge's respectively. Manual uploads may be
|
|
unsigned behind an explicit "unsigned dev image" warning.
|
|
6. The image must fit the 200 MiB slot; the build fails past the size
|
|
gate rather than producing an unflashable release.
|
|
7. Boot selection refuses targets that fail the content probe (no
|
|
kernel / no recognizable rootfs).
|
|
8. Verify a written slot (fwup on-the-fly hashes, or an explicit
|
|
readback/mount check for raw writes) before flipping boot to it.
|
|
|
|
## Phase 0 — enablers (no eMMC flashing)
|
|
|
|
- **0.1 Slot-agnostic images.** Goal: one image boots unmodified from
|
|
p1, p2, or SD, steered only by the saved env (U-Boot's `mmcargs`
|
|
already takes `root=${mmcroot}` from the env). Audit what our
|
|
`/boot/uEnv.txt` currently sets; strip it to entries that are not
|
|
per-location (`fdt_file` etc.); bench-verify by flipping env alone.
|
|
This removes the mount-and-sed step from every flash path.
|
|
*Exit: the same built image boots from two locations with no
|
|
per-slot edit.*
|
|
- **0.2 fwup toolchain + keys.** Yocto recipe for fwup (target) and a
|
|
host-side pack step. Generate the ForgeFIRM Ed25519 keypair
|
|
(custody: offline on the build host, passphrase-protected, backed
|
|
up). Compatibility gates, both directions: (a) a `.fw` we pack must
|
|
apply with the **factory's** fwup 0.14.2 (the installer runs on
|
|
factory firmware; fall back to shipping a static armv7 fwup with the
|
|
installer if archive-format drift bites), and (b) our shipped fwup
|
|
must apply a **factory** `.fw` verified against the GF pubkeys
|
|
(carried from the factory image) for cloud restore.
|
|
- **0.3 Build outputs.** `forgefirm-image` additionally emits the raw
|
|
ext4 rootfs and a packed+signed `forgefirm-<ver>.fw` with
|
|
`upgrade.a` / `upgrade.b` tasks in the factory pattern
|
|
(partition-relative raw writes, unmounted-destination +
|
|
on-the-fly-verify options); size gate enforced here. A `complete`
|
|
full-provisioning task joins with the Phase 5 recovery work. The wic
|
|
stays for SD/dev burns.
|
|
|
|
## Phase 1 — ffboot v2 + slot probe
|
|
|
|
- Atomic env flip (invariant 4) — fixes the existing gaps: three
|
|
separate `fw_setenv` calls today, and `mmchwpart` never set (relies
|
|
on the saved 0).
|
|
- `ffboot -l` (or a sibling tool): inventory every candidate — eMMC
|
|
p1/p2, legacy p4, SD — by read-only mount: factory `/etc/version` or
|
|
`/etc/forgefirm-version`, kernel presence; plus the current env
|
|
selection. Machine-parsable output; this is the probe the GUI and
|
|
the installer both reuse.
|
|
- *Exit: bench-verified flips SD ↔ eMMC slots; inventory correct for
|
|
factory / ForgeFIRM / empty slots.*
|
|
|
|
## Phase 2 — slot installer (factory → ForgeFIRM)
|
|
|
|
Rewrite `install-forgefirm.sh` as a **single-stage** script run from
|
|
factory firmware:
|
|
|
|
1. Sanity: factory 3-partition layout, both slots 200 MiB, active slot
|
|
detected (`rdev`), enough `/data` space.
|
|
2. Archive: **every factory slot version** not already archived —
|
|
`dd | gzip` to `/data/forgefirm/archive/factory-rootfs-<ver>.img.gz`
|
|
with a manifest line (slot, version, date, md5); also dump
|
|
boot0/boot1 (32 MiB) into the archive now, ahead of Phase 5. With
|
|
both slots archived, any later overwrite needs no second archive
|
|
step.
|
|
3. Fetch `forgefirm.fw` from GitHub releases (fixed asset name — the
|
|
`releases/latest/download/` URL needs one; the version lives in the
|
|
fwup metadata and the release tag), or take a local file argument
|
|
for offline/dev installs. Verify the signature against the
|
|
ForgeFIRM pubkey embedded in the installer (raw 32-byte form for
|
|
the factory's fwup; a dev key until the production ceremony).
|
|
4. Apply to the **inactive** slot (fwup + our pubkey). The booted
|
|
factory install stays bootable in the other slot.
|
|
5. Atomic env flip (embed the flip logic — the factory rootfs has no
|
|
ffboot v2), reboot.
|
|
|
|
No repartitioning, no `/data` backup/restore dance, no stage 2.
|
|
Rewrite `INSTALL.md` accordingly (serial console procedure stays).
|
|
|
|
*Exit: a factory machine converts in one pass; `ffboot` returns it to
|
|
the intact factory slot; `/data` (calibration, credentials, logs)
|
|
demonstrably untouched.*
|
|
|
|
## Phase 2b — legacy p4 migration
|
|
|
|
- Boot-time init script (before `/data` mounts), gated on: booted from
|
|
`mmcblk2p1`/`p2` (never SD, never p4) AND legacy geometry present
|
|
(p4 exists, or p3 ends short of the disk). Actions: delete p4,
|
|
extend p3's end to the disk (starts unchanged), `resize2fs`.
|
|
Idempotent and power-safe: every step keyed off actual disk state,
|
|
re-runnable after interruption.
|
|
- Existing p4 users reach the new scheme by running the new installer
|
|
from their running ForgeFIRM (same flow as Phase 2; both factory
|
|
slots intact → archive newer, overwrite older), then the boot-time
|
|
check reclaims p4/p3 on first slot boot.
|
|
- *Exit: a legacy-layout machine migrates with `/data` contents intact
|
|
and grown to full size; re-boot is a no-op.*
|
|
|
|
## Phase 3 — release pipeline
|
|
|
|
- `scripts/release.sh` (build host): gates → kas build → pack `.fw` →
|
|
sign → `sha256sums.txt` → staged assets + `gh release create`
|
|
command (`--publish` runs it where gh is authenticated). Gates:
|
|
clean tree, version single-source, rootfs-vs-slot size
|
|
(warn ≥ 170 MiB / fail ≥ 195 MiB, under bitbake's own hard cap),
|
|
**installer-embedded pubkey must match the signing key**,
|
|
factory-era fwup (0.14.2) verification of the packed archive, and the
|
|
**release acceptance gate**: `releases/v<version>/acceptance.json`
|
|
(exported by forgetest on the bench) must authorize the built rootfs
|
|
per the site (Developers, "Acceptance") - the gate recomputes every catalog test's
|
|
domain fingerprint from `/etc/forgefirm-manifest.json` inside the
|
|
release ext4. The artifact is attached to the GitHub release.
|
|
- One version source: `FORGEFIRM_RELEASE` = git tag =
|
|
`/etc/forgefirm-version` = `.fw` meta-version; the script enforces
|
|
agreement.
|
|
- Cloud-mode compatibility baseline: the cloud client's connect-time
|
|
probe records `{latest_gf_version, tested_against_gf}` to
|
|
`/data/forgefirm/gf-latest.json`, and forgectrl's panel warns when
|
|
the live Glowforge service has moved past the tested version (cloud
|
|
mode may break). `tested_against_gf` is the cloud client's configured
|
|
firmware version (`FACTORY_FIRMWARE.FW_VERSION`, the same value it
|
|
advertises to the service); it is **not** release metadata — neither
|
|
`release.sh` nor the `.fw` meta carries such a field.
|
|
- `release.sh --dev` packs a **dev-key-signed** `forgefirm-dev.fw`
|
|
from the release rootfs for the GUI upload path (decides open
|
|
question 4: dev archives are signed with the dev key, never
|
|
unsigned — the GUI exercises the same verification path either
|
|
way).
|
|
- GitHub Actions: per-push compile checks for grblHAL-glowforge and
|
|
forgectrl (minutes, no Yocto); optional `workflow_dispatch`
|
|
cold-Yocto reproducibility build whose only product is a checksum.
|
|
|
|
## Phase 4 — forgectrl update manager (GUI)
|
|
|
|
Endpoints in `forgectrl/src/update.c`, driven from the panel's System
|
|
tab; trust anchors in `/etc/forgefirm/keys` (`forgefirm-keys` recipe:
|
|
the release pubkey + the Glowforge keyring). Release version resolves
|
|
from the fixed-name asset redirect (`.../releases/latest/download/forgefirm.fw`
|
|
→ `.../download/v<ver>/...`), so no GitHub API / rate limits. All slot
|
|
writes run on one background job (polled `/update/status`), take the
|
|
installer's `/data/forgefirm/update.lock`, require idle + no diagnostic,
|
|
refuse the booted root slot, verify signature before writing, and
|
|
re-verify the written filesystem. `GET /slots` inventory, `POST /boot`
|
|
(probe-gated), `POST /update/{check,download,apply,upload}`,
|
|
`POST /restore/factory` (archive md5 checked), `POST /system/reboot`.
|
|
Every state-changing call is behind forgectrl's auth layer (bearer
|
|
token + origin checks; unsigned installs additionally require the
|
|
physical button held).
|
|
|
|
Functions of the panel page:
|
|
|
|
- **Inventory**: slot contents (Phase 1 probe), current/next boot
|
|
selection, archive presence/version.
|
|
- **Update check** against the GitHub releases API (manual button +
|
|
periodic while idle; offline-tolerant, rate-limit friendly).
|
|
- **Apply release**: download `.fw` to `/data`, verify signature,
|
|
apply to inactive slot, verify, then flip only on explicit user
|
|
confirmation, prompt reboot.
|
|
- **Upload**: streamed multipart to `/data` (never RAM-buffered);
|
|
accepts `.fw` (verify; warn if unsigned) and `.wic.gz`/`.ext4.gz`
|
|
(dev; size + superblock sanity checks).
|
|
- **Boot selector** incl. SD, with warnings — most prominently on
|
|
switch-to-factory: the factory updater may auto-update and overwrite
|
|
the other slot. Refuses unprobeable targets.
|
|
- **Factory restore**: from the `/data` archive (offline) or cloud
|
|
latest (gfutilities device auth → GF-signed `.fw` → verify with GF
|
|
pubkeys) → inactive slot → flip. Optional cleanup of ForgeFIRM
|
|
residue in `/data` for true factory condition.
|
|
- Interlocks throughout: idle-only, update lock, never the active
|
|
slot, rollback = flip back to the previous slot.
|
|
|
|
*Exit: full loop on the bench — GUI upgrade, rollback via boot
|
|
selector, factory restore and return — without touching a shell.*
|
|
|
|
## Phase 5 — recovery refresh (not yet built)
|
|
|
|
- **v1 scope**: replace only the boot0 recovery squashfs (boot1 `/usr`
|
|
only if needed). Never write below offset 0xC0000 in boot0 — U-Boot
|
|
is physically untouchable by the refresh tool. Factory DTB and
|
|
kernel 3.14.28 stay.
|
|
- Userspace: static busybox + fwup + a small C webapp (ulfius) +
|
|
hostapd/wpa_supplicant. No Python. Must carry 3.14.28-matched WiFi
|
|
modules (decision gate: lift from the factory recovery vs rebuild
|
|
from Glowforge's published GPL kernel source).
|
|
- Functions: button-hold → AP + web UI (factory UX): upload a `.fw`
|
|
(verified against our **and** GF pubkeys — either firmware
|
|
installable), install from the `/data` archive, set boot target,
|
|
export logs.
|
|
- Flash tool: boot0/boot1 archived first (Phase 2 already does),
|
|
`force_ro` unlock, write high regions only, readback verify; if both
|
|
partitions are written, boot1 first, boot0 last.
|
|
- First flashes bench-gated on an attached serial console.
|
|
- Documented recovery ladder from then on: other slot → button-hold
|
|
recovery → SD card → serial console.
|
|
|
|
## Contracts
|
|
|
|
- **Artifacts** (consumers: installer, GUI updater, recovery):
|
|
`forgefirm.fw` (fixed asset name; signed; version in the fwup
|
|
metadata = release tag `v<semver>`; tasks `upgrade.a`/`upgrade.b`,
|
|
`complete` from Phase 5), `sha256sums.txt`,
|
|
`forgefirm-image-glowforge.rootfs.wic.gz` (SD burns).
|
|
- **Env**: SD = `0/0/1//dev/mmcblk1p1`; slot N = `1/0/N//dev/mmcblk2pN`
|
|
(`mmcdev/mmchwpart/mmcpart/mmcroot`, always one transaction).
|
|
- **Archive layout**: `/data/forgefirm/archive/` —
|
|
`factory-rootfs-<ver>.img.gz`, `boot0.img`, `boot1.img`,
|
|
`manifest` (slot versions, dates, checksums).
|
|
|
|
## Decisions
|
|
|
|
- uEnv.txt keeps its `mmcargs` override with `root=${mmcroot}` — the
|
|
image is slot-agnostic, steered only by the saved env.
|
|
- Modern-fwup-packed signed archives apply with the factory 0.14.2
|
|
binary (raw 32-byte pubkey form); no shipped fwup is needed on the
|
|
factory side.
|
|
- Size gates live in two layers: bitbake fails past the 200 MiB slot;
|
|
`release.sh` warns ≥ 170 MiB and fails ≥ 195 MiB.
|
|
- Dev archives are always signed with the dedicated dev key
|
|
(`release.sh --dev`), never unsigned.
|
|
- Production signing key: held offline by the operator (never in the
|
|
repo, CI, or cloud-synced plaintext), public key embedded in the
|
|
installer. Production-signed archives verify with fwup 1.16 and the
|
|
factory's 0.14.2 (raw pubkey form); dev-signed archives are rejected.
|
|
Custody optimizes against compromise over loss: loss means users
|
|
re-run a fresh installer; compromise means attacker-signed firmware
|
|
on fielded machines.
|
|
- U-Boot bootcount/auto-revert is out of scope — the recovery ladder
|
|
covers bad flips.
|
|
|
|
## Open items
|
|
|
|
- Periodic GUI update check default-on vs opt-in (it pings GitHub;
|
|
proposal: on by default, apply always manual, config switch to
|
|
disable).
|
|
- Recovery kernel modules: carried from the factory image vs rebuilt
|
|
from GPL source (Phase 5 gate).
|
|
|
|
## Dependencies between the phases
|
|
|
|
0 → 1 → 2 + 2b → 3 → 4 → 5: everything after Phase 0 assumes
|
|
slot-agnostic images and working `.fw` round-trips; the GUI (4) reuses
|
|
the probe (1) and pipeline (3); recovery (5) is an independent
|
|
mini-project on top of the stable slot scheme.
|