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-jwtcredential 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-1Protected-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/revokeNative/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
livenessThe 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_hash6. 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
kidagainst VettiGuard's issuer key registry; iss,vct,nbf, andexp;- stored credential hash and active credential state;
- active/non-expired source reusable identity;
cnf.jwkagainst 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/revokeVettiGuard sets the allocated status-list bit and publishes a signed status-list JWT through:
GET /api/v1/credentials/status-list?list=revocation-1Status-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=180The 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 runRequired migration:
202609220001_standard_digital_credentialsThen validate:
php bin/vettiguard-phase105-check.php
php bin/vettiguard-api-contract.php validate
php tests/run.php