diff --git a/Dockerfile.build b/Dockerfile.build index a9b6962..f9df7d5 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -15,6 +15,7 @@ RUN apt-get -qq update && \ fdisk \ findutils \ gdisk \ + gnupg \ gzip \ kmod \ make \ @@ -23,6 +24,7 @@ RUN apt-get -qq update && \ python3 \ python3-pip \ qemu-efi-aarch64 \ + rpm \ rpm2cpio \ sbsigntool \ util-linux \ diff --git a/Makefile b/Makefile index a38242d..a3e151a 100644 --- a/Makefile +++ b/Makefile @@ -22,66 +22,16 @@ STAGE0_DIR = crates/stage0 ARCH ?= $(shell uname -m) .DEFAULT_GOAL := build -# ---- Docker images (shared lockboot family; built locally, never published) ---- -BUILD_IMAGE = lockboot:build -HARNESS_IMAGE = lockboot:harness - -.PHONY: docker-build-base docker-build-harness -docker-build-base: - docker build -f Dockerfile.build -t $(BUILD_IMAGE) . +# ---- Shared build harness (docker images + DOCKER_RUN plumbing) ---- +# CANONICAL SOURCE lives here (stage0/build.mk); vendored into stage1/vaportpm via the +# workspace `make sync-harness` and guarded by `make check-harness`. Edit build.mk, not copies. +include build.mk +# stage0 owns the QEMU harness image (Dockerfile.harness); stage1 borrows the built lockboot:harness. +.PHONY: docker-build-harness docker-build-harness: docker build -f Dockerfile.harness -t $(HARNESS_IMAGE) . -# ---- Docker run plumbing ---- -# Own build artifacts by whoever owns the checkout, not the caller's euid. Under -# `gh act` the caller is root but the bind-mounted tree is still yours, so stat -# keeps output user-owned instead of trampling the project dir with root files. -# On a normal host/devcontainer run this equals `id -u`/`id -g`, so nothing changes. -USER_ID := $(shell stat -c %u .) -GROUP_ID := $(shell stat -c %g .) - -KVM_GID := $(shell stat -c %g /dev/kvm 2>/dev/null || echo "") -KVM_MOUNT := $(shell test -e /dev/kvm && echo "-v /dev/kvm:/dev/kvm") -DOCKER_OPT_KVM := $(if $(KVM_GID),--group-add $(KVM_GID)) $(KVM_MOUNT) - -DOCKER_SAMEUSER := -u $(USER_ID):$(GROUP_ID) - -# Host-path translation for docker-in-devcontainer. Inside the devcontainer /src is -# a host bind mount and the inner Docker talks to the HOST daemon, which cannot -# resolve /src/... paths; translate $(CURDIR) to the real host path (the bracketed -# subpath findmnt reports for the /src bind). On the host CURDIR is not under /src, -# so this is a pass-through and your workflow is unchanged. Keep identical across repos. -HOST_DIR := $(CURDIR) -ifneq ($(filter /src/%,$(CURDIR)),) - SRC_BIND := $(shell findmnt -fnro SOURCE --target /src 2>/dev/null | sed -n 's/.*\[\(.*\)\]$$/\1/p') - ifneq ($(SRC_BIND),) - HOST_DIR := $(SRC_BIND)$(CURDIR:/src%=%) - endif -endif - -# Mount the WORKSPACE (parent of this repo) at /src so builds reuse the shared -# workspace-level .cargo/.rustup (matching the devcontainer), instead of creating -# per-repo copies. The repo then lives at /src/$(REPO_NAME). -REPO_NAME := $(notdir $(HOST_DIR)) -HOST_WS := $(patsubst %/,%,$(dir $(HOST_DIR))) - -# Under CI / `gh act` (CI=true, runs as root) keep cargo/rustup caches ephemeral -# inside the container, so root-owned dirs never land in the bind-mounted project. -# Locally (no CI) the image's CARGO_HOME=/src/.cargo + RUSTUP_HOME=/src/.rustup win, -# i.e. the shared workspace caches. -CACHE_ENV := $(if $(CI),-e CARGO_HOME=/tmp/.cargo -e RUSTUP_HOME=/tmp/.rustup) - -DOCKER_RUN = docker run --rm \ - --privileged \ - -v $(HOST_WS):/src \ - -h lockboot \ - --add-host lockboot:127.0.0.1 \ - -e OWNER_UID=$(USER_ID) \ - -e OWNER_GID=$(GROUP_ID) \ - $(CACHE_ENV) \ - -w /src/$(REPO_NAME) - # ---- Secure Boot keys: stage0's own snakeoil PK/KEK/db (regenerated per build) ---- # The pattern rule generates the whole set into build/keys via tools/gen-keys.sh; # release.pem (below) is matched by its explicit rule instead. diff --git a/build.mk b/build.mk new file mode 100644 index 0000000..38a24e7 --- /dev/null +++ b/build.mk @@ -0,0 +1,77 @@ +# ---- Lock.Boot shared build harness ---------------------------------------------------------- +# Runs every cargo/tool invocation inside the locally-built lockboot:build image, so builds are +# cc-free-by-design and byte-reproducible (rust-lld + musl, shared /src/.cargo + /src/.rustup). +# +# CANONICAL SOURCE: stage0/build.mk. This file is vendored byte-identically into each participating +# repo (stage1, vaportpm, ...) because CI checks out each repo ALONE (no workspace parent), so a +# shared harness cannot be a cross-repo include -- it must live in the repo. Do NOT hand-edit the +# copies: edit stage0/build.mk, then run `make sync-harness` from the workspace ($(CANON)=stage0), +# guarded by `make check-harness`. +# +# Each repo's Makefile does `include build.mk` and defines its own targets, invoking cargo as +# $(DOCKER_RUN) $(DOCKER_SAMEUSER) $(BUILD_IMAGE) cargo ... +# with a `docker-build-base` prerequisite so the image is built on demand (incl. standalone CI). + +# ---- Docker images (shared lockboot family; built locally, never published) ---- +BUILD_IMAGE = lockboot:build +HARNESS_IMAGE = lockboot:harness + +.PHONY: docker-build-base +docker-build-base: + docker build -f Dockerfile.build -t $(BUILD_IMAGE) . + +# ---- Docker run plumbing (keep identical across repos) ---- +# Own build artifacts by whoever owns the checkout, not the caller's euid. Under +# `gh act` the caller is root but the bind-mounted tree is still yours, so stat +# keeps output user-owned instead of trampling the project dir with root files. +# On a normal host/devcontainer run this equals `id -u`/`id -g`, so nothing changes. +USER_ID := $(shell stat -c %u .) +GROUP_ID := $(shell stat -c %g .) + +KVM_GID := $(shell stat -c %g /dev/kvm 2>/dev/null || echo "") +KVM_MOUNT := $(shell test -e /dev/kvm && echo "-v /dev/kvm:/dev/kvm") +DOCKER_OPT_KVM := $(if $(KVM_GID),--group-add $(KVM_GID)) $(KVM_MOUNT) + +# Recursive-docker passthrough: rules that shell out to the HOST docker daemon (e.g. stage1's UKI +# rootfs extraction / runtime-image buildx) forward the socket + its gid. Defined here for every +# repo; harmless (expands empty) when a repo has no such rule. +DOCKER_SOCK_GID := $(shell stat -c %g /var/run/docker.sock 2>/dev/null || echo "") +DOCKER_SOCK_MOUNT := $(shell test -e /var/run/docker.sock && echo "-v /var/run/docker.sock:/var/run/docker.sock") +DOCKER_OPT_DOCKER := $(DOCKER_SOCK_MOUNT) $(if $(DOCKER_SOCK_GID),--group-add $(DOCKER_SOCK_GID)) + +DOCKER_SAMEUSER := -u $(USER_ID):$(GROUP_ID) + +# Host-path translation for docker-in-devcontainer. Inside the devcontainer /src is +# a host bind mount and the inner Docker talks to the HOST daemon, which cannot +# resolve /src/... paths; translate $(CURDIR) to the real host path (the bracketed +# subpath findmnt reports for the /src bind). On the host CURDIR is not under /src, +# so this is a pass-through and your workflow is unchanged. Keep identical across repos. +HOST_DIR := $(CURDIR) +ifneq ($(filter /src/%,$(CURDIR)),) + SRC_BIND := $(shell findmnt -fnro SOURCE --target /src 2>/dev/null | sed -n 's/.*\[\(.*\)\]$$/\1/p') + ifneq ($(SRC_BIND),) + HOST_DIR := $(SRC_BIND)$(CURDIR:/src%=%) + endif +endif + +# Mount the WORKSPACE (parent of this repo) at /src so builds reuse the shared +# workspace-level .cargo/.rustup (matching the devcontainer), instead of creating +# per-repo copies. The repo then lives at /src/$(REPO_NAME). +REPO_NAME := $(notdir $(HOST_DIR)) +HOST_WS := $(patsubst %/,%,$(dir $(HOST_DIR))) + +# Under CI / `gh act` (CI=true, runs as root) keep cargo/rustup caches ephemeral +# inside the container, so root-owned dirs never land in the bind-mounted project. +# Locally (no CI) the image's CARGO_HOME=/src/.cargo + RUSTUP_HOME=/src/.rustup win, +# i.e. the shared workspace caches. +CACHE_ENV := $(if $(CI),-e CARGO_HOME=/tmp/.cargo -e RUSTUP_HOME=/tmp/.rustup) + +DOCKER_RUN = docker run --rm \ + --privileged \ + -v $(HOST_WS):/src \ + -h lockboot \ + --add-host lockboot:127.0.0.1 \ + -e OWNER_UID=$(USER_ID) \ + -e OWNER_GID=$(GROUP_ID) \ + $(CACHE_ENV) \ + -w /src/$(REPO_NAME) diff --git a/crates/stage0-test-payload/Cargo.lock b/crates/stage0-test-payload/Cargo.lock index 2343d7f..24c9bee 100644 --- a/crates/stage0-test-payload/Cargo.lock +++ b/crates/stage0-test-payload/Cargo.lock @@ -255,8 +255,8 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "vaportpm-attest" -version = "0.1.0" -source = "git+https://github.com/lockboot/vaportpm#15770b11a477de3cb92207a76ae27a43ece695de" +version = "0.3.0" +source = "git+https://github.com/lockboot/vaportpm?rev=7041f461eb33bc15ad914466042b7befe358d119#7041f461eb33bc15ad914466042b7befe358d119" dependencies = [ "anyhow", "hex", diff --git a/crates/stage0-test-payload/Cargo.toml b/crates/stage0-test-payload/Cargo.toml index 764aa7f..270ff24 100644 --- a/crates/stage0-test-payload/Cargo.toml +++ b/crates/stage0-test-payload/Cargo.toml @@ -13,7 +13,7 @@ path = "src/main.rs" [dependencies] uefi = { version = "0.35", features = ["alloc", "global_allocator", "panic_handler"] } -vaportpm-attest = { git = "https://github.com/lockboot/vaportpm", default-features = false } +vaportpm-attest = { git = "https://github.com/lockboot/vaportpm", rev = "7041f461eb33bc15ad914466042b7befe358d119", version = "=0.3.0", default-features = false } anyhow = { version = "1.0", default-features = false } hex = { version = "0.4", default-features = false, features = ["alloc"] } diff --git a/crates/stage0/Cargo.lock b/crates/stage0/Cargo.lock index 80bddfe..d4ebdf1 100644 --- a/crates/stage0/Cargo.lock +++ b/crates/stage0/Cargo.lock @@ -328,8 +328,8 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "vaportpm-attest" -version = "0.1.0" -source = "git+https://github.com/lockboot/vaportpm#15770b11a477de3cb92207a76ae27a43ece695de" +version = "0.3.0" +source = "git+https://github.com/lockboot/vaportpm?rev=7041f461eb33bc15ad914466042b7befe358d119#7041f461eb33bc15ad914466042b7befe358d119" dependencies = [ "anyhow", "hex", diff --git a/crates/stage0/Cargo.toml b/crates/stage0/Cargo.toml index 98e97f2..a64ee52 100644 --- a/crates/stage0/Cargo.toml +++ b/crates/stage0/Cargo.toml @@ -21,7 +21,9 @@ uefi-raw = "0.11" # no_std TPM core — provides TpmTransport + PCR ops over our EFI_TCG2 transport. # Consumed from git like stage1 (the build never sees a local vaportpm checkout). -vaportpm-attest = { git = "https://github.com/lockboot/vaportpm", default-features = false } +# Pinned to the vaportpm v0.3.0 release commit: immutable SHA + a `version` guard (the build fails +# if the rev ever drifts to a commit whose crate version isn't 0.3.0). Tag not GPG-signed yet. +vaportpm-attest = { git = "https://github.com/lockboot/vaportpm", rev = "7041f461eb33bc15ad914466042b7befe358d119", version = "=0.3.0", default-features = false } # no_std JSON + hashing for the metadata doc and payload integrity check. serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } diff --git a/tools/build.sh b/tools/build.sh index 490808e..a6e0d37 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -47,11 +47,15 @@ DISK_SIZE_MB=$((PARTITION_SIZE_MB + 2)) DISK_IMAGE="${OUTPUT_DIR}/boot.disk" dd if=/dev/zero of="${DISK_IMAGE}" bs=1M count=${DISK_SIZE_MB} status=none -# Deterministic GUIDs/volume-id derived from the signed binary's hash. +# Fixed lockboot GUIDs / FAT volume-id: CONSTANT across releases, so the GPT (and hence +# the firmware's PCR 5 measurement) is byte-stable, and a lockboot disk is identifiable +# by GUID. "4c4f434b424f4f54" spells "LOCKBOOT"; the last group is a role index +# (disk = 0, ESP = partition 1). +DISK_GUID="4c4f434b-424f-4f54-0000-000000000000" +PART_GUID="4c4f434b-424f-4f54-0000-000000000001" +VOLUME_ID="4c4f434b" +# stage0.efi hash — still used for BUILD_ID below. EFI_HASH=$(sha256sum "${SIGNED_EFI}" | cut -d' ' -f1) -DISK_GUID="${EFI_HASH:0:8}-${EFI_HASH:8:4}-${EFI_HASH:12:4}-${EFI_HASH:16:4}-${EFI_HASH:20:12}" -PART_GUID="${EFI_HASH:32:8}-${EFI_HASH:36:4}-${EFI_HASH:40:4}-${EFI_HASH:44:4}-${EFI_HASH:48:12}" -VOLUME_ID="${EFI_HASH:0:8}" sfdisk "${DISK_IMAGE}" <