Standards / Advanced / 14 min

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:

  1. Audit trails: “Prove which key signed this credential on July 15, 2026”—can’t verify if key was rotated since then
  2. Compromise detection: Someone hacks example.com, changes DID document → no way to detect unauthorized change
  3. Key rotation disputes: “I never authorized this key”—no tamper-evident log to prove what happened
  4. 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:

  1. Web-based: Resolves via HTTPS (like did:web)—no blockchain, no special infrastructure
  2. Version history: Every change creates a new version—immutable log of all updates
  3. Tamper-evident: Cryptographic hash chain prevents unauthorized changes (like Git commits)
  4. Time-travelable: Resolve DID as it was at any point in history
  5. 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:

  1. Compute hash of version 1 → result: abc123...
  2. Check version 2’s previousVersionHash field → matches? ✓
  3. 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 previousVersionHash still points to abc123...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:

  1. Fetch entire .jsonl file (all versions)
  2. Filter versions: versionTime ≤ June 15, 2026
  3. Result: Versions 1 and 2 (version 3 is July 20, after cutoff)
  4. 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 updateKeyscannot 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 updateKeys signed 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:webvhdid:webBlockchain DIDs
ResolutionHTTPS (.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 .jsonl file (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 .jsonl file (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 .jsonl file 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 CaseRecommendationWhy
Agent identitydid:webvhFast resolution (ms), free hosting, GDPR-compliant
Supply chain provenancedid:webvhTamper-evident log, no gas fees, web-based resolution
Government digital IDBlockchain DIDPublic trust (no single hosting point of failure), immutability
Cryptocurrency walletsBlockchain DIDAlready on-chain, native integration with blockchain
Enterprise IAMdid:webvhSelf-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

Start building →

Documentation:


Technical Deep Dives


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.

Cookie Preferences

We use cookies to enhance your experience. You can manage your preferences below. For more information, read our Cookie Policy.

Strictly Necessary Always Active

These cookies are essential for core website functions such as security, session integrity, and cookie preference storage. They cannot be disabled.

  • _cf_bm: Distinguishes humans from bots (Cloudflare) · 30m
  • _cfuvid: Ensures secure browsing (Cloudflare) · Session
  • __hs_initial_opt_in: Prevents HubSpot's banner · 7 days
  • _gtm_debug: GTM debug mode (testing only) · Session
Analytics

These cookies help us understand how visitors interact with the site so we can improve content and performance. All data is aggregated and anonymous.

  • _ga, _gid, _gat: Google Analytics · Session – 2 years
  • __hstc, hubspotutk, __hssrc: HubSpot visitor tracking · 13 months
  • __hs_opt_out: HubSpot opt-out preference · 6 months
Marketing & Targeting

These cookies allow us and our partners to serve personalised ads and measure campaign performance.

  • _gcl_au, _gcl_dc: Google Ads conversion tracking · 90 days
  • IDE: Google Display Network personalisation · 1 year
  • _fbp: Meta / Facebook remarketing · 90 days
  • li_gc, _li_fat_id, bcookie: LinkedIn tracking · 1–24 months
  • guest_id, personalization_id: Twitter/X analytics · 2 years