Trust, Evidence & Independent Verification
How Passport anchors evidence, computes receipts, and lets a third party independently verify a receipt without trusting Passport's word. This is the specification for exactly what bytes get hashed and signed, so you can reproduce the math offline.
1. Evidence Ingestion — the six source types
POST /api/v1/passport/agents/:subject_commitment/evidence accepts exactly these source_type values. The payload must be a JSON OBJECT (never a raw string).
| source_type | Required payload shape (all optional unless noted) |
|---|---|
| github_push_webhook | { ref?, repository?: { full_name?, html_url? }, head_commit?: { id?, sha?, message?, author?: { name?, email? } }, commits?: [{ id?, sha?, message?, raw?, html_url?, author? }] } |
| github_commit_payload | { sha?, html_url?, commit?: { message?, author?: { name?, email? }, committer?: { name?, email? } } } |
| github_issue_event | { agent_identity?, repository?, issue?: { id?, number?, url?, title? }, labels?: string[], action?, summary?, transcript_url?, observed_at? } |
| compliance_report | { agent_identity?, control_domain?, report_id?, report?: { id?, url?, title? }, action?, transcript_url?, observed_at? } |
| otel_genai_trace | { name?, attributes?: Record<string, unknown>, status?: { code?: string | number, message? }, startTimeUnixNano?, endTimeUnixNano?, start_time?, end_time? } |
| task_deliverable | { task_id: string, digest: "64-hex" (REQUIRED), observed_at? } |
2. Canonicalization before signing (critical)
The agent signs the payload digest. Getting this exactly right is the most common integration failure.
- Serialize the payload as JSON with object keys recursively sorted (canonical JSON).
- Compute
event_digest = sha256-hex(canonicalJson(payload)). - Sign the UTF-8 bytes of that hex digest with the agent's Ed25519 private key.
- Send
signatureas the 128-hex Ed25519 signature over that digest.
The payload is a JSON object. If you send a string, ingestion fails with 400 / 401. Do not use JSON.stringify(obj) on only the top level — every nesting level must be key-sorted, matching canonicalJson.
3. Receipt content_hash canonicalization (offline verification)
To independently verify a receipt, recompute the content_hash from the canonical field set, then check the Ed25519 signature over that hash.
receipt_id, issued_at, operator_id, agent_id, receipt_type, status, input_digest, authority_scope, expiry, revocation_statusOptional fields (included only when defined): output_hash, refusal_reason, terminal_reason, prev_receipt_hash, domain, error_tranche
- Build the canonical object from the fields above (exclude
signatureandcontent_hash). - Serialize with key-sorted canonical JSON where omission of undefined optional fields is preserved.
- Compute
content_hash = sha256-hex(canonicalJson(payload)). - The Ed25519 signature signs the UTF-8 bytes of
content_hash.
A masked public manifest is available at GET /api/v1/receipts/:id/public-manifest with the commitment hash, signature, verification status, and Merkle inclusion path. Public key resolution uses GET /api/v1/public-key (current) and /api/v1/public-key/key-history (per-kid).
custody receipt via the evidence bridge. Enable with EVIDENCE_BRIDGE_AUTO_ENABLED=true and provision a dedicated minter operator via EVIDENCE_BRIDGE_OPERATOR_ID (its credit balance funds the minting). The bridge is idempotent on eventCommitmentHash, so replayed events never double-mint.4. Signed webhook contract
How Passport signs webhook events so consumers can verify authenticity.
Events: evidence.anchored, enrollment.completed, reputation.degraded, reputation.restored, reputation.milestone.
Headers: X-Passport-Event (event name) and X-Passport-Signature.
Signing scheme: signature = sha256-hex(canonicalJson({ event, data, timestamp }) + secret) where secret is the per-subscription whsec_... returned at registration.
Body shape: { "event": string, "data": object, "timestamp": ISO-8601 }
Retry semantics: Delivery retries up to 3 attempts with exponential backoff (1s, 2s, 4s) and a 5-second per-attempt timeout; a delivery that still fails is marked deadLetter: true.
Receiver verification kit: recompute sha256-hex(bodyBytes + secret) over the exact serialized bytes you received (do not re-stringify), compare constant-time against X-Passport-Signature, and enforce the timestamp is within ~5 minutes to block replays. Passport ships verifyWebhookSignature (constant-time + optional freshness) in src/lib/webhooks/webhook-service.ts. Reference guide: GET /api/v1/webhooks/verify-guide. A runnable known-answer fixture + example verifier lives in examples/webhook-verifier/ (see its README.md) — run it to mechanically confirm the rule end-to-end.
gen_ai.* span attributes. Recognized gen_ai.operation.name values: chat, completion, embeddings, tool, agent, invoke_agent, run_agent, task, team. Agent identity is read from gen_ai.agent.id, gen_ai.agent.name, gen_ai.participant.id, or falling back to gen_ai.request.model. Token usage is read from gen_ai.usage.input_tokens/output_tokens (and prompt_tokens/completion_tokens variants). Status code may be the OTel string "ERROR" or the integer 2.AgentReputationCredential (Ed25519-signed, W3C VC 2.0) that travels between gateways. Fetch it at GET /api/v1/credentials/:commitment and verify at POST /api/v1/credentials/verify without calling Passport at request time. The A2A agent card (/.well-known/agent.json) embeds the portable_reputation reference so standing is embeds the portable_reputationreference so standing is discoverable with a device's identity.GET /api/v1/badge/:commitment/attestation returns a shareable Passport Verified — Authenticated AI Build card (SVG, or ?format=jsonmetadata) framing "this build/artifact is authenticated by Passport — not an impostor". Use it as the artifact's proof-of-origin stamp on READMEs, product pages, and agent discovery.5. Production environment requirement: INGESTION_COMMITMENT_SALT
Evidence anchoring requires a stable commitment salt in production.
A correctly-signed evidence POST returns 500 INGESTION_COMMITMENT_SALT is required outside test environments unless the operator has set INGESTION_COMMITMENT_SALT in the production environment. This salt is a long random value that must be stable across all app instances and never committed to git. The admin command center surfaces its status in the health panel. If it is missing, no agent can anchor evidence — treat it as a production incident.