Deterministic Cross-Reference Binding
Binding SIM signal hashes, VC subject hashes, and passkey credential IDs into a single auditable trust chain.
Overview
A cross-reference is one row saying that a carrier signal and a Verifiable Credential subject belong to the same entity, optionally with a passkey attached. It is the join between the three identity layers, and it holds no plaintext: both identifiers are keyed-hashed before they are stored.
shield-cross-reference owns the row. It is reachable at https://api.passkeybridge.io/v1/shield-cross-reference and authenticates with a dashboard session JWT only. There is no API-key lane and no scope that grants access: a pb_live_ key cannot call it. The tenant is taken from the caller's membership, so a tenant_id in the body is ignored.
The Enterprise plan is enforced in the function. Any other plan answers:
{
"error": "Deterministic Cross-Reference requires an Enterprise subscription"
}establish, bind_passkey and revoke additionally require the caller to be a tenant admin, checked with the is_tenant_admin database function. verify and list are open to any member of the tenant.
Rows are also written by shield-vc-present when a verification passes bind_to_signal_hash. Those rows are stamped hash_version: 1 because the signal digest arrives pre-computed; rows created here from raw identifiers are stamped hash_version: 2. Read them in the dashboard under SIM and credential bindings.
Entity profile hash computation
The entity profile hash is the correlation key. It is a SHA-256 over the two stored digests plus two descriptive fields:
entity_profile_hash = SHA-256("<sim_signal_hash>:<vc_subject_hash>:<carrier>:<credential_type>")The inputs are the digests as stored, so the value is recomputable from the row and from nothing else: the raw phone number and subject DID are never written down. carrier defaults to unknown and credential_type to unknown when not supplied, and both take part in the hash, so establishing the same pair with a different carrier string yields a different composite.
Rows written by shield-vc-present use a different composite: "<sim_signal_hash>:<vc_subject_hash>:native:VerifiableCredential", with the literals native and VerifiableCredential in place of carrier and type. Recompute with the formula matching the row's writer, which is recorded in metadata.source.
Match scoring compares the two event timestamps you supply:
| Condition | Score | match_method |
|---|---|---|
| Timestamps within 5 minutes | 1.00 | exact |
| Within 1 hour | 0.95 | temporal |
| Within 24 hours | 0.85 | fuzzy |
| More than 24 hours apart | 0.70 | fuzzy |
| One or both timestamps missing | 0.80 | partial |
| A timestamp present but unparseable | 0.60 | invalid_timestamps |
The score is stored and returned; the function takes no action on it. There is no threshold that flags a binding for review, so if you want one, compare match_score yourself and treat invalid_timestamps as a data-quality signal rather than a weak match.
API actions
Every call is a POST with an action field and a session JWT.
| Action | Who | Effect |
|---|---|---|
establish | Tenant admin | Create a binding from two raw identifiers |
verify | Tenant member | Look one up and re-check its signature |
bind_passkey | Tenant admin | Attach a passkey credential id to a binding |
revoke | Tenant admin | Deactivate a binding |
list | Tenant member | The 100 newest bindings for the tenant |
curl -X POST https://api.passkeybridge.io/v1/shield-cross-reference \
-H "Authorization: Bearer <dashboard session JWT>" \
-H "Content-Type: application/json" \
-d '{
"action": "establish",
"sim_signal_identifier": "+15551234567",
"vc_subject_identifier": "did:web:example.com:users:alice",
"sim_carrier": "vonage",
"sim_signal_type": "sim_swap_clear",
"vc_credential_type": "VerifiableId",
"vc_issuer": "did:web:issuer.example.com",
"sim_event_time": "2026-09-16T10:00:00Z",
"vc_event_time": "2026-09-16T10:02:00Z"
}'| Field | Type | Default | Meaning |
|---|---|---|---|
sim_signal_identifier | string | required | The raw carrier identifier, usually an E.164 number. Keyed server-side |
vc_subject_identifier | string | required | The raw VC subject id, usually a DID. Keyed server-side |
sim_carrier | string | unknown | Carrier name, part of the composite hash |
vc_credential_type | string | unknown | Credential type, part of the composite hash |
vc_issuer | string | null | Issuer DID or URL, stored for display |
sim_signal_type | string | null | Signal that produced the attestation |
sim_event_time | string | null | ISO 8601. Feeds the match score |
vc_event_time | string | null | ISO 8601. Feeds the match score |
metadata | object | {} | Stored verbatim on the row |
Send raw identifiers, not digests. The function applies HMAC-SHA-256 under the server-held pepper itself, choosing the identifier class from the value's shape, and a missing pepper fails the call rather than writing an unkeyed row. A value you pre-hashed is stored as an opaque subject digest and will not correlate with anything.
{
"cross_reference": {
"id": "0b8f2c41-...",
"match_score": 1.0,
"match_method": "exact",
"entity_profile_hash": "9d4c...",
"verified_at": "2026-09-16T10:02:31.884Z",
"expires_at": "2026-12-15T10:02:31.884Z",
"pqc_algorithm": "ML-DSA-65"
}
}Bindings expire 90 days after creation. pqc_algorithm is null unless the tenant has an active VC provider with PQC enabled; when it is set, the row also carries a hybrid signature over <entity hash>:<sim hash>:<vc hash>:<score>, made of an HMAC-SHA-256 layer and an ML-DSA layer, both verified server-side. Signing failures degrade to an unsigned row rather than failing the call.
Verify, bind a passkey, revoke and list
`verify` takes either an entity_profile_hash or both raw identifiers, and re-checks the stored signature:
curl -X POST https://api.passkeybridge.io/v1/shield-cross-reference \
-H "Authorization: Bearer <dashboard session JWT>" \
-H "Content-Type: application/json" \
-d '{
"action": "verify",
"sim_signal_identifier": "+15551234567",
"vc_subject_identifier": "did:web:example.com:users:alice"
}'{
"verified": true,
"match_score": 1.0,
"match_method": "exact",
"entity_profile_hash": "9d4c...",
"sim_carrier": "vonage",
"vc_credential_type": "VerifiableId",
"vc_issuer": "did:web:issuer.example.com",
"passkey_bound": false,
"verified_at": "2026-09-16T10:02:31.884Z",
"expires_at": "2026-12-15T10:02:31.884Z",
"hash_version": 2,
"pqc_verified": true,
"pqc_algorithm": "ML-DSA-65"
}verified: true means a row was found and is still active. pqc_verified is true or false when the row carries a signature and null when it does not, so read it separately from verified. A missing row answers 200 with {"verified": false, "reason": "No matching cross-reference found"}, and an expired row deactivates itself and answers {"verified": false, "reason": "Cross-reference has expired"}. Identifier lookups match the current pepper, the previous pepper during a rotation, and the legacy unkeyed digest, so older rows still resolve.
`bind_passkey` attaches a passkey to an existing active binding. It takes cross_reference_id (a UUID) and passkey_credential_id, and the passkey must already exist in shield_passkey_credentials for the same tenant. It answers {"bound": true}.
`revoke` takes cross_reference_id and sets is_active false with a revoked_at timestamp, answering {"revoked": true}. The row is kept for audit.
`list` takes no other fields and returns the 100 newest bindings for the tenant, active and inactive, each with id, match_score, match_method, sim_carrier, sim_signal_type, vc_credential_type, vc_issuer, entity_profile_hash, passkey_credential_id, verified_at, expires_at, is_active, hash_version and pqc_algorithm. There is no pagination; the dashboard reads the same rows directly.
Errors
| Status | Body | Cause |
|---|---|---|
400 | Unknown action. Valid: establish, verify, bind_passkey, revoke, list | Unrecognised or missing action |
400 | Missing sim_signal_identifier or vc_subject_identifier | establish without both identifiers |
400 | Provide entity_profile_hash OR both sim_signal_identifier and vc_subject_identifier | verify with neither lookup key |
400 | Invalid cross_reference_id | Not a UUID, on bind_passkey or revoke |
400 | Missing passkey_credential_id | bind_passkey without the credential id |
400 | Invalid JSON body | Body did not parse |
401 | Missing authorization header or Invalid auth token | No session JWT, or one that failed validation |
403 | Deterministic Cross-Reference requires an Enterprise subscription | Tenant plan is not enterprise |
403 | Only admins can establish cross-references | Non-admin calling a write action |
404 | No tenant found | The caller has no tenant membership |
404 | Passkey credential not found in this tenant | bind_passkey with an unknown credential id |
404 | Cross-reference not found or inactive | bind_passkey against a missing or revoked row |
404 | Cross-reference not found or already revoked | revoke against a missing or revoked row |
409 | Cross-reference already exists plus existing_id | An active binding for this pair already exists |
409 | Cross-reference already exists (concurrent create) | Two establishes raced; the unique index won |
413 | Payload too large | Body over the small JSON limit |
502 | Failed to resolve tenant plan and similar | A database read failed |
500 | Internal server error | Unhandled failure, including a missing hash pepper |
bind_passkey and revoke report the same 404 whether the row is absent, belongs to another tenant, or is already inactive.
Revocation behaviour today
A carrier hard signal does not revoke cross-references today. The cascade orchestrator revokes agent delegates and A2A negotiations; it does not touch shield_cross_references, shield_vc_issued or passkey credentials, and no other code path revokes a binding automatically. Bindings end in one of two ways: action: "revoke", or the 90-day expiry, which verify applies lazily the next time the row is read.
To act on a hard signal, do it from your own handler: receive the signal through a playbook webhook, then call shield-cross-reference with action: "revoke" for the binding and shield-vc-status with action: "revoke" for each credential you want invalidated. A passkey attached with bind_passkey is a recorded association, so removing it is a separate call against the passkey surface.
Automatic cross-pillar revocation is the subject of patent applications 19/553,357 and 64/006,812, both pending USPTO examination. Treat it as a filed design rather than shipped behaviour.
Confirm a revocation: verify answers {"verified": false, "reason": "No matching cross-reference found"} for the pair, and the row in Dashboard > SIM and credential bindings carries a Revoked badge.
Related from the blog
- Credential Chaining: Deriving Trust from a Sequence of Issuer Attestationsengineering · 18 min read
- The Missing Link in Identity Assurance: Cross-Referencing SIM Signals with Verifiable Credentialssecurity · 15 min read
- Passkeys Are Not Enough: Why Biometric Binding Needs a Carrier Signal Layersecurity · 11 min read