Skip to main content
Before trusting any parsed transaction, your wallet must verify that the response came from a genuine enclave running the expected code, and that nothing changed it on the way. For background on why verification matters, see Security Model.

Prerequisites

  • visualsign-turnkeyclient, the reference verifier (recommended). It performs every check on this page. To build your own, its verify/service.go is the reference implementation, and awsnitroverifier handles the attestation document.
  • Expected values for the deployment you trust: PCR0-3, plus the pivot binary hash or manifest hash. See Expected values.

What a response carries

A successful parse response has two parts that matter for verification: On a self-hosted parser_http_server, error responses carry a bootProof with every field empty. Treat any response without a verifiable proof as unverified.

Verification levels

Level 1: Payload binding and signature

  1. Check the digests against your request: inputPayloadDigest is the SHA-256 of the exact unsigned_payload string you sent, and metadataDigest is the SHA-256 of the Borsh-encoded chain metadata you sent (SHA-256 of empty input when you sent none). This ties the response to your transaction rather than someone else’s.
  2. Recompute the signed message: the SHA-256 of the Borsh-encoded ParsedTransactionPayload (signablePayload, inputPayloadDigest, metadataDigest, plus intermediateOutput when present). It must equal signature.message. Without this check, an attacker on the transport could replace signablePayload and keep a valid signature over some other message.
  3. Verify signature.signature, a P-256 ECDSA signature over SHA-256 of the message bytes. signature.publicKey is 130 bytes: QOS’s encryption key followed by its signing key, each a 65-byte uncompressed SEC1 point. Verify with the second 65 bytes.
On its own, this proves only that someone holding that key signed the payload. Level 2 proves the key belongs to an attested enclave.

Level 2: Attestation document

  1. Decode bootProof.awsAttestationDocB64 and verify its certificate chain against the AWS Nitro root CA.
  2. Compare PCR0-3 against your allowlist:
  3. Check that the document’s public_key is the same signing key as signature.publicKey. The document may carry the 65-byte signing key alone or the same 130-byte form, so compare signing halves. This is what makes the signature attested: the enclave committed to the key at attestation time.
The parser binary isn’t measured into any PCR. QOS loads it after boot and binds it through the manifest, which is what Level 3 checks.

Level 3: Manifest verification

The attestation document’s user_data is the hash of the QOS manifest the enclave booted with. That manifest names the parser binary (pivot.hash, its sha256), its arguments (pivot.args), whether debug mode is on, the expected PCRs, and the operators who approved it.
  1. Decode bootProof.qosManifestB64 and hash it the way QOS does for its schema version:
    • v2 (TVC writes this for new apps): the field carries QOS storage JSON, and the hash is QOS’s canonical-JSON manifest hash.
    • v0/v1: the field carries Borsh, and the hash is SHA-256 of those bytes.
    A field that starts with { is v2 JSON; anything else is Borsh. The hash must equal user_data.
  2. Check the manifest’s contents against the deployment you trust:
    • pivot.hash is the sha256 of the parser binary you expect.
    • pivot.args are the arguments you expect, such as the ABI trust posture and --boot-proof-source nsm.
    • Debug mode is off.
    • The manifest set lists the operators you expect.
    Alternatively, once you’ve done this once, pin the manifest hash itself. Any change to the deployment changes it.

Verifying with visualsign-turnkeyclient

verify performs Levels 1 and 2, and the Level 3 hash check, in one call:
  • --pcrs pins the PCRs. Without it, the PCRs are only reported, not checked, and verify still exits 0.
  • --qos-manifest-hex pins the manifest hash against user_data. Without it, the client checks that the returned manifest hashes to user_data and accepts any manifest that does. If the response carries no manifest, it skips that check entirely.
  • --pivot-binary-hash-hex is despite its name another way to pass the manifest hash: it’s also compared with user_data, so the binary’s sha256 fails it. Likewise, the pivotBinaryHash field in verify’s JSON output is the manifest hash.
  • --save-qos-manifest writes the envelope so you can inspect it:
This prints the pivot binary hash, args, debug mode, PCRs, manifest set and manifest hash for the Level 3 content checks. The manifest set appears only as a threshold and member count; add --json to see each member’s pubKey. turnkeyclient doesn’t compare the binary hash, args or operators for you. For the hosted service, --host is https://api.turnkey.com. For your own deployment, it’s the app URL; see Self-Hosted TEE.

Freshness

The attestation document isn’t bound to a request: it has no nonce, and the same document serves many responses. Validate its certificate chain at the document’s own timestamp, not your current clock. turnkeyclient skips the timestamp check entirely. On a self-hosted parser_http_server (--boot-proof-source nsm), the parser caches its document and re-attests before the chain expires. NSM’s leaf certificate lasts about 3 hours and is reused across attestations, so a served document can be up to about 3 hours old. If your verifier enforces freshness, allow for that.

PCR management

Updating your allowlist

When the deployed QOS runtime changes, PCR0-2 change. Follow this process:
  1. Watch for announcements of a new QOS runtime version. This is the version whose EIF boots in the enclave, not the qos library version the parser links against; see Generating PCRs from source.
  2. Reproduce the new PCRs with scripts/extract-nitro-pcrs.sh --rev <qos-commit>, and confirm they match the published values.
  3. Add the new PCRs to your allowlist, and deploy.
  4. Remove the old PCRs once the migration completes.
PCR3 changes when the app moves to a different TVC cluster, and it can’t be reproduced from source. Take it from the cluster announcement. A new parser release changes the pivot binary hash and the manifest hash, but not the PCRs.

Supporting multiple versions

During a migration, accept more than one PCR set:

Troubleshooting

PCR mismatch

  • The QOS runtime was updated, so PCR0-2 changed. Reproduce the new values.
  • The app moved to a different TVC cluster, so PCR3 changed.
  • The deployment runs in debug mode, which zeroes the PCRs. Never trust a debug-mode attestation.

Manifest hash mismatch

  • The deployment changed. Every new deployment has a new manifest, even with the same binary.
  • The verifier hashed v2 JSON with the v0/v1 method, or the reverse. Check the field’s first byte.

Public key mismatch

  • The signature and the attestation came from different keys. Treat the response as unverified.

Signature verification failed

  • The recomputed message doesn’t match signature.message. The payload was modified, or the verifier’s Borsh encoding is wrong. Include intermediateOutput if the response has one.
  • Confirm you verify over SHA-256 of the message bytes, using the key from the attestation.

Next steps

Resources