feat: scripts/extract-nitro-pcrs.sh + self-hosted-tee docs#300
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
Adds a bash tool that reads the tkhq/qos git rev pinned across the qos_* workspace dependencies in src/Cargo.toml, clones qos at that rev, runs `make out/qos_enclave/index.json`, and extracts /nitro.pcrs from the resulting OCI image using a containerized skopeo (no host install required) plus docker. Also fills in the self-hosted-tee getting-started page (previously "coming soon") with usage, options, a how-it-works walkthrough, and a reproducibility check, and updates the attestation page's "Updating your allowlist" section to point at the new tool. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
44f373d to
55f7a77
Compare
There was a problem hiding this comment.
Pull request overview
Adds a reproducible tool for wallet integrators to derive the qos enclave PCR values needed to populate their attestation allowlist, and replaces the previously stubbed self-hosted-TEE getting-started page with concrete guidance pointing at that tool.
Changes:
- New
scripts/extract-nitro-pcrs.shthat reads the qos rev pinned insrc/Cargo.toml, buildsqos_enclave, and extracts/nitro.pcrsvia a containerized skopeo + docker pipeline. - Fleshed-out
getting-started.mdx(prereqs, usage, options table, how-it-works, reproducibility check). attestation.mdxallowlist-update flow now references the new script + doc anchor.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| scripts/extract-nitro-pcrs.sh | New ~145-line bash tool implementing the rev-read → clone → make → skopeo → docker-cp pipeline with cleanup trap. |
| docs/wallet-integration/self-hosted-tee/getting-started.mdx | Replaces the "coming soon" stub with usage docs, options table, reproducibility example, and what-the-PCRs-measure note. |
| docs/wallet-integration/self-hosted-tee/attestation.mdx | Updates step 2 of "Updating your allowlist" to invoke the new script, and rewords the trigger from "parser is updated" to "qos enclave runtime is updated". |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| docker run --rm \ | ||
| -v "$oci_dir:/src:ro" \ | ||
| -v "$STAGE_DIR:/dst" \ | ||
| "$SKOPEO_IMAGE" \ | ||
| copy oci:/src:latest "docker-archive:/dst/qos_enclave.tar:qos-enclave:latest" | ||
|
|
||
| docker load -i "$STAGE_DIR/qos_enclave.tar" | ||
| CID="$(docker create qos-enclave:latest)" | ||
|
|
||
| mkdir -p "$(dirname "$OUTPUT")" | ||
| docker cp "$CID:/nitro.pcrs" "$OUTPUT" | ||
| [[ -s "$OUTPUT" ]] || die "nitro.pcrs not found in qos-enclave image" | ||
| } | ||
|
|
||
| cleanup() { | ||
| if [[ -n "$CID" ]]; then | ||
| docker rm "$CID" >/dev/null 2>&1 || true | ||
| fi | ||
| if [[ -n "$STAGE_DIR" ]]; then | ||
| rm -rf "$STAGE_DIR" | ||
| fi |
There was a problem hiding this comment.
Fixed in 41489bf: skopeo now runs with --user "$(id -u):$(id -g)" so the staged tar is host-owned and rm -rf succeeds without root.
| --cargo-toml PATH Workspace Cargo.toml (default: $CARGO_TOML) | ||
| --qos-dir PATH Where to clone/reuse qos (default: mktemp -d, removed on exit) | ||
| --output PATH Where to write nitro.pcrs (default: $OUTPUT) | ||
| --skopeo-image REF Pinned skopeo image (default: $SKOPEO_IMAGE) |
There was a problem hiding this comment.
Fixed in 41489bf: default SKOPEO_IMAGE is now pinned by digest (quay.io/skopeo/stable@sha256:4d60d6c0…) and the help text reads "Skopeo image (pinned by digest)". --skopeo-image still accepts an override.
| main() { | ||
| parse_args "$@" | ||
| if [[ -z "$REV" ]]; then | ||
| read_rev | ||
| fi | ||
| trap cleanup EXIT | ||
| ensure_qos_checkout | ||
| build_qos_enclave | ||
| extract_pcrs |
There was a problem hiding this comment.
Pushing back: existing scripts in scripts/ (auto-version.sh, fuzz_all_idls.sh) do not preflight tool availability either, and set -euo pipefail will surface a missing binary on the first call with a clear bash: foo: command not found and exit nonzero. Adding a project-wide preflight pattern is out of scope for this PR; I would rather pick up the convention everywhere or nowhere.
| copy oci:/src:latest "docker-archive:/dst/qos_enclave.tar:qos-enclave:latest" | ||
|
|
||
| docker load -i "$STAGE_DIR/qos_enclave.tar" | ||
| CID="$(docker create qos-enclave:latest)" | ||
|
|
||
| mkdir -p "$(dirname "$OUTPUT")" | ||
| docker cp "$CID:/nitro.pcrs" "$OUTPUT" |
There was a problem hiding this comment.
Fixed in 41489bf: the loaded image is now tagged qos-enclave:extract-$$-${RANDOM} per run.
| cleanup() { | ||
| if [[ -n "$CID" ]]; then | ||
| docker rm "$CID" >/dev/null 2>&1 || true | ||
| fi | ||
| if [[ -n "$STAGE_DIR" ]]; then | ||
| rm -rf "$STAGE_DIR" | ||
| fi | ||
| if [[ "$QOS_DIR_AUTO" -eq 1 && -n "$QOS_DIR" ]]; then | ||
| rm -rf "$QOS_DIR" | ||
| fi | ||
| } |
There was a problem hiding this comment.
Fixed in 41489bf: cleanup now runs docker rmi "$DOCKER_TAG" after docker rm "$CID", so the per-run image is dropped from the host store on exit.
| if [[ -d "$QOS_DIR/.git" ]]; then | ||
| local head | ||
| head="$(git -C "$QOS_DIR" rev-parse HEAD)" | ||
| if [[ "$head" == "$REV" ]]; then | ||
| echo "Reusing qos checkout at $QOS_DIR (HEAD=$head)" >&2 | ||
| return | ||
| fi | ||
| [[ -z "$(git -C "$QOS_DIR" status --porcelain)" ]] \ | ||
| || die "qos checkout $QOS_DIR has uncommitted changes" | ||
| echo "Updating qos checkout in $QOS_DIR to $REV" >&2 | ||
| git -C "$QOS_DIR" fetch --quiet origin "$REV" | ||
| git -C "$QOS_DIR" checkout --quiet "$REV" | ||
| return | ||
| fi | ||
|
|
||
| echo "Cloning tkhq/qos into $QOS_DIR" >&2 | ||
| git clone --quiet https://github.com/tkhq/qos.git "$QOS_DIR" | ||
| git -C "$QOS_DIR" checkout --quiet "$REV" | ||
| } |
There was a problem hiding this comment.
Fixed in 41489bf: ensure_qos_checkout now reads git remote get-url origin first and refuses to mutate the directory unless origin equals https://github.com/tkhq/qos.git.
| read_rev() { | ||
| [[ -f "$CARGO_TOML" ]] || die "Cargo.toml not found: $CARGO_TOML" | ||
| local revs | ||
| mapfile -t revs < <( | ||
| grep -oE 'git = "https://github.com/tkhq/qos\.git", rev = "[0-9a-f]{40}"' "$CARGO_TOML" \ | ||
| | grep -oE '[0-9a-f]{40}' \ | ||
| | sort -u | ||
| ) | ||
| [[ ${#revs[@]} -ge 1 ]] || die "No qos git deps found in $CARGO_TOML" | ||
| [[ ${#revs[@]} -eq 1 ]] || die "qos revs disagree in $CARGO_TOML: ${revs[*]}" | ||
| REV="${revs[0]}" | ||
| } |
There was a problem hiding this comment.
Resolved differently: rather than hardening the dep-line regex, the script now reads the deployment rev from a dedicated # qos-deployment-rev = <40-hex> marker comment in src/Cargo.toml (the dep rev = "..." values are the qos library version, not the deployed runtime — see r-n-o thread above). The marker has a single fixed format, so the parsing is much more robust than the previous regex would have been.
| When the qos enclave runtime is updated, PCR values change. Follow this process: | ||
|
|
||
| 1. Subscribe to parser release announcements | ||
| 2. Verify new PCR values against published hashes | ||
| 3. Add new PCRs to your allowlist | ||
| 4. Deploy to production | ||
| 5. Remove old PCRs after migration completes | ||
| 1. Subscribe to parser release announcements. | ||
| 2. Verify the new PCR values: run `scripts/extract-nitro-pcrs.sh` against the updated qos rev — see [Generating PCR values](./getting-started#generating-pcr-values) — and confirm the script's output matches the values in the release. |
There was a problem hiding this comment.
Fixed in 41489bf: step 1 now reads "Watch for deployment announcements that change the qos runtime rev (the rev whose EIF boots in the enclave, not the qos library rev parser_app links against)", which is the correct trigger.
|
|
||
| - Docker 26+ | ||
| - Git, GNU Make | ||
| - ~10 GB free RAM and disk for the qos build (StageX base images and a clean cargo compile) |
There was a problem hiding this comment.
Fixed in 41489bf: split into "~8 GB of free RAM" and "~15 GB of free disk". Numbers are rough but separated.
| Build the tkhq/qos enclave image at the rev pinned in src/Cargo.toml and | ||
| extract /nitro.pcrs from the resulting OCI image. |
There was a problem hiding this comment.
Worth pointing out that the PCR measurements you'll get from TVC aren't necessarily the measurements you'll get here.
That's because the qos version in your cargo.toml is only used to include software in your own parser app (using QOS as a Rust lib, essentially). But the PCR measurements you get are a measurement of what is used at boot (the EIF). For TVC you specify the version of QOS you use at deployment time, and right now the only option that's available is v2026.2.6 (QOS commit: d866f2c6cbc58cc08c24eab4828f0824ad16a226, build showing PCR measurements here -- you can expand the "run make" step)
Bottom-line: expected PCR measurements from TVC app boot proofs right now:
d787eb65da5541d0e3cffdd2fa39cadc9fa98534854d3e78e3c8d03f10c3a5fe38ff554c824d70dae5079689c22253abd787eb65da5541d0e3cffdd2fa39cadc9fa98534854d3e78e3c8d03f10c3a5fe38ff554c824d70dae5079689c22253ab21b9efbc184807662e966d34f390821309eeac6802309798826296bf3e8bec7c10edb30948c90ba67310f7b964fc500a
There was a problem hiding this comment.
Confirmed and addressed in 41489bf.
You're right — the rev in src/Cargo.toml is the library version compiled into parser_app, not the boot-time qos rev. The PCR2 match in the original commit was incidental (the ramdisk that contains qos's init happened to be unchanged between 365ba7e and d866f2c6); PCR0/PCR1 actually differ between those two revs.
Changes:
- Added a
# qos-deployment-rev = d866f2c6cbc58cc08c24eab4828f0824ad16a226marker above the qos dep block insrc/Cargo.toml, with a link to the commit and to the upstream qos CI run that publishes its PCRs. - The script now reads from that marker rather than from the dep lines.
- End-to-end validated: built locally at
d866f2c6and fetched a liveapi.turnkey.comattestation via visualsign-turnkeyclientverify --debug. PCR0/1/2 match byte-for-byte (d787eb65…22253ab,d787eb65…22253ab,21b9efbc…fc500a). - Docs rewritten to explain the library vs deployment rev split up front, and a "Cross-check against a live prod attestation" section walks through the verification flow.
Review (r-n-o): the qos rev in src/Cargo.toml's dependency lines is the library version compiled into parser_app and is NOT what boots in the enclave. The PCRs an attestation document carries come from the deployment EIF, which TVC pins separately (currently v2026.2.6, commit d866f2c6). Reading the lib rev would give wrong allowlist values. Adds a `# qos-deployment-rev = ...` marker comment to Cargo.toml above the qos dep block, with links to the upstream commit and the qos CI run that publishes its PCRs. The script now reads the deployment rev from that marker rather than the dep lines. Validated end-to-end: built locally at d866f2c6 and compared to a live api.turnkey.com attestation fetched via visualsign-turnkeyclient. PCR0/1/2 match byte-for-byte: PCR0 d787eb65...22253ab PCR1 d787eb65...22253ab PCR2 21b9efbc...fc500a Also folds in Copilot review fixes: * Pin the skopeo image by digest (not :latest) so the help text's "Pinned skopeo image" claim is honest. * Run skopeo as the host UID/GID so staging files clean up cleanly on non-root Linux hosts. * Tag the loaded docker image per-run (qos-enclave:extract-$$-$RANDOM) and remove it in cleanup; previous code reused the global tag `qos-enclave:latest`, which races and leaks images across runs. * Verify `--qos-dir`'s origin URL is tkhq/qos before mutating it, so a misdirected --qos-dir can't be silently hijacked into a foreign repo. Docs: rewrite the getting-started page to explain the library vs deployment rev split, split the RAM/disk prereqs, and add a "Cross-check against a live prod attestation" section walking through the visualsign-turnkeyclient build + verify --debug + comparison flow. The attestation page's allowlist-update step now points at deployment revs, not parser releases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both anchorageoss/visualsign-turnkeyclient and its awsnitroverifier dependency are public repos and resolve through proxy.golang.org, so GOPRIVATE is unnecessary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The PCR-extraction tool and the visualsign-turnkeyclient cross-check target Turnkey's deployed enclave (TVC), not a self-hosted deployment, so the docs belong under wallet-integration/hosted, not wallet-integration/self-hosted-tee. Move the full "Generating PCRs from source" + "Cross-checking against a live prod attestation" + reproducibility sections into hosted/getting-started.mdx (replacing the "coming soon" stub there), and revert self-hosted-tee/getting-started.mdx to its previous stub. Update the cross-reference in attestation.mdx to point at the new location. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
scripts/extract-nitro-pcrs.sh— reads the tkhq/qos rev pinned insrc/Cargo.toml, clones qos at that rev, runsmake out/qos_enclave/index.json, and extracts/nitro.pcrsviaquay.io/skopeo/stable(containerized — no host install) plus docker.Closes #299.
Why
Wallet integrators need the qos enclave PCR values to build their attestation allowlist. The reproduction sequence was manual and undocumented; this script captures it so it can be re-run on any qos rev bump and used by CI later.
The PCRs measure the qos enclave runtime (PCR0/PCR1 cover the EIF and kernel; PCR2 covers the boot ramdisk containing qos's
init).parser_appis loaded into qos at runtime over vsock and is not part of these measurements — its integrity is established separately via qos manifest verification (Level 3 attestation).Test plan
shellcheck scripts/extract-nitro-pcrs.shis clean.scripts/extract-nitro-pcrs.sh --helpprints usage and exits 0.out/nitro.pcrswith three lines ending inPCR0,PCR1,PCR2.diffempty).365ba7ed529bc5af617bcfb27502c3efce8b37ae) matches the example in qos's README (21b9efbc…fc500a), confirming reproducibility against the upstream artifact.getting-started.mdxcleanly (reviewer to confirm).🤖 Generated with Claude Code