diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d92f6ae..5c3de93 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,11 +27,11 @@ jobs: with: name: ${{ matrix.arch }} path: | - ${{ matrix.arch }}/boot.disk - ${{ matrix.arch }}/busybox - ${{ matrix.arch }}/stage1 - ${{ matrix.arch }}/os-release - ${{ matrix.arch }}/efi-vars.* + tools/build-uki/${{ matrix.arch }}/boot.disk + tools/build-uki/${{ matrix.arch }}/busybox + tools/build-uki/${{ matrix.arch }}/stage1 + tools/build-uki/${{ matrix.arch }}/os-release + tools/build-uki/${{ matrix.arch }}/efi-vars.* release-docker: runs-on: ubuntu-latest @@ -51,13 +51,13 @@ jobs: uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: x86_64 - path: x86_64 + path: tools/build-uki/x86_64 - name: Download aarch64 artifacts uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: aarch64 - path: aarch64 + path: tools/build-uki/aarch64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 diff --git a/.gitignore b/.gitignore index f78083d..33a35d4 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ qemu-10.* __pycache__ old .docker -aarch64 -x86_64 user-data.json -.bashrc \ No newline at end of file +.bashrc +.lesshst diff --git a/Cargo.lock b/Cargo.lock index 37b5e18..20d34e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1655,7 +1655,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "vaportpm-attest" version = "0.1.0" -source = "git+https://github.com/lockboot/vaportpm#66fd00340fd853695d9adf1d89ae5625a00a4939" +source = "git+https://github.com/lockboot/vaportpm#785438f42e76111a33116535ecfa523e0400a9f2" dependencies = [ "anyhow", "base64", diff --git a/Dockerfile.dev b/Dockerfile.dev index 6ea4b92..1aa1baa 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,30 +1,29 @@ FROM lockboot:build +RUN mkdir -p -m 755 /etc/apt/keyrings \ + && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + && cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ + && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && mkdir -p -m 755 /etc/apt/sources.list.d \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null + # Install development tools and Claude Code RUN DEBIAN_FRONTEND=noninteractive apt-get -qq update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + procps nano net-tools iputils-ping ca-certificates less xz-utils openssh-client bash-completion \ docker.io \ - qemu-system-x86 \ - qemu-system-arm \ + qemu-utils qemu-system-x86 qemu-system-arm \ git \ - vim \ build-essential \ gpg \ iptables \ iproute2 \ dnsmasq \ - swtpm swtpm-tools + swtpm swtpm-tools \ + tpm2-tools xxd \ + gh -# Install Claude Code -#RUN curl -fsSL https://claude.ai/install.sh | bash -# Download the Claude Code binary manually -ENV CLAUDE_CODE_URL=https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819 -RUN ARCH=$(uname -m | sed 's/x86_64/x64/; s/aarch64/arm64/') && \ - wget -O claude-version ${CLAUDE_CODE_URL}/claude-code-releases/stable && \ - VERSION=$(cat claude-version) && \ - wget -O /usr/local/bin/claude "${CLAUDE_CODE_URL}/claude-code-releases/${VERSION}/linux-${ARCH}/claude" && \ - chmod +x /usr/local/bin/claude -ENV DISABLE_AUTOUPDATER=1 +RUN groupadd -g 1000 vscode-dc && useradd -u 1000 -g 1000 -d /src -s /bin/bash vscode-dc # Development environment - keep same reproducible build settings from base WORKDIR /src diff --git a/Dockerfile.runtime b/Dockerfile.runtime index 81b02f2..f81ca85 100644 --- a/Dockerfile.runtime +++ b/Dockerfile.runtime @@ -5,8 +5,8 @@ ARG TARGETARCH # Map Docker's TARGETARCH to our directory structure # Create symlinks to map amd64->x86_64 and arm64->aarch64 -COPY x86_64/ /binaries/x86_64/ -COPY aarch64/ /binaries/aarch64/ +COPY tools/build-uki/x86_64/ /binaries/x86_64/ +COPY tools/build-uki/aarch64/ /binaries/aarch64/ # Create arch-specific directory based on TARGETARCH RUN mkdir -p /target && \ diff --git a/Makefile b/Makefile index 1ffafb3..d657977 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ -.PRECIOUS: keys/% downloads/% +.PRECIOUS: tools/build-uki/keys/% tools/build-uki/% all: build ARCHS=x86_64 aarch64 build: $(ARCHS) -amd64 x86_64: x86_64/boot.disk -arm64 aarch64: aarch64/boot.disk +amd64 x86_64: tools/build-uki/x86_64/boot.disk +arm64 aarch64: tools/build-uki/aarch64/boot.disk DEFAULT_STAGE2_URL = https://lockboot.s3.us-east-1.amazonaws.com/examples/stage2/user-data.json user-data.json: @@ -17,29 +17,30 @@ BUILD_IMAGE = lockboot:build DEV_IMAGE = lockboot:dev RUNTIME_IMAGE ?= lockboot:latest -keys/%: - $(MAKE) -C keys +tools/build-uki/keys/%: + $(MAKE) -C tools/build-uki/keys clean: - rm -rf $(ARCHS) + rm -rf tools/build-uki/x86_64/boot.disk tools/build-uki/x86_64/stage1 tools/build-uki/x86_64/tmp tools/build-uki/x86_64/*.img tools/build-uki/x86_64/*.efi tools/build-uki/x86_64/config-* tools/build-uki/x86_64/efi-vars.ovmf + rm -rf tools/build-uki/aarch64/boot.disk tools/build-uki/aarch64/stage1 tools/build-uki/aarch64/tmp tools/build-uki/aarch64/*.img tools/build-uki/aarch64/*.efi tools/build-uki/aarch64/config-* tools/build-uki/aarch64/efi-vars.ovmf distclean: clean - $(MAKE) -C downloads clean - $(MAKE) -C keys clean + $(MAKE) -C tools/build-uki clean + $(MAKE) -C tools/build-uki/keys clean + $(MAKE) -C tools/qemu-test clean -# Generate separate rules for each artifact to avoid grouped target behavior -define ARTIFACT_RULE -.PRECIOUS: %/$(1) -%/$(1):: - @mkdir -p $$* - @make -C downloads $$@ - @cp downloads/$$@ $$@ -endef +# Download dependencies via tools/build-uki Makefile +tools/build-uki/%/busybox: + $(MAKE) -C tools/build-uki $*/busybox -$(foreach artifact,busybox stub.efi kernel.rpm,$(eval $(call ARTIFACT_RULE,$(artifact)))) +tools/build-uki/%/stub.efi: + $(MAKE) -C tools/build-uki $*/stub.efi -downloads/%: - $(MAKE) -C downloads $* +tools/build-uki/%/kernel.rpm: + $(MAKE) -C tools/build-uki $*/kernel.rpm + +tools/qemu-test/%: + $(MAKE) -C tools/qemu-test $* ##################################################################### @@ -72,7 +73,7 @@ docker-buildx-setup: docker buildx inspect --bootstrap # Build runtime image for current platform only and load into Docker -docker-runtime: x86_64/busybox x86_64/stage1 aarch64/busybox aarch64/stage1 +docker-runtime: tools/build-uki/x86_64/busybox tools/build-uki/x86_64/stage1 tools/build-uki/aarch64/busybox tools/build-uki/aarch64/stage1 docker buildx build \ -f Dockerfile.runtime \ -t $(RUNTIME_IMAGE) \ @@ -80,7 +81,7 @@ docker-runtime: x86_64/busybox x86_64/stage1 aarch64/busybox aarch64/stage1 . # Build multi-arch and export to OCI tar (for local multi-arch without registry) -docker-runtime-oci: x86_64/busybox x86_64/stage1 aarch64/busybox aarch64/stage1 +docker-runtime-oci: tools/build-uki/x86_64/busybox tools/build-uki/x86_64/stage1 tools/build-uki/aarch64/busybox tools/build-uki/aarch64/stage1 docker buildx build \ --platform linux/amd64,linux/arm64 \ -f Dockerfile.runtime \ @@ -129,17 +130,17 @@ docker-shell-dev: docker-build-dev # Build the UKI and boot disk for a specific architecture # This creates: UKI, disk image with EFI boot structure -%/boot.disk: %/busybox %/stage1 %/stub.efi %/kernel.rpm keys/db.crt +tools/build-uki/%/boot.disk: tools/build-uki/%/busybox tools/build-uki/%/stage1 tools/build-uki/%/stub.efi tools/build-uki/%/kernel.rpm tools/build-uki/keys/db.crt $(DOCKER_RUN) $(DOCKER_OPT_DOCKER) -e ARCH=$* \ - $(BUILD_IMAGE) ./scripts/build.sh + $(BUILD_IMAGE) ./tools/build-uki/build.sh -boot-%: downloads/ec2-metadata-mock-linux-amd64 %/boot.disk user-data.json +boot-%: tools/qemu-test/ec2-metadata-mock-linux-amd64 tools/build-uki/%/boot.disk user-data.json $(DOCKER_RUN) -e ARCH=$* $(DOCKER_OPT_KVM) \ -e YES_INSIDE_DOCKER_DO_DANGEROUS_IPTABLES=1 --cap-add=NET_ADMIN --device=/dev/net/tun \ - $(DEV_IMAGE) ./scripts/boot.sh + $(DEV_IMAGE) ./tools/qemu-test/boot.sh -%/stage1: docker-build-base - mkdir -p $* +tools/build-uki/%/stage1: docker-build-base + mkdir -p tools/build-uki/$* $(DOCKER_RUN) -e ARCH=$* $(DOCKER_SAMEUSER) $(BUILD_IMAGE) \ bash -c "rustup target add $*-unknown-linux-musl && cargo build --release --locked --all --target $*-unknown-linux-musl" cp target/$*-unknown-linux-musl/release/stage1 $@ diff --git a/crates/stage1/README.md b/crates/stage1/README.md index bb75868..49df755 100644 --- a/crates/stage1/README.md +++ b/crates/stage1/README.md @@ -144,6 +144,18 @@ Before executing stage2, the TPM PCRs are extended with cryptographic measuremen | **PCR 14** | Stage2 Binary | SHA256 hash of the downloaded binary | | **PCR 15** | Configuration | SHA256 hash of the entire JSON config | +## Attestation Trust Model + +The attestation is generated in the **virgin state** - before any user-provided code runs and before PCRs are extended. This ordering is critical, but it only provides security guarantees because the AK is a **restricted signing key**. + +A restricted signing key can only sign digests that the TPM itself produces. When the AK signs a `TPM2_Quote`, the quote contains the actual PCR values at signing time - generated internally by the TPM, not by software. This means even if stage2 has access to the AK handle, it can only produce quotes reflecting the *current* PCR state (with stage2 already measured into PCR14). It cannot forge a quote showing virgin-state PCRs. + +Without the restricted key constraint, the trust model collapses into tautology: stage2 could simply sign "I'm in virgin state" and the attestation would prove nothing. + +The attestation uses `H(H(binary) || H(config))` as the nonce - a hash of the binary hash concatenated with the config hash - binding the attestation to the specific intended workload. Once this virgin-state attestation exists, any future use of the same AK inherits this trust anchor. A verifier can reason: "this AK was attested in a clean PCR state with config X, therefore subsequent quotes from this AK come from a system that started from that trusted state." + +The restricted signing constraint is imposed by cloud vTPMs (notably GCP), which limits the AK to operations like `TPM2_Quote`, `TPM2_Certify`, and `TPM2_CertifyCreation`. While this can feel limiting, it's precisely what makes the trust model sound. + ## Building From the repository root: diff --git a/scripts/.dockerignore b/tools/build-uki/.dockerignore similarity index 100% rename from scripts/.dockerignore rename to tools/build-uki/.dockerignore diff --git a/downloads/.gitignore b/tools/build-uki/.gitignore similarity index 100% rename from downloads/.gitignore rename to tools/build-uki/.gitignore diff --git a/downloads/Makefile b/tools/build-uki/Makefile similarity index 88% rename from downloads/Makefile rename to tools/build-uki/Makefile index 36134e8..4d67dd0 100644 --- a/downloads/Makefile +++ b/tools/build-uki/Makefile @@ -33,15 +33,10 @@ AMAZON_KERNEL_RPM_aarch64 := $(AMAZON_KERNEL_PACKAGE)-$(AMAZON_KERNEL_VERSION).a AMAZON_KERNEL_URL_x86_64 := https://cdn.amazonlinux.com/al2023/blobstore/$(AMAZON_KERNEL_SHA256_x86_64)/$(AMAZON_KERNEL_RPM_x86_64) AMAZON_KERNEL_URL_aarch64 := https://cdn.amazonlinux.com/al2023/blobstore/$(AMAZON_KERNEL_SHA256_aarch64)/$(AMAZON_KERNEL_RPM_aarch64) -# Amazon EC2 Metadata Mock -AEMM_VERSION := v1.13.0 -AEMM_URL := https://github.com/aws/amazon-ec2-metadata-mock/releases/download/$(AEMM_VERSION)/ec2-metadata-mock-linux-amd64 -AEMM_SHA256 := 4f89ddc71ac53ce540bda1f9c340526d558eed8e41349761f2798acf1b254950 - ARCHS = x86_64 aarch64 TARGETS = busybox stub.efi kernel.rpm -all: $(ARCHS) ec2-metadata-mock-linux-amd64 +all: $(ARCHS) x86_64: $(addprefix x86_64/,$(TARGETS)) aarch64: $(addprefix aarch64/,$(TARGETS)) @@ -85,17 +80,12 @@ aarch64/$(AMAZON_KERNEL_RPM_aarch64): aarch64/kernel.rpm: aarch64/$(AMAZON_KERNEL_RPM_aarch64) @ln -sf $(notdir $<) $@ -# Download Amazon EC2 Metadata Mock -ec2-metadata-mock-linux-amd64: - @./download-and-verify.sh "$@" "$(AEMM_SHA256)" "$(AEMM_URL)" - @chmod +x $@ - # Retrieve package URLs from dnf kernel-hash-%: docker run --rm --platform linux/amd64 amazonlinux:latest bash -c "dnf repoquery --forcearch=$* --arch=$* --location $(AMAZON_KERNEL_PACKAGE)-$(AMAZON_KERNEL_VERSION).$*" clean: - rm -rf x86_64 aarch64 ec2-metadata-mock-linux-amd64 + rm -rf x86_64 aarch64 # Check all download URLs are still valid check-urls: @@ -106,4 +96,3 @@ check-urls: @curl -sfI "$(SYSTEMD_BOOT_URL_aarch64)" > /dev/null && echo "OK: systemd-boot aarch64" || echo "FAIL: systemd-boot aarch64" @curl -sfI "$(AMAZON_KERNEL_URL_x86_64)" > /dev/null && echo "OK: kernel x86_64" || echo "FAIL: kernel x86_64" @curl -sfI "$(AMAZON_KERNEL_URL_aarch64)" > /dev/null && echo "OK: kernel aarch64" || echo "FAIL: kernel aarch64" - @curl -sfI "$(AEMM_URL)" > /dev/null && echo "OK: ec2-metadata-mock" || echo "FAIL: ec2-metadata-mock" diff --git a/scripts/build.sh b/tools/build-uki/build.sh similarity index 96% rename from scripts/build.sh rename to tools/build-uki/build.sh index 13082da..4729817 100755 --- a/scripts/build.sh +++ b/tools/build-uki/build.sh @@ -4,16 +4,16 @@ set -euo pipefail # Get the absolute path of the script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Setup secure boot keys -KEYDIR=keys +# Setup secure boot keys (relative to script directory) +KEYDIR="${SCRIPT_DIR}/keys" # Get architecture from environment (default to x86_64) ARCH=${ARCH:-x86_64} echo "=== Building UKI for Amazon Linux 2023 (${ARCH}) ===" -# Create architecture-specific output directory -OUTPUT_DIR="${ARCH}" +# Output directory (same as dependencies - everything self-contained) +OUTPUT_DIR="${SCRIPT_DIR}/${ARCH}" mkdir -p "${OUTPUT_DIR}/tmp" # Determine the correct systemd-efistub filename, PE format, and objcopy command based on architecture @@ -29,14 +29,14 @@ else fi # Use systemd-boot stub extracted by Makefile from Amazon Linux RPM -if [ ! -f "${ARCH}/stub.efi" ]; then - echo "ERROR: systemd-boot stub not found at ${ARCH}/stub.efi" - echo "Please run 'make ${ARCH}/stub.efi' first" +if [ ! -f "${OUTPUT_DIR}/stub.efi" ]; then + echo "ERROR: systemd-boot stub not found at ${OUTPUT_DIR}/stub.efi" + echo "Please run 'make tools/build-uki/${ARCH}/stub.efi' first" exit 1 fi echo "Extracting kernel RPM..." -rpm2cpio "downloads/${ARCH}"/kernel6.12-*.rpm | (cd "${OUTPUT_DIR}/tmp" && cpio --quiet -idmu) +rpm2cpio "${OUTPUT_DIR}"/kernel6.12-*.rpm | (cd "${OUTPUT_DIR}/tmp" && cpio --quiet -idmu) if [ ! -d "${OUTPUT_DIR}/tmp/lib/modules" ]; then echo "ERROR: kernel modules not found after extraction!" exit 1 @@ -78,11 +78,11 @@ mkdir -p "${INITRAMFS_DIR}"/{bin,sbin,etc,proc,sys,dev,lib,lib64,tmp} # Copy busybox and create symlinks echo "Installing busybox..." -cp "${ARCH}/busybox" "${INITRAMFS_DIR}/bin/" +cp "${OUTPUT_DIR}/busybox" "${INITRAMFS_DIR}/bin/" # Copy stage1 binary echo "Installing stage1..." -cp "${ARCH}/stage1" "${INITRAMFS_DIR}/bin/stage1" +cp "${OUTPUT_DIR}/stage1" "${INITRAMFS_DIR}/bin/stage1" chmod +x "${INITRAMFS_DIR}/bin/stage1" # Copy required kernel modules diff --git a/downloads/download-and-verify.sh b/tools/build-uki/download-and-verify.sh similarity index 100% rename from downloads/download-and-verify.sh rename to tools/build-uki/download-and-verify.sh diff --git a/scripts/init b/tools/build-uki/init similarity index 100% rename from scripts/init rename to tools/build-uki/init diff --git a/keys/.gitignore b/tools/build-uki/keys/.gitignore similarity index 100% rename from keys/.gitignore rename to tools/build-uki/keys/.gitignore diff --git a/keys/Makefile b/tools/build-uki/keys/Makefile similarity index 100% rename from keys/Makefile rename to tools/build-uki/keys/Makefile diff --git a/scripts/normalize-fat-timestamps.py b/tools/build-uki/normalize-fat-timestamps.py similarity index 100% rename from scripts/normalize-fat-timestamps.py rename to tools/build-uki/normalize-fat-timestamps.py diff --git a/scripts/udhcpc.script b/tools/build-uki/udhcpc.script similarity index 100% rename from scripts/udhcpc.script rename to tools/build-uki/udhcpc.script diff --git a/tools/publish/azure/.gitkeep b/tools/publish/azure/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/ec2-create-ami.sh b/tools/publish/ec2/create-ami.sh similarity index 97% rename from scripts/ec2-create-ami.sh rename to tools/publish/ec2/create-ami.sh index 50899bf..275d1d3 100755 --- a/scripts/ec2-create-ami.sh +++ b/tools/publish/ec2/create-ami.sh @@ -30,8 +30,6 @@ else EC2_ARCH="${ARCH}" fi -WORK_DIR="${ARCH}" - # Download and verify from GitHub release or use local files if [ "${VERSION}" != "local" ]; then echo "=== Downloading release ${VERSION} from GitHub ===" @@ -70,6 +68,10 @@ if [ "${VERSION}" != "local" ]; then echo "Using verified release files from ${VERSION}" else echo "=== Using local build files ===" + # Get script directory and compute repo root + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" + WORK_DIR="${REPO_ROOT}/tools/build-uki/${ARCH}" fi IMAGE_FILE="${WORK_DIR}/boot.disk" diff --git a/scripts/ec2-create-vmimport-role.sh b/tools/publish/ec2/create-vmimport-role.sh similarity index 100% rename from scripts/ec2-create-vmimport-role.sh rename to tools/publish/ec2/create-vmimport-role.sh diff --git a/tools/publish/gcp/.gitkeep b/tools/publish/gcp/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/.gitignore b/tools/qemu-test/.gitignore similarity index 100% rename from scripts/.gitignore rename to tools/qemu-test/.gitignore diff --git a/tools/qemu-test/Makefile b/tools/qemu-test/Makefile new file mode 100644 index 0000000..7ac89b6 --- /dev/null +++ b/tools/qemu-test/Makefile @@ -0,0 +1,19 @@ +.PHONY: all clean + +# Amazon EC2 Metadata Mock +AEMM_VERSION := v1.13.0 +AEMM_URL := https://github.com/aws/amazon-ec2-metadata-mock/releases/download/$(AEMM_VERSION)/ec2-metadata-mock-linux-amd64 +AEMM_SHA256 := 4f89ddc71ac53ce540bda1f9c340526d558eed8e41349761f2798acf1b254950 + +all: ec2-metadata-mock-linux-amd64 + +ec2-metadata-mock-linux-amd64: + @echo "Downloading EC2 metadata mock..." + @curl -fsSL -o "$@.tmp" "$(AEMM_URL)" + @echo "$(AEMM_SHA256) $@.tmp" | sha256sum -c - || (rm -f "$@.tmp"; exit 1) + @mv "$@.tmp" "$@" + @chmod +x "$@" + @echo "✓ Downloaded and verified: $@" + +clean: + rm -f ec2-metadata-mock-linux-amd64 diff --git a/scripts/boot.sh b/tools/qemu-test/boot.sh similarity index 85% rename from scripts/boot.sh rename to tools/qemu-test/boot.sh index c4a67c7..6b675c7 100755 --- a/scripts/boot.sh +++ b/tools/qemu-test/boot.sh @@ -4,6 +4,9 @@ set -euox pipefail # Get the absolute path of the script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Repository root (computed from script location: tools/qemu-test -> ../..) +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + if [ "$YES_INSIDE_DOCKER_DO_DANGEROUS_IPTABLES" != 1 ]; then echo "Error: not inside docker, refusing to do dangerous stuff!!" exit 1 @@ -13,14 +16,14 @@ fi ARCH=${ARCH:-x86_64} # Default user-data file -KEYDIR=keys -USER_DATA="user-data.json" +KEYDIR="${REPO_ROOT}/tools/build-uki/keys" +USER_DATA="${REPO_ROOT}/user-data.json" TMP=/tmp echo "=== Booting UKI with Secure Boot + TPM 2.0 (${ARCH}) ===" echo "User-data file: ${USER_DATA}" -AMMM=${SCRIPT_DIR}/../downloads/ec2-metadata-mock-linux-amd64 +AMMM=${SCRIPT_DIR}/ec2-metadata-mock-linux-amd64 # Check dependencies if [ ! -f ${AMMM} ]; then @@ -28,13 +31,15 @@ if [ ! -f ${AMMM} ]; then exit 1 fi -if [ ! -f "${ARCH}/boot.disk" ]; then - echo "Error: ${ARCH}/boot.disk not found. Run 'make ${ARCH}' first." +if [ ! -f "${USER_DATA}" ]; then + echo "Error: User-data file '${USER_DATA}' not found." exit 1 fi -if [ ! -f "${USER_DATA}" ]; then - echo "Error: User-data file '${USER_DATA}' not found." +# Boot disk location (in tools/build-uki) +BOOT_DISK="${REPO_ROOT}/tools/build-uki/${ARCH}/boot.disk" +if [ ! -f "${BOOT_DISK}" ]; then + echo "Error: ${BOOT_DISK} not found. Run 'make ${ARCH}' first." exit 1 fi @@ -66,7 +71,7 @@ else exit 1 fi -OVMF_VARS_ORIG="${ARCH}/efi-vars.ovmf" +OVMF_VARS_ORIG="${REPO_ROOT}/tools/build-uki/${ARCH}/efi-vars.ovmf" OVMF_VARS="/tmp/efi-vars.ovmf" if [ ! -f "${OVMF_CODE}" ]; then @@ -79,12 +84,17 @@ cp "${OVMF_VARS_ORIG}" "${OVMF_VARS}" # Setup TPM state directory mkdir -p $TMP/tpm-state -# Start software TPM +# Provision NV indices for GCP-style attestation (idempotent) +# This starts its own swtpm instance with tpm2-tools-compatible sockets, then shuts it down +${SCRIPT_DIR}/provision-test-tpm.sh $TMP/tpm-state + +# Start swtpm for QEMU (original way - just ctrl socket) swtpm socket --tpmstate dir=$TMP/tpm-state \ --ctrl type=unixio,path=$TMP/swtpm-sock \ --tpm2 \ --pid file=$TMP/swtpm.pid \ --daemon +sleep 1 # Cleanup function cleanup() { @@ -175,7 +185,7 @@ ${QEMU_CMD} \ -chardev socket,id=chrtpm,path=$TMP/swtpm-sock \ -tpmdev emulator,id=tpm0,chardev=chrtpm \ -device ${TPM_DEVICE},tpmdev=tpm0 \ - -drive file=${ARCH}/boot.disk,format=raw,if=none,id=boot \ + -drive file=${BOOT_DISK},format=raw,if=none,id=boot \ -device nvme,serial=boot,drive=boot,bootindex=0 \ -netdev tap,id=net0,ifname=tap0,script=no \ -device virtio-net-pci,netdev=net0 \ diff --git a/tools/qemu-test/provision-test-tpm.sh b/tools/qemu-test/provision-test-tpm.sh new file mode 100755 index 0000000..0c1d564 --- /dev/null +++ b/tools/qemu-test/provision-test-tpm.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# Provision swtpm with GCP-style NV indices for testing +# +# This script sets up a software TPM with the NV indices that vaportpm +# expects for GCP-style attestation, enabling local QEMU testing. +# +# Usage: provision-test-tpm.sh [socket_path] +# socket_path: Path to swtpm control socket (default: /tmp/swtpm-sock) + +set -euox pipefail + +TPM_STATE_DIR="${1:-/tmp/tpm-state}" +SOCKET="/tmp/swtpm-provision-sock" +export TPM2TOOLS_TCTI="swtpm:path=$SOCKET" +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT +cd $TMPDIR + +# NV indices (GCP standard) +NV_RSA_TEMPLATE=0x01c10001 +NV_ECC_CERT=0x01c10002 +NV_ECC_TEMPLATE=0x01c10003 + +# Start swtpm with both server+ctrl sockets (required for tpm2-tools TCTI) +swtpm socket --tpmstate dir=$TPM_STATE_DIR \ + --server type=unixio,path=$SOCKET \ + --ctrl type=unixio,path=${SOCKET}.ctrl \ + --tpm2 \ + --daemon + +sleep 1 + +# Initialize and startup TPM +swtpm_ioctl --unix ${SOCKET}.ctrl -i +tpm2_startup -c + +# Check if already provisioned (idempotent) +if tpm2_nvreadpublic $NV_ECC_TEMPLATE 2>/dev/null; then + echo "TPM already provisioned, skipping" + exit 0 +fi + +echo "Provisioning TPM for GCP-style attestation..." + +# 1. Create hardcoded TPMT_PUBLIC template (empty unique) +# Based on vaportpm's template but WITHOUT restricted attribute for tpm2-tools compatibility: +# type=ECC(0x0023), nameAlg=SHA256(0x000b), attrs=0x00040072 (no restricted) +# symmetric=Null, scheme=ECDSA-SHA256, curve=P256, kdf=Null +# unique x_size=0, y_size=0 +# Note: tpm2_createprimary can't create restricted ECC signing keys (symmetric bug) +echo -n "0023000b00040072000000100018000b0003001000000000" | xxd -r -p > template.bin + +# 2. Write template to NV FIRST (before creating key) +tpm2_nvdefine $NV_ECC_TEMPLATE -s 24 -a "ownerread|ownerwrite|authread|authwrite" +tpm2_nvwrite $NV_ECC_TEMPLATE -i template.bin + +# 3. Create key (non-restricted to work around tpm2-tools symmetric bug) +tpm2_createprimary -C e -G ecc:ecdsa-sha256 \ + -a 'fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign' \ + -c ak.ctx + +# 4. Extract public key as PEM for certificate +tpm2_readpublic -c ak.ctx -f pem -o ak.pub.pem + +# 5. Create test CA +openssl ecparam -genkey -name prime256v1 -noout -out ca.key +openssl req -new -x509 -key ca.key -out ca.crt -days 3650 \ + -subj "/CN=LockBoot Test CA" -batch + +# 6. Create AK cert with TPM's public key (using -force_pubkey) +openssl req -new -key ca.key -subj "/CN=Test AK" -out ak.csr -batch +openssl x509 -req -in ak.csr -CA ca.crt -CAkey ca.key \ + -force_pubkey ak.pub.pem -out ak.crt -days 3650 \ + -extfile <(echo "keyUsage=critical,digitalSignature") \ + -CAcreateserial + +# 7. Convert cert to DER and write to NV +openssl x509 -in ak.crt -outform DER -out ak.crt.der +tpm2_nvdefine $NV_ECC_CERT -s $(stat -c%s ak.crt.der) -a "ownerread|ownerwrite|authread|authwrite" +tpm2_nvwrite $NV_ECC_CERT -i ak.crt.der + +# 8. Cleanup TPM transient objects +tpm2_flushcontext -t + +# 9. Clean shutdown to save state (boot.sh will restart swtpm) +tpm2_shutdown +swtpm_ioctl --unix ${SOCKET}.ctrl -s + +echo "TPM provisioned successfully for GCP-style attestation"