Verifiable Credentials
Cryptographic claims anyone can verify—no callback to the issuer required
The Problem Verifiable Credentials Solve
Today’s credentials require the verifier to call back to the issuer:
- Diploma — Employer calls university: “Did Alice graduate?”
- Employment verification — Landlord calls company: “Does Bob work there?”
- Agent authorization — Partner calls your system: “Is Agent-42 allowed to do this?”
Problems:
- Privacy leak — Issuer learns who’s verifying and when
- Dependency — Verifier can’t verify if issuer’s system is down
- Latency — Real-time verification requires API calls
- Revocation checking — Must check status with issuer every time
Verifiable Credentials (VCs) solve this: cryptographically signed claims that anyone can verify without calling the issuer.
What Is a Verifiable Credential?
A VC is a digitally signed claim issued by one party (issuer) about another party (holder).
Example: University issues diploma to Alice
{
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"issuer": "did:webvh:stanford.edu",
"issuanceDate": "2024-06-15T00:00:00Z",
"credentialSubject": {
"id": "did:webvh:alice.com",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science in Computer Science"
}
},
"proof": {
"type": "Ed25519Signature2020",
"created": "2024-06-15T00:00:00Z",
"proofPurpose": "assertionMethod",
"verificationMethod": "did:webvh:stanford.edu#key-1",
"proofValue": "z3FXQi...signature..."
}
}
Key properties:
- Issuer — Who made the claim (Stanford)
- Holder — Who the claim is about (Alice)
- Claims — What’s being asserted (degree earned)
- Proof — Cryptographic signature from issuer
- Verifiable — Anyone can check the signature without calling Stanford
How Verifiable Credentials Work
1. Issuance
Issuer signs the credential:
// Issuer (Stanford) creates credential for Alice
const credential = {
issuer: 'did:webvh:stanford.edu',
credentialSubject: {
id: 'did:webvh:alice.com',
degree: { type: 'BachelorDegree', name: 'Computer Science' }
},
issuanceDate: new Date().toISOString()
}
// Sign with issuer's private key
const signedVC = await sign(credential, issuerPrivateKey)
// Alice stores it (in wallet, database, etc.)
2. Presentation
Holder shares the credential:
// Alice presents the VC to an employer
const presentation = {
type: 'VerifiablePresentation',
holder: 'did:webvh:alice.com',
verifiableCredential: [signedVC],
proof: { /* Alice's signature proving she controls the DID */ }
}
3. Verification
Verifier checks the credential:
// Employer verifies:
// 1. Issuer's signature is valid
const issuerDID = presentation.verifiableCredential[0].issuer
const issuerKey = await resolve(issuerDID) // Get Stanford's public key
const isValidSignature = await verify(signedVC, issuerKey)
// 2. Holder controls the DID (Alice signed the presentation)
const holderKey = await resolve(presentation.holder)
const isValidPresentation = await verify(presentation.proof, holderKey)
// 3. Credential hasn't been revoked (optional, check status list)
const isRevoked = await checkRevocation(signedVC)
// ✅ All checks pass — credential is valid
No callback to Stanford required!
Verifiable Credentials vs Traditional Credentials
| Aspect | Traditional (e.g., API call) | Verifiable Credential |
|---|---|---|
| Verification | Call issuer’s API | Verify signature locally |
| Privacy | Issuer sees every verification | Issuer sees nothing after issuance |
| Availability | Fails if issuer is down | Works offline |
| Latency | Network round-trip | Instant (local verification) |
| Selective disclosure | All-or-nothing | Share only what’s needed |
| Revocation | Check with issuer every time | Check status list (cacheable) |
Real-World Use Cases
1. Agent Authorization Mandate
Problem: Bank A’s agent needs to prove it’s authorized to trade. Bank B must verify in real-time.
Solution: Bank A issues a VC (mandate) to the agent. Bank B verifies the signature without calling Bank A.
{
"type": ["VerifiableCredential", "TradingMandate"],
"issuer": "did:webvh:bankA.com",
"credentialSubject": {
"id": "did:webvh:bankA.com:agent-42",
"mandate": {
"authorizedBy": "did:webvh:bankA.com:trader-alice",
"permissions": ["execute-trades"],
"expiry": "2026-12-31"
}
},
"proof": { "proofValue": "z3FXQi..." }
}
Bank B’s verification:
// 1. Check Bank A's signature (is this really from Bank A?)
const isValid = await verifyCredential(mandate)
// 2. Check expiry (is it still valid?)
const isExpired = new Date() > new Date(mandate.credentialSubject.mandate.expiry)
// 3. Check revocation status (did Bank A revoke it?)
const isRevoked = await checkRevocation(mandate)
// ✅ Verified—no API call to Bank A
2. PHI Access Authorization
Problem: AI agent needs to prove a case worker authorized PHI access—HIPAA requires proof.
Solution: Issue a VC linking agent to case worker. Audit log captures the VC—proof is cryptographic, not just a log entry.
{
"type": ["VerifiableCredential", "PHIAccessAuthorization"],
"issuer": "did:webvh:hospital.com",
"credentialSubject": {
"id": "did:webvh:hospital.com:agent-99",
"authorization": {
"authorizedBy": "did:webvh:hospital.com:caseworker-bob",
"patient": "patient-12345",
"purpose": "treatment-review",
"expiry": "2026-07-22T00:00:00Z"
}
},
"proof": { "proofValue": "..." }
}
Audit trail:
// Log entry
{
timestamp: '2026-07-21T19:30:00Z',
action: 'access-PHI',
agent: 'did:webvh:hospital.com:agent-99',
authorization: vcAsJson, // The full VC
verifiedBy: 'gateway-node-5'
}
// Auditor verifies the VC signature months later—still valid
3. Selective Disclosure (Age Verification)
Problem: Prove you’re over 21 without revealing your birthdate.
Solution: VC with BBS+ signatures allows proving properties without revealing the full claim.
// Issued VC (full claims)
const vc = {
credentialSubject: {
name: 'Alice',
birthdate: '1995-03-15',
address: '123 Main St'
}
}
// Selective disclosure—prove age > 21 without revealing birthdate
const derivedVC = await deriveCredential(vc, {
reveal: ['over21: true'], // Only reveal this
hide: ['name', 'birthdate', 'address'] // Hide everything else
})
// Verifier sees:
{
credentialSubject: {
over21: true // Proven cryptographically, no other data revealed
}
}
Revocation: Status List 2021
Problem: How do you revoke a VC that’s already out in the world?
Solution: Status List 2021 — a bitstring hosted by the issuer. Each VC points to a bit:
{
"credentialStatus": {
"id": "https://issuer.com/status/42#94567",
"type": "StatusList2021Entry",
"statusPurpose": "revocation",
"statusListIndex": "94567",
"statusListCredential": "https://issuer.com/status/42"
}
}
Verification:
// 1. Fetch the status list (cacheable)
const statusList = await fetch('https://issuer.com/status/42')
// 2. Check bit 94567
const isRevoked = statusList.bitstring[94567] === 1
// ✅ If 0 → valid, if 1 → revoked
Privacy: The verifier learns “is this credential revoked?” but the issuer doesn’t see who checked.
Verifiable Credentials in the Affinidi Stack
Elements Services issues VCs:
const vc = await elements.issueCredential({
holder: 'did:webvh:agent-42.com',
type: 'TradingMandate',
claims: {
authorizedBy: 'trader-alice',
permissions: ['execute-trades'],
expiry: '2026-12-31'
}
})
Agent Gateway verifies VCs before allowing actions:
// Agent presents VC with request
const request = {
action: 'execute-trade',
credential: vcAsJson
}
// Gateway verifies
const isValid = await agentGateway.verifyCredential(request.credential)
if (!isValid) throw new Error('Unauthorized')
// Proceeds if valid
Trust Registry stores revocation status:
// Check if VC is revoked
const isRevoked = await trustRegistry.checkRevocation({
credentialId: 'urn:uuid:abc-123',
statusListUrl: 'https://issuer.com/status/42'
})
Getting Started
For Developers
Issue a Verifiable Credential:
npm install @affinidi/affinidi-tdk
import { VC } from '@affinidi/affinidi-tdk'
const credential = await VC.issue({
issuer: 'did:webvh:yourcompany.com',
holder: 'did:webvh:agent-42.com',
type: 'EmploymentCredential',
claims: {
position: 'Trading Agent',
authorizedBy: 'manager@yourcompany.com',
startDate: '2026-01-01'
}
})
console.log(credential) // Signed VC ready to share
Verify a Credential:
const isValid = await VC.verify(credential)
console.log(isValid) // true/false
For Architects
When to use VCs:
- ✅ Authorization proofs that must survive offline
- ✅ Cross-org trust without shared infrastructure
- ✅ Privacy-preserving verification
- ✅ Audit trails with cryptographic proof
When NOT to use VCs:
- ❌ High-frequency updates (VCs are issued once, updated by reissuing)
- ❌ Real-time streaming data
- ❌ Internal systems where API calls are fine
Further Reading
W3C Specification:
Affinidi Docs:
Related Deep Dives:
- Decentralized Identifiers — DIDs are the issuer and holder identifiers in VCs
- Verifiable Presentations — How holders share VCs with verifiers
Related Solutions:
- Per-Agent Attribution — VCs prove who authorized what
- Runtime Agent Governance — VCs carry authorization mandates