Decentralized Identifiers (DIDs)
Cryptographic identities that work across systems—no central authority required
The Problem DIDs Solve
Traditional identifiers depend on a central authority:
- email@company.com — Company controls it; if you leave, you lose it
- @username — Platform controls it; if they suspend you, it’s gone
- API key abc123 — Service provider issues and can revoke it
When AI agents need to operate across organizational boundaries, who issues their identity? If Bank A’s agent talks to Bank B’s system, Bank B won’t trust an identifier Bank A controls—and vice versa.
Decentralized Identifiers (DIDs) solve this: cryptographic identifiers where the holder controls the keys, not a central authority.
What Is a DID?
A DID is a globally unique identifier that looks like this:
did:webvh:example.com:agent-42
Breaking it down:
did:— signals this is a DIDwebvh— the DID method (how it resolves)example.com:agent-42— the method-specific identifier
Key properties:
- Self-owned — You generate the DID and hold the private keys
- Verifiable — Anyone can cryptographically verify you control it
- Resolvable — The DID resolves to a DID Document containing public keys and service endpoints
- Persistent — It works as long as you hold the keys, regardless of which service you’re using
How DIDs Work
1. Creating a DID
// Generate a key pair
const keyPair = await generateKeyPair('Ed25519')
// Create a DID (method: webvh)
const did = `did:webvh:yourcompany.com:agent-${uuid()}`
// The DID Document (stored at a well-known location)
const didDocument = {
"@context": "https://www.w3.org/ns/did/v1",
"id": did,
"verificationMethod": [{
"id": `${did}#key-1`,
"type": "Ed25519VerificationKey2020",
"controller": did,
"publicKeyMultibase": keyPair.publicKey
}]
}
2. Resolving a DID
Anyone can resolve a DID to get its DID Document:
// Resolver fetches the DID Document
const didDoc = await resolve('did:webvh:yourcompany.com:agent-42')
// Returns public keys, service endpoints, etc.
console.log(didDoc.verificationMethod[0].publicKeyMultibase)
3. Proving Control
To prove you control a DID, sign a challenge with your private key:
// Verifier sends a challenge
const challenge = 'prove-you-control-this-DID'
// You sign it with your private key
const signature = await sign(challenge, privateKey)
// Verifier checks the signature against your DID's public key
const isValid = await verify(signature, challenge, didDoc.verificationMethod[0].publicKeyMultibase)
// → true
DID Methods: did:webvh
There are many DID methods (web, key, ion, ethr, etc.). Affinidi primarily uses did:webvh (Web Verifiable History):
Why did:webvh?
- Web-based — Resolves via HTTPS, no blockchain required
- Verifiable history — Tracks key rotation in a tamper-evident log
- Time-travelable — You can resolve what the DID looked like at any point in time
- Safe key rotation — If a key is compromised, rotate to a new key without losing the DID
How it works:
- DID Document stored at
https://yourcompany.com/.well-known/did.json - Every update creates a new version-controlled entry
- Hash chain ensures history can’t be rewritten
- Old signatures remain verifiable even after key rotation
Example:
did:webvh:affinidi.com:agent-gateway
→ resolves to https://affinidi.com/.well-known/did.json
→ returns DID Document with current public keys + version history
DIDs vs Traditional Identifiers
| Aspect | Traditional (e.g., API key) | Decentralized Identifier (DID) |
|---|---|---|
| Ownership | Service provider controls | You control (private keys) |
| Portability | Tied to one service | Works across any system that supports DIDs |
| Revocation | Provider can revoke anytime | Only you can revoke (or rotate keys) |
| Verification | Provider must confirm | Anyone can verify cryptographically |
| Cross-org trust | Requires federation/SSO | Native—no shared infrastructure needed |
Real-World Use Cases
1. AI Agent Identity
Problem: Agents share service accounts, so logs show “account acted” not “which agent.”
Solution: Each agent gets a DID. Every action signed with the agent’s private key → audit trail shows which agent acted.
const agent = {
did: 'did:webvh:company.com:agent-42',
privateKey: '...'
}
// Agent signs every request
const request = { action: 'access-PHI', patient: '12345' }
const signature = await sign(request, agent.privateKey)
// Log records: Agent-42 (DID + signature) accessed PHI
2. Cross-Org Agent Trust
Problem: Bank A’s agent talks to Bank B’s system—how does Bank B trust it?
Solution: Agent carries a DID + signed mandate. Bank B verifies the DID signature and checks authorization.
// Bank A's agent
const agentDID = 'did:webvh:bankA.com:trading-agent-5'
// Mandate (signed by Bank A)
const mandate = {
agent: agentDID,
authorizedBy: 'did:webvh:bankA.com:trader-alice',
permissions: ['execute-trades'],
expiry: '2026-12-31'
}
// Bank B verifies:
// 1. Agent's DID signature is valid
// 2. Mandate signature from Bank A is valid
// 3. Agent is still authorized (check Trust Registry)
3. Per-Agent Attribution
Problem: Incident response—“which agent modified production?”
Solution: Every agent has a DID, every change logged with DID + signature.
// Audit log entry
{
timestamp: '2026-07-21T19:15:00Z',
action: 'modify-config',
actor: 'did:webvh:company.com:agent-99',
signature: '...',
authorizer: 'did:webvh:company.com:engineer-bob'
}
// Instant attribution—no detective work
DIDs in the Affinidi Stack
Agent Gateway issues DIDs for every agent:
// Agent onboarding
const newAgent = await agentGateway.createAgent({
name: 'Trading Agent 5',
authorizedBy: 'trader-alice@bankA.com'
})
// Returns:
{
did: 'did:webvh:bankA.com:agent-5',
keyPair: { public, private },
didDocument: { ... }
}
Trust Registry stores authorization policies linked to DIDs:
// Query: Is this DID authorized?
const isAuthorized = await trustRegistry.query({
agent: 'did:webvh:bankA.com:agent-5',
action: 'execute-trades',
resource: 'AAPL'
})
// → { authorized: true, authorizedBy: 'did:webvh:bankA.com:trader-alice' }
Elements Services issues verifiable credentials to DIDs:
// Issue a mandate credential
const mandate = await elements.issueCredential({
holder: 'did:webvh:bankA.com:agent-5',
type: 'TradingMandate',
claims: {
authorizedBy: 'trader-alice',
permissions: ['execute-trades'],
expiry: '2026-12-31'
}
})
Getting Started
For Developers
Create a DID:
npm install @affinidi/affinidi-tdk
import { DID } from '@affinidi/affinidi-tdk'
const did = await DID.create({
method: 'webvh',
domain: 'yourcompany.com'
})
console.log(did.id) // did:webvh:yourcompany.com:...
console.log(did.keyPair) // { public, private }
Resolve a DID:
const didDoc = await DID.resolve('did:webvh:affinidi.com:agent-gateway')
console.log(didDoc.verificationMethod)
For Architects
When to use DIDs:
- ✅ Agents operating across organizational boundaries
- ✅ Audit trails that need to prove identity cryptographically
- ✅ Avoiding shared credentials/service accounts
- ✅ Long-lived agent identity that survives key rotation
When NOT to use DIDs:
- ❌ Internal human authentication (use SSO/OIDC)
- ❌ Short-lived, ephemeral processes
- ❌ Systems where cryptographic verification is overkill
Further Reading
W3C Specification:
Affinidi Docs:
Related Deep Dives:
- Verifiable Credentials — Cryptographic claims issued to DIDs
- DIDComm Messaging — Encrypted agent-to-agent communication using DIDs
Related Solutions:
- Cross-Org Agent Trust — DIDs enable trust across boundaries
- Per-Agent Attribution — DIDs make attribution automatic