{"id":51621397,"url":"https://github.com/lockboot/stage1","last_synced_at":"2026-07-12T20:30:46.194Z","repository":{"id":323138452,"uuid":"1092237587","full_name":"lockboot/stage1","owner":"lockboot","description":"Secure two-stage bootloader with AWS Nitro \u0026 GCP vTPM attestation. Multi-architecture (x86_64/ARM64) UEFI boot system with verified execution and PCR measurements","archived":false,"fork":false,"pushed_at":"2026-07-05T06:08:30.000Z","size":215,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-05T08:06:19.045Z","etag":null,"topics":["attestation","aws-ec2","aws-nitro","azure","gcp","rust","tpm2","uefi","uefi-secureboot","verified-boot"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lockboot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-08T08:56:14.000Z","updated_at":"2026-07-05T06:08:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lockboot/stage1","commit_stats":null,"previous_names":["harryr/lockboot","lockboot/lockboot","lockboot/stage1"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/lockboot/stage1","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lockboot%2Fstage1","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lockboot%2Fstage1/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lockboot%2Fstage1/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lockboot%2Fstage1/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lockboot","download_url":"https://codeload.github.com/lockboot/stage1/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lockboot%2Fstage1/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35402741,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-12T02:00:06.386Z","response_time":87,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["attestation","aws-ec2","aws-nitro","azure","gcp","rust","tpm2","uefi","uefi-secureboot","verified-boot"],"created_at":"2026-07-12T20:30:44.421Z","updated_at":"2026-07-12T20:30:46.188Z","avatar_url":"https://github.com/lockboot.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🔒 stage1 — the Lock.Boot netboot UKI\n\nPart of [Lock.Boot](https://github.com/lockboot) — see the org page for the whole boot chain. **stage1** is the netboot **UKI**: a Unified Kernel Image (Linux kernel + minimal initramfs + the `stage1` bootloader as PID 1) that [stage0](https://github.com/lockboot/stage0) fetches over the network, verifies, measures into PCR 14, and chain-loads.\n\nOnce running, `stage1` reads a `_stage2` manifest from cloud metadata (IMDSv2), downloads the stage2 payload, **admits it by a pinned `sha256` or an `ed25519` signature**, extends **PCR 14 with the payload hash** (loaded code only — never config), generates an attestation, and `exec`s it as PID 1 from a sealed in-memory image (never a file on disk).\n\n## Build\n\n```bash\nmake x86_64            # -\u003e tools/build-uki/x86_64/linux.efi   (the UKI)\nmake aarch64\nmake stage2-x86_64     # -\u003e build/x86_64/stage2                (the example leaf)\n```\n\nEverything compiles inside the shared `lockboot:build` image (built from [stage0](https://github.com/lockboot/stage0)'s canonical `Dockerfile.build`); no host toolchain is needed. `vaportpm` is pulled from git, so the repo builds standalone — no sibling checkout required.\n\n## Test the whole chain\n\n`stage0 → UKI → stage1 → example-stage2`, under QEMU + KVM. stage0 is the harness: build its boot disk in the sibling repo, then run the chain test — it borrows `../stage0/build/\u003carch\u003e/boot.disk` and the shared `lockboot:harness` image, serves the UKI + leaf + a signed/pinned manifest, and boots it:\n\n```bash\n(cd ../stage0 \u0026\u0026 make build-x86_64)\nmake test-chain-x86_64            # sha256 admission (default)\nmake test-chain-x86_64 SIGN=1     # ed25519 signed-manifest admission\n```\n\n## stage2 admission (`_stage2`)\n\nstage1 admits its stage2 payload from a `_stage2` block in the instance's user-data, per architecture. Each arch entry is a **discriminated union** — exactly one of a `payload` (admit a binary now) or a `manifest` (resolve a signed manifest first):\n\n**payload / sha256** — pin an exact binary:\n\n```json\n{\n  \"_stage2\": {\n    \"x86_64\":  { \"payload\": { \"url\": \"https://host/stage2-amd64\", \"sha256\": \"abc123...\", \"args\": [\"--flag\", \"value\"] } },\n    \"aarch64\": { \"payload\": { \"url\": \"https://host/stage2-arm64\", \"sha256\": \"def456...\" } }\n  }\n}\n```\n\n**payload / ed25519** — pin a long-term release **public key** (base64 of 32 bytes). The binary rolls forward with **no reconfiguration**: re-sign it, push it, reboot. stage1 fetches a detached signature at `\u003curl\u003e.sig` (override with `sig_url`; `{sha256}` is substituted) and verifies it against the pinned key:\n\n```json\n{\n  \"_stage2\": {\n    \"x86_64\": { \"payload\": {\n      \"url\": \"https://host/stage2-amd64\",\n      \"ed25519\": \"BASE64_32BYTE_PUBKEY\",\n      \"args_url\": \"https://host/args.json\"\n    } }\n  }\n}\n```\n\n`args_url` (ed25519 mode only) fetches a **signed** JSON array of strings — verified against the same key via `\u003cargs_url\u003e.sig` (or an explicit `args_sig_url`) — that **overrides** inline `args`.\n\n**manifest** — pin a release key **and** a manifest URL. stage1 fetches the signed manifest (itself a `_stage2` user-data fragment), verifies its detached signature (`\u003curl\u003e.sig`, override with `sig_url`) against the pinned `ed25519` key, **deep-merges the whole document into the received user-data at the top level** (manifest wins on conflict), and re-evaluates the entry. It loops — a manifest may resolve to a `payload` (done) or delegate to a *fresh* `manifest` (per-hop key delegation) — until a payload is reached; a repeated `(url, sha256)` is a cycle and fails closed. Binding the binary + args under one manifest signature stops a hostile mirror from mixing-and-matching independently-signed pieces:\n\n```json\n{\n  \"_stage2\": {\n    \"x86_64\": { \"manifest\": { \"url\": \"https://host/stage2.manifest.json\", \"ed25519\": \"BASE64_32BYTE_PUBKEY\" } }\n  }\n}\n```\n\nThe optional `manifest.sha256` also pins the manifest's own bytes. Every hop is recorded in the merged entry's `resolved_manifests` array (each with the resolved hash + verifying key) — verifier-authoritative provenance the payload sees on stdin. You don't hand-write these docs: the [`deploy` tool](#deploy) below signs the payloads/manifests and generates the `user-data.json`.\n\n**Domain separation.** Every ed25519 signature in the chain is over a fixed 64-byte preimage `sha256(domain_tag) || sha256(message)`, where the tag names the exact role. There are six roles across the two hops — `lockboot.v1.stage1.uki` / `.stage1.args` / `.stage1.manifest` (stage0 admits the UKI hop) and `lockboot.v1.stage2.payload` / `.stage2.args` / `.stage2.manifest` (stage1 admits the payload hop). Because the role is bound into what gets signed, a signature minted for one context is structurally invalid in every other: a signed-args blob can't be replayed as a payload signature, a `_stage1` manifest can't stand in for a `_stage2` one, and so on. `deploy` and `stage1` share the framing via the `ed25519-sign` crate; stage0's independent verifier is pinned to it byte-for-byte by a shared golden known-answer test.\n\n**Fallback URLs.** Every URL field (`url`, `sig_url`, `args_url`, `args_sig_url`, `manifest.url`) accepts either a single string **or a list of strings** tried in order — for mirror resiliency. Because the payload is cryptographically pinned, any mirror that yields verifying bytes is accepted; a dead or wrong mirror is simply skipped. URLs may be `http://` or `https://`, and the `*_url` fields may contain a `{sha256}` placeholder (replaced with the payload's — or manifest's — hex digest, for content-addressed signatures):\n\n```json\n{\n  \"_stage2\": {\n    \"x86_64\": { \"payload\": {\n      \"url\": [\"https://cdn1/stage2\", \"https://cdn2/stage2\"],\n      \"ed25519\": \"BASE64_32BYTE_PUBKEY\",\n      \"sig_url\": [\"https://cdn1/sigs/{sha256}.sig\", \"https://cdn2/sigs/{sha256}.sig\"]\n    } }\n  }\n}\n```\n\n**Measurement is code-only.** stage1 extends **PCR 14** with the SHA-256 of the stage2 binary and nothing else — the admission pin / key / signature and the config JSON are *not* measured. This keeps the platform quote reproducible from the boot artifacts alone (stage0 → UKI → app), and leaves a stage2 app free to measure whatever config *it* deems trust-relevant (PCR 15 is left untouched for it).\n\n**Execution is pathless.** stage1 loads the payload into a sealed `memfd` (`F_SEAL_WRITE`) and `execveat`s it directly, so the bytes measured into PCR 14 are immutable and are exactly what runs — nothing is written to a named path where it could be swapped between measurement and exec. The payload receives the raw user-data JSON on **stdin** (a second in-memory file, so any runtime that reads stdin works — no extra-fd convention that would trip up Bun/Node single-file executables), and the pre-exec attestation at `/tmp/stage1.attest`.\n\nAny statically-linked Linux ELF works, as long as it reads its config from stdin; the minimal rootfs provides `/bin/{busybox,stage1}` (plus `udhcpc.script`) and `/tmp`.\n\n## Arguments and config model\n\nTwo distinct hops, don't conflate them:\n\n- **stage1's own config** comes from the cloud **metadata** service (the PID-1 boot path) or, when stage1 is run as a normal process, from a user-data doc **piped on stdin** (`stage1 \u003c user-data.json`). There are no `--url`/`--file` flags — pipe it in. `--attest` remains for diagnostics.\n- **The stage2 app's argv** comes from the payload's inline **`args`** or its signed `args_url` (which overrides inline); in manifest mode these ride inside the signed manifest. They are handed to the payload as `argv[1..]` (with `argv[0] = \"stage2\"`).\n\nNote on `_stage1.args`: that field belongs to **stage0**, which sets the booted EFI program's UEFI *LoadOptions* from it — the generic contract for any EFI stage1. For **this Linux UKI**, the kernel command line is baked into the signed, measured `.cmdline` and is authoritative: under Secure Boot the stub **ignores** LoadOptions, so `_stage1.args` cannot (and must not) alter the UKI cmdline. Configure a UKI-based stage1 through **`_stage2`**, not the kernel cmdline. See the [stage0 repo](https://github.com/lockboot/stage0) for the LoadOptions contract.\n\n## Deploy\n\nThe **`deploy`** tool (binary `lockboot-deploy`) turns local build artifacts into an upload-ready deployment: it signs (or hashes) the UKI + stage2 as `payload` entries — or, with `--manifest`, wraps each in a **signed manifest** and pins a `manifest` entry — composes **mirror URL lists** from repeated `--base-url`, and emits a directory plus a merged `user-data.json` carrying both `_stage1` (the UKI hop) and `_stage2` (the payload hop).\n\n```bash\nlockboot-deploy create --arch x86_64 \\\n  --uki tools/build-uki/x86_64/linux.efi --stage2 build/x86_64/stage2 \\\n  --key release.pem \\                          # ed25519 signed mode (omit for sha256 pins)\n  --base-url http://cdn1 --base-url http://cdn2 \\\n  --out ./deploy\nlockboot-deploy validate ./deploy              # check against the admission rules\nlockboot-deploy modify ./deploy --add-base-url http://cdn3   # add / --remove-base-url a mirror\n```\n\n`create` writes `deploy/\u003carch\u003e/{linux.efi,stage2}` (+ `.sig` in signed mode) and merges `deploy/user-data.json`; sync the directory to each mirror and pass `user-data.json` as the instance's user-data. It shares the `metadata` types with the stage1 verifier, so what it emits is exactly what stage0/stage1 accept. (`tools/publish.sh` remains as a simpler UKI-only uploader; the bootable cloud image — the stage0 Secure Boot root — is published from the [stage0 repo](https://github.com/lockboot/stage0).)\n\nThe release key comes from `lockboot-deploy keygen --out release.pem --pub release.pub.b64` (a PKCS#8 ed25519 key; randomness is read from `/dev/urandom`, so it builds with no host C toolchain), and `lockboot-deploy sign --domain \u003crole\u003e --key release.pem --in \u003cfile\u003e --out \u003cfile\u003e.sig` produces one domain-separated signature — the low-level primitive the test Makefile drives for each artifact.\n\n## Crates\n\n- **`stage1`** — the on-instance PID-1 bootloader baked into the UKI (verify-only: admit → measure → exec).\n- **`metadata`** — the `_stage1`/`_stage2` wire types + `validate()`, shared by the stage1 verifier and the deploy emitter (one source of truth, no drift).\n- **`ed25519-sign`** — the domain-separated ed25519 sign/verify (`sha256(domain_tag) || sha256(message)`) + sha256 primitive (the cross-repo wire contract, with a golden known-answer test), used by `mkuki`, `deploy`, and `stage1`.\n- **`mkuki`** — reproducible UKI assembler (kernel + gzip'd cpio layers → PE, optional `ed25519` signature); a build-host tool.\n- **`deploy`** — the deployment tool above (`lockboot-deploy`); a build-host tool.\n- **`example-stage2`** — a minimal example leaf payload; copy it as a template for your own stage2.\n\n## License\n\nApache-2.0 OR MIT, at your option.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flockboot%2Fstage1","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flockboot%2Fstage1","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flockboot%2Fstage1/lists"}