# Verifiable credentials

Truora Pass can package a user's verified identity into a **verifiable credential**: a cryptographically signed document that the user holds in their own Truora Pass wallet and presents to your application on demand. Unlike the OAuth 2.0 flows — where your application reads identity data from the [UserInfo endpoint](/truora-pass/userinfo_and_claims/) on every access — a verifiable credential is verified **locally, against Truora Pass's published public keys**. Once you hold a presentation, no Truora API call is required to check that the data is authentic.

Truora Pass issues credentials in the **SD-JWT VC** format (IETF SD-JWT-based Verifiable Credentials), following the OpenID for Verifiable Credential Issuance (OpenID4VCI) and OpenID for Verifiable Presentations (OpenID4VP) profiles.

## The trust triangle

Verifiable credentials involve three roles:

| Role | Who | What they do |
|---|---|---|
| **Issuer** | Truora Pass | Verifies the user's identity (document + face verification), signs a credential over the verified claims, and publishes its public keys and revocation status lists. |
| **Holder** | The user, through the Truora Pass wallet | Stores the credential on their own device and decides — claim by claim — what to disclose, and to whom. |
| **Verifier** | Your application | Receives a presentation from the holder and verifies the issuer's signature, the holder's proof of possession, and the revocation status — all cryptographically. |

The issuer and the verifier never need to talk to each other at presentation time: the verifier trusts the issuer's **signature**, not a live API response. See [Verifying credential presentations](/truora-pass/verifying_presentations/) for the verifier integration.

## The SD-JWT VC format

A Truora Pass credential is a compact JWT signed by Truora Pass with **ES256** (ECDSA over the P-256 curve), with the JOSE header `typ: dc+sd-jwt` and a `kid` that references a key in the Truora Pass [JWKS](/truora-pass/verifying_presentations/#discovery-endpoints). The credential travels as the signed JWT followed by `~`-separated **disclosures**:

```text
<issuer-signed JWT>~<disclosure 1>~<disclosure 2>~…~<disclosure N>~
```

The issuer-signed JWT payload contains the registered claims plus digests of the selectively disclosable claims:

```json
{
  "iss": "https://pass.truora.com",
  "vct": "urn:truora:pass:identity:1",
  "iat": 1719945600,
  "exp": 1751481600,
  "cnf": {
    "jwk": { "kty": "EC", "crv": "P-256", "x": "<holder key x>", "y": "<holder key y>" }
  },
  "status": {
    "status_list": {
      "idx": 42,
      "uri": "https://api.pass.truora.com/v1/credentials/status-lists/identity-v1"
    }
  },
  "_sd": ["<digest 1>", "<digest 2>", "…"],
  "_sd_alg": "sha-256"
}
```

| Claim | Meaning |
|---|---|
| `iss` | The credential issuer — always `https://pass.truora.com` |
| `vct` | The credential type — `urn:truora:pass:identity:1` for the identity credential |
| `iat` / `exp` | Issued-at and expiration times |
| `cnf.jwk` | The holder's public key ([holder binding](#holder-binding)) |
| `status.status_list` | Where to check [revocation](#revocation): a status list URI and this credential's index in it |
| `_sd` / `_sd_alg` | Digests of the disclosable claims, and the digest algorithm (`sha-256`) |

## Selective disclosure

The identity claims are **not** in the signed payload in clear text. Each one is a separate **disclosure** — the base64url encoding of a JSON array `[<salt>, <claim name>, <claim value>]`:

```text
["6qMQvRL5haj…", "given_name", "Ana"]  →  base64url  →  WyI2cU1RdlJMNWhhai4uLiIsICJnaXZlbl9uYW1lIiwgIkFuYSJd
```

Only the SHA-256 digest of each disclosure appears in the signed `_sd` array. When the user presents the credential, the wallet attaches **only the disclosures the user approved** — the rest of the claims stay hidden, while the issuer's signature remains verifiable. A verifier recomputes each attached disclosure's digest and matches it against `_sd`, so disclosed values cannot be forged or altered.

This is what makes the credential privacy-preserving: the user can prove their `given_name` and `family_name` to one application and additionally their `document_number` to another, from the same signed credential.

## The identity credential

Truora Pass issues one credential type, `vct: urn:truora:pass:identity:1`, over the user's **verified identity**. Issuance requires the user to have, inside Truora Pass:

- a **verified, unexpired identity document**, and
- a completed **face verification**.

The credential carries these selectively disclosable claims, sourced from the verified document:

| Claim | Description |
|---|---|
| `given_name` | Given name(s), as printed on the verified document |
| `family_name` | Family name(s), as printed on the verified document |
| `birthdate` | Date of birth, `YYYY-MM-DD` |
| `document_number` | The verified document's number |
| `document_type` | The verified document's type (for example `national-id`, `passport`) |
| `country` | The document's issuing country |
| `nationality` | The user's nationality |

**Note**: the credential's expiration never outlives the document it was issued from — `exp` is capped at the verified document's expiration date (and at most one year from issuance). Users can re-issue at any time; re-issuing revokes the previous identity credential, so each user has at most one active identity credential.

## Holder binding

Every credential is bound to a key pair generated on the user's device when the credential is issued. The public key is embedded in the signed credential as `cnf.jwk`; the private key is non-extractable and **never leaves the device**.

When the user presents the credential, the wallet appends a **Key Binding JWT (KB-JWT)** — a short JWT with header `typ: kb+jwt`, signed with the holder's private key, containing the verifier's `nonce`, the `aud`ience, and `sd_hash` (a digest of the exact presented credential and disclosures). The KB-JWT proves the presenter *possesses* the credential's bound key: a copied or leaked SD-JWT cannot be replayed by anyone else, and a captured presentation cannot be replayed against a different request, because each nonce is single-use.

## Revocation

Truora Pass publishes signed **status lists** (Token Status List format). Each credential's `status.status_list` claim points to a list URI and the credential's bit index in that list. The list endpoint is public and requires no authentication:

```bash
curl https://api.pass.truora.com/v1/credentials/status-lists/identity-v1
```

It returns a compact JWT (`Content-Type: application/statuslist+jwt`) signed by Truora Pass, whose payload carries one bit per credential — bit set means **revoked**. A credential is revoked when the user deletes it from their wallet, or automatically when they re-issue a new one.

Verifiers must check the status list as part of verification — a presentation of a revoked credential is rejected with the `credential_revoked` error code. See [Verifying credential presentations](/truora-pass/verifying_presentations/#verify-the-chain-yourself) for the full verification procedure.

## How presentations reach your application

There are two ways your application can receive a credential presentation:

1. **You request it (OpenID4VP)** — your backend creates a presentation request with the claims you need; the user approves (or trims) the disclosure in Truora Pass, and you collect the verified result. This is the integration described in [Verifying credential presentations](/truora-pass/verifying_presentations/).
2. **The user shares it (share link + QR)** — the user creates a share link or QR code from their wallet, choosing which claims to include; anyone with the link sees the verified claims and can independently verify the raw presentation. See [Issuing and sharing credentials](/truora-pass/sharing_credentials/).
