Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Per-target rustc flags for stage0. Self-contained: CI checks this repo out ALONE (no workspace
# parent, CARGO_HOME redirected to an empty dir), so the flags a build needs must live here, not in
# the workspace-level config. Per target (not one flag set for all) because +crt-static must reach
# the musl/uefi targets but not the host, where it makes proc-macro dylibs impossible to build.
#
# The UEFI flags reproduce the measured binaries byte-for-byte (stage0.efi, ena.efi, test-payload);
# do not change them without re-pinning PCR4/PCR14 expectations. No `[build] target`: every stage0
# build passes an explicit `--target`.

[target.x86_64-unknown-linux-musl]
# Fully static musl for the host-side signer (stage0-sign); rust-lld, no system cc.
rustflags = ["-C", "linker=rust-lld", "-C", "target-feature=+crt-static", "-C", "link-arg=-static"]

[target.aarch64-unknown-linux-musl]
rustflags = ["-C", "linker=rust-lld", "-C", "target-feature=+crt-static", "-C", "link-arg=-static"]

[target.x86_64-unknown-uefi]
# The measured bootloader + payloads. Static, rust-lld.
rustflags = ["-C", "linker=rust-lld", "-C", "target-feature=+crt-static", "-C", "link-arg=-static"]

[target.aarch64-unknown-uefi]
rustflags = ["-C", "linker=rust-lld", "-C", "target-feature=+crt-static", "-C", "link-arg=-static"]

# Host target for proc-macros and build scripts: rust-lld only (no crt-static), so they link with no
# system cc. Kept out of the shared workspace config because the gnu-host repos link C and need cc.
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "linker=rust-lld"]

[env]
# Reproducible builds.
SOURCE_DATE_EPOCH = "0"
3 changes: 2 additions & 1 deletion Dockerfile.build
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ RUN apt-get -qq update && \
# Reproducible builds environment
ENV SOURCE_DATE_EPOCH=0
ENV CARGO_INCREMENTAL=0
ENV RUSTFLAGS="-C linker=rust-lld -C target-feature=+crt-static -C link-arg=-static"
# rustflags are per-target in .cargo/config.toml; a global RUSTFLAGS would force +crt-static onto
# the host and break proc-macros.

# User environment (project-specific cargo/rustup data)
ENV HOME=/src
Expand Down
34 changes: 23 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,26 @@ DOCKER_RUN = docker run --rm \
build/keys/%: docker-build-base
$(DOCKER_RUN) $(DOCKER_SAMEUSER) $(BUILD_IMAGE) ./tools/gen-keys.sh build/keys

# ---- stage0-sign: this repo's standalone host-side signer (ed25519 keygen + domain-separated
# signatures), so stage0 builds + tests without stage1's deploy tool. Built + run in the container.
# Built for the musl target (like stage1's deploy) so the signer is a fully static host binary.
# NB: named STAGE0_SIGN, not SIGN -- SIGN is the boot-test matrix's signed-mode flag (SIGN=1),
# and a command-line `SIGN=1` from the recursive $(MAKE) boot-% would otherwise clobber this path.
STAGE0_SIGN := crates/stage0-sign/target/x86_64-unknown-linux-musl/debug/stage0-sign
.PHONY: sign-bin sign-test
sign-bin: docker-build-base
$(DOCKER_RUN) $(DOCKER_SAMEUSER) $(BUILD_IMAGE) cargo build --manifest-path crates/stage0-sign/Cargo.toml --target x86_64-unknown-linux-musl
# Run stage0-sign's tests (incl. the golden known-answer vector shared with stage1's signer).
sign-test: docker-build-base
$(DOCKER_RUN) $(DOCKER_SAMEUSER) $(BUILD_IMAGE) cargo test --manifest-path crates/stage0-sign/Cargo.toml --target x86_64-unknown-linux-musl

# ---- ed25519 release key for "signed mode" payload admission ----
# Vendor key; stage0 only ever sees the public half, pinned in the metadata doc.
build/keys/release.pem: docker-build-base
# Vendor key; stage0 only ever sees the public half, pinned in the metadata doc. Generated by
# stage0-sign keygen (the domain-separated signer the verifier admits against).
build/keys/release.pem: docker-build-base | sign-bin
$(DOCKER_RUN) $(DOCKER_SAMEUSER) $(BUILD_IMAGE) bash -c "\
mkdir -p build/keys && \
openssl genpkey -algorithm ed25519 -out build/keys/release.pem && \
openssl pkey -in build/keys/release.pem -pubout -outform DER \
| tail -c 32 | base64 -w0 > build/keys/release.pub.b64"
$(STAGE0_SIGN) keygen --out build/keys/release.pem --pub build/keys/release.pub.b64"

# ---- Build the ena.efi UEFI driver (embedded into stage0 for EC2/ENA netboot) ----
# A resident SimpleNetworkProtocol driver for the AWS ENA NIC, which Nitro's UEFI
Expand Down Expand Up @@ -128,21 +140,21 @@ build-arm64 build-aarch64: build/aarch64/boot.disk
# maps payload.lockboot.test -> 10.0.2.1. Override SERVE_HOST=10.0.2.1:8000 to skip DNS.
SERVE_HOST ?= payload.lockboot.test:8000
PAYLOAD_URL ?= http://$(SERVE_HOST)/payload.efi
build/%/payload.efi: docker-build-base build/keys/release.pem
build/%/payload.efi: docker-build-base build/keys/release.pem | sign-bin
$(DOCKER_RUN) -e ARCH=$* $(DOCKER_SAMEUSER) $(BUILD_IMAGE) \
bash -c "mkdir -p build/$* && rustup target add $*-unknown-uefi && cargo build --release --manifest-path crates/stage0-test-payload/Cargo.toml --target $*-unknown-uefi && \
cp crates/stage0-test-payload/target/$*-unknown-uefi/release/stage0-test-payload.efi $@ && \
openssl pkeyutl -sign -inkey build/keys/release.pem -rawin -in $@ -out $@.sig"
$(STAGE0_SIGN) sign --domain stage1.uki --key build/keys/release.pem --in $@ --out $@.sig"

# ---- Signed remote LoadOptions for SIGN_ARGS=1: a JSON array of strings, ed25519-signed like
# the payload. stage0 fetches args.json + args.json.sig, verifies against the pinned key, and
# uses them as the child's UEFI LoadOptions (overriding inline args). ----
build/%/args.json.sig: build/keys/release.pem
build/%/args.json.sig: build/keys/release.pem | sign-bin
$(DOCKER_RUN) $(DOCKER_SAMEUSER) $(BUILD_IMAGE) bash -c "\
mkdir -p build/$* && \
printf '%s' '[\"--from\",\"signed-args\",\"--nosleep\"]' > build/$*/args.json && \
openssl pkeyutl -sign -inkey build/keys/release.pem -rawin \
-in build/$*/args.json -out build/$*/args.json.sig"
$(STAGE0_SIGN) sign --domain stage1.args --key build/keys/release.pem \
--in build/$*/args.json --out build/$*/args.json.sig"

# ---- QEMU harness: the lean harness image bakes qemu-test.sh as its entrypoint
# (and the EC2_MOCK_CACHE + iptables-ack env), so we just append CLI args. ----
Expand Down Expand Up @@ -186,7 +198,7 @@ boot-%: build/%/boot.disk build/%/payload.efi docker-build-harness \
SHA=$$(sha256sum "$$D/payload.efi" | cut -d' ' -f1); \
printf '{ "_stage1": { "%s": { "payload": { "url": %s, "sha256": "%s"%s } } } }\n' "$*" "$$URLVAL" "$$SHA" "$$INLINE_ARGS" > "$$D/stage1.manifest.json"; \
$(DOCKER_RUN) $(DOCKER_SAMEUSER) $(BUILD_IMAGE) bash -c \
"openssl pkeyutl -sign -inkey build/keys/release.pem -rawin -in $$D/stage1.manifest.json -out $$D/stage1.manifest.json.sig"; \
"$(STAGE0_SIGN) sign --domain stage1.manifest --key build/keys/release.pem --in $$D/stage1.manifest.json --out $$D/stage1.manifest.json.sig"; \
printf '{\n "_stage1": { "%s": { "manifest": { "url": "%s/stage1.manifest.json", "ed25519": "%s" } } }\n}\n' "$*" "$$H" "$$PUB" > user-data.stage0.json; \
echo "user-data: _stage1 via signed manifest (pubkey $$PUB)"; \
else \
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ binary now) or a `manifest` (resolve a signed manifest first):
{ "_stage1": { "x86_64": { "manifest": { "url": "http://cdn.example.com/app.manifest.json", "ed25519": "<base64 pubkey>" } } } }
```

Signatures are **domain-separated**: every ed25519 signature is over a fixed 64-byte preimage
`sha256(domain_tag) || sha256(message)`, where the tag names the exact role — `lockboot.v1.stage1.uki`
for the payload, `lockboot.v1.stage1.args` for signed LoadOptions, `lockboot.v1.stage1.manifest` for a
manifest. A signature minted for one role is structurally invalid in another, so a signed-args blob can
never be replayed as a payload signature, nor a `_stage1` manifest as anything else. stage0 admits only
these three `stage1.*` roles; the signer is `crates/stage0-sign` in this repo, whose framing is pinned
byte-for-byte to stage1's `deploy` by a shared golden known-answer test.

The payload must be a UEFI PE. However the firmware `db` feels about it, stage0
admits it by your pin/signature and measures it into **PCR 14** (= its SHA-256).

Expand All @@ -65,7 +73,9 @@ fallback), or `ARGS='[…]'` (inline LoadOptions).
`stage0 → test-payload` and verifies the payload actually chain-loaded (proving fetch → admit →
PCR-measure → chain-load), printing a per-mode PASS/FAIL summary. It needs nested KVM (local only)
and takes ~2 minutes. Everything is regenerated and signed reproducibly from the Makefile — no
manual steps.
manual steps. The release key and every detached signature are produced by `stage0-sign` (this
repo's host-side signer: `make sign-bin`, or `make sign-test` for its golden vector), so the test
path dogfoods the same domain-separated signing a deployer would use.

Under the test harness the payload runs with `--nosleep` in its LoadOptions (skipping its
EC2-only ~60s serial-flush hold, which QEMU doesn't need) and powers the machine off at the end
Expand Down
Loading