Skip to main content

did:webvh explained: did:web with a verifiable history

did:webvh — "did:web + Verifiable History" — is a DID method that publishes an identifier at an HTTPS location exactly like did:web, but adds an append-only, cryptographically chained log of every version the DID has ever had. Version 1.0 was released by the Decentralized Identity Foundation on 7 August 2025. The method was previously called did:tdw; that name is retired and the two are not interchangeable in a log.

The problem did:web leaves open​

did:web is the easiest DID method to adopt: put a did.json file on a domain you already own, serve it over TLS, done. No ledger, no fees, no new infrastructure. That simplicity is also the whole of its security model, and it has four consequences.

  • No history. The DID document is whatever the web server returns right now. Yesterday's version is gone, and nothing ties it to today's.
  • A silent rewrite is undetectable. Anyone who can write to the web root — a compromised CI pipeline, a hijacked hosting account, an insider — can replace the signing key in did.json. Every credential ever issued under that DID now appears to have been signed by the attacker's key, and a verifier has no way to notice the substitution.
  • Trust bottoms out at DNS and TLS. The identifier means "whoever controls this hostname today". A lapsed domain registration transfers the identity with it.
  • The identifier is welded to the hostname. Changing domain means a new DID, and every credential issued under the old one stops resolving.

For a demo, an internal integration, or a short-lived verifier, that is a reasonable trade. For a long-lived issuer whose credentials must still verify in five years, it is not.

did:webvh keeps the deployment model — one static file on a domain you control — and replaces the security model. The identifier is derived from the inception of the DID rather than from the hostname, and every subsequent change is an entry in a hash-chained log that a resolver replays and verifies in full.

The identifier​

A did:webvh DID carries a self-certifying identifier (SCID) as its first segment:

webvh-method-specific-id = scid ":" webvh-domain *( ":" webvh-path-segment )
scid = 46(base58btc-char)

For example:

did:webvh:QmdmPkUdYzbr9txmx8gM2rsHPgr5L6m3gHjJGAf4vUFoGE:issuer.example.com:dids:hr

The SCID is a 46-character base58btc string: the hash of the DID's first log entry. Because it is a hash of the inception event, it cannot be chosen, and it cannot be changed for the life of the DID. This is what "self-certifying" means — the identifier commits to its own origin, so the domain name is no longer the thing that makes the DID this DID.

Cryptographic validity is not trust

Verifying a did:webvh log proves the history is internally consistent and was signed by the keys the controller committed to. It says nothing about who that controller is. Trust still has to come from somewhere external — a trusted list, a trust registry, or the credentials published at the DID's /whois endpoint.

The DID log (did.jsonl)​

The history lives in a JSON Lines file, did.jsonl, with one JSON object per line and one line per version of the DID. Each entry has five properties:

PropertyContents
versionId<version number>-<entryHash> — the version counter (starting at 1, incrementing by exactly one) and the hash of this entry
versionTimeUTC ISO 8601 timestamp asserted by the controller, with an explicit Z or +00:00
parametersThe processing options in force from this entry onward (see Parameters)
stateThe DID document for this version
proofA Data Integrity proof over the entry, signed by a key authorised to update the DID

The chaining happens inside entryHash. The hash is computed over the entry without its proof, and with the versionId field temporarily set to the previous entry's versionId — for the first entry, to the SCID. Each entry therefore commits to its predecessor, and the file behaves as a microledger: you cannot rewrite entry 3 without invalidating entries 4, 5 and 6.

How the hashes are built​

Both the SCID and every entryHash use the same function:

base58btc(multihash(JCS(entry), SHA-256))
  • JCS is the JSON Canonicalization Scheme (RFC 8785), which produces one deterministic byte sequence for a given JSON object, so two implementations hash the same thing.
  • multihash prefixes the digest with its algorithm identifier and length, so the hash is self-describing.
  • For the SCID specifically, the input is the preliminary first entry, in which every place the SCID will eventually appear still holds the literal placeholder {SCID}. Once the hash is computed, the controller substitutes it everywhere.

Under method: "did:webvh:1.0" the only permitted hash algorithm is SHA-256 and the only permitted Data Integrity cryptosuite — for both log-entry proofs and witness proofs — is eddsa-jcs-2022. A resolver must check the proof's cryptosuite property explicitly; checking only proofPurpose is not sufficient.

From DID to URL​

The log is fetched over plain HTTPS. The transformation drops the SCID and maps the remaining segments onto a path — which is why the SCID does not normally appear in the URL at all.

DIDLog URL
did:webvh:{SCID}:example.comhttps://example.com/.well-known/did.jsonl
did:webvh:{SCID}:issuer.example.comhttps://issuer.example.com/.well-known/did.jsonl
did:webvh:{SCID}:example.com:dids:issuerhttps://example.com/dids/issuer/did.jsonl
did:webvh:{SCID}:example.com%3A3000:dids:issuerhttps://example.com:3000/dids/issuer/did.jsonl
did:webvh:{SCID}:jp納豆.例.jp:用户https://xn--jp-cd2fp15c.xn--fsq.jp/%E7%94%A8%E6%88%B7/did.jsonl

A port is carried in the DID as the percent-encoded separator %3A. Internationalised domains are normalised with IDNA2008. IP addresses are rejected. The content type of did.jsonl should be text/jsonl.

The same transformation locates the other files a did:webvh DID may publish. For witness proofs, replace the final did.jsonl with did-witness.json and leave the rest of the URL — including any .well-known/ segment — untouched. For DID URL paths, including whois.vp, the .well-known/ segment is omitted and the path is appended to the web location instead: the log of did:webvh:{SCID}:example.com lives at https://example.com/.well-known/did.jsonl, but its whois presentation lives at https://example.com/whois.vp.

Resolution​

A resolver fetches the log once and then replays it from the first line. Every entry is verified — a resolver may not skip intermediate entries on the grounds that it only wants the latest document.

Concretely, for each entry the resolver checks that:

  1. The active parameters are valid for the declared method version.
  2. The Data Integrity proof is valid, has proofPurpose: assertionMethod, and was made with a key in the currently active updateKeys.
  3. The version number is exactly the previous one plus one — a gap terminates resolution — and the entryHash after the dash recomputes correctly.
  4. versionTime is strictly greater than the previous entry's. Equal timestamps are rejected, and so is a timestamp more than a small tolerance (no more than five minutes is recommended) in the future.
  5. The SCID segment of state.id is byte-for-byte the SCID from the first entry — in every entry, not just the first.

Because the resolver has the whole history, it can also answer questions about the past. The DID URL query parameters versionId and versionTime select a historical version, and did:webvh adds versionNumber for selecting by integer. That is what makes a signature from 2024 checkable in 2029: the verifier retrieves the key that was active at the time the credential was issued, and can see that it was later rotated rather than silently replaced.

Parameters​

The parameters object in each entry carries the configuration that governs this and all later entries. A parameter left out of a later entry keeps its previous value; the JSON null value must not be used to mean "default" or "off".

ParameterTypeDefaultNotes
methodstring—Required in the first entry. did:webvh:1.0 is the current value. An unknown value must terminate resolution, never be downgraded
scidstring—Required in the first entry, forbidden in all later ones
updateKeysarray of multikey—Required in the first entry. The public keys authorised to sign log entries. Set to [] when deactivating
nextKeyHashesarray of string[]Hashes of the keys that may appear in the next entry's updateKeys. Non-empty means pre-rotation is active
witnessobject{}threshold plus a list of witness did:key DIDs
watchersarray of URL[]URLs of services that have agreed to monitor this DID
portablebooleanfalseMay only be set to true in the first entry
deactivatedbooleanfalseOnce true, no further updates are permitted
ttlinteger3600Seconds a resolver should cache the resolved DID, analogous to a DNS TTL

Hardening options​

The log alone protects against one attacker rewriting history. Two optional mechanisms raise the bar further, and a third helps you notice when something went wrong anyway.

Pre-rotation​

With nextKeyHashes, the controller commits in advance to the hashes of the keys that will be allowed to sign the next entry. An attacker who steals the currently active update key still cannot publish a valid update, because they do not hold a key whose hash was pre-committed.

The trap to know about: once pre-rotation is active, both updateKeys and nextKeyHashes must be present explicitly in every subsequent entry, and every multikey in updateKeys — not just the ones that look new — must hash to an entry in the previous nextKeyHashes. An entry that omits updateKeys intending to inherit the previous value must be rejected; allowing inheritance would bypass the commitment entirely. Pre-rotation is switched off by setting nextKeyHashes to [].

Witnesses​

Witnesses are third parties that co-sign each new version before it is published. The witness parameter names them and sets a threshold:

{
"witness": {
"threshold": 2,
"witnesses": [
{ "id": "did:key:z6Mkkc51mg2vpQzKWAbWQZupeGYhowaBjYkmvcKMTqteqHB4" },
{ "id": "did:key:z6MkuDdJdKLCgwZuQuEi9xG6LVgJJ9Tebr74CXPYPSumqgJs" },
{ "id": "did:key:z6MkoSWmQyp4fTk4ZQy4KUsss9dFX51XfEUzKKKj1J1JUsrF" }
]
}
}

Witness id values must be did:key DIDs and must be unique. Their proofs go in a separate file, did-witness.json, keyed by versionId, which must be published before the updated log. The threshold is counted over distinct witness ids, not over raw proof count, so duplicate proofs from one witness do not help an attacker. A threshold rather than unanimity keeps one unresponsive witness from blocking updates.

With witnesses in place, an attacker needs the controller's update key, write access to the web server, and a threshold of witnesses before they can rewrite history undetected.

Watchers​

Watchers are independent services that poll a DID and cache its log. They exist for three reasons: to make resolution cheaper, to keep a DID resolvable after the controller's web server goes away, and — most usefully — to notice when a controller republishes an altered log. The watchers parameter lists the ones a controller collaborates with, and resolvers surface that list in the resolution metadata, but anyone may watch any did:webvh DID without the controller's involvement.

Watchers expose a small HTTP API keyed by SCID rather than by URL, for example GET <watcher>/log?scid=<SCID>, which is precisely what lets a watcher keep serving a DID that has moved or disappeared.

Portability​

A DID created with portable: true in its first entry can later move to a different domain or path while keeping its identifier and its whole history. The move is just another log entry that changes the host and path portion of state.id; the SCID segment stays byte-for-byte identical, and the previous DID string is added to alsoKnownAs. Portability cannot be switched on later — an entry that introduces portable: true after the first must be rejected — and setting it to false at any point disables it permanently.

This is the direct answer to did:web's domain lock-in: with portability enabled, losing or changing a domain costs you a log entry rather than your identity.

A prior domain in the history proves nothing

Because a DID may name a domain that never actually hosted its log, resolvers and relying parties must ignore prior domain components when judging a DID's history or trustworthiness. Only the current hosting location and the verifiable history count.

whois: publishing credentials about the DID​

Every did:webvh DID implicitly supports a /whois path backed by a Linked Verifiable Presentation service. Resolving <did>/whois fetches a Verifiable Presentation from whois.vp at the DID's web location:

{
"@context": "https://identity.foundation/linked-vp/contexts/v1",
"id": "#whois",
"type": "LinkedVerifiablePresentation",
"serviceEndpoint": "<did-to-https-translation>/whois.vp"
}

The presentation is signed by the DID and must contain at least one Verifiable Credential whose credentialSubject.id is the DID. Which credentials go in is entirely the controller's choice — a chamber-of-commerce registration, a sector accreditation, an ISO certificate. A controller may override the implicit service by declaring an explicit #whois service in the DID document.

This is the piece that turns cryptographic validity into something a relying party can act on. The log tells you the history is intact; whois tells you who is standing behind it, in credentials you can verify against issuers you already trust.

Publishing a parallel did:web​

Because did.jsonl and did.json live at the same web location, a controller can publish both. The procedure is: take the resolved did:webvh document, add the implicit #files and #whois services if they are not already there, text-replace did:webvh:<SCID>: with did:web: throughout, add the full did:webvh DID to alsoKnownAs, and publish the result as did.json.

The benefit is reach: resolvers that do not yet support did:webvh keep working, and a did:web resolver that does know about did:webvh can follow alsoKnownAs to the verifiable history. The cost is that anything relying on the parallel did:web DID gets did:web's security properties, not did:webvh's — so the parallel DID is a migration aid, not a permanent arrangement.

did:web vs did:webvh​

Dimensiondid:webdid:webvh
Published filedid.jsondid.jsonl (plus an optional parallel did.json)
Identifier derived fromThe hostname and pathThe SCID — a hash of the inception entry
HistoryNone. Only the current document existsEvery version, in an append-only hash-chained log
Silent rewrite of the DID documentUndetectableBreaks the chain; resolution fails
Key rotationOverwrite the key; nothing links old to newAn explicit, signed log entry that a verifier can replay
Verifying a signature from years agoImpossible once the key is overwrittenSupported via the versionId / versionTime / versionNumber query parameters
Protection if the web server is compromisedNoneOptional witnesses and pre-rotation
Third-party monitoringNot definedWatchers, keyed by SCID
Changing domainNew DID; old credentials stop resolvingA log entry, if portable: true was set at creation
DeactivationDelete the fileExplicit deactivated: true parameter, or an empty updateKeys
Trust signals about the controllerNot definedImplicit /whois Linked Verifiable Presentation
Infrastructure neededA web server with TLSA web server with TLS
Resolver supportNear-universalGrowing; DIF lists mature Python, TypeScript and Rust implementations
Cost to verifyOne HTTPS GETOne HTTPS GET, plus replaying and verifying every log entry

When to use which​

Reach for did:web when the identifier is short-lived or low-stakes, when broad resolver support matters more than auditability, or when you are prototyping and the DID will be thrown away. It is also fine for a verifier-side identifier that signs nothing durable.

Reach for did:webvh when any of the following is true:

  • You issue credentials with a long lifetime, and a verifier must still be able to check them after you have rotated keys.
  • You need an auditable record of key rotations — for regulatory reasons, or because your security policy requires that a key change be evidenced rather than asserted.
  • A compromise of your web hosting must not be enough on its own to take over the identity. Add pre-rotation, and witnesses if the stakes justify the coordination.
  • You may need to move the identifier to another domain without reissuing everything. Decide this at creation time — portable cannot be switched on later.
  • You want relying parties to be able to discover verifiable claims about you, via /whois, rather than relying on the domain name as the only signal.

The practical migration path is to create did:webvh identifiers for new issuance while publishing the parallel did:web document for compatibility, and to drop the parallel document once the relying parties you care about have did:webvh resolvers.

did:webvh in the Business Wallet​

The Credenco Business Wallet supports did:webvh as an identifier type alongside did:web.

  • Creating one. WEBVH is a value of the identifier type on the public create-identifier API, and appears in the identifier type list in the Business Wallet UI. The domain and path fields work exactly as they do for did:web, so an identifier created on a wallet domain you have verified is published under your own hostname.
  • The log. The wallet generates the SCID, maintains did.jsonl, and appends a signed entry on every change to the DID document. Entries are written with method: "did:webvh:1.0" and signed with an Ed25519 update key using the eddsa-jcs-2022 cryptosuite.
  • Resolution. The log is served at /public/did/{path}/did.jsonl with content type text/jsonl. A plain did:web identifier returns 404 at that URL, which is the expected way to tell the two apart.
  • did.json at the same path. The current DID document is also served at /public/did/{path}/did.json. Note that this is the did:webvh document as-is: its id is the did:webvh: DID, and the wallet does not perform the specification's parallel-did:web rewrite (replacing did:webvh:<SCID>: with did:web: and adding alsoKnownAs). So it is a convenient JSON view of the current version, not a resolvable did:web identifier.
  • whois. When an identifier has linked credentials, the wallet builds a Verifiable Presentation and serves it at /public/did/{path}/whois as application/vp+jwt, and writes a matching LinkedVerifiablePresentation service entry into the DID document. Because that endpoint is a VP-JWT at /whois rather than a W3C VCDM presentation at whois.vp, it is an explicit #whois service that overrides the implicit one — which is exactly the override the specification provides for. When there are no linked credentials, the endpoint returns 404 and the service entry is removed.
Not yet supported

Pre-rotation (nextKeyHashes), witnesses (did-witness.json), watchers and portability are part of the did:webvh specification but are not implemented in the Business Wallet today. Identifiers are created without them, which means portable is false and cannot be enabled later on an existing identifier.

DID · DID document · did:web · SCID · JSON Lines · Data Integrity · JCS · multihash · multikey · pre-rotation · witness · watcher · Linked Verifiable Presentation

Frequently asked questions​

Is did:webvh a ledger?​

Not in the distributed sense. There is no consensus network and no shared state. It is a microledger: a single-writer, append-only, hash-chained log that one controller publishes and anyone can verify. The verifiability comes from the chain, not from replication — although witnesses and watchers add independent parties who would notice a rewrite.

What happened to did:tdw?​

did:tdw was the original name of this method; it was renamed to did:webvh before v1.0. The method parameter in a log entry pins the exact specification version, and a resolver must reject any value it does not recognise rather than guessing — so old did:tdw logs and 1.0 did:webvh logs are not silently interchangeable.

Can I migrate an existing did:web to did:webvh?​

Not as the same identifier. The SCID is a hash of the inception entry, so a did:webvh DID necessarily starts a new history and is a different DID string. The realistic path is to create the did:webvh identifier, publish a parallel did:web document at the same location for compatibility, and reference the old identifier in alsoKnownAs. Credentials already issued under the old did:web keep resolving as long as you keep serving its did.json.

How much slower is resolving a did:webvh DID?​

It is still a single HTTPS GET; the extra cost is replaying the log, which grows with the number of versions rather than with time. A resolver that has already verified a prefix of the log may resume from its cached state instead of reprocessing everything, provided that cached state came from a full verification. The ttl parameter lets the controller suggest how long a resolved DID may be cached, defaulting to one hour.

What happens when a DID is deactivated?​

Setting deactivated: true in the parameters stops all further updates, and a resolver must return resolution metadata saying so rather than returning the document. A controller who wants the final document to keep resolving can instead set updateKeys to [], which makes further updates impossible while leaving the document retrievable. Earlier versions stay reachable in both cases through the versionId, versionTime and versionNumber query parameters.

Sources​

  1. did:webvh v1.0 — DID Method Specification
  2. did:webvh specification repository
  3. did:webvh information site, including worked examples
  4. DIF celebrates v1.0 release of did:webvh
  5. RFC 8785 — JSON Canonicalization Scheme
  6. Data Integrity EdDSA Cryptosuites v1.0
  7. Linked Verifiable Presentation specification
  8. did:web Method Specification

This page is informational and does not constitute legal advice. For authoritative guidance consult the did:webvh specification directly.