Deterministic Cross-Reference Binding

Binding SIM signal hashes, VC subject hashes, and passkey credential IDs into a single auditable trust chain.

Last reviewed September 16, 2026Fresh

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:

ConditionScorematch_method
Timestamps within 5 minutes1.00exact
Within 1 hour0.95temporal
Within 24 hours0.85fuzzy
More than 24 hours apart0.70fuzzy
One or both timestamps missing0.80partial
A timestamp present but unparseable0.60invalid_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.

ActionWhoEffect
establishTenant adminCreate a binding from two raw identifiers
verifyTenant memberLook one up and re-check its signature
bind_passkeyTenant adminAttach a passkey credential id to a binding
revokeTenant adminDeactivate a binding
listTenant memberThe 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"
  }'
FieldTypeDefaultMeaning
sim_signal_identifierstringrequiredThe raw carrier identifier, usually an E.164 number. Keyed server-side
vc_subject_identifierstringrequiredThe raw VC subject id, usually a DID. Keyed server-side
sim_carrierstringunknownCarrier name, part of the composite hash
vc_credential_typestringunknownCredential type, part of the composite hash
vc_issuerstringnullIssuer DID or URL, stored for display
sim_signal_typestringnullSignal that produced the attestation
sim_event_timestringnullISO 8601. Feeds the match score
vc_event_timestringnullISO 8601. Feeds the match score
metadataobject{}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

StatusBodyCause
400Unknown action. Valid: establish, verify, bind_passkey, revoke, listUnrecognised or missing action
400Missing sim_signal_identifier or vc_subject_identifierestablish without both identifiers
400Provide entity_profile_hash OR both sim_signal_identifier and vc_subject_identifierverify with neither lookup key
400Invalid cross_reference_idNot a UUID, on bind_passkey or revoke
400Missing passkey_credential_idbind_passkey without the credential id
400Invalid JSON bodyBody did not parse
401Missing authorization header or Invalid auth tokenNo session JWT, or one that failed validation
403Deterministic Cross-Reference requires an Enterprise subscriptionTenant plan is not enterprise
403Only admins can establish cross-referencesNon-admin calling a write action
404No tenant foundThe caller has no tenant membership
404Passkey credential not found in this tenantbind_passkey with an unknown credential id
404Cross-reference not found or inactivebind_passkey against a missing or revoked row
404Cross-reference not found or already revokedrevoke against a missing or revoked row
409Cross-reference already exists plus existing_idAn active binding for this pair already exists
409Cross-reference already exists (concurrent create)Two establishes raced; the unique index won
413Payload too largeBody over the small JSON limit
502Failed to resolve tenant plan and similarA database read failed
500Internal server errorUnhandled 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