From 55f7a77de2cb29642a179af3fae27464510100ba Mon Sep 17 00:00:00 2001 From: Prasanna Gautam Date: Wed, 13 May 2026 22:07:59 +0000 Subject: [PATCH 1/4] feat: add scripts/extract-nitro-pcrs.sh and document its use 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) --- .../self-hosted-tee/attestation.mdx | 12 +- .../self-hosted-tee/getting-started.mdx | 63 +++++++- scripts/extract-nitro-pcrs.sh | 145 ++++++++++++++++++ 3 files changed, 212 insertions(+), 8 deletions(-) create mode 100755 scripts/extract-nitro-pcrs.sh diff --git a/docs/wallet-integration/self-hosted-tee/attestation.mdx b/docs/wallet-integration/self-hosted-tee/attestation.mdx index 1c29dc29b..79a94e446 100644 --- a/docs/wallet-integration/self-hosted-tee/attestation.mdx +++ b/docs/wallet-integration/self-hosted-tee/attestation.mdx @@ -241,13 +241,13 @@ func verifyPCRs(pcrs map[int][]byte) error { ### Updating your allowlist -When the parser is updated, PCR values change. Follow this process: +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. +3. Add new PCRs to your allowlist. +4. Deploy to production. +5. Remove old PCRs after migration completes. ### Supporting multiple versions diff --git a/docs/wallet-integration/self-hosted-tee/getting-started.mdx b/docs/wallet-integration/self-hosted-tee/getting-started.mdx index b4455570c..07695ded9 100644 --- a/docs/wallet-integration/self-hosted-tee/getting-started.mdx +++ b/docs/wallet-integration/self-hosted-tee/getting-started.mdx @@ -4,11 +4,70 @@ description: Deploy VisualSign in your own AWS Nitro Enclaves --- -Detailed instructions are coming soon. Implementation details are evolving as we finalize the self-hosted deployment architecture. +End-to-end deployment instructions for running the parser in your own enclaves are still in progress. The piece below covers reproducing the PCR values you need on the verification side — see [Attestation Verification](./attestation) for how to use them. +VisualSign runs inside an AWS Nitro Enclave whose runtime is provided by [tkhq/qos](https://github.com/tkhq/qos), pinned to a specific git rev in this repo's `src/Cargo.toml`. To trust attestation documents from your deployment, you need the PCR measurements of that qos enclave image. + +The `scripts/extract-nitro-pcrs.sh` tool reproduces those PCR values locally from the pinned rev — useful both when you bootstrap your allowlist and when you want to audit a qos rev bump before merging it. + +## Prerequisites + +- Docker 26+ +- Git, GNU Make +- ~10 GB free RAM and disk for the qos build (StageX base images and a clean cargo compile) + +## Generating PCR values + +```bash +./scripts/extract-nitro-pcrs.sh +``` + +The default invocation reads the qos rev from `src/Cargo.toml`, builds `qos_enclave` in an ephemeral checkout, writes `out/nitro.pcrs`, and prints the three measurements: + +``` + PCR0 + PCR1 + PCR2 +``` + +These are the values you place in your PCR allowlist (see [Attestation → PCR management](./attestation#pcr-management)). + +### Options + +| Flag | Default | Purpose | +| --- | --- | --- | +| `--cargo-toml PATH` | `src/Cargo.toml` | Source of the pinned rev. | +| `--qos-dir PATH` | `mktemp -d` | qos checkout location. The default is ephemeral and removed on exit; pass an explicit path to keep cached cargo and docker layers between runs. | +| `--output PATH` | `out/nitro.pcrs` | Where to write the PCRs file. | +| `--skopeo-image REF` | `quay.io/skopeo/stable:latest` | Container image used to convert the OCI layout to a docker-loadable tar (no host `skopeo` install needed). | +| `--rev REV` | (from `Cargo.toml`) | Override the rev — useful when auditing a prospective qos bump. | + +### How it works + +1. **Read the rev** — the script greps `src/Cargo.toml` for the `git = "https://github.com/tkhq/qos.git", rev = "..."` pattern across all qos workspace deps and fails if they don't agree. +2. **Check out qos** — clones `tkhq/qos` into the work dir and checks out the rev. If `--qos-dir` points at an existing clone whose `HEAD` already matches the rev, the script reuses it. +3. **Build `qos_enclave`** — runs `make out/qos_enclave/index.json` inside the qos checkout. This is qos's deterministic [StageX](https://codeberg.org/stagex/stagex)-based build: it produces a `nitro.eif` and `nitro.pcrs` via `eif_build` and bakes them into the OCI image. The full pipeline lives in qos's [`qos_enclave` Containerfile](https://github.com/tkhq/qos/blob/main/src/images/qos_enclave/Containerfile). +4. **Extract `nitro.pcrs`** — runs `quay.io/skopeo/stable` in a container to convert the OCI directory output to a docker-archive tar, loads it, and copies `/nitro.pcrs` out of a freshly created container. + +### What the PCRs measure + +The PCRs cover the qos enclave runtime: PCR0 and PCR1 measure the Nitro EIF and kernel, and PCR2 measures the boot ramdisk that contains qos's `init` binary. The `parser_app` binary is loaded into qos at runtime over vsock and is **not** part of these PCR measurements — its integrity is established separately via the qos manifest, which is covered at [Level 3 attestation](./attestation#level-3-manifest-verification). + +## Reproducibility + +The qos build is deterministic. Running the script twice should produce byte-identical `nitro.pcrs` files. Pass `--qos-dir` to the second run so the cargo and docker layer caches are reused — the second build should be a no-op: + +```bash +./scripts/extract-nitro-pcrs.sh --output /tmp/pcrs.a --qos-dir /tmp/qos +./scripts/extract-nitro-pcrs.sh --output /tmp/pcrs.b --qos-dir /tmp/qos +diff /tmp/pcrs.a /tmp/pcrs.b +``` + +If the diff is empty, the build reproduced. + ## Next steps -- [Security Model](./security-model) — Understand the TEE threat model +- [Security Model](./security-model) — Threat model and trust architecture - [Attestation](./attestation) — Implement attestation verification - [gRPC API Reference](/api-reference) — Full API documentation diff --git a/scripts/extract-nitro-pcrs.sh b/scripts/extract-nitro-pcrs.sh new file mode 100755 index 000000000..df05d2baf --- /dev/null +++ b/scripts/extract-nitro-pcrs.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# Build the tkhq/qos enclave image at the rev pinned in src/Cargo.toml and +# extract /nitro.pcrs from the resulting OCI image. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)" + +CARGO_TOML="$REPO_ROOT/src/Cargo.toml" +OUTPUT="$REPO_ROOT/out/nitro.pcrs" +SKOPEO_IMAGE="quay.io/skopeo/stable:latest" +QOS_DIR="" +REV="" + +QOS_DIR_AUTO=0 +STAGE_DIR="" +CID="" + +usage() { + cat <&2 + exit 1 +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --cargo-toml) CARGO_TOML="$2"; shift 2 ;; + --qos-dir) QOS_DIR="$2"; shift 2 ;; + --output) OUTPUT="$2"; shift 2 ;; + --skopeo-image) SKOPEO_IMAGE="$2"; shift 2 ;; + --rev) REV="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "Unknown argument: $1" ;; + esac + done +} + +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]}" +} + +ensure_qos_checkout() { + if [[ -z "$QOS_DIR" ]]; then + QOS_DIR="$(mktemp -d -t visualsign-qos.XXXXXX)" + QOS_DIR_AUTO=1 + fi + + 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" +} + +build_qos_enclave() { + echo "Building qos_enclave at rev $REV (may take several minutes)..." >&2 + make -C "$QOS_DIR" out/qos_enclave/index.json +} + +extract_pcrs() { + local oci_dir="$QOS_DIR/out/qos_enclave" + [[ -f "$oci_dir/index.json" ]] || die "qos build did not produce $oci_dir/index.json" + + STAGE_DIR="$(mktemp -d -t visualsign-pcrs.XXXXXX)" + + 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 + if [[ "$QOS_DIR_AUTO" -eq 1 && -n "$QOS_DIR" ]]; then + rm -rf "$QOS_DIR" + fi +} + +main() { + parse_args "$@" + if [[ -z "$REV" ]]; then + read_rev + fi + trap cleanup EXIT + ensure_qos_checkout + build_qos_enclave + extract_pcrs + echo "Wrote $OUTPUT:" >&2 + cat "$OUTPUT" + echo +} + +main "$@" From 41489bf1e50ab8f74261d3e0ea6ba19d464d5f7e Mon Sep 17 00:00:00 2001 From: Prasanna Gautam Date: Wed, 13 May 2026 23:21:45 +0000 Subject: [PATCH 2/4] fix: separate library vs deployment qos rev; address review feedback 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) --- .../self-hosted-tee/attestation.mdx | 6 +- .../self-hosted-tee/getting-started.mdx | 72 +++++++++++++++---- scripts/extract-nitro-pcrs.sh | 55 ++++++++------ src/Cargo.toml | 12 ++++ 4 files changed, 108 insertions(+), 37 deletions(-) diff --git a/docs/wallet-integration/self-hosted-tee/attestation.mdx b/docs/wallet-integration/self-hosted-tee/attestation.mdx index 79a94e446..5c330b688 100644 --- a/docs/wallet-integration/self-hosted-tee/attestation.mdx +++ b/docs/wallet-integration/self-hosted-tee/attestation.mdx @@ -241,10 +241,10 @@ func verifyPCRs(pcrs map[int][]byte) error { ### Updating your allowlist -When the qos enclave runtime is updated, PCR values change. Follow this process: +When the deployed qos enclave runtime is updated, PCR values change. Follow this process: -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. +1. 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 — see [Generating PCR values](./getting-started#generating-pcr-values) for the distinction). +2. Reproduce the new PCR values: run `scripts/extract-nitro-pcrs.sh` with the updated deployment rev and confirm the script's output matches the values published with the release. 3. Add new PCRs to your allowlist. 4. Deploy to production. 5. Remove old PCRs after migration completes. diff --git a/docs/wallet-integration/self-hosted-tee/getting-started.mdx b/docs/wallet-integration/self-hosted-tee/getting-started.mdx index 07695ded9..9f7255d48 100644 --- a/docs/wallet-integration/self-hosted-tee/getting-started.mdx +++ b/docs/wallet-integration/self-hosted-tee/getting-started.mdx @@ -7,15 +7,28 @@ description: Deploy VisualSign in your own AWS Nitro Enclaves End-to-end deployment instructions for running the parser in your own enclaves are still in progress. The piece below covers reproducing the PCR values you need on the verification side — see [Attestation Verification](./attestation) for how to use them. -VisualSign runs inside an AWS Nitro Enclave whose runtime is provided by [tkhq/qos](https://github.com/tkhq/qos), pinned to a specific git rev in this repo's `src/Cargo.toml`. To trust attestation documents from your deployment, you need the PCR measurements of that qos enclave image. +VisualSign runs inside an AWS Nitro Enclave whose runtime is provided by [tkhq/qos](https://github.com/tkhq/qos). To trust attestation documents from your deployment you need the PCR measurements of that qos enclave image. The `scripts/extract-nitro-pcrs.sh` tool reproduces them locally from a given qos rev. -The `scripts/extract-nitro-pcrs.sh` tool reproduces those PCR values locally from the pinned rev — useful both when you bootstrap your allowlist and when you want to audit a qos rev bump before merging it. +## Two qos revs + +The repo pins qos in two distinct places: + +- **Library rev** — the `rev = "..."` on each `qos_*` crate in `src/Cargo.toml`. This is qos compiled into `parser_app` (qos_core, qos_crypto, etc.). It does not affect PCRs. +- **Deployment rev** — the qos version whose EIF actually boots in the enclave and is measured by PCRs. This is recorded as a marker comment in `src/Cargo.toml`: + + ```toml + # qos-deployment-rev = d866f2c6cbc58cc08c24eab4828f0824ad16a226 + ``` + + Currently the deployment rev tracks Turnkey Verifiable Compute (TVC) `v2026.2.6`. The script reads this marker by default. + +The two revs are not required to match. The PCRs you put in your attestation allowlist must be reproduced from the **deployment rev**, never from the library rev. ## Prerequisites - Docker 26+ -- Git, GNU Make -- ~10 GB free RAM and disk for the qos build (StageX base images and a clean cargo compile) +- Git and GNU Make +- ~8 GB of free RAM and ~15 GB of free disk (StageX base images, a clean cargo compile, and the qos OCI output) ## Generating PCR values @@ -23,7 +36,7 @@ The `scripts/extract-nitro-pcrs.sh` tool reproduces those PCR values locally fro ./scripts/extract-nitro-pcrs.sh ``` -The default invocation reads the qos rev from `src/Cargo.toml`, builds `qos_enclave` in an ephemeral checkout, writes `out/nitro.pcrs`, and prints the three measurements: +The default invocation reads the deployment rev from `src/Cargo.toml`, builds `qos_enclave`, writes `out/nitro.pcrs`, and prints the three measurements: ``` PCR0 @@ -33,22 +46,28 @@ The default invocation reads the qos rev from `src/Cargo.toml`, builds `qos_encl These are the values you place in your PCR allowlist (see [Attestation → PCR management](./attestation#pcr-management)). +To audit a prospective rev bump before updating `src/Cargo.toml`, pass `--rev` explicitly: + +```bash +./scripts/extract-nitro-pcrs.sh --rev <40-char-qos-sha> +``` + ### Options | Flag | Default | Purpose | | --- | --- | --- | -| `--cargo-toml PATH` | `src/Cargo.toml` | Source of the pinned rev. | -| `--qos-dir PATH` | `mktemp -d` | qos checkout location. The default is ephemeral and removed on exit; pass an explicit path to keep cached cargo and docker layers between runs. | +| `--cargo-toml PATH` | `src/Cargo.toml` | Source of the deployment-rev marker. | +| `--qos-dir PATH` | `mktemp -d` | qos checkout location. The default is ephemeral and removed on exit; pass an explicit path to keep cached cargo and docker layers between runs. The script verifies the directory's `origin` URL matches `tkhq/qos` before mutating it. | | `--output PATH` | `out/nitro.pcrs` | Where to write the PCRs file. | -| `--skopeo-image REF` | `quay.io/skopeo/stable:latest` | Container image used to convert the OCI layout to a docker-loadable tar (no host `skopeo` install needed). | -| `--rev REV` | (from `Cargo.toml`) | Override the rev — useful when auditing a prospective qos bump. | +| `--skopeo-image REF` | digest-pinned `quay.io/skopeo/stable` | Container image used to convert the OCI layout to a docker-loadable tar (no host `skopeo` install needed). | +| `--rev REV` | (from `Cargo.toml` marker) | Override the rev. | ### How it works -1. **Read the rev** — the script greps `src/Cargo.toml` for the `git = "https://github.com/tkhq/qos.git", rev = "..."` pattern across all qos workspace deps and fails if they don't agree. -2. **Check out qos** — clones `tkhq/qos` into the work dir and checks out the rev. If `--qos-dir` points at an existing clone whose `HEAD` already matches the rev, the script reuses it. +1. **Read the rev** — the script extracts a 40-character hex from the `# qos-deployment-rev = ...` line in `src/Cargo.toml`. If `--rev` is supplied it bypasses this step. +2. **Check out qos** — clones `tkhq/qos` into the work dir and checks out the rev. If `--qos-dir` points at an existing clone whose `origin` is the upstream `tkhq/qos` URL and whose `HEAD` already matches the rev, the script reuses it; if the origin differs the script refuses to touch the directory. 3. **Build `qos_enclave`** — runs `make out/qos_enclave/index.json` inside the qos checkout. This is qos's deterministic [StageX](https://codeberg.org/stagex/stagex)-based build: it produces a `nitro.eif` and `nitro.pcrs` via `eif_build` and bakes them into the OCI image. The full pipeline lives in qos's [`qos_enclave` Containerfile](https://github.com/tkhq/qos/blob/main/src/images/qos_enclave/Containerfile). -4. **Extract `nitro.pcrs`** — runs `quay.io/skopeo/stable` in a container to convert the OCI directory output to a docker-archive tar, loads it, and copies `/nitro.pcrs` out of a freshly created container. +4. **Extract `nitro.pcrs`** — runs the pinned skopeo image (as your host user, so the staging files clean up) to convert the OCI directory output to a docker-archive tar, loads it under a unique per-run tag, copies `/nitro.pcrs` out of a freshly created container, and removes both the container and the image on exit. ### What the PCRs measure @@ -66,6 +85,35 @@ diff /tmp/pcrs.a /tmp/pcrs.b If the diff is empty, the build reproduced. +## Cross-check against a live prod attestation + +You can confirm the script's PCRs match what the production enclave actually serves by fetching a fresh attestation through [visualsign-turnkeyclient](https://github.com/anchorageoss/visualsign-turnkeyclient). + +```bash +# 1. Build the client (requires GOPRIVATE for the awsnitroverifier dependency) +git clone https://github.com/anchorageoss/visualsign-turnkeyclient.git +cd visualsign-turnkeyclient +GOPRIVATE=github.com/anchorageoss/* go build -o ./bin/visualsign-turnkeyclient . + +# 2. Fetch a real prod attestation and print PCRs (any well-formed unsigned +# tx works — the parser returns the same enclave attestation regardless). +./bin/visualsign-turnkeyclient verify \ + --host https://api.turnkey.com \ + --organization-id \ + --key-name \ + --chain CHAIN_ETHEREUM \ + --unsigned-payload \ + --debug 2>&1 | grep -E '^[[:space:]]+PCR\[[012]\]' + +# 3. Compare against the script output (content only — the file lacks a +# trailing newline, so use `cmp -n` against the file size minus the newline, +# or just visually compare). +./scripts/extract-nitro-pcrs.sh --output /tmp/local.pcrs +cat /tmp/local.pcrs +``` + +If both sets of PCR0/PCR1/PCR2 hex values match, the build reproduces the deployed runtime byte for byte. (PCR3 in the live attestation measures the AWS IAM role and won't appear in `nitro.pcrs` — that's expected.) + ## Next steps - [Security Model](./security-model) — Threat model and trust architecture diff --git a/scripts/extract-nitro-pcrs.sh b/scripts/extract-nitro-pcrs.sh index df05d2baf..1884b0074 100755 --- a/scripts/extract-nitro-pcrs.sh +++ b/scripts/extract-nitro-pcrs.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash -# Build the tkhq/qos enclave image at the rev pinned in src/Cargo.toml and -# extract /nitro.pcrs from the resulting OCI image. +# Build the tkhq/qos enclave image at the deployment rev declared in +# src/Cargo.toml (the `# qos-deployment-rev = …` marker, not the library +# `rev = "..."` on each qos crate) and extract /nitro.pcrs from the +# resulting OCI image. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -8,27 +10,32 @@ REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)" CARGO_TOML="$REPO_ROOT/src/Cargo.toml" OUTPUT="$REPO_ROOT/out/nitro.pcrs" -SKOPEO_IMAGE="quay.io/skopeo/stable:latest" +SKOPEO_IMAGE="quay.io/skopeo/stable@sha256:4d60d6c00b62b463d0a99a7aeedc49358a32c8222540c72d561606e188afb168" +QOS_REMOTE="https://github.com/tkhq/qos.git" QOS_DIR="" REV="" QOS_DIR_AUTO=0 STAGE_DIR="" CID="" +DOCKER_TAG="" usage() { cat </dev/null || true)" + [[ "$origin" == "$QOS_REMOTE" ]] \ + || die "qos checkout $QOS_DIR has origin '$origin'; expected '$QOS_REMOTE'. Refusing to mutate." local head head="$(git -C "$QOS_DIR" rev-parse HEAD)" if [[ "$head" == "$REV" ]]; then @@ -87,7 +93,7 @@ ensure_qos_checkout() { fi echo "Cloning tkhq/qos into $QOS_DIR" >&2 - git clone --quiet https://github.com/tkhq/qos.git "$QOS_DIR" + git clone --quiet "$QOS_REMOTE" "$QOS_DIR" git -C "$QOS_DIR" checkout --quiet "$REV" } @@ -101,15 +107,17 @@ extract_pcrs() { [[ -f "$oci_dir/index.json" ]] || die "qos build did not produce $oci_dir/index.json" STAGE_DIR="$(mktemp -d -t visualsign-pcrs.XXXXXX)" + DOCKER_TAG="qos-enclave:extract-$$-${RANDOM}" docker run --rm \ + --user "$(id -u):$(id -g)" \ -v "$oci_dir:/src:ro" \ -v "$STAGE_DIR:/dst" \ "$SKOPEO_IMAGE" \ - copy oci:/src:latest "docker-archive:/dst/qos_enclave.tar:qos-enclave:latest" + copy "oci:/src:latest" "docker-archive:/dst/qos_enclave.tar:$DOCKER_TAG" docker load -i "$STAGE_DIR/qos_enclave.tar" - CID="$(docker create qos-enclave:latest)" + CID="$(docker create "$DOCKER_TAG")" mkdir -p "$(dirname "$OUTPUT")" docker cp "$CID:/nitro.pcrs" "$OUTPUT" @@ -120,6 +128,9 @@ cleanup() { if [[ -n "$CID" ]]; then docker rm "$CID" >/dev/null 2>&1 || true fi + if [[ -n "$DOCKER_TAG" ]]; then + docker rmi "$DOCKER_TAG" >/dev/null 2>&1 || true + fi if [[ -n "$STAGE_DIR" ]]; then rm -rf "$STAGE_DIR" fi diff --git a/src/Cargo.toml b/src/Cargo.toml index d0d6ff8c9..d2c433263 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -40,6 +40,18 @@ repository = "https://github.com/anchorageoss/visualsign-parser" [workspace.dependencies] # See https://doc.rust-lang.org/nightly/cargo/reference/workspaces.html#the-dependencies-table + +# qos dependencies. Two distinct revs matter: +# * Library rev (the `rev = "..."` on each crate below) — compiled into parser_app +# so it links against qos_core etc. +# * Deployment rev — the qos version whose EIF actually boots in the Nitro enclave +# and is measured by PCRs in attestation documents. They are not required to +# match. The deployment rev currently tracks TVC v2026.2.6: +# https://github.com/tkhq/qos/commit/d866f2c6cbc58cc08c24eab4828f0824ad16a226 +# PCRs from upstream CI: +# https://github.com/tkhq/qos/actions/runs/22202233633/job/64217796901 +# scripts/extract-nitro-pcrs.sh reads the marker line below to reproduce them. +# qos-deployment-rev = d866f2c6cbc58cc08c24eab4828f0824ad16a226 qos_client = { git = "https://github.com/tkhq/qos.git", rev = "365ba7ed529bc5af617bcfb27502c3efce8b37ae", default-features = false } qos_core = { git = "https://github.com/tkhq/qos.git", rev = "365ba7ed529bc5af617bcfb27502c3efce8b37ae", default-features = false } qos_crypto = { git = "https://github.com/tkhq/qos.git", rev = "365ba7ed529bc5af617bcfb27502c3efce8b37ae", default-features = false } From 81817c9a0a4a9e412e0149ece3618e398d9c1c76 Mon Sep 17 00:00:00 2001 From: Prasanna Gautam Date: Wed, 13 May 2026 23:23:34 +0000 Subject: [PATCH 3/4] docs: drop GOPRIVATE from visualsign-turnkeyclient build instructions 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) --- docs/wallet-integration/self-hosted-tee/getting-started.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/wallet-integration/self-hosted-tee/getting-started.mdx b/docs/wallet-integration/self-hosted-tee/getting-started.mdx index 9f7255d48..27564a9ce 100644 --- a/docs/wallet-integration/self-hosted-tee/getting-started.mdx +++ b/docs/wallet-integration/self-hosted-tee/getting-started.mdx @@ -90,10 +90,10 @@ If the diff is empty, the build reproduced. You can confirm the script's PCRs match what the production enclave actually serves by fetching a fresh attestation through [visualsign-turnkeyclient](https://github.com/anchorageoss/visualsign-turnkeyclient). ```bash -# 1. Build the client (requires GOPRIVATE for the awsnitroverifier dependency) +# 1. Build the client git clone https://github.com/anchorageoss/visualsign-turnkeyclient.git cd visualsign-turnkeyclient -GOPRIVATE=github.com/anchorageoss/* go build -o ./bin/visualsign-turnkeyclient . +go build -o ./bin/visualsign-turnkeyclient . # 2. Fetch a real prod attestation and print PCRs (any well-formed unsigned # tx works — the parser returns the same enclave attestation regardless). From b9fa4d30b1d20f44ca2d091c2ed926253435d4e6 Mon Sep 17 00:00:00 2001 From: Prasanna Gautam Date: Wed, 13 May 2026 23:25:21 +0000 Subject: [PATCH 4/4] docs: move PCR extraction flow from self-hosted-tee to hosted 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) --- .../hosted/getting-started.mdx | 112 +++++++++++++++--- .../self-hosted-tee/attestation.mdx | 2 +- .../self-hosted-tee/getting-started.mdx | 111 +---------------- 3 files changed, 99 insertions(+), 126 deletions(-) diff --git a/docs/wallet-integration/hosted/getting-started.mdx b/docs/wallet-integration/hosted/getting-started.mdx index c26e194f1..8072e6d31 100644 --- a/docs/wallet-integration/hosted/getting-started.mdx +++ b/docs/wallet-integration/hosted/getting-started.mdx @@ -3,16 +3,12 @@ title: Turnkey Hosted description: Integrate with VisualSign via Turnkey's hosted enclave service --- -{/* -TODO: Fill in when Turnkey integration details are available. -*/} - Turnkey operates VisualSign parser instances in AWS Nitro Enclaves, providing transaction parsing with attestation verification without requiring you to manage enclave infrastructure. ## Prerequisites -- Turnkey account with API credentials -- Understanding of [attestation verification](../self-hosted-tee/attestation) +- Turnkey account with API credentials. +- Understanding of [attestation verification](../self-hosted-tee/attestation). ## Integration overview @@ -31,21 +27,105 @@ sequenceDiagram W->>W: Display to user ``` -## Getting started +## Attestation verification - -Integration details coming soon. Contact Turnkey for early access. - +Every parse response carries an AWS Nitro attestation document. To trust the parsed output you must: -## Attestation verification +1. Verify the attestation's certificate chain back to the AWS Nitro root CA. +2. Check the document's PCR values against an allowlist you control. +3. Verify the parser's ECDSA signature over the parsed payload using the public key from the attestation. + +See [Attestation Verification](../self-hosted-tee/attestation) for implementation details of all three steps. The remainder of this page covers (2) — specifically, how to obtain the PCR values you should put in your allowlist. + +## PCR allowlist values + +The PCRs are determined by the qos enclave runtime version Turnkey has deployed (currently TVC `v2026.2.6`, qos commit [`d866f2c6cbc58cc08c24eab4828f0824ad16a226`](https://github.com/tkhq/qos/commit/d866f2c6cbc58cc08c24eab4828f0824ad16a226)). You can reproduce them locally from that qos rev — never trust an externally supplied PCR file without verifying it. + +### Generating PCRs from source + +The `scripts/extract-nitro-pcrs.sh` tool in this repo clones tkhq/qos at the deployment rev declared in `src/Cargo.toml`, builds `qos_enclave`, and extracts `/nitro.pcrs`: + +```bash +./scripts/extract-nitro-pcrs.sh +``` + +The deployment rev lives in a marker comment in `src/Cargo.toml`: + +```toml +# qos-deployment-rev = d866f2c6cbc58cc08c24eab4828f0824ad16a226 +``` + +This is distinct from the `rev = "..."` on each `qos_*` crate in the same file, which pins the qos library code that `parser_app` compiles against and does not affect PCRs. To audit a prospective deployment bump before updating the marker, pass `--rev` explicitly: + +```bash +./scripts/extract-nitro-pcrs.sh --rev <40-char-qos-sha> +``` + +#### Prerequisites -Even with a hosted solution, you must verify attestations. The attestation document proves: +- Docker 26+ +- Git and GNU Make +- ~8 GB of free RAM and ~15 GB of free disk (StageX base images, a clean cargo compile, and the qos OCI output). -1. The response came from a genuine AWS Nitro Enclave -2. The enclave is running the expected VisualSign parser code -3. The parsed output hasn't been tampered with +#### Options + +| Flag | Default | Purpose | +| --- | --- | --- | +| `--cargo-toml PATH` | `src/Cargo.toml` | Source of the deployment-rev marker. | +| `--qos-dir PATH` | `mktemp -d` | qos checkout location. The default is ephemeral and removed on exit; pass an explicit path to keep cached cargo and docker layers between runs. The script verifies the directory's `origin` URL matches `tkhq/qos` before mutating it. | +| `--output PATH` | `out/nitro.pcrs` | Where to write the PCRs file. | +| `--skopeo-image REF` | digest-pinned `quay.io/skopeo/stable` | Container image used to convert the OCI layout to a docker-loadable tar (no host `skopeo` install needed). | +| `--rev REV` | (from `Cargo.toml` marker) | Override the rev. | + +#### How it works + +1. **Read the rev** — the script extracts a 40-character hex from the `# qos-deployment-rev = ...` line in `src/Cargo.toml`. If `--rev` is supplied it bypasses this step. +2. **Check out qos** — clones `tkhq/qos` into the work dir and checks out the rev. If `--qos-dir` points at an existing clone whose `origin` is the upstream `tkhq/qos` URL and whose `HEAD` already matches the rev, the script reuses it; if the origin differs the script refuses to touch the directory. +3. **Build `qos_enclave`** — runs `make out/qos_enclave/index.json` inside the qos checkout. This is qos's deterministic [StageX](https://codeberg.org/stagex/stagex)-based build: it produces a `nitro.eif` and `nitro.pcrs` via `eif_build` and bakes them into the OCI image. The full pipeline lives in qos's [`qos_enclave` Containerfile](https://github.com/tkhq/qos/blob/main/src/images/qos_enclave/Containerfile). +4. **Extract `nitro.pcrs`** — runs the pinned skopeo image (as your host user, so the staging files clean up) to convert the OCI directory output to a docker-archive tar, loads it under a unique per-run tag, copies `/nitro.pcrs` out of a freshly created container, and removes both the container and the image on exit. + +#### What the PCRs measure + +PCR0 and PCR1 measure the Nitro EIF and kernel, and PCR2 measures the boot ramdisk that contains qos's `init` binary. The `parser_app` binary is loaded into qos at runtime over vsock and is **not** part of these PCR measurements — its integrity is established separately via the qos manifest, which is covered at [Level 3 attestation](../self-hosted-tee/attestation#level-3-manifest-verification). PCR3 in a live attestation measures the AWS IAM role and is intentionally absent from `nitro.pcrs`. + +### Cross-checking against a live prod attestation + +You can confirm the locally built PCRs match what the production enclave actually serves by fetching a fresh attestation through [visualsign-turnkeyclient](https://github.com/anchorageoss/visualsign-turnkeyclient). + +```bash +# 1. Build the client +git clone https://github.com/anchorageoss/visualsign-turnkeyclient.git +cd visualsign-turnkeyclient +go build -o ./bin/visualsign-turnkeyclient . + +# 2. Fetch a real prod attestation and print PCRs (any well-formed unsigned +# tx works — the parser returns the same enclave attestation regardless). +./bin/visualsign-turnkeyclient verify \ + --host https://api.turnkey.com \ + --organization-id \ + --key-name \ + --chain CHAIN_ETHEREUM \ + --unsigned-payload \ + --debug 2>&1 | grep -E '^[[:space:]]+PCR\[[012]\]' + +# 3. Compare against the script output. +./scripts/extract-nitro-pcrs.sh --output /tmp/local.pcrs +cat /tmp/local.pcrs +``` + +If both sets of PCR0/PCR1/PCR2 hex values match, the local build reproduces the deployed runtime byte for byte. + +## Reproducibility + +The qos build is deterministic. Running the script twice should produce byte-identical `nitro.pcrs` files. Pass `--qos-dir` to the second run so the cargo and docker layer caches are reused — the second build should be a no-op: + +```bash +./scripts/extract-nitro-pcrs.sh --output /tmp/pcrs.a --qos-dir /tmp/qos +./scripts/extract-nitro-pcrs.sh --output /tmp/pcrs.b --qos-dir /tmp/qos +diff /tmp/pcrs.a /tmp/pcrs.b +``` -See [Attestation Verification](../self-hosted-tee/attestation) for implementation details. +If the diff is empty, the build reproduced. ## API reference diff --git a/docs/wallet-integration/self-hosted-tee/attestation.mdx b/docs/wallet-integration/self-hosted-tee/attestation.mdx index 5c330b688..abbb9e593 100644 --- a/docs/wallet-integration/self-hosted-tee/attestation.mdx +++ b/docs/wallet-integration/self-hosted-tee/attestation.mdx @@ -243,7 +243,7 @@ func verifyPCRs(pcrs map[int][]byte) error { When the deployed qos enclave runtime is updated, PCR values change. Follow this process: -1. 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 — see [Generating PCR values](./getting-started#generating-pcr-values) for the distinction). +1. 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 — see [Generating PCRs from source](../hosted/getting-started#generating-pcrs-from-source) for the distinction). 2. Reproduce the new PCR values: run `scripts/extract-nitro-pcrs.sh` with the updated deployment rev and confirm the script's output matches the values published with the release. 3. Add new PCRs to your allowlist. 4. Deploy to production. diff --git a/docs/wallet-integration/self-hosted-tee/getting-started.mdx b/docs/wallet-integration/self-hosted-tee/getting-started.mdx index 27564a9ce..b4455570c 100644 --- a/docs/wallet-integration/self-hosted-tee/getting-started.mdx +++ b/docs/wallet-integration/self-hosted-tee/getting-started.mdx @@ -4,118 +4,11 @@ description: Deploy VisualSign in your own AWS Nitro Enclaves --- -End-to-end deployment instructions for running the parser in your own enclaves are still in progress. The piece below covers reproducing the PCR values you need on the verification side — see [Attestation Verification](./attestation) for how to use them. +Detailed instructions are coming soon. Implementation details are evolving as we finalize the self-hosted deployment architecture. -VisualSign runs inside an AWS Nitro Enclave whose runtime is provided by [tkhq/qos](https://github.com/tkhq/qos). To trust attestation documents from your deployment you need the PCR measurements of that qos enclave image. The `scripts/extract-nitro-pcrs.sh` tool reproduces them locally from a given qos rev. - -## Two qos revs - -The repo pins qos in two distinct places: - -- **Library rev** — the `rev = "..."` on each `qos_*` crate in `src/Cargo.toml`. This is qos compiled into `parser_app` (qos_core, qos_crypto, etc.). It does not affect PCRs. -- **Deployment rev** — the qos version whose EIF actually boots in the enclave and is measured by PCRs. This is recorded as a marker comment in `src/Cargo.toml`: - - ```toml - # qos-deployment-rev = d866f2c6cbc58cc08c24eab4828f0824ad16a226 - ``` - - Currently the deployment rev tracks Turnkey Verifiable Compute (TVC) `v2026.2.6`. The script reads this marker by default. - -The two revs are not required to match. The PCRs you put in your attestation allowlist must be reproduced from the **deployment rev**, never from the library rev. - -## Prerequisites - -- Docker 26+ -- Git and GNU Make -- ~8 GB of free RAM and ~15 GB of free disk (StageX base images, a clean cargo compile, and the qos OCI output) - -## Generating PCR values - -```bash -./scripts/extract-nitro-pcrs.sh -``` - -The default invocation reads the deployment rev from `src/Cargo.toml`, builds `qos_enclave`, writes `out/nitro.pcrs`, and prints the three measurements: - -``` - PCR0 - PCR1 - PCR2 -``` - -These are the values you place in your PCR allowlist (see [Attestation → PCR management](./attestation#pcr-management)). - -To audit a prospective rev bump before updating `src/Cargo.toml`, pass `--rev` explicitly: - -```bash -./scripts/extract-nitro-pcrs.sh --rev <40-char-qos-sha> -``` - -### Options - -| Flag | Default | Purpose | -| --- | --- | --- | -| `--cargo-toml PATH` | `src/Cargo.toml` | Source of the deployment-rev marker. | -| `--qos-dir PATH` | `mktemp -d` | qos checkout location. The default is ephemeral and removed on exit; pass an explicit path to keep cached cargo and docker layers between runs. The script verifies the directory's `origin` URL matches `tkhq/qos` before mutating it. | -| `--output PATH` | `out/nitro.pcrs` | Where to write the PCRs file. | -| `--skopeo-image REF` | digest-pinned `quay.io/skopeo/stable` | Container image used to convert the OCI layout to a docker-loadable tar (no host `skopeo` install needed). | -| `--rev REV` | (from `Cargo.toml` marker) | Override the rev. | - -### How it works - -1. **Read the rev** — the script extracts a 40-character hex from the `# qos-deployment-rev = ...` line in `src/Cargo.toml`. If `--rev` is supplied it bypasses this step. -2. **Check out qos** — clones `tkhq/qos` into the work dir and checks out the rev. If `--qos-dir` points at an existing clone whose `origin` is the upstream `tkhq/qos` URL and whose `HEAD` already matches the rev, the script reuses it; if the origin differs the script refuses to touch the directory. -3. **Build `qos_enclave`** — runs `make out/qos_enclave/index.json` inside the qos checkout. This is qos's deterministic [StageX](https://codeberg.org/stagex/stagex)-based build: it produces a `nitro.eif` and `nitro.pcrs` via `eif_build` and bakes them into the OCI image. The full pipeline lives in qos's [`qos_enclave` Containerfile](https://github.com/tkhq/qos/blob/main/src/images/qos_enclave/Containerfile). -4. **Extract `nitro.pcrs`** — runs the pinned skopeo image (as your host user, so the staging files clean up) to convert the OCI directory output to a docker-archive tar, loads it under a unique per-run tag, copies `/nitro.pcrs` out of a freshly created container, and removes both the container and the image on exit. - -### What the PCRs measure - -The PCRs cover the qos enclave runtime: PCR0 and PCR1 measure the Nitro EIF and kernel, and PCR2 measures the boot ramdisk that contains qos's `init` binary. The `parser_app` binary is loaded into qos at runtime over vsock and is **not** part of these PCR measurements — its integrity is established separately via the qos manifest, which is covered at [Level 3 attestation](./attestation#level-3-manifest-verification). - -## Reproducibility - -The qos build is deterministic. Running the script twice should produce byte-identical `nitro.pcrs` files. Pass `--qos-dir` to the second run so the cargo and docker layer caches are reused — the second build should be a no-op: - -```bash -./scripts/extract-nitro-pcrs.sh --output /tmp/pcrs.a --qos-dir /tmp/qos -./scripts/extract-nitro-pcrs.sh --output /tmp/pcrs.b --qos-dir /tmp/qos -diff /tmp/pcrs.a /tmp/pcrs.b -``` - -If the diff is empty, the build reproduced. - -## Cross-check against a live prod attestation - -You can confirm the script's PCRs match what the production enclave actually serves by fetching a fresh attestation through [visualsign-turnkeyclient](https://github.com/anchorageoss/visualsign-turnkeyclient). - -```bash -# 1. Build the client -git clone https://github.com/anchorageoss/visualsign-turnkeyclient.git -cd visualsign-turnkeyclient -go build -o ./bin/visualsign-turnkeyclient . - -# 2. Fetch a real prod attestation and print PCRs (any well-formed unsigned -# tx works — the parser returns the same enclave attestation regardless). -./bin/visualsign-turnkeyclient verify \ - --host https://api.turnkey.com \ - --organization-id \ - --key-name \ - --chain CHAIN_ETHEREUM \ - --unsigned-payload \ - --debug 2>&1 | grep -E '^[[:space:]]+PCR\[[012]\]' - -# 3. Compare against the script output (content only — the file lacks a -# trailing newline, so use `cmp -n` against the file size minus the newline, -# or just visually compare). -./scripts/extract-nitro-pcrs.sh --output /tmp/local.pcrs -cat /tmp/local.pcrs -``` - -If both sets of PCR0/PCR1/PCR2 hex values match, the build reproduces the deployed runtime byte for byte. (PCR3 in the live attestation measures the AWS IAM role and won't appear in `nitro.pcrs` — that's expected.) - ## Next steps -- [Security Model](./security-model) — Threat model and trust architecture +- [Security Model](./security-model) — Understand the TEE threat model - [Attestation](./attestation) — Implement attestation verification - [gRPC API Reference](/api-reference) — Full API documentation