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
tvcCLI logged in (tvc login). - An operator key held locally (
~/.config/turnkey/orgs/<org-alias>/operator.json, where<org-alias>is the org nametvc loginsaved). That key is what approves your deployments. - Docker, to pull the image and hash the binary inside it.
tvc-deployfrom this repo. It isn’t installed on yourPATH: wherever this page runstvc-deploy, runcargo 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>.publicand<api-key-name>.private(the Turnkey CLI’s key format).tvc logindoesn’t create these files, and visualsign-turnkeyclient refuses to run without them, even when your parse routes are open. Your organization ID is theidunder your org in~/.config/turnkey/tvc.config.toml.
1. Pick and pin the image
Images are published toghcr.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.
2. Create the app
Generate a template withtvc 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:
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:
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.
A pass means all of the following held:
inputPayloadDigestis the SHA-256 of the payload you sent, and the response’s signed message is the hash of exactly thesignablePayload,inputPayloadDigestandmetadataDigestit returned, so the payload can’t be swapped in transit.- The attestation document (
bootProof.awsAttestationDocB64) verifies against the AWS Nitro root CA, and PCR0-3 match your pinned values. - The attestation’s
public_keyis the key that signed the response. - The manifest from
bootProofhashes to the attestation’suser_data. If a response carries no manifest at all,verifyskips this check, which is why later runs should pin--qos-manifest-hex. - The response signature verifies.
- Pivot Config / Binary Hash equals the
$PIVOT_DIGESTyou computed in step 1. This ties the attestation to the exact binary. - Args are exactly the
pivotArgsyou deployed. - DebugMode is
false. - Manifest Set lists only your operators. The human-readable output shows only the threshold and member count, so add
--jsonand compare eachmanifest.manifestSet.members[].pubKeywith thepublic_keyin youroperator.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.
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
Everydeploy-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.
Deploying from a fork
The images underghcr.io/anchorageoss are built and pushed by this repo’s stagex workflow. To deploy a parser you changed, run the same workflow in your fork. It pushes to your own namespace, ghcr.io/<owner>/<target>, and you deploy that image with steps 1 to 4 above.
Why a pull request can’t publish your image
Workflows for a pull request from a fork run in this repository, with a read-onlyGITHUB_TOKEN and none of its secrets. That stops code in a pull request from writing to this repository’s packages or reading its credentials. Those runs still build every image, but they skip the registry login and push. To get a published image, run the workflow in your fork, where it uses your fork’s own token.
Publish from your fork
- In your fork, open the Actions tab and enable workflows. GitHub disables them on new forks.
- Start a
stagexrun in one of these ways:- Run it manually: Actions → stagex → Run workflow, or
gh workflow run stagex.yml --repo <owner>/visualsign-parser --ref <branch-or-tag>. - Push a
v*.*.*tag. - Push to
mainwith changes undersrc/,images/,scripts/,.github/or theMakefile.
- Run it manually: Actions → stagex → Run workflow, or
- Find the images at
ghcr.io/<owner>/parser_app,parser_cli,parser_gatewayandparser_http_server.<owner>is your user or organization name in lowercase, since ghcr.io only accepts lowercase paths. Every build is tagged with its full commit SHA. Manual runs and tag pushes are also tagged with the branch or tag name (a character a registry tag can’t hold, such as the/infeature/x, becomes-), and pushes tomainwithlatest,mainandv<semver>. A manual run onmaingetslatestandmaintoo, but notv<semver>. - New packages on ghcr.io are private. TVC has to pull the image, so make each package you deploy public: on the package’s page, open Package settings and change its visibility. Only
ghcr.io/anchorageoss/*images are official. A publicghcr.io/<owner>/...package is yours and carries no endorsement from this project. If your TVC setup can pull a private image with credentials, that is the alternative to making it public; this page hasn’t confirmed that TVC supports it.
org.opencontainers.image.source label names the repository that built it. Treat it as a label, not proof: an image is identified by its registry path and digest.
You don’t need to add secrets or change settings. The job requests packages: write itself and pushes with your fork’s GITHUB_TOKEN.
The DOCKERHUB_API_KEY secret is optional. It only logs in to Docker Hub to avoid pull rate limits on the stagex base images, and the login is skipped when the secret isn’t set. Leave it unset in your fork: the login uses this repo’s Docker Hub username, so another account’s key fails.
Pushing to main in your fork also runs the release workflow. It tags a version, creates a GitHub release in your fork and starts stagex for that tag. Disable the release workflow in your fork if you don’t want that.
Deploying your image to TVC
TheTVC Deploy workflow only runs in anchorageoss/visualsign-parser, because it needs that repository’s Turnkey credentials. Deploy your image from your own machine with tvc-deploy, against your own Turnkey organization, as in steps 1 to 4: set IMAGE to your ghcr.io/<owner>/parser_http_server:<tag>. tvc-deploy authenticates with your tvc login configuration, or with TVC_ORG_ID, TVC_API_KEY_PUBLIC and TVC_API_KEY_PRIVATE set in the environment.
For parser_app and parser_http_server, each stagex run’s summary lists the pinned image URL and the expected pivot digest for your image. On a tagged build with a matching release in your fork, the run also adds them to that release’s notes.
Verifying an image someone else built
Before you deploy an image you didn’t build, such as one from a contributor’s fork, rebuild it rather than trusting it. The images are built with StageX, so the same commit and build inputs produce the same binary. A rebuild that matches proves the binary was built from that source. It does not prove the source is safe, so read the source before you deploy it. The rebuild also runs the fork’s build files on your machine: itsMakefile (including any $(shell ...) call) and its Containerfiles. Review them first, or do the rebuild in a disposable VM.
Every stagex run’s summary lists, for each target, the Commit, the VERSION, the SOURCE_URL and the OCI manifest digest it built. VERSION is compiled into the binary and SOURCE_URL is recorded in the image’s source label, so pass the same values when you rebuild. The run summary comes from the fork, so treat each value as untrusted input. Save these steps as one script, which reads the values as data, checks them, shows you the fork’s changes, and builds only after you confirm. Paste each value at its prompt; don’t edit them into the script, where a quote or ; in a value would run as shell code. The build needs Docker with the containerd image store enabled and a docker-container buildx builder, as in CI (create it once with docker buildx create --driver docker-container --name build --use):
VERSION. They matter because the Makefile passes both values to the shell, so a value with a quote or a ; in it would run commands on your machine.
A manifest digest mismatch is not conclusive. This reads the first entry of index.json, which assumes it has a single entry, and this page hasn’t confirmed that it equals the registry digest after docker load and push. The pivot hash below is what decides.
Runs started by a pull request build a temporary merge commit that isn’t in the fork. Their summary lists the PR head and base commits instead, and they can’t be reproduced exactly. Verify a manual, tag or main run.
Then compare the pivot binary from your build with the one in the published image. This is the hash TVC measures, so it’s the check that matters:
IMAGE_URL) and the PIVOT_DIGEST from your own build, as in step 3. deploy-pivot re-derives the digest from the image it pulls and refuses a mismatch, and the attested manifest records the hash you approved.
Next steps
- Attestation Verification — What each check proves, and how to implement it
- Security Model — The TEE threat model
- API Reference — Request and response formats