> ## Documentation Index
> Fetch the complete documentation index at: https://visualsign.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Attestation Verification

> Verify that a parser response came from the expected code in a genuine AWS Nitro Enclave

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](https://visualsign.dev/wallet-integration/self-hosted-tee/security-model.md).

## Prerequisites

* [visualsign-turnkeyclient](https://github.com/anchorageoss/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](https://github.com/anchorageoss/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](https://visualsign.dev/wallet-integration/self-hosted-tee/getting-started.md#expected-values).

## What a response carries

A successful parse response has two parts that matter for verification:

| Field | Contents |
| - | - |
| `response.parsedTransaction.payload` | `signablePayload` (the JSON your wallet renders), `inputPayloadDigest`, `metadataDigest`, and optionally `intermediateOutput` |
| `response.parsedTransaction.signature` | `message`, `publicKey`, `scheme` and `signature`: the enclave's P-256 signature over the payload |
| `bootProof.awsAttestationDocB64` | The AWS Nitro attestation document (COSE Sign1, CBOR), from the enclave's NSM device |
| `bootProof.qosManifestB64`, `bootProof.qosManifestEnvelopeB64` | The QOS manifest the enclave booted with, and the envelope carrying its approvals |
| `bootProof.ephemeralPublicKeyHex` | The enclave's ephemeral key, which signs responses |

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 | Verifies | Use case |
| - | - | - |
| Level 1 | Payload binding + signature | Development and testing |
| Level 2 | Level 1 + attestation document + PCRs + key binding | Production |
| Level 3 | Level 2 + manifest: the exact binary and configuration | High-security environments, and anyone running their own deployment |

## 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:

   | PCR | Measures |
   | - | - |
   | PCR0 | The enclave image file (the QOS EIF) |
   | PCR1 | The Linux kernel and bootstrap |
   | PCR2 | The application in the enclave image: QOS's boot ramdisk, *not* the parser binary |
   | PCR3 | The IAM role of the parent instance; TVC sets this per cluster |

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:

```bash theme={null}
visualsign-turnkeyclient verify \
  --host <parser-url> \
  --organization-id <org-id> --key-name <api-key-name> \
  --chain CHAIN_ETHEREUM --unsigned-payload <hex-or-base64-tx> \
  --pcrs "0:<pcr0>,1:<pcr1>,2:<pcr2>,3:<pcr3>" \
  --qos-manifest-hex <manifest-hash> \
  --save-qos-manifest envelope.bin
```

* `--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:

```bash theme={null}
visualsign-turnkeyclient decode-manifest envelope --file envelope.bin
```

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](https://visualsign.dev/wallet-integration/self-hosted-tee/getting-started.md#4-verify-the-deployment-end-to-end).

## 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](https://visualsign.dev/wallet-integration/hosted/getting-started.md#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:

```yaml theme={null}
# pcr-allowlist.yaml
production:
  - pcr0: "<current pcr0>"   # current QOS runtime
    pcr1: "<current pcr1>"
    pcr2: "<current pcr2>"
    pcr3: "<current pcr3>"
  - pcr0: "<previous pcr0>"  # remove after the migration
    pcr1: "<previous pcr1>"
    pcr2: "<previous pcr2>"
    pcr3: "<previous pcr3>"
```

## 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

* [Self-Hosted TEE](https://visualsign.dev/wallet-integration/self-hosted-tee/getting-started.md) — Deploy and verify your own parser
* [Security Model](https://visualsign.dev/wallet-integration/self-hosted-tee/security-model.md) — The trust architecture

## Resources

* [visualsign-turnkeyclient](https://github.com/anchorageoss/visualsign-turnkeyclient)
* [awsnitroverifier](https://github.com/anchorageoss/awsnitroverifier)
* [AWS Nitro Enclaves: attestation](https://docs.aws.amazon.com/enclaves/latest/user/set-up-attestation.html)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.