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
2 changes: 2 additions & 0 deletions Dockerfile.build
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ RUN apt-get -qq update && \
fdisk \
findutils \
gdisk \
gnupg \
gzip \
kmod \
make \
Expand All @@ -23,6 +24,7 @@ RUN apt-get -qq update && \
python3 \
python3-pip \
qemu-efi-aarch64 \
rpm \
rpm2cpio \
sbsigntool \
util-linux \
Expand Down
62 changes: 6 additions & 56 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
77 changes: 77 additions & 0 deletions build.mk
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 2 additions & 2 deletions crates/stage0-test-payload/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/stage0-test-payload/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand Down
4 changes: 2 additions & 2 deletions crates/stage0/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crates/stage0/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
12 changes: 8 additions & 4 deletions tools/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}" <<EOF
label: gpt
Expand Down