Skip to main content
You can run your own VisualSign parser as a Turnkey Verifiable Cloud (TVC) app. You own the app and the operator keys that approve its deployments, and you pick the parser image and its configuration. Turnkey runs the AWS Nitro Enclave and the QuorumOS (QOS) runtime underneath it. The binary to deploy is parser_http_server. It serves the same Turnkey HTTP+JSON envelope as the hosted service. Each successful response carries a bootProof with a real NSM attestation document, so any client can verify the response without trusting you, Turnkey’s API, or the network in between.
parser_http_server reads the manifest and ephemeral key that QOS provisions (/qos.manifest, /qos.ephemeral.key), so it runs as a QOS pivot. It does not run on a bare Nitro Enclave without QOS.

Prerequisites

  • A Turnkey organization, with the tvc CLI logged in (tvc login).
  • An operator key held locally (~/.config/turnkey/orgs/<org-alias>/operator.json, where <org-alias> is the org name tvc login saved). That key is what approves your deployments.
  • Docker, to pull the image and hash the binary inside it.
  • tvc-deploy from this repo. It isn’t installed on your PATH: wherever this page runs tvc-deploy, run cargo run --manifest-path tools/tvc-deploy/Cargo.toml -- from a checkout of this repo instead.
  • visualsign-turnkeyclient, to verify the deployment.
  • A Turnkey API key for visualsign-turnkeyclient, stored as ~/.config/turnkey/keys/<api-key-name>.public and <api-key-name>.private (the Turnkey CLI’s key format). tvc login doesn’t create these files, and visualsign-turnkeyclient refuses to run without them, even when your parse routes are open. Your organization ID is the id under your org in ~/.config/turnkey/tvc.config.toml.

1. Pick and pin the image

Images are published to ghcr.io/anchorageoss/parser_http_server. Always reference one by tag and digest: a tag alone is mutable, and a bare digest is shown back as :latest.
Hash the pivot binary yourself. This is the value QOS measures, and the value a verifier will later find in the attested manifest:

2. Create the app

Generate a template with tvc app init -o app.json and set name and manifestSetParams. Make sure the manifest set contains only operators whose keys you hold. If it doesn’t, nobody can approve a deployment and the app is stranded. The template’s newOperators already holds the public key of your local operator.json, so for a first app you can keep it. To reuse an existing operator instead, list it by ID and clear newOperators:
Leave dangerousEnableDebugModeDeployments false. Debug mode zeroes the PCRs, so an attesting parser can never go healthy in it.
--no-operator-reuse stops tvc from substituting the operator from your last app create (without it, tvc prints Reusing operator ... and uses that one). Check that the printed Manifest Set Operator IDs are the ones you expect. That ID is the <your-operator-id> you pass to deploy-pivot below, and App ID is the <app-id>.

3. Deploy

deploy-pivot re-derives the binary digest from the image and refuses to continue on a mismatch. It then creates the deployment, approves it with your operator key, waits until it’s healthy, and sets it live. Everything after -- becomes the deployment’s pivotArgs, and those are part of the manifest the attestation commits to: TVC has no environment-variable injection: pivotArgs is the parser’s entire configuration, and anyone verifying the deployment can read it from the attested manifest. With --boot-proof-source nsm, GET /health returns 503 until the parser holds a verified, unexpired attestation. A replica whose NSM attestation fails never goes healthy, so “healthy” means each replica has attested successfully. deploy-pivot approves with tvc deploy approve --dangerous-skip-interactive, so it doesn’t show you the manifest before signing. Step 4 is where you check what you approved. The app is served at https://<publicDomain>, where publicDomain comes from tvc app list --message-format json. For new apps it’s app-<app-id>.app.turnkey.cloud, but older apps use app-<app-id>.turnkey.cloud, so read it rather than constructing it. The routes are GET /health, POST /visualsign/api/v1/parse and POST /visualsign/api/v2/parse. The parse routes take the Turnkey JSON envelope, not the gRPC messages in the API reference:
A curl response proves nothing on its own; verify it as described in the next step.

4. Verify the deployment end to end

visualsign-turnkeyclient checks everything a wallet should check, in one command. Pin the PCRs (see Expected values below), and save the manifest so you can inspect it:
--key-name stamps the request. The parser only checks the stamp if you set --allowed-stamp-pubkeys-hex, in which case the key must be on the list; the client needs the key files either way.
Always pass --pcrs. Without it, verify still exits 0 and prints PCRs verified: 32 PCRs found, but it hasn’t compared them to anything. Take the PCR values from an independent source (see Expected values), never from verify’s own output or from the manifest’s Enclave section: both come from the server you’re checking.
A pass means all of the following held:
  1. inputPayloadDigest is the SHA-256 of the payload you sent, and the response’s signed message is the hash of exactly the signablePayload, inputPayloadDigest and metadataDigest it returned, so the payload can’t be swapped in transit.
  2. The attestation document (bootProof.awsAttestationDocB64) verifies against the AWS Nitro root CA, and PCR0-3 match your pinned values.
  3. The attestation’s public_key is the key that signed the response.
  4. The manifest from bootProof hashes to the attestation’s user_data. If a response carries no manifest at all, verify skips this check, which is why later runs should pin --qos-manifest-hex.
  5. The response signature verifies.
Then confirm the manifest describes the deployment you intended:
Check that:
  • Pivot Config / Binary Hash equals the $PIVOT_DIGEST you computed in step 1. This ties the attestation to the exact binary.
  • Args are exactly the pivotArgs you deployed.
  • DebugMode is false.
  • Manifest Set lists only your operators. The human-readable output shows only the threshold and member count, so add --json and compare each manifest.manifestSet.members[].pubKey with the public_key in your operator.json.
  • Hashes / Manifest is the manifest hash. Record it: later runs can pin it with verify --qos-manifest-hex <hash>, which fails on any change to the deployment.
Don’t use verify’s JSON output for the binary check: its pivotBinaryHash field holds the manifest hash (user_data), not the binary’s sha256. For the same reason, verify --pivot-binary-hash-hex is compared with user_data, so passing it your $PIVOT_DIGEST fails.

Expected values

For how each check works and how to implement them in your own verifier, see Attestation Verification.

Updating

Every deploy-pivot creates a new deployment with a new manifest, so the manifest hash changes. The binary hash and PCRs change only if you change the image or TVC changes the QOS runtime. Re-run step 4 after each deploy and update the values you pin.

Next steps