Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 96 additions & 16 deletions docs/wallet-integration/hosted/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -31,21 +27,105 @@ sequenceDiagram
W->>W: Display to user
```

## Getting started
## Attestation verification

<Note>
Integration details coming soon. Contact Turnkey for early access.
</Note>
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 <your-org-id> \
--key-name <your-turnkey-api-key> \
--chain CHAIN_ETHEREUM \
--unsigned-payload <hex-or-base64-tx> \
--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

Expand Down
12 changes: 6 additions & 6 deletions docs/wallet-integration/self-hosted-tee/attestation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 deployed 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. 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.
5. Remove old PCRs after migration completes.

### Supporting multiple versions

Expand Down
156 changes: 156 additions & 0 deletions scripts/extract-nitro-pcrs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env bash
# 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)"
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@sha256:4d60d6c00b62b463d0a99a7aeedc49358a32c8222540c72d561606e188afb168"
QOS_REMOTE="https://github.com/tkhq/qos.git"
QOS_DIR=""
REV=""

QOS_DIR_AUTO=0
STAGE_DIR=""
CID=""
DOCKER_TAG=""

usage() {
cat <<EOF
Usage: $(basename "$0") [options]

Build the tkhq/qos enclave image at the deployment rev declared in
src/Cargo.toml (the '# qos-deployment-rev = ...' marker comment) and
extract /nitro.pcrs from the resulting OCI image. Pass --rev to target
any other qos rev — useful when auditing a prospective deployment bump
before updating Cargo.toml.

Options:
--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 Skopeo image (pinned by digest) (default: pinned upstream)
--rev REV Override the rev (skip Cargo.toml marker)
-h, --help Show this help and exit
EOF
}

die() {
echo "$*" >&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 marker
marker=$(grep -oE '^#[[:space:]]*qos-deployment-rev[[:space:]]*=[[:space:]]*[0-9a-f]{40}' "$CARGO_TOML" || true)
[[ -n "$marker" ]] || die "No '# qos-deployment-rev = ...' marker in $CARGO_TOML"
REV=$(echo "$marker" | grep -oE '[0-9a-f]{40}')
}
Comment on lines +62 to +68
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.


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 origin
origin="$(git -C "$QOS_DIR" remote get-url origin 2>/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
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 "$QOS_REMOTE" "$QOS_DIR"
git -C "$QOS_DIR" checkout --quiet "$REV"
}
Comment on lines +76 to +98
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.


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_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:$DOCKER_TAG"

docker load -i "$STAGE_DIR/qos_enclave.tar"
CID="$(docker create "$DOCKER_TAG")"

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 "$DOCKER_TAG" ]]; then
docker rmi "$DOCKER_TAG" >/dev/null 2>&1 || true
fi
if [[ -n "$STAGE_DIR" ]]; then
rm -rf "$STAGE_DIR"
fi
Comment on lines +112 to +136
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.

if [[ "$QOS_DIR_AUTO" -eq 1 && -n "$QOS_DIR" ]]; then
rm -rf "$QOS_DIR"
fi
}
Comment on lines +127 to +140
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.


main() {
parse_args "$@"
if [[ -z "$REV" ]]; then
read_rev
fi
trap cleanup EXIT
ensure_qos_checkout
build_qos_enclave
extract_pcrs
Comment on lines +142 to +150
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.

echo "Wrote $OUTPUT:" >&2
cat "$OUTPUT"
echo
}

main "$@"
12 changes: 12 additions & 0 deletions src/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Loading