Cached Trust Proofs

Store a short-lived, encrypted proof of a completed verification and check it again later, including from a terminal that cannot reach the identity provider.

Last reviewed September 16, 2026Fresh

Overview

A cached trust proof is a record that a subject was verified, stored encrypted with an expiry, and checked again later by its id. The payload is whatever the issuing system wants to carry forward, for example a credential type and issuer. The platform encrypts it, stores a content digest for integrity, and answers validity questions about it until it expires or is revoked.

The name describes the intent, keeping a verification usable where connectivity is poor. Validation itself is an online call to shield-cached-proof: the terminal presenting a proof must reach PasskeyBridge, and nothing in the platform verifies a proof offline.

Who can call it. A signed-in user's JWT, sent as Authorization: Bearer [session JWT]. There is no API key lane. The tenant is resolved from the caller's oldest membership rather than from the request body, so a user in several tenants always acts in the same one here. The tenant's plan must be enterprise, checked directly against the tenant row.

Where it lives. https://api.passkeybridge.io/v1/shield-cached-proof. The function is edge locked, so a call to the Supabase functions host answers 403 Direct access denied.

Identifiers. user_identifier is sent raw and hashed server side with keyed HMAC-SHA-256 under the server-held pepper before anything is stored. The stored user_hash is never reversible, and a missing pepper fails the write closed. Rows written before keyed hashing carry hash version 1 and are still matched on read.

Proof lifecycle

Store. A proof is created after your own verification succeeds. The payload is serialised, digested with SHA-256 into proof_hash for the integrity check, encrypted with AES-256-GCM, and inserted with an expiry.

Validate. A caller presents the proof id. The platform checks that the row exists in this tenant, that is_valid is true and revoked_at is unset, and that expires_at is in the future, and marks the row invalid if it has passed. When the caller also sends proof_payload, the platform re-digests it and compares it with the stored proof_hash. Without a payload only existence and status are confirmed, and the response says so in reason.

Revalidate. An explicit call that stamps revalidated_at, sets network_available to true and writes a cached_proof_revalidated event. It is an action you invoke when a terminal comes back online. Nothing queues it, and no background job runs it.

Revoke. An admin-only call that sets is_valid false and stamps revoked_at.

Proofs are also invalidated without anyone calling this function:

  • cleanup_expired_records() flips is_valid to false for every proof whose expires_at has passed.
  • The freeze_credentials playbook action invalidates every valid proof in the tenant and stamps revoked_at.
  • The quarantine playbook action does the same alongside cross-references and shadow identities. Both are tenant-wide rather than per subject; see playbooks.
  • An Art. 17 erasure overwrites proof_payload_encrypted with SHREDDED, clears the signature and sets is_valid false for the subject's proofs.

Trust levels are standard, high and critical, with standard the default. They are labels you choose and store; the platform does not derive them from how a subject was verified and does not treat one differently from another at validation time.

network_available records whether the proof was stored while the issuer had connectivity. It defaults to true and is set false only when you send network_available: false.

Hybrid signatures on proofs

A proof is signed only when the tenant has an active credential provider row with pqc_enabled set. Otherwise pqc_signature and pqc_algorithm are stored as null and the proof is protected by encryption and the content digest alone.

When signing is enabled, the platform signs the proof's content digest twice and stores both parts in one JSON value:

  • A classical HMAC-SHA-256 signature under a key derived from the platform key and the tenant domain. This layer is symmetric and server held.
  • An ML-DSA signature, FIPS 204. The parameter set follows the tenant's configured level: ML-DSA-65 by default, ML-DSA-87 when the tenant is set to level 5.

pqc_algorithm records which parameter set was used, so a later verifier selects the right key. The stored value also carries the quantum nonce mixed into the signed payload and the key fingerprint.

Two limits matter:

  • Validation does not check the signature. The validate action checks expiry, revocation and, when you supply the payload, the SHA-256 content digest. The stored hybrid signature is not verified on that path today, by this function or by the dashboard.
  • Signing failures are not fatal. If signing raises, the error is logged and the proof is stored unsigned. A null pqc_algorithm in the store response is how you detect that.

Entropy provenance for the signature nonce is written to the entropy pool before the proof row that names it, so a signed proof can be traced to its seed. See PQC migration.

Actions, requests and responses

Five actions on POST https://api.passkeybridge.io/v1/shield-cached-proof, all with an Authorization bearer header carrying the session JWT.

ActionWhoPurpose
storeAny tenant memberCreate a proof
validateAny tenant memberCheck a proof by id
revalidateAny tenant memberStamp a proof as re-checked and online
revokeTenant adminInvalidate a proof
listAny tenant member, admin when filtering by identifierList up to 100 proofs

Store request.

{
  "action": "store",
  "user_identifier": "+15551234567",
  "proof_type": "vc_cache",
  "proof_payload": { "credential_type": "government_id", "issuer": "did:web:issuer.example.com" },
  "trust_level": "high",
  "expires_in_hours": 24,
  "network_available": true,
  "metadata": { "terminal": "lobby-1" }
}
FieldTypeRequiredDefault and rules
user_identifierstringyesNon-empty. Hashed server side, never stored raw
proof_payloadobject or stringyesSerialised before encryption
proof_typestringnotrust_token, vc_cache or passkey_attestation. Any other value silently becomes trust_token
trust_levelstringnostandard, high or critical. Any other value silently becomes standard
expires_in_hoursnumberno1 to 720. Default 72. Outside the range the request is rejected
network_availablebooleannoDefaults to true; only an explicit false changes it
metadataobjectnoStored as given, defaults to an empty object

There is no tenant_id field and no ttl_hours field. The unrecognised-value coercions mean a typo in proof_type or trust_level returns 200 with a different value than you sent, so read the response back.

Store response.

{
  "proof": {
    "id": "6f0a8e1c-5f3a-4a2e-9f12-2a7d4c9b8e30",
    "proof_type": "vc_cache",
    "proof_hash": "<sha256 hex of the payload>",
    "trust_level": "high",
    "issued_at": "2026-09-16T14:30:00.000Z",
    "expires_at": "2026-09-17T14:30:00.000Z",
    "network_available": true,
    "pqc_algorithm": "ML-DSA-65"
  }
}

Validate request and response. proof_id is a UUID; proof_payload is optional and turns on the integrity check.

{ "action": "validate", "proof_id": "6f0a8e1c-5f3a-4a2e-9f12-2a7d4c9b8e30", "proof_payload": { "credential_type": "government_id", "issuer": "did:web:issuer.example.com" } }
{
  "valid": true,
  "integrity_checked": true,
  "reason": "Proof is valid, integrity verified",
  "proof_type": "vc_cache",
  "trust_level": "high",
  "issued_at": "2026-09-16T14:30:00.000Z",
  "expires_at": "2026-09-17T14:30:00.000Z",
  "network_available": true,
  "revalidated_at": null
}

A revoked or expired proof answers 200 with valid: false and reason set to Proof has been revoked or Proof has expired. A payload mismatch answers 200 with valid: false and Integrity check failed, payload hash mismatch.

Revalidate takes proof_id and returns { "revalidated": true, "proof_type": ..., "trust_level": ..., "revalidated_at": ... }. An expired proof answers 200 with revalidated: false and is marked invalid.

Revoke takes proof_id and returns { "revoked": true }.

List takes no required fields and returns { "proofs": [...] }, the 100 most recent rows for the tenant with their type, hash, trust level, user hash, hash version, timestamps, validity and network flag. Adding user_identifier narrows the list to one subject, matched across the current pepper, the previous pepper during a rotation and the legacy unkeyed digest. That filter is admin only, because submitting a raw identifier and reading back whether a row exists would otherwise be a way to confirm membership.

Errors

StatusBodyCause
401Missing or malformed authorization headerNo bearer token
401Invalid auth tokenToken did not resolve to a user
404No tenant foundThe user has no tenant membership
403Cached Proof System requires an Enterprise subscriptionTenant plan is below Enterprise; the body also carries upgrade_hint
403Direct access denied. Use https://api.passkeybridge.io/v1/{function}.Request bypassed the public host
400Unknown action. Valid: store, validate, revalidate, revoke, listAction missing or unrecognised
400Missing or invalid user_identifier (must be a non-empty string)Store without an identifier
400Missing proof_payloadStore without a payload
400expires_in_hours must be between 1 and 720TTL out of range or not a number
400Missing or invalid proof_id (must be UUID)Validate, revalidate or revoke without a UUID
400Invalid JSON bodyBody did not parse
413Payload too largeRequest body above 64 KB
403Only admins can revoke cached proofsRevoke by a non-admin member
403Only admins can list cached proofs by user_identifierFiltered list by a non-admin member
404{ "valid": false, "reason": "Proof not found" }Unknown id, or a proof in another tenant
404{ "revoked": false, "reason": "Proof not found, already revoked, or belongs to another tenant" }Revoke that matched nothing
502Failed to store cached proof and similarThe database call failed; the specific message is in the function log
500Internal server errorAnything unhandled, including a missing hash pepper

Note the split: a lookup that finds nothing answers 404, while a proof that is found and unusable answers 200 with valid: false. Treat only valid: true as a pass.

Verify it worked

  1. Read the store response. id is what you present later, and pqc_algorithm tells you whether the proof was signed.
  2. Call validate with that id and the same payload. valid is true and integrity_checked is true.
  3. Open Dashboard, Offline trust tokens. The tab is Enterprise only and lists the 100 most recent proofs with tiles for total, valid, revalidated and revoked. Find your proof by the first 16 characters of its proof hash and check the Status column reads Valid and Network reads Online.
  4. After a revalidate, the Revalidated tile increases and an event of type cached_proof_revalidated is written for the tenant.

One display quirk to expect: the tab maps the trust levels High, Standard and Low. A proof stored as critical is a valid value that the table renders as Unknown in the Trust column. The stored value is unchanged, and list and validate both return critical correctly.

Related from the blog