Skip to content

Commit 32459c4

Browse files
kariyclaude
andauthored
feat(tee-vm): dedicated snp-tools release stream, pinned in build-config (#637)
## Why Every `tee-vm-v*` release rebuilt `snp-digest` from scratch just to compute its measurement, and #636's installer additionally required a Rust toolchain on every SNP host to verify measurements. The verifier changes far less often than the VM image — it should be released once and reused. ## What - **New `amdsev-snp-tools-release` workflow** (dispatch with a version): `cargo test --locked`, build, smoke-run, publish `snp-tools-v<X.Y.Z>` with `snp-digest-<tag>` + a bare-hex `.sha256`. Release notes print the exact pin lines to adopt it. - **`build-config` pins the chosen release** (`SNP_DIGEST_RELEASE` + `SNP_DIGEST_SHA256`) — the checksum is trusted because build-config is versioned in git at the consuming tag, so the pin travels with every tee-vm tag. - **`amdsev-release.yml`**: "Build snp-tools" → "Obtain snp-digest": downloads the pinned binary (checksum-verified, smoke-run) to compute the published measurement, so the publisher and every verifying host run the *identical* binary; release notes name the pinned verifier. Empty pins fall back to the source build. - **`install.sh`**: reads the pins from the installed tag's `build-config` (sed extraction, not sourcing), downloads once with checksum + smoke-run gates, caches across re-runs, and drops the binary where `verify-build.sh`'s discovery already looks. Fallback chain: pinned prebuilt → cargo build → checksum-only with a loud warning. Also hardens all downloads with connect timeouts, retries, and a 1 KB/s speed floor — a rate-limited GitHub API call used to hang the install indefinitely. - The prebuilt is a convenience copy, never the trust root: auditors rebuild from the snp-tools tag's pinned source, and its checksum deliberately stays out of `build-info.txt` so `reproduce-release.sh` is unaffected. Docs updated (`release-pipeline.md` stream + runbook, `install.md` prerequisites). ## Validation shellcheck clean (CI set), 46 unit tests green (`test-install.sh`, incl. new `build_config_get` extraction tests), actionlint clean on both workflows, and a live `--dry-run` install against the just-published `tee-vm-v0.3.0+katana-v1.8.0-rc.8` (no pins there yet → correct source-build fallback messaging). ## Rollout 1. Merge; dispatch `amdsev-snp-tools-release` with `0.1.0`. 2. Follow-up PR bumps the two `build-config` pins from the release notes — from then on neither the release pipeline nor any SNP host builds `snp-digest`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e2ae88a commit 32459c4

7 files changed

Lines changed: 361 additions & 38 deletions

File tree

.github/workflows/amdsev-release.yml

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -374,22 +374,47 @@ jobs:
374374
echo "KATANA_ASSET_VARIANT=native"
375375
} >> output/qemu/build-info.txt
376376
377-
- name: Build snp-tools
378-
# snp-tools binaries are NOT shipped as release artifacts (separate
379-
# release stream). We build them here only so the next step can run
380-
# snp-digest to compute the published launch measurement.
381-
# snp-tools/.cargo/config.toml pins build.target, so the binary lands
382-
# under target/<triple>/release; locate it explicitly rather than
383-
# hardcoding the triple here.
384-
working-directory: misc/AMDSEV/snp-tools
377+
- name: Obtain snp-digest
378+
# Needed by the next step to compute the published launch measurement.
379+
# Preferred source: the snp-tools-v* release pinned in build-config
380+
# (SNP_DIGEST_RELEASE + SNP_DIGEST_SHA256, cut by the
381+
# amdsev-snp-tools-release workflow), checksum-verified — the same
382+
# binary install.sh uses on SNP hosts, so the publisher and the
383+
# verifiers run identical code. Empty pins fall back to building the
384+
# crate from this checkout (snp-tools/.cargo/config.toml pins
385+
# build.target, so the binary lands under target/<triple>/release;
386+
# locate it explicitly rather than hardcoding the triple).
387+
env:
388+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
385389
run: |
386-
cargo build --release
387-
SNP_DIGEST="$(find target -type f -name snp-digest -perm -u+x | head -n1)"
388-
if [ -z "$SNP_DIGEST" ]; then
389-
echo "snp-digest binary not found after build" >&2
390-
exit 1
390+
. ./build-config
391+
if [ -n "${SNP_DIGEST_RELEASE:-}" ]; then
392+
if [[ ! "${SNP_DIGEST_SHA256:-}" =~ ^[0-9a-f]{64}$ ]]; then
393+
echo "SNP_DIGEST_RELEASE is pinned but SNP_DIGEST_SHA256 is not a sha256" >&2
394+
exit 1
395+
fi
396+
echo "Downloading pinned snp-digest from ${SNP_DIGEST_RELEASE}"
397+
gh release download "$SNP_DIGEST_RELEASE" --repo "$GITHUB_REPOSITORY" \
398+
--pattern "snp-digest-${SNP_DIGEST_RELEASE}" --output snp-digest
399+
actual="$(sha256sum snp-digest | awk '{print $1}')"
400+
if [ "$actual" != "$SNP_DIGEST_SHA256" ]; then
401+
echo "pinned snp-digest checksum mismatch: got $actual, want $SNP_DIGEST_SHA256" >&2
402+
exit 1
403+
fi
404+
chmod +x snp-digest
405+
./snp-digest --help >/dev/null
406+
echo "SNP_DIGEST=$PWD/snp-digest" >> "$GITHUB_ENV"
407+
else
408+
echo "No SNP_DIGEST_RELEASE pin in build-config — building snp-tools from source"
409+
cd snp-tools
410+
cargo build --release
411+
SNP_DIGEST="$(find target -type f -name snp-digest -perm -u+x | head -n1)"
412+
if [ -z "$SNP_DIGEST" ]; then
413+
echo "snp-digest binary not found after build" >&2
414+
exit 1
415+
fi
416+
echo "SNP_DIGEST=$PWD/$SNP_DIGEST" >> "$GITHUB_ENV"
391417
fi
392-
echo "SNP_DIGEST=$PWD/$SNP_DIGEST" >> "$GITHUB_ENV"
393418
394419
- name: Compute sealed launch measurement
395420
run: |
@@ -444,6 +469,8 @@ jobs:
444469
tag="${VM_TAG}"
445470
# shellcheck disable=SC2153
446471
katana_ver="${KATANA_VER}"
472+
# For SNP_DIGEST_RELEASE (the pinned verifier named in the notes).
473+
. ./build-config
447474
# Pull values from build-info.txt for the human-readable summary.
448475
info_get() {
449476
awk -F= -v k="$1" '$1 == k { sub(/^[^=]*=/, ""); print; exit }' output/qemu/build-info.txt
@@ -521,6 +548,17 @@ jobs:
521548
echo "non-zero on any mismatch. For full from-source reproduction, see"
522549
echo "\`misc/AMDSEV/reproduce-release.sh\`."
523550
echo
551+
if [ -n "${SNP_DIGEST_RELEASE:-}" ]; then
552+
echo "The measurement above was computed with snp-digest from"
553+
echo "\`${SNP_DIGEST_RELEASE}\` (pinned in \`misc/AMDSEV/build-config\`) —"
554+
echo "hosts running \`misc/AMDSEV/install.sh\` download and checksum-verify"
555+
echo "the same pinned binary."
556+
else
557+
echo "The measurement above was computed with snp-digest built from this"
558+
echo "tag's \`misc/AMDSEV/snp-tools\` source (no prebuilt pinned in"
559+
echo "\`misc/AMDSEV/build-config\`)."
560+
fi
561+
echo
524562
echo "## Full build provenance"
525563
echo
526564
echo "<details>"
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
name: amdsev-snp-tools-release
2+
3+
# Publishes a prebuilt snp-digest as a dedicated `snp-tools-v<X.Y.Z>` GitHub
4+
# Release. snp-digest changes far less often than the TEE VM image, so it
5+
# gets its own release stream instead of being rebuilt and re-attached on
6+
# every tee-vm-v* release; consumers pin ONE snp-tools release:
7+
#
8+
# - misc/AMDSEV/build-config carries SNP_DIGEST_RELEASE + SNP_DIGEST_SHA256.
9+
# The tee-vm release pipeline (amdsev-release.yml) downloads that pinned
10+
# binary to compute the published launch measurement, and install.sh
11+
# downloads it on SNP hosts so measurement verification needs no Rust
12+
# toolchain. Empty pins mean both fall back to building from source.
13+
#
14+
# Runbook: dispatch this workflow with the new version, then open a PR
15+
# bumping the two pins in build-config to the new tag + the SHA256 printed
16+
# in the release notes. The prebuilt is a convenience copy, not the trust
17+
# root — auditors rebuild snp-digest from the tagged snp-tools source
18+
# (pinned rust-toolchain.toml + Cargo.lock) rather than trusting the asset.
19+
#
20+
# Cut a new release when the snp-tools crate changes in a way that affects
21+
# snp-digest's output or interface (measurement algorithm inputs, CLI flags),
22+
# then bump the pins. Runner-only build: snp-digest is pure userspace
23+
# hashing; no SNP hardware involved.
24+
25+
on:
26+
workflow_dispatch:
27+
inputs:
28+
version:
29+
description: 'snp-tools version to publish (SemVer, no prefix; e.g. 0.1.0)'
30+
required: true
31+
type: string
32+
33+
concurrency:
34+
group: amdsev-snp-tools-release
35+
cancel-in-progress: false
36+
37+
defaults:
38+
run:
39+
working-directory: misc/AMDSEV/snp-tools
40+
41+
jobs:
42+
release:
43+
runs-on: ubuntu-latest
44+
timeout-minutes: 30
45+
permissions:
46+
contents: write
47+
steps:
48+
- uses: actions/checkout@v4
49+
50+
- name: Validate version and tag availability
51+
id: ctx
52+
env:
53+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54+
VERSION: ${{ inputs.version }}
55+
run: |
56+
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then
57+
echo "version must be plain SemVer (optionally -rc.N), got: $VERSION" >&2
58+
exit 1
59+
fi
60+
tag="snp-tools-v${VERSION}"
61+
if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
62+
echo "release $tag already exists" >&2
63+
exit 1
64+
fi
65+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
66+
echo "sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT"
67+
68+
- uses: Swatinem/rust-cache@v2
69+
with:
70+
workspaces: misc/AMDSEV/snp-tools
71+
72+
- name: Test
73+
# Same gate as PR CI (amdsev-snp-tools.yml): snp-digest computes the
74+
# measurement verifiers pin against — never ship an untested build.
75+
run: cargo test --release --locked
76+
77+
- name: Build snp-digest
78+
# snp-tools/.cargo/config.toml pins build.target, so the binary lands
79+
# under target/<triple>/release; locate it explicitly rather than
80+
# hardcoding the triple here.
81+
run: |
82+
cargo build --release --locked
83+
SNP_DIGEST="$(find target -type f -name snp-digest -perm -u+x | head -n1)"
84+
if [ -z "$SNP_DIGEST" ]; then
85+
echo "snp-digest binary not found after build" >&2
86+
exit 1
87+
fi
88+
"$SNP_DIGEST" --help >/dev/null
89+
echo "SNP_DIGEST=$PWD/$SNP_DIGEST" >> "$GITHUB_ENV"
90+
91+
- name: Stage release artifacts
92+
id: stage
93+
env:
94+
TAG: ${{ steps.ctx.outputs.tag }}
95+
run: |
96+
mkdir -p release
97+
cp "$SNP_DIGEST" "release/snp-digest-${TAG}"
98+
sha="$(sha256sum "release/snp-digest-${TAG}" | awk '{print $1}')"
99+
printf '%s\n' "$sha" > "release/snp-digest-${TAG}.sha256"
100+
echo "sha=$sha" >> "$GITHUB_OUTPUT"
101+
102+
- name: Generate release notes
103+
env:
104+
TAG: ${{ steps.ctx.outputs.tag }}
105+
SHA: ${{ steps.stage.outputs.sha }}
106+
run: |
107+
toolchain="$(sed -n 's/^channel = "\(.*\)"$/\1/p' rust-toolchain.toml | head -n1)"
108+
{
109+
echo "# snp-tools ${TAG}"
110+
echo
111+
echo "Prebuilt \`snp-digest\` (x86_64 Linux, Rust ${toolchain}, \`--locked\`) — the"
112+
echo "SEV-SNP launch-measurement calculator used by the TEE VM release pipeline"
113+
echo "and by \`misc/AMDSEV/install.sh\` on SNP hosts."
114+
echo
115+
echo '```'
116+
echo "snp-digest-${TAG} SHA-256: ${SHA}"
117+
echo '```'
118+
echo
119+
echo "To adopt this release, bump both pins in \`misc/AMDSEV/build-config\`:"
120+
echo
121+
echo '```sh'
122+
echo "SNP_DIGEST_RELEASE=\"${TAG}\""
123+
echo "SNP_DIGEST_SHA256=\"${SHA}\""
124+
echo '```'
125+
echo
126+
echo "This is a convenience copy, not the trust root: auditors should rebuild"
127+
echo "\`snp-digest\` from this tag's \`misc/AMDSEV/snp-tools\` source (pinned"
128+
echo "\`rust-toolchain.toml\` + \`Cargo.lock\`)."
129+
} > release/release-notes.md
130+
131+
- name: Create GitHub Release
132+
uses: softprops/action-gh-release@v2
133+
with:
134+
tag_name: ${{ steps.ctx.outputs.tag }}
135+
target_commitish: ${{ steps.ctx.outputs.sha }}
136+
prerelease: ${{ contains(inputs.version, '-rc.') }}
137+
body_path: misc/AMDSEV/snp-tools/release/release-notes.md
138+
files: |
139+
misc/AMDSEV/snp-tools/release/snp-digest-*

misc/AMDSEV/build-config

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,15 @@ LIBC6_DEV_PKG_SHA256="bbf5a155039042634961a61276650631ee47b9e721f91f8dbb731b0bbe
103103
# KATANA_LUKS_UUID); their measurement will differ from the published one and
104104
# they must recompute it themselves via sealed-cmdline.sh + snp-digest.
105105
KATANA_CANONICAL_LUKS_UUID="00000000-0000-0000-0000-000000000001"
106+
107+
# Prebuilt snp-digest — a snp-tools-v* GitHub release cut by the
108+
# amdsev-snp-tools-release workflow. The tee-vm release pipeline downloads it
109+
# (instead of rebuilding the crate) to compute the published measurement, and
110+
# install.sh downloads it on SNP hosts so measurement verification needs no
111+
# Rust toolchain; both verify the download against SNP_DIGEST_SHA256, which
112+
# is trusted because THIS file is versioned in git at the consuming tag.
113+
# Empty pins = build snp-digest from source (the prebuilt is a convenience
114+
# copy, never the trust root). Bump both together, from the values printed
115+
# in the snp-tools release notes.
116+
SNP_DIGEST_RELEASE=""
117+
SNP_DIGEST_SHA256=""

misc/AMDSEV/docs/install.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,15 @@ The installer needs no root; `run.sh` invokes `start-vm.sh` via `sudo`
3434
host has another version (or none), the installer offers to build it from
3535
source via the release's `scripts/build-qemu.sh` — locally under the
3636
install root (default) or into `/usr/local`.
37-
- **Rust (optional but recommended):** `cargo` is needed to build
38-
`snp-digest`, which verifies the release's published launch measurement
39-
and computes the expected measurement for your configuration. Without it
40-
the install still completes (artifact checksums are always verified), but
41-
attestation verification has no local expected value until you install
42-
Rust and run `install.sh verify`.
37+
- **Rust (optional):** measurement verification uses `snp-digest`. Each
38+
release's `build-config` pins a prebuilt `snp-tools-v*` release (tag +
39+
SHA-256) that the installer downloads and checksum-verifies automatically,
40+
so `cargo` is only needed as a fallback — for releases predating the pin,
41+
or if you prefer building the verifier from source (the prebuilt is a
42+
convenience copy, not the trust root). Without either, the install still
43+
completes (artifact checksums are always verified), but attestation
44+
verification has no local expected value until you run `install.sh verify`
45+
with one of them available.
4346

4447
## What gets installed
4548

@@ -155,7 +158,8 @@ a fresh file to keep the old disk recoverable under the old release.
155158
then reload `kvm_amd` (or reboot). The host kernel must support SNP.
156159
- **QEMU build fails** — install the build deps first:
157160
`sudo apt-get install -y build-essential ninja-build pkg-config libglib2.0-dev libpixman-1-dev python3-venv flex bison wget`.
158-
- **`cargo` missing / measurement not computed** — install Rust
161+
- **Measurement not computed** — the release pins no prebuilt `snp-digest`
162+
(its `build-config` predates the pin) and `cargo` is missing. Install Rust
159163
(https://rustup.rs), then `~/.katana/tee-vm/install.sh verify`.
160164
- **vCPUs locked to 1** — the chosen release's launcher predates
161165
configurable vCPUs; pick a newer `tee-vm-v*` release.

misc/AMDSEV/docs/release-pipeline.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,41 @@ fetched from the **repo source at the same tag**
264264
`misc/AMDSEV` tree part of the published interface: renaming `start-vm.sh`,
265265
its flags/env vars (`KATANA_VCPUS`, `KATANA_MEMORY`, `HOST_RPC_PORT`, ...),
266266
`scripts/sealed-cmdline.sh`, `verify-build.sh`, the `snp-tools` crate path,
267-
or the tarball layout changes what installed hosts download on upgrade — the
268-
installer feature-detects where it can, but treat such renames as breaking.
267+
the `build-config` pin keys, or the tarball layout changes what installed
268+
hosts download on upgrade — the installer feature-detects where it can, but
269+
treat such renames as breaking.
270+
271+
## The snp-tools release stream
272+
273+
`snp-digest` (the measurement calculator) is published on its own release
274+
line, `snp-tools-v<X.Y.Z>`, cut on demand by the
275+
[`amdsev-snp-tools-release`](../../../.github/workflows/amdsev-snp-tools-release.yml)
276+
workflow (assets: `snp-digest-<tag>` for x86_64 Linux + a bare-hex
277+
`.sha256`). It changes far less often than the VM image, so one snp-tools
278+
release serves many tee-vm releases instead of being rebuilt per release.
279+
280+
Consumers pin it in `build-config` (`SNP_DIGEST_RELEASE` +
281+
`SNP_DIGEST_SHA256`):
282+
283+
- **This pipeline** downloads the pinned binary (checksum-verified) to
284+
compute the published measurement — no cargo build in the release path.
285+
- **`install.sh`** reads the pins from the installed tag's `build-config`
286+
and downloads the same binary on SNP hosts, so measurement verification
287+
needs no Rust toolchain. The checksum is trusted because `build-config` is
288+
versioned in git at the consuming tag.
289+
- **Empty pins** mean both fall back to building `snp-tools` from source
290+
(also the behavior for tags predating the pins).
291+
292+
The prebuilt is a plain runner build — a convenience copy, **not** the trust
293+
root, and not part of the reproducibility story (its checksum stays out of
294+
`build-info.txt`, so `reproduce-release.sh` is unaffected). Auditors verify
295+
it by rebuilding from the snp-tools tag's pinned source
296+
(`rust-toolchain.toml` + `Cargo.lock`).
297+
298+
Runbook for a new snp-tools release: dispatch `amdsev-snp-tools-release`
299+
with the next version, then open a PR bumping both `build-config` pins to
300+
the tag + SHA-256 printed in its release notes. Cut one whenever the crate
301+
changes in a way that affects `snp-digest`'s output or interface.
269302

270303
## What moves the measurement between releases
271304

0 commit comments

Comments
 (0)