did:webvh — Tamper-Evident Identifiers
Web-based DIDs with version history—time-travelable, auditable, and tamper-evident identity without blockchain
The Problem: DIDs Need History
Decentralized Identifiers (DIDs) give agents and people cryptographic identities—but most DID methods have no history:
Example: did:web (standard web-based DID)
DID: did:web:example.com:alice
Resolution:
GET https://example.com/.well-known/did.json
→ Returns current DID document (keys, service endpoints)
Problem: You only see the current state—no history of changes.
Questions you can’t answer:
- ❓ “Was this key valid when the credential was signed (6 months ago)?”
- ❓ “Did someone tamper with the DID document (change keys without authorization)?”
- ❓ “When did this DID rotate keys?”
- ❓ “Which key signed this old transaction?”
Use cases broken by missing history:
- Audit trails: “Prove which key signed this credential on July 15, 2026”—can’t verify if key was rotated since then
- Compromise detection: Someone hacks example.com, changes DID document → no way to detect unauthorized change
- Key rotation disputes: “I never authorized this key”—no tamper-evident log to prove what happened
- Long-lived credentials: Credential issued in 2026, verified in 2036—was signing key valid at issuance time?
did:webvh solves this: A web-based DID method with tamper-evident version history.
What Is did:webvh?
did:webvh = did:web + version history
Core properties:
- Web-based: Resolves via HTTPS (like
did:web)—no blockchain, no special infrastructure - Version history: Every change creates a new version—immutable log of all updates
- Tamper-evident: Cryptographic hash chain prevents unauthorized changes (like Git commits)
- Time-travelable: Resolve DID as it was at any point in history
- Key rotation safe: Old keys remain in history—verify past signatures even after rotation
Example:
DID: did:webvh:example.com:alice
Resolution (current):
GET https://example.com/.well-known/did.jsonl
→ Returns all versions (append-only log)
→ Latest version = current state
Resolution (historical):
"Show me did:webvh:example.com:alice as it was on July 15, 2026"
→ Replay log up to that date → reconstruct DID document
Result: Auditable, tamper-evident identity with full history.
How did:webvh Works: Version History as Hash Chain
Structure: Append-Only Log
DID document stored as .jsonl file (JSON Lines—one JSON object per line):
{"versionId":1,"versionTime":"2026-01-15T00:00:00Z","parameters":{"method":"webvh","scid":"..."}, "state":{"verificationMethod":[{"id":"#key-1","type":"Ed25519VerificationKey2020","publicKeyMultibase":"z6MkpTHR..."}]}}
{"versionId":2,"versionTime":"2026-06-01T00:00:00Z","parameters":{"updateKeys":["#key-1"],"prerotation":true},"state":{"verificationMethod":[{"id":"#key-1","type":"Ed25519VerificationKey2020","publicKeyMultibase":"z6MkpTHR..."},{"id":"#key-2","type":"Ed25519VerificationKey2020","publicKeyMultibase":"z6Mknew..."}]}}
{"versionId":3,"versionTime":"2026-07-20T00:00:00Z","parameters":{"updateKeys":["#key-2"],"deactivated":false},"state":{"verificationMethod":[{"id":"#key-2","type":"Ed25519VerificationKey2020","publicKeyMultibase":"z6Mknew..."}]}}
Each line = one version:
- Version 1 (2026-01-15): DID created, key-1 added
- Version 2 (2026-06-01): Key-2 added (key rotation—prerotation stage)
- Version 3 (2026-07-20): Key-1 removed, key-2 now sole key
Append-only: New versions added to end, old versions never deleted.
Tamper-Evidence: Hash Chain
Each version includes hash of previous version:
{
"versionId": 2,
"versionTime": "2026-06-01T00:00:00Z",
"previousVersionHash": "abc123...", // Hash of version 1
"state": { ... }
}
Verification:
- Compute hash of version 1 → result:
abc123... - Check version 2’s
previousVersionHashfield → matches? ✓ - Repeat for version 3, 4, …
If someone tampers (e.g., changes version 2’s key):
- Hash of version 2 changes →
abc123...→xyz789... - Version 3’s
previousVersionHashstill points toabc123...→ hash mismatch - Tampering detected
Result: Tamper-evident log—like Git commit history.
Time Travel: Reconstruct Past State
Query: “Show me did:webvh:example.com:alice as it was on June 15, 2026”
Process:
- Fetch entire
.jsonlfile (all versions) - Filter versions:
versionTime ≤ June 15, 2026 - Result: Versions 1 and 2 (version 3 is July 20, after cutoff)
- Apply version 2’s state → DID document had keys 1 and 2
Use case:
- Credential signed on June 15, 2026 with key-1
- Today (July 22, 2026), key-1 has been rotated out
- Verifier asks: “Was key-1 valid on June 15?”
- Time-travel: Reconstruct DID as of June 15 → key-1 was active ✓
- Signature valid
Without version history: Key-1 not in current DID document → signature verification fails (false negative).
Key Features
1. Safe Key Rotation (Prerotation)
Problem: Key rotation is risky:
- Immediate rotation: Old key removed → can’t verify past signatures
- Keep old keys forever: Compromised keys remain in DID document → security risk
did:webvh solution: Prerotation
Phase 1: Add new key (version 2)
{
"versionId": 2,
"state": {
"verificationMethod": [
{ "id": "#key-1", "type": "Ed25519...", "publicKeyMultibase": "z6MkOLD..." },
{ "id": "#key-2", "type": "Ed25519...", "publicKeyMultibase": "z6MkNEW..." }
]
},
"parameters": { "updateKeys": ["#key-1"], "prerotation": true }
}
Both keys active: Agent can sign with either key-1 or key-2.
Phase 2: Remove old key (version 3)
{
"versionId": 3,
"state": {
"verificationMethod": [
{ "id": "#key-2", "type": "Ed25519...", "publicKeyMultibase": "z6MkNEW..." }
]
},
"parameters": { "updateKeys": ["#key-2"] }
}
Only key-2 active: New signatures use key-2.
Benefits:
- ✅ Gradual migration: Both keys work during transition (no downtime)
- ✅ Past signatures remain valid: key-1 in version 2 → old signatures verify
- ✅ Compromised key removed: key-1 gone from current state → can’t sign new messages
2. Compromise Detection
Scenario: Attacker hacks example.com, changes DID document to add attacker’s key.
Without version history (did:web):
- Attacker modifies
/did.json→ adds attacker’s key - No way to detect unauthorized change (no audit log)
- Result: Attacker can impersonate DID owner
With version history (did:webvh):
- Attacker adds new version with attacker’s key
- But: New version requires signature from updateKeys (authorized keys from previous version)
- Attacker doesn’t have private key of authorized
updateKeys→ cannot sign new version - Result: Unauthorized version rejected (invalid signature)
If attacker modifies existing version:
- Hash chain breaks (version N’s hash ≠ version N+1’s
previousVersionHash) - Result: Tampering detected
3. Auditable Changes
Every change logged:
- Version 1: DID created
- Version 2: Key added
- Version 3: Key removed
- Version 4: Service endpoint added
- Version 5: DID deactivated
Audit questions answered:
- “When was key-2 added?” → Version 2, June 1, 2026
- “Who authorized key-2?” → Version 1’s
updateKeyssigned version 2 - “How many key rotations?” → Count versions with changed
verificationMethod - “Was DID ever compromised?” → Check hash chain integrity
Use case: Compliance audits
- Auditor: “Prove this DID was authorized by CFO on June 15, 2026”
- Response: Version log shows CFO’s key signed version 2 on June 15 ✓
4. Deactivation (Immutable)
Deactivate DID:
{
"versionId": 6,
"versionTime": "2026-12-31T00:00:00Z",
"parameters": { "deactivated": true },
"state": null
}
Effect:
- DID marked as deactivated
- No further updates allowed
- History remains: All past versions still accessible (audit trail preserved)
Use case:
- Employee leaves company → DID deactivated
- Company can still verify past credentials signed by employee’s DID
- Cannot issue new credentials (DID deactivated)
did:webvh vs. did:web vs. Blockchain DIDs
| did:webvh | did:web | Blockchain DIDs | |
|---|---|---|---|
| Resolution | HTTPS (.jsonl file) | HTTPS (.json file) | Blockchain query |
| Version history | ✓ Yes (append-only log) | ✗ No (current state only) | ✓ Yes (transactions on-chain) |
| Tamper-evident | ✓ Hash chain | ✗ No (can overwrite) | ✓ Blockchain immutability |
| Time-travel | ✓ Reconstruct past state | ✗ No | ✓ Query historical blocks |
| Key rotation safety | ✓ Prerotation support | ⚠ Manual (no guidance) | ⚠ Depends on method |
| Hosting | ✓ Web server (self-host) | ✓ Web server (self-host) | ✗ Blockchain (gas fees) |
| Cost | ✓ Free (web hosting) | ✓ Free (web hosting) | ✗ Gas fees per update |
| Speed | ✓ HTTPS (ms) | ✓ HTTPS (ms) | ✗ Block confirmation (seconds to minutes) |
| Privacy | ✓ Off-chain (GDPR-friendly) | ✓ Off-chain | ✗ On-chain (public ledger) |
did:webvh combines best of both:
- ✅ Web-based (no blockchain overhead)
- ✅ Version history (auditability)
- ✅ Tamper-evident (hash chain)
Implementation: How to Create a did:webvh
Step 1: Generate Initial DID Document
import { createDID, signVersion } from 'did-webvh-sdk'
// Generate key pair
const keyPair = generateEd25519KeyPair()
// Create DID document (version 1)
const didDoc = {
versionId: 1,
versionTime: new Date().toISOString(),
parameters: {
method: 'webvh',
scid: generateSCID(), // Self-certifying identifier (hash of initial state)
},
state: {
verificationMethod: [{
id: '#key-1',
type: 'Ed25519VerificationKey2020',
publicKeyMultibase: encodeMultibase(keyPair.publicKey)
}],
authentication: ['#key-1']
}
}
// Sign version 1 (self-signed)
const signedVersion1 = signVersion(didDoc, keyPair.privateKey)
Step 2: Host DID Document
# Save to .jsonl file (JSON Lines format)
echo '{"versionId":1,...}' > did.jsonl
# Host on web server
cp did.jsonl /var/www/html/.well-known/did.jsonl
# DID identifier:
did:webvh:example.com
# Resolves to: https://example.com/.well-known/did.jsonl
Step 3: Update DID Document (Add Key)
// Fetch current DID document
const currentDoc = await fetch('https://example.com/.well-known/did.jsonl')
const versions = parseJSONL(currentDoc)
const latestVersion = versions[versions.length - 1]
// Generate new key
const newKeyPair = generateEd25519KeyPair()
// Create version 2 (add key-2)
const version2 = {
versionId: latestVersion.versionId + 1,
versionTime: new Date().toISOString(),
previousVersionHash: hash(latestVersion),
parameters: {
updateKeys: ['#key-1'], // Only key-1 can authorize this update
prerotation: true
},
state: {
verificationMethod: [
latestVersion.state.verificationMethod[0], // Keep key-1
{
id: '#key-2',
type: 'Ed25519VerificationKey2020',
publicKeyMultibase: encodeMultibase(newKeyPair.publicKey)
}
],
authentication: ['#key-1', '#key-2']
}
}
// Sign version 2 with key-1 (authorized updateKey)
const signedVersion2 = signVersion(version2, keyPair.privateKey)
// Append to did.jsonl
appendToFile('/var/www/html/.well-known/did.jsonl', signedVersion2)
Step 4: Rotate Key (Remove Old Key)
// Create version 3 (remove key-1, keep key-2)
const version3 = {
versionId: version2.versionId + 1,
versionTime: new Date().toISOString(),
previousVersionHash: hash(version2),
parameters: {
updateKeys: ['#key-2'], // Now key-2 is authorized to update
},
state: {
verificationMethod: [{
id: '#key-2',
type: 'Ed25519VerificationKey2020',
publicKeyMultibase: encodeMultibase(newKeyPair.publicKey)
}],
authentication: ['#key-2']
}
}
// Sign version 3 with key-2 (new authorized updateKey)
const signedVersion3 = signVersion(version3, newKeyPair.privateKey)
// Append to did.jsonl
appendToFile('/var/www/html/.well-known/did.jsonl', signedVersion3)
Result: Key rotation complete, history preserved.
Resolution: How Verifiers Use did:webvh
Resolve Current State
// Resolve did:webvh:example.com
const did = 'did:webvh:example.com'
// Fetch .jsonl file
const response = await fetch('https://example.com/.well-known/did.jsonl')
const versions = parseJSONL(response)
// Verify hash chain
for (let i = 1; i < versions.length; i++) {
const prevHash = hash(versions[i - 1])
const declaredHash = versions[i].previousVersionHash
if (prevHash !== declaredHash) {
throw new Error('Hash chain broken—tampering detected')
}
}
// Return latest version's state
const currentState = versions[versions.length - 1].state
Time-Travel Resolution
// Resolve did:webvh:example.com as of June 15, 2026
const targetDate = new Date('2026-06-15T00:00:00Z')
// Fetch all versions
const versions = await fetchVersions('did:webvh:example.com')
// Filter versions up to target date
const historicalVersions = versions.filter(v =>
new Date(v.versionTime) <= targetDate
)
// Return state from last version before target date
const historicalState = historicalVersions[historicalVersions.length - 1].state
Use case:
- Verify credential signed on June 15, 2026
- Check if signing key was valid at that time
- Result: Historical resolution shows key was active ✓
Real-World Use Cases
1. Supply Chain Provenance
Scenario: Product batch signed by manufacturer on Jan 15, 2026. Retailer verifies in Dec 2026.
Problem: Manufacturer rotated keys in June 2026—signature verification fails (key not in current DID document).
did:webvh solution:
- Time-travel: Resolve manufacturer’s DID as of Jan 15, 2026
- Historical DID shows original key was active
- Signature valid (even though key rotated since then)
2. Agent Key Rotation
Scenario: Agent rotates keys every 90 days (security best practice). Old transactions signed with old keys.
Problem: After 4 rotations, original key is 360 days old—how to verify old transactions?
did:webvh solution:
- Version history shows all keys ever used
- Time-travel to transaction date → reconstruct DID with that key
- Old transactions remain verifiable
3. Compromise Detection
Scenario: Attacker hacks company website, tries to add attacker’s key to DID document.
Without version history (did:web):
- Attacker modifies
did.json→ adds attacker’s key - No way to detect unauthorized change
With version history (did:webvh):
- Attacker tries to append new version with attacker’s key
- New version requires signature from updateKeys (authorized keys from previous version)
- Attacker doesn’t have private key → cannot sign valid version
- Attack blocked
If attacker overwrites file:
- Hash chain breaks → tampering detected
- Verifiers reject DID document (invalid history)
4. Regulatory Compliance (Audit Trails)
Scenario: Financial regulator audits: “Prove agent X was authorized by CFO on July 15, 2026.”
did:webvh solution:
- Version log shows: Version 5 (July 15, 2026) added agent X’s key
- Version 5 signed by CFO’s key (from version 4’s
updateKeys) - Audit trail: Cryptographic proof CFO authorized agent X
Without version history: Can’t prove what happened on July 15—only see current state.
Limitations and Trade-offs
1. File Size Growth
Problem: Append-only log grows over time.
- 1 version = ~1 KB
- 1,000 versions = ~1 MB
- 10,000 versions (daily updates for 27 years) = ~10 MB
Mitigation:
- Compression: gzip
.jsonlfile (reduces size by 70-90%) - Archival: Move old versions to separate file (e.g.,
did-archive.jsonl) - Pruning: After N years, remove very old versions (keep checkpoint versions only)
Trade-off: Pruning loses full history—balance between storage and auditability.
2. Resolution Overhead
Problem: Fetching entire version history is slower than single document (did:web).
- did:web: Fetch
did.json(~1 KB) - did:webvh: Fetch
did.jsonl(~1 MB for 1,000 versions)
Mitigation:
- Caching: Resolver caches
.jsonlfile (refresh every 24 hours) - Partial fetching: HTTP range requests—fetch only recent versions
- Summary endpoint: Host
/did-summary.json(current state + version count) for fast resolution
Trade-off: Full history resolution is slower, but current-state resolution can be fast with caching.
3. Hosting Dependency
Problem: did:webvh relies on web hosting (domain + HTTPS).
- If domain expires → DID unresolvable
- If website hacked → DID compromised (though hash chain detects tampering)
Mitigation:
- Domain longevity: Use long-lived domains (not temporary domains)
- Backup hosting: Mirror
.jsonlfile on multiple servers (CDN, IPFS) - Monitoring: Alert if DID document modified unexpectedly
Trade-off: Same as did:web (both rely on web hosting).
did:webvh vs. Blockchain: When to Use Which?
| Use Case | Recommendation | Why |
|---|---|---|
| Agent identity | did:webvh | Fast resolution (ms), free hosting, GDPR-compliant |
| Supply chain provenance | did:webvh | Tamper-evident log, no gas fees, web-based resolution |
| Government digital ID | Blockchain DID | Public trust (no single hosting point of failure), immutability |
| Cryptocurrency wallets | Blockchain DID | Already on-chain, native integration with blockchain |
| Enterprise IAM | did:webvh | Self-hosted, no blockchain overhead, fast resolution |
General guidance:
- Use did:webvh when: Web hosting acceptable, want fast resolution, avoid gas fees, need GDPR compliance
- Use blockchain DID when: Public trust required, already on-chain, willing to pay gas fees, immutability non-negotiable
Get Started with did:webvh
Affinidi’s DID implementation uses did:webvh:
- Agent Gateway: Issues did:webvh identifiers to agents
- Trust Registry: Resolves did:webvh with time-travel support
- Elements Services: Signs credentials with did:webvh keys
Create your first did:webvh:
npm install did-webvh-sdk
# Generate DID
did-webvh create --domain example.com --output did.jsonl
# Host DID document
cp did.jsonl /var/www/html/.well-known/did.jsonl
# DID identifier: did:webvh:example.com
Documentation:
Related Resources
Technical Deep Dives
- Decentralized Identifiers (DIDs) - What DIDs are and why they matter
- Verifiable Credentials - What did:webvh signs
- Trust Registries & TRQP - How Trust Registries resolve did:webvh
Related Solutions
- Cross-Org Agent Trust - Agents use did:webvh for identity
- Supply Chain Provenance - Tamper-evident custody chains with did:webvh
- Per-Agent Attribution - Audit trails with did:webvh time-travel
Summary
did:webvh = did:web + version history
Core features:
- ✅ Version history: Append-only log of all changes
- ✅ Tamper-evident: Hash chain prevents unauthorized modifications
- ✅ Time-travelable: Resolve DID as it was at any point in history
- ✅ Safe key rotation: Prerotation support, past keys remain verifiable
- ✅ Auditable: Every change logged with timestamp + signature
vs. did:web:
- did:web: Current state only (no history)
- did:webvh: Full history (auditability + time-travel)
vs. Blockchain DIDs:
- Blockchain: On-chain (gas fees, public ledger)
- did:webvh: Web-based (free hosting, GDPR-compliant)
Use cases:
- Agent identity (time-travel for old signatures)
- Supply chain provenance (tamper-evident custody chains)
- Regulatory compliance (audit trails with cryptographic proof)
- Key rotation (safe migration without breaking old signatures)
Where Affinidi uses did:webvh:
- Agent Gateway (issues did:webvh to agents)
- Trust Registry (resolves did:webvh with time-travel)
- Elements Services (signs credentials with did:webvh keys)
Result: Web-based DIDs with blockchain-like auditability—tamper-evident, time-travelable, no gas fees.