Prerequisites
- visualsign-turnkeyclient, the reference verifier (recommended). It performs every check on this page. To build your own, its
verify/service.gois 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
- Check the digests against your request:
inputPayloadDigestis the SHA-256 of the exactunsigned_payloadstring you sent, andmetadataDigestis 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. - Recompute the signed message: the SHA-256 of the Borsh-encoded
ParsedTransactionPayload(signablePayload,inputPayloadDigest,metadataDigest, plusintermediateOutputwhen present). It must equalsignature.message. Without this check, an attacker on the transport could replacesignablePayloadand keep a valid signature over some other message. - Verify
signature.signature, a P-256 ECDSA signature over SHA-256 of the message bytes.signature.publicKeyis 130 bytes: QOS’s encryption key followed by its signing key, each a 65-byte uncompressed SEC1 point. Verify with the second 65 bytes.
Level 2: Attestation document
-
Decode
bootProof.awsAttestationDocB64and verify its certificate chain against the AWS Nitro root CA. -
Compare PCR0-3 against your allowlist:
-
Check that the document’s
public_keyis the same signing key assignature.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.
Level 3: Manifest verification
The attestation document’suser_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.
-
Decode
bootProof.qosManifestB64and 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.
{is v2 JSON; anything else is Borsh. The hash must equaluser_data. -
Check the manifest’s contents against the deployment you trust:
pivot.hashis the sha256 of the parser binary you expect.pivot.argsare 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.
Verifying with visualsign-turnkeyclient
verify performs Levels 1 and 2, and the Level 3 hash check, in one call:
--pcrspins the PCRs. Without it, the PCRs are only reported, not checked, andverifystill exits 0.--qos-manifest-hexpins the manifest hash againstuser_data. Without it, the client checks that the returned manifest hashes touser_dataand accepts any manifest that does. If the response carries no manifest, it skips that check entirely.--pivot-binary-hash-hexis despite its name another way to pass the manifest hash: it’s also compared withuser_data, so the binary’s sha256 fails it. Likewise, thepivotBinaryHashfield inverify’s JSON output is the manifest hash.--save-qos-manifestwrites the envelope so you can inspect it:
--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-hostedparser_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:- 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.
- Reproduce the new PCRs with
scripts/extract-nitro-pcrs.sh --rev <qos-commit>, and confirm they match the published values. - Add the new PCRs to your allowlist, and deploy.
- Remove the old PCRs once the migration completes.
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. IncludeintermediateOutputif the response has one. - Confirm you verify over SHA-256 of the message bytes, using the key from the attestation.
Next steps
- Self-Hosted TEE — Deploy and verify your own parser
- Security Model — The trust architecture