Skip to main content

How to Verify a TrustRelay Decision Record Offline

A how-to for verifying an Agent Change Passport without network access to TrustRelay: what the envelope contains, which checks the verifier runs, and how to read a failure.

A signed decision record is only useful if someone other than the issuer can check it. This guide covers how to verify an Agent Change Passport offline, using the standalone verifier and a public key you already hold. You do not need an account, an API token, or network access to TrustRelay.

When to use this

Use this procedure when you need to confirm that a decision record is authentic and unaltered:

  • An auditor asks you to prove a specific change was authorized by policy.
  • You are validating TrustRelay during a security review and want to check the cryptography yourself.
  • You are reproducing a decision months after it was made and cannot reach the service.
  • You are building a downstream system that consumes passports and must not trust an unsigned claim.

This is a verification procedure, not a setup procedure. It assumes a passport envelope file and the signing key's public PEM are already in your possession.

What the envelope contains

The stored envelope is self-contained. Everything needed to verify it travels inside the file, except the public key, which you supply.

FieldPurpose
envelope_versionFormat version. The verifier accepts 1.0.0.
passport_idIdentity of this passport.
payloadThe decision: ids, outcome, and the revision the decision applies to.
payload_bytesThe canonical serialization of payload.
payload_hashSHA-256 of payload_bytes.
signaturesOne or more signatures over payload_bytes.
verificationKey ARN, key version, signing algorithm, and the verification instructions address.

The distinction between payload and payload_bytes matters. The signature covers payload_bytes, not the JSON object you see when you pretty-print the file. If a tool re-serializes the payload before checking the signature, the signature will not verify even though nothing is wrong. This is why the verifier compares the canonical form of payload against payload_bytes rather than trusting either alone.

Prerequisites

  • Node.js 20 or later.
  • The passport envelope as a JSON file, for example passport.json.
  • The public key for the signing key named in verification.kms_key_arn, as a PEM file or PEM string.
  • A checkout of the verifier package, or the package installed from your registry. It is not published to the public npm registry, so there is no bare verifier command to run: you invoke it through Node or import it as a library.

The public key is published alongside the passport. Confirm that the key you use matches the kms_key_arn in the envelope before you treat a passing result as meaningful; a valid signature under the wrong key proves nothing about this record.

Run the verifier

Build the package, then run its CLI entry point with Node. Pass the envelope and the public key as file paths.

pnpm --filter @trust-relay/verifier build
node dist/cli.js --envelope passport.json --public-key signing-key.pem

You can also supply the key inline, or set the inputs through the environment.

node dist/cli.js --envelope passport.json --public-key "$(cat signing-key.pem)"
export PASSPORT_ENVELOPE_FILE=passport.json
export PASSPORT_PUBLIC_KEY_FILE=signing-key.pem
node dist/cli.js

The verifier writes a JSON result to standard output and sets the exit code.

{
  "passportId": "passport-001",
  "signatureValid": true,
  "payloadHashMatch": true,
  "envelopeVersionSupported": true,
  "timestamp": "2026-10-06T12:00:00.000Z",
  "errors": [],
  "valid": true
}

Interpret the exit code

Exit codeMeaning
0The envelope is valid: the hash matches and at least one signature verifies.
1The envelope parsed, but verification failed. Read errors for the specific check that failed.
2The verifier could not run. The inputs were missing, unreadable, or an option was unknown.

An exit code of 2 is not a statement about the passport. It means the verifier never reached a verdict, so do not treat it as a failed verification in an audit record.

What the verifier checks

The verifier runs every check and reports all of them, so a single run tells you everything that is wrong rather than only the first problem it found.

CheckWhat it establishes
Envelope structureThe required fields are present and correctly typed.
Envelope versionenvelope_version is a version the verifier supports.
Canonical bytesThe canonical form of payload equals payload_bytes.
Payload hashSHA-256 of payload_bytes equals payload_hash.
SignatureAt least one signature verifies against payload_bytes under the supplied public key.

Read the list in that order when you are debugging. A canonical-bytes or hash mismatch means the content changed after signing, which makes the signature comparison meaningless; fix the earlier failure first, then re-run.

Read a failure

valid: false always arrives with a non-empty errors array. The verifier reports these strings, and each one points at a different problem:

ErrorWhat it meansWhat to do
Invalid passport envelopeA required field is missing or the wrong type.Re-export the envelope; the file is truncated or was hand-edited.
payload_bytes_mismatchThe canonical form of payload does not equal payload_bytes.The payload was re-serialized by something other than the canonical encoder, or edited after signing.
Payload hash mismatch — content may have been tampered withpayload_bytes does not hash to payload_hash.The file changed after it was written. Treat the record as unverified.
No signature verifiedThe hash is intact but no signature verifies under the key you supplied.Confirm the PEM is the key named in verification.kms_key_arn and that you have the right key version.
Unsupported envelope version: <value>The envelope was written by a newer format.Upgrade the verifier rather than working around it.
Failed to parse passport JSONThe input is not valid JSON.Re-export the envelope; the file is corrupted.

If the hash matches and the signature verifies, the record is authentic: it was signed by the holder of that key over exactly those bytes. Verification does not tell you whether the decision was correct. It tells you the record has not been altered since it was signed, which is the property an audit needs.

Verify from your own code

The verifier is a library as well as a command. Import the verification function directly when you are checking passports inside a build, a test, or a batch job. This is the path to use in production systems, because it does not depend on the CLI entry point.

import { readFileSync } from "node:fs";
import { verifyPassport } from "@trust-relay/verifier";

const envelope = readFileSync("passport.json", "utf8");
const publicKey = readFileSync("signing-key.pem", "utf8");

const result = verifyPassport(envelope, publicKey);
if (!result.valid) {
  throw new Error(`Passport failed verification: ${result.errors.join(", ")}`);
}

The library returns the same fields the CLI prints, so a script that consumes one can consume the other. The package is private, so resolving @trust-relay/verifier requires either a workspace checkout or an install from the registry that hosts it.

Limitations

Verification proves integrity and origin for the bytes you were given. It does not prove that the evidence inside the decision was complete, that the policy was correct, or that the decision was the right one. Those are properties of the decision process, not of the signature. A passport that verifies cleanly is a trustworthy record of what was decided, and nothing more than that.

The public key registry is the trust anchor. If you accept a key from an untrusted source, a valid signature under that key tells you nothing. Fetch the key from a channel you already trust, or pin the expected key ARN and version in your verification step.