Skip to content
Draft
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
69 changes: 66 additions & 3 deletions .github/workflows/seismic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
# the VHD → the `dev` container of the seismicimages storage
# account, as seismic_<version>.vhd (Azure's disk
# import reads only blob storage)
# the tarball → gs://seismic-tee-gcp-images, as seismic_<version>.tar.gz,
# and registered in project testnet-477314 as the GCE
# image seismic-<version with . as ->
# the measurements → a GitHub prerelease tagged seismic_<version>, as
# measurements.azure-tdx.json + SHA256SUMS
# measurements.azure-tdx.json, measurements.gcp-tdx.json
# + SHA256SUMS
# the founding inputs → the same release: the seismic-reth and summit
# binaries taken back out of the built initrd and
# the two genesis files a network founded on this
Expand Down Expand Up @@ -54,6 +58,32 @@
# The second grant is for image.json alone: `az storage account show` reads
# the account's ARM ID from the management plane, and the blob role does not
# include that read. Reader on the one account is the narrowest form.
#
# The GCP twin. Two repo secrets — GCP_WORKLOAD_IDENTITY_PROVIDER (the
# provider's full resource name) and GCP_SERVICE_ACCOUNT (its email). No GCP
# credential is stored either: google-github-actions/auth exchanges the OIDC
# token through Workload Identity Federation for a token of the service
# account below, which may write objects in the one bucket and register
# images in the one project, and trusts the same subject:
#
# PROJECT=testnet-477314
# NUMBER=$(gcloud projects describe "$PROJECT" --format='value(projectNumber)')
# gcloud iam workload-identity-pools create seismic-tee-github --project "$PROJECT" --location global
# gcloud iam workload-identity-pools providers create-oidc github --project "$PROJECT" --location global \
# --workload-identity-pool seismic-tee-github \
# --issuer-uri https://token.actions.githubusercontent.com \
# --attribute-mapping 'google.subject=assertion.sub,attribute.repository=assertion.repository' \
# --attribute-condition "assertion.sub == 'repo:SeismicSystems/seismic-images:ref:refs/heads/seismic'"
# gcloud iam service-accounts create seismic-tee-images-publish --project "$PROJECT"
# SA=seismic-tee-images-publish@$PROJECT.iam.gserviceaccount.com
# gcloud iam service-accounts add-iam-policy-binding "$SA" --project "$PROJECT" \
# --role roles/iam.workloadIdentityUser \
# --member "principalSet://iam.googleapis.com/projects/$NUMBER/locations/global/workloadIdentityPools/seismic-tee-github/attribute.repository/SeismicSystems/seismic-images"
# gcloud storage buckets add-iam-policy-binding gs://seismic-tee-gcp-images --member "serviceAccount:$SA" --role roles/storage.objectUser
# gcloud projects add-iam-policy-binding "$PROJECT" --member "serviceAccount:$SA" --role roles/compute.storageAdmin
#
# GCP_WORKLOAD_IDENTITY_PROVIDER=projects/$NUMBER/locations/global/workloadIdentityPools/seismic-tee-github/providers/github
# GCP_SERVICE_ACCOUNT=$SA
name: Seismic image CI

on:
Expand Down Expand Up @@ -221,6 +251,12 @@ jobs:
echo "measurement_id $stamped does not name the built VHD $vhd" >&2
exit 1
}
stamped=$(jq -r .measurement_id build/measurements.gcp-tdx.json)
tarball=$(basename "$(compgen -G 'build/seismic_*.tar.gz')")
[ "$stamped" = "$tarball" ] || {
echo "measurement_id $stamped does not name the built tarball $tarball" >&2
exit 1
}

- name: Show build artifacts
run: |
Expand Down Expand Up @@ -270,7 +306,9 @@ jobs:
name: image-seismic
path: |
build/seismic_*.vhd
build/seismic_*.tar.gz
build/measurements.azure-tdx.json
build/measurements.gcp-tdx.json
build/SHA256SUMS
build/seismic-reth
build/summit
Expand Down Expand Up @@ -298,6 +336,9 @@ jobs:
env:
AZURE_STORAGE_ACCOUNT: seismicimages
AZURE_CONTAINER: dev
GCP_PROJECT: testnet-477314
GCP_BUCKET: seismic-tee-gcp-images
GCP_LOCATION: us-central1
steps:
# For the Makefile: `make push-azure` is the one definition of the
# upload and the blob URL, for CI and for a human alike.
Expand All @@ -320,6 +361,10 @@ jobs:
echo "the measurements name $stamped, which is not among the built artifacts" >&2
exit 1
}
[ -f "build/$image.tar.gz" ] || {
echo "build/$image.tar.gz is not among the built artifacts" >&2
exit 1
}
echo "image=$image" >> "$GITHUB_OUTPUT"

- uses: azure/login@v3
Expand All @@ -335,6 +380,20 @@ jobs:
AZURE_CONTAINER="$AZURE_CONTAINER"
VHD="build/${{ steps.image.outputs.image }}.vhd"

- uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v3

- name: Mirror the tarball to GCS and register the GCE image
run: >-
make push-gcp
GCP_PROJECT="$GCP_PROJECT"
GCP_BUCKET="$GCP_BUCKET"
GCP_LOCATION="$GCP_LOCATION"
TARBALL="build/${{ steps.image.outputs.image }}.tar.gz"

# The machine-readable record of this release: the blob URL just
# pushed to and the account's ARM ID (asked of Azure here, where the
# job is logged in, rather than written down anywhere), the
Expand All @@ -343,7 +402,7 @@ jobs:
# notes, which are rendered from it so the two cannot disagree. The
# script owns the shape.
- name: Write image.json
run: make image-json COMMIT="$GITHUB_SHA"
run: make image-json COMMIT="$GITHUB_SHA" GCP_PROJECT="$GCP_PROJECT" GCP_BUCKET="$GCP_BUCKET"

# Build provenance over every asset the release will carry, signed
# with a short-lived Sigstore certificate issued to this workflow on
Expand All @@ -364,6 +423,7 @@ jobs:
with:
subject-path: |
build/measurements.azure-tdx.json
build/measurements.gcp-tdx.json
build/SHA256SUMS
build/seismic-reth
build/summit
Expand All @@ -383,14 +443,16 @@ jobs:
run: |
fact() { jq -r "$1" build/image.json; }
vhd_blob_url=$(fact '.targets["azure-tdx"].vhd_blob_url')
gce_image=$(fact '.targets["gcp-tdx"].gce_image')
reth=$(fact .sources.seismic_reth); summit=$(fact .sources.summit); enclave=$(fact .sources.enclave)
cat > "$RUNNER_TEMP/notes.md" <<EOF
Seismic node image \`$IMAGE\`, built by this repository at $GITHUB_SHA.

| | |
|---|---|
| \`vhd_blob_url\` | \`$vhd_blob_url\` |
| measurements | [\`measurements.azure-tdx.json\`](https://github.com/$GITHUB_REPOSITORY/releases/download/$IMAGE/measurements.azure-tdx.json) |
| \`gce_image\` | \`$gce_image\` |
| measurements | [\`measurements.azure-tdx.json\`](https://github.com/$GITHUB_REPOSITORY/releases/download/$IMAGE/measurements.azure-tdx.json), [\`measurements.gcp-tdx.json\`](https://github.com/$GITHUB_REPOSITORY/releases/download/$IMAGE/measurements.gcp-tdx.json) |
| \`image.json\` | where the bytes are and what they are, machine-readably: the blob URL and its storage account's ARM ID, the measurements asset, the UKI's sha256 and the source commits, per cloud target |
| checksums | \`SHA256SUMS\` over the \`.efi\` (the UKI the VHD wraps), \`image.json\` and every founding input below |

Expand Down Expand Up @@ -442,6 +504,7 @@ jobs:
fail_on_unmatched_files: true
files: |
build/measurements.azure-tdx.json
build/measurements.gcp-tdx.json
build/SHA256SUMS
build/seismic-reth
build/summit
Expand Down
42 changes: 38 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,39 @@ push-azure: ## Upload the built .vhd to Azure blob storage (uses AZURE_CONTAINER
push-azure-dev: ## Upload the .vhd to dev/ (ephemeral — default)
@$(MAKE) push-azure AZURE_CONTAINER=dev

# GCE boots a registered image, not a bucket object, so the push is two
# steps: the tarball into the bucket, then the image registered from it under
# the tag with `_` and `.` as `-`, the characters a GCE name refuses.
GCP_PROJECT ?= testnet-477314
GCP_BUCKET ?= seismic-tee-gcp-images
GCP_LOCATION ?= us-central1
TARBALL ?= $(shell ls build/seismic_*.tar.gz 2>/dev/null | head -1)
push-gcp: ## Upload the built .tar.gz to GCS and register it as a GCE image (uses GCP_PROJECT, GCP_BUCKET, GCP_LOCATION, TARBALL)
@if [ ! -e "$(TARBALL)" ]; then \
echo "Error: $(TARBALL) not found. Run 'make build' or 'make build-dev' first, or pass TARBALL=<path>." >&2; \
exit 1; \
fi; \
OBJECT=$$(basename $$(realpath $(TARBALL))); \
GCE=$$(echo "$${OBJECT%.tar.gz}" | tr '_.' '--'); \
if gcloud storage objects describe "gs://$(GCP_BUCKET)/$$OBJECT" --project $(GCP_PROJECT) >/dev/null 2>&1; then \
echo "$$OBJECT is already in gs://$(GCP_BUCKET)/; not overwriting."; \
else \
echo "Uploading $$OBJECT → gs://$(GCP_BUCKET)/ ..."; \
gcloud storage cp --no-clobber $(TARBALL) "gs://$(GCP_BUCKET)/$$OBJECT" --project $(GCP_PROJECT); \
fi; \
if gcloud compute images describe "$$GCE" --project $(GCP_PROJECT) >/dev/null 2>&1; then \
echo "$$GCE is already registered in $(GCP_PROJECT); not replacing."; \
else \
echo "Registering $$GCE from gs://$(GCP_BUCKET)/$$OBJECT ..."; \
gcloud compute images create "$$GCE" --project $(GCP_PROJECT) \
--source-uri "gs://$(GCP_BUCKET)/$$OBJECT" \
--guest-os-features=UEFI_COMPATIBLE,GVNIC,TDX_CAPABLE \
--storage-location $(GCP_LOCATION); \
fi; \
echo ""; \
echo "Here is the GCE image:"; \
echo " projects/$(GCP_PROJECT)/global/images/$$GCE"

push-azure-releases: ## Upload the .vhd to releases/ (long-term)
@$(MAKE) push-azure AZURE_CONTAINER=releases

Expand Down Expand Up @@ -147,8 +180,8 @@ measure-gcp: ## Predict RTMR1 and RTMR2 for a GCP TDX boot (no vTPM) of the buil
# summit binaries out of the initrd, both genesis files, and SHA256SUMS over
# them, the measurements and the UKI. See the readme's "Founding inputs".
.PHONY: founding-inputs
founding-inputs: measure ## Gather the founding inputs into build/ and write SHA256SUMS (uses INITRD, FILE)
@$(WRAPPER) scripts/seismic/founding_inputs.sh $(INITRD) $(FILE) $(MEASUREMENTS_AZURE)
founding-inputs: measure measure-gcp ## Gather the founding inputs into build/ and write SHA256SUMS (uses INITRD, FILE)
@$(WRAPPER) scripts/seismic/founding_inputs.sh $(INITRD) $(FILE) $(MEASUREMENTS_AZURE) $(MEASUREMENTS_GCP)

# Everything a release carries that a rebuild can reproduce, from one FILE
# and INITRD: the measurements and the founding inputs with their
Expand All @@ -167,10 +200,11 @@ release-assets: founding-inputs ## Measure the UKI and gather the founding input
SUMS ?= build/SHA256SUMS
COMMIT ?= $(shell git rev-parse HEAD)
.PHONY: image-json
image-json: ## Write build/image.json and add it to SHA256SUMS (uses AZURE_STORAGE_ACCOUNT, AZURE_CONTAINER, AZURE_STORAGE_ACCOUNT_ID, COMMIT)
image-json: ## Write build/image.json and add it to SHA256SUMS (uses AZURE_STORAGE_ACCOUNT, AZURE_CONTAINER, AZURE_STORAGE_ACCOUNT_ID, GCP_PROJECT, GCP_BUCKET, COMMIT)
@AZURE_STORAGE_ACCOUNT=$(AZURE_STORAGE_ACCOUNT) AZURE_CONTAINER=$(AZURE_CONTAINER) \
AZURE_STORAGE_ACCOUNT_ID=$(AZURE_STORAGE_ACCOUNT_ID) \
scripts/seismic/image_json.sh $(MEASUREMENTS_AZURE) $(SUMS) $(COMMIT)
GCP_PROJECT=$(GCP_PROJECT) GCP_BUCKET=$(GCP_BUCKET) \
scripts/seismic/image_json.sh $(MEASUREMENTS_AZURE) $(MEASUREMENTS_GCP) $(SUMS) $(COMMIT)

##@ Utilities

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Where the image's bytes are and what they are, in one machine-readable file, so
}
```

`targets` is keyed by attestation type, one entry per cloud the image is published for — Azure only today; a GCP image adds an entry, not a schema. Per target: the artifact the nodes boot from, the storage account's ARM ID (Azure's managed-disk import refuses to read a blob from another subscription or resource group without it, and the URL names the account but neither of those; a SAS URL would avoid the requirement but expires, so it has no place in a release), the measurements asset for that target, and the sha256 of the artifact where it is reproducible — the `.efi`, since the VHD's wrapping is not. Once per file: the image name (the tag, and the stem of `measurement_id`), this repository's commit, and the three source pins from `sources.yaml`. The publish job writes it (`make image-json`, [`scripts/seismic/image_json.sh`](scripts/seismic/image_json.sh)) after the VHD is pushed, asking Azure for the account's ID rather than carrying it in this repo, and renders the release notes from it. It refuses a measurements file and a `SHA256SUMS` that name different UKIs, so the two halves of a release cannot come from different builds. It is the one asset `make release-assets` does not produce, since the ID is a fact about where the bytes were put, not about the build.
`targets` is keyed by attestation type, one entry per cloud the image is published for: `azure-tdx` (the VHD's blob and storage account) and `gcp-tdx` (the registered GCE image and the tarball it was made from). Per target: the artifact the nodes boot from, the storage account's ARM ID (Azure's managed-disk import refuses to read a blob from another subscription or resource group without it, and the URL names the account but neither of those; a SAS URL would avoid the requirement but expires, so it has no place in a release), the measurements asset for that target, and the sha256 of the artifact where it is reproducible — the `.efi`, since the VHD's wrapping is not. Once per file: the image name (the tag, and the stem of `measurement_id`), this repository's commit, and the three source pins from `sources.yaml`. The publish job writes it (`make image-json`, [`scripts/seismic/image_json.sh`](scripts/seismic/image_json.sh)) after the VHD is pushed, asking Azure for the account's ID rather than carrying it in this repo, and renders the release notes from it. It refuses a measurements file and a `SHA256SUMS` that name different UKIs, so the two halves of a release cannot come from different builds. It is the one asset `make release-assets` does not produce, since the ID is a fact about where the bytes were put, not about the build.

## What's in the image

Expand Down
11 changes: 9 additions & 2 deletions scripts/seismic/founding_inputs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
# (the Makefile's INITRD, FILE and MEASUREMENTS_AZURE)
set -euo pipefail

usage='usage: founding_inputs.sh <initrd> <efi> <measurements>'
usage='usage: founding_inputs.sh <initrd> <efi> <measurements-azure> <measurements-gcp>'
initrd=${1:?$usage}
efi=${2:?$usage}
measurements=${3:?$usage}
gcp_measurements=${4:?$usage}
# zstd ignores a symlink, and the build leaves `latest.initrd` as one.
initrd=$(realpath "$initrd")
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
Expand Down Expand Up @@ -100,6 +101,12 @@ stamped=$(jq -r '.measurement_id // empty' "$measurements")
echo "$measurements measures ${stamped:-no named image}, not $vhd — it is another build's" >&2
exit 1
}
tarball=${uki%.efi}.tar.gz
stamped=$(jq -r '.measurement_id // empty' "$gcp_measurements")
[ "$stamped" = "$tarball" ] || {
echo "$gcp_measurements measures ${stamped:-no named image}, not $tarball — it is another build's" >&2
exit 1
}

# One SHA256SUMS over everything the release carries: the UKI — the whole
# image identity, since the VHD only wraps it on a FAT partition, and that
Expand All @@ -111,6 +118,6 @@ stamped=$(jq -r '.measurement_id // empty' "$measurements")
# `--ignore-missing` checks whichever subset was taken.
(cd build && sha256sum \
"$uki" seismic-reth summit reth-genesis.json summit-genesis-starter.toml \
"$(basename "$measurements")") \
"$(basename "$measurements")" "$(basename "$gcp_measurements")") \
> build/SHA256SUMS
cat build/SHA256SUMS
32 changes: 28 additions & 4 deletions scripts/seismic/image_json.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
# was built from. Shaped per cloud target from the start, so a GCP image
# adds an entry rather than a schema.
#
# Usage: image_json.sh <measurements> <SHA256SUMS> <commit> (`make image-json`)
# Usage: image_json.sh <measurements> <measurements-gcp> <SHA256SUMS> <commit> (`make image-json`)
# measurements the stamped measurements.azure-tdx.json; its measurement_id
# names the image, as `<image>.vhd`
# measurements-gcp the stamped measurements.gcp-tdx.json, `<image>.tar.gz`
# SHA256SUMS the checksum file `make release-assets` wrote; the UKI's line
# is read from it (and must name the same image), and
# image.json's own line is appended to it. image.json is
Expand All @@ -26,16 +27,23 @@
# AZURE_CONTAINER container within it (dev)
# AZURE_STORAGE_ACCOUNT_ID the account's ARM ID; asked of `az` when unset,
# which needs an `az login` that can read the account
# GCP_PROJECT project `make push-gcp` registered the GCE image in
# GCP_BUCKET bucket it pushed the tarball to
set -euo pipefail

measurements=${1:?usage: image_json.sh <measurements> <SHA256SUMS> <commit>}
sums=${2:?usage: image_json.sh <measurements> <SHA256SUMS> <commit>}
commit=${3:?usage: image_json.sh <measurements> <SHA256SUMS> <commit>}
usage='usage: image_json.sh <measurements> <measurements-gcp> <SHA256SUMS> <commit>'
measurements=${1:?$usage}
gcp_measurements=${2:?$usage}
sums=${3:?$usage}
commit=${4:?$usage}
account=${AZURE_STORAGE_ACCOUNT:-seismicimages}
container=${AZURE_CONTAINER:-dev}
gcp_project=${GCP_PROJECT:-testnet-477314}
gcp_bucket=${GCP_BUCKET:-seismic-tee-gcp-images}
# Resolved before the cd below, so the arguments are read from where the
# caller spelled them.
measurements=$(realpath "$measurements")
gcp_measurements=$(realpath "$gcp_measurements")
sums=$(realpath "$sums")
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
sources=modules/seismic/sources.yaml
Expand All @@ -47,6 +55,13 @@ if [ "$image" = "$stamped" ] || [ -z "$image" ]; then
echo "$measurements: measurement_id $stamped does not name a VHD" >&2
exit 1
fi
gcp_stamped=$(jq -r .measurement_id "$gcp_measurements")
[ "$gcp_stamped" = "$image.tar.gz" ] || {
echo "$gcp_measurements measures $gcp_stamped, not $image.tar.gz: the two measurements are not one build's" >&2
exit 1
}
# The GCE image name `make push-gcp` registers: the tag with `_` and `.` as `-`.
gce_image=$(printf '%s' "$image" | tr '_.' '--')

# The one line of SHA256SUMS that names the UKI, and it must name *this*
# image's: the measurements and the founding inputs are made by separate
Expand Down Expand Up @@ -94,6 +109,9 @@ jq -n \
--arg vhd_blob_url "$vhd_blob_url" \
--arg storage_account_id "$account_id" \
--arg measurements "$(basename "$measurements")" \
--arg gcp_measurements "$(basename "$gcp_measurements")" \
--arg gce_image "projects/$gcp_project/global/images/$gce_image" \
--arg tarball_url "https://storage.googleapis.com/$gcp_bucket/$image.tar.gz" \
--arg efi_sha256 "$efi_sha256" \
'{
image: $image,
Expand All @@ -105,6 +123,12 @@ jq -n \
storage_account_id: $storage_account_id,
measurements: $measurements,
efi_sha256: $efi_sha256
},
"gcp-tdx": {
gce_image: $gce_image,
tarball_url: $tarball_url,
measurements: $gcp_measurements,
efi_sha256: $efi_sha256
}
}
}' > "$out"
Expand Down