Skip to content

feat: scripts/extract-nitro-pcrs.sh + self-hosted-tee docs#300

Merged
prasanna-anchorage merged 4 commits into
mainfrom
feat/extract-nitro-pcrs
May 14, 2026
Merged

feat: scripts/extract-nitro-pcrs.sh + self-hosted-tee docs#300
prasanna-anchorage merged 4 commits into
mainfrom
feat/extract-nitro-pcrs

Conversation

@prasanna-anchorage
Copy link
Copy Markdown
Contributor

Summary

  • New scripts/extract-nitro-pcrs.sh — reads the tkhq/qos rev pinned in src/Cargo.toml, clones qos at that rev, runs make out/qos_enclave/index.json, and extracts /nitro.pcrs via quay.io/skopeo/stable (containerized — no host install) plus docker.
  • Fills in the previously stubbed self-hosted-tee getting-started page with usage, options, a how-it-works walkthrough, and a reproducibility check.
  • Updates the attestation page's "Updating your allowlist" section to point at the new tool.

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_app is 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.sh is clean.
  • scripts/extract-nitro-pcrs.sh --help prints usage and exits 0.
  • End-to-end run produces out/nitro.pcrs with three lines ending in PCR0, PCR1, PCR2.
  • Two consecutive runs are byte-identical (diff empty).
  • PCR2 for the pinned rev (365ba7ed529bc5af617bcfb27502c3efce8b37ae) matches the example in qos's README (21b9efbc…fc500a), confirming reproducibility against the upstream artifact.
  • Mintlify preview renders the updated getting-started.mdx cleanly (reviewer to confirm).

🤖 Generated with Claude Code

@mintlify
Copy link
Copy Markdown
Contributor

mintlify Bot commented May 13, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
visualsign-parser 🟢 Ready View Preview May 13, 2026, 10:10 PM

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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.sh that reads the qos rev pinned in src/Cargo.toml, builds qos_enclave, and extracts /nitro.pcrs via a containerized skopeo + docker pipeline.
  • Fleshed-out getting-started.mdx (prereqs, usage, options table, how-it-works, reproducibility check).
  • attestation.mdx allowlist-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.

Comment on lines +105 to +125
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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/extract-nitro-pcrs.sh Outdated
--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)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +131 to +139
main() {
parse_args "$@"
if [[ -z "$REV" ]]; then
read_rev
fi
trap cleanup EXIT
ensure_qos_checkout
build_qos_enclave
extract_pcrs
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/extract-nitro-pcrs.sh Outdated
Comment on lines +109 to +115
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"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 41489bf: the loaded image is now tagged qos-enclave:extract-$$-${RANDOM} per run.

Comment on lines +119 to +129
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
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +74 to +92
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"
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +55 to +66
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]}"
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +244 to +247
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.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 41489bf: split into "~8 GB of free RAM" and "~15 GB of free disk". Numbers are rough but separated.

Comment thread scripts/extract-nitro-pcrs.sh Outdated
Comment on lines +23 to +24
Build the tkhq/qos enclave image at the rev pinned in src/Cargo.toml and
extract /nitro.pcrs from the resulting OCI image.
Copy link
Copy Markdown
Collaborator

@r-n-o r-n-o May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • d787eb65da5541d0e3cffdd2fa39cadc9fa98534854d3e78e3c8d03f10c3a5fe38ff554c824d70dae5079689c22253ab
  • d787eb65da5541d0e3cffdd2fa39cadc9fa98534854d3e78e3c8d03f10c3a5fe38ff554c824d70dae5079689c22253ab
  • 21b9efbc184807662e966d34f390821309eeac6802309798826296bf3e8bec7c10edb30948c90ba67310f7b964fc500a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = d866f2c6cbc58cc08c24eab4828f0824ad16a226 marker above the qos dep block in src/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 d866f2c6 and fetched a live api.turnkey.com attestation via visualsign-turnkeyclient verify --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>
@prasanna-anchorage prasanna-anchorage merged commit 68bbe06 into main May 14, 2026
9 checks passed
@prasanna-anchorage prasanna-anchorage deleted the feat/extract-nitro-pcrs branch May 14, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tooling: extract nitro.pcrs from the pinned tkhq/qos rev

3 participants