VettiGuard

Digital Credentials & Selective Disclosure

Browse the complete reference in the browser, copy request examples, and follow the documented validation contract without opening packaged Markdown files.

Complete browser reference

Digital Credentials & Selective Disclosure

VettiGuard can issue a holder-bound selective-disclosure credential from an approved, active reusable VettiGuard identity. The credential lets a holder reveal only the bounded facts a verifier needs instead of repeatedly sharing the underlying identity case.

Assurance boundary: VettiGuard implements the selective-disclosure mechanics of RFC 9901. The dc+sd-jwt credential profile and Token Status List profile used here track current IETF Internet-Drafts and are therefore labelled work-in-progress profiles. This surface does not claim OpenID4VCI, OpenID4VP, a W3C VC Data Model 2.0 representation, government/issuer identity, or a legal electronic signature.

Endpoints

Public metadata:

GET /api/v1/credentials/issuer
GET /api/v1/credentials/jwks
GET /api/v1/credentials/types/verified-identity-v1
GET /api/v1/credentials/status-list?list=revocation-1

Protected-site flow (recommended scoped API permission: site.credentials):

POST /api/v1/credentials/issue
POST /api/v1/credentials/presentation/challenge
POST /api/v1/credentials/present
POST /api/v1/credentials/verify
POST /api/v1/credentials/revoke

Native/mobile equivalents are available below /api/v1/mobile/credentials/* (recommended scoped API permission: mobile.credentials) and remain subject to the application's configured mobile-integrity policy.

1. Create the reusable identity first

A credential can only be issued from an approved documentary identity case that has already been promoted to a reusable VettiGuard identity. The reusable identity holder receives a one-time vgid_... holder token. Raw relying-application subject identifiers are not persisted in the credential service.

2. Create a holder key

The holder generates its own signing key. VettiGuard supports P-256 EC and RSA holder public JWKs. The private key stays with the holder.

Example public P-256 JWK:

{
  "kty": "EC",
  "crv": "P-256",
  "x": "base64url-x-coordinate",
  "y": "base64url-y-coordinate"
}

The JWK is embedded in cnf.jwk; VettiGuard also persists its RFC 7638 thumbprint and checks the signed credential against that thumbprint during presentation verification.

3. Issue a credential

POST /api/v1/credentials/issue
Authorization: Bearer vg_secret_...
Content-Type: application/json
{
  "holder_token": "vgid_...",
  "holder_jwk": {
    "kty": "EC",
    "crv": "P-256",
    "x": "...",
    "y": "..."
  },
  "claims": [
    "identity_verified",
    "assurance_level",
    "age_over_18"
  ],
  "valid_days": 180
}

Supported bounded identity claims are:

identity_verified
assurance_level
age_over_18
document_type
issuing_country
face_match
liveness

The service deliberately does not issue raw document numbers, document images, portraits, names, addresses, phone numbers, or the application's raw subject identifier through this credential type.

The response contains a serialized dc+sd-jwt credential, its type (vct), holder-binding information, expiry, and Token Status List reference.

4. Create a verifier challenge

Before verification, the verifier creates a short-lived one-use challenge:

POST /api/v1/credentials/presentation/challenge
Authorization: Bearer vg_secret_...
Content-Type: application/json
{
  "audience": "https://app.example.com"
}

The response includes an opaque challenge_token, a cryptographic nonce, audience, and expiry. The challenge expires after five minutes and its token/nonce/audience are stored only as keyed hashes.

5. Select disclosures

POST /api/v1/credentials/present
Authorization: Bearer vg_secret_...
Content-Type: application/json
{
  "credential": "issuer-jwt~disclosure~disclosure~",
  "claims": ["identity_verified", "age_over_18"],
  "audience": "https://app.example.com",
  "nonce": "nonce-returned-by-challenge"
}

VettiGuard returns the issuer JWT plus only the selected disclosures, the SD hash, and a kb+jwt payload template. The holder signs the KB-JWT with the private key corresponding to cnf.jwk.

The KB-JWT must contain:

typ = kb+jwt
iat
aud
nonce
sd_hash

6. Verify the holder-bound presentation

POST /api/v1/credentials/verify
Authorization: Bearer vg_secret_...
Content-Type: application/json
{
  "presentation": "issuer-jwt~selected-disclosure~holder-kb-jwt",
  "challenge_token": "opaque-challenge-token",
  "audience": "https://app.example.com",
  "nonce": "nonce-returned-by-challenge"
}

Verification checks:

  • issuer signature and kid against VettiGuard's issuer key registry;
  • iss, vct, nbf, and exp;
  • stored credential hash and active credential state;
  • active/non-expired source reusable identity;
  • cnf.jwk against the persisted holder-key thumbprint;
  • every disclosure digest against _sd;
  • KB-JWT signature and typ=kb+jwt;
  • exact sd_hash;
  • exact audience and nonce;
  • one-use challenge ownership and expiry.

Successful verification consumes the challenge. VettiGuard persists presentation metadata and claim names only; disclosed claim values are returned to the verifier but are not written to the presentation table.

Revocation and status

A holder can revoke a credential using its credential UUID and original vgid_... holder token:

POST /api/v1/credentials/revoke

VettiGuard sets the allocated status-list bit and publishes a signed status-list JWT through:

GET /api/v1/credentials/status-list?list=revocation-1

Status-list capacity is bounded and rolls to the next list when full. Status responses are cacheable for short periods; verifiers should still enforce credential expiry and issuer-signature checks independently.

Issuer keys and rotation

Credential issuer keys are RSA/RS256 signing keys generated inside VettiGuard and protected with the platform secret-encryption facility. Public keys remain available from /credentials/jwks after retirement so valid historical credentials can still be verified until expiry.

Automatic key age is configured with:

DIGITAL_CREDENTIAL_KEY_MAX_AGE_DAYS=180

The accepted range is bounded by the service. Rotation does not change holder keys.

Privacy model

Persistent credential records contain:

  • UUID/public credential reference;
  • workspace and source reusable-identity references;
  • keyed subject hash;
  • credential format/type and issuer key ID;
  • credential hash, not serialized credential;
  • holder JWK thumbprint, not holder private key;
  • status-list location/index;
  • claim names only;
  • lifecycle timestamps/status.

Persistent presentation records similarly contain hashes and disclosed claim names, not disclosed values.

What this does not mean

A valid presentation establishes that VettiGuard issued the signed credential from an approved VettiGuard identity, that the credential is active, that selected disclosures are genuine, and that the presenter controls the holder key bound into the credential.

It does not, by itself, mean that a government database was consulted, that the credential is a national digital identity, that the credential is a W3C VC Data Model representation, or that the presentation is a legal electronic signature.

Deployment

Apply the Phase 105 migration before enabling issuance:

php bin/vettiguard-migrate.php status
php bin/vettiguard-migrate.php dry-run
php bin/vettiguard-migrate.php run

Required migration:

202609220001_standard_digital_credentials

Then validate:

php bin/vettiguard-phase105-check.php
php bin/vettiguard-api-contract.php validate
php tests/run.php