did:web & verifiable credentials
A portable web identity, resolved straight from the address — no registrar, no platform account, no app to install.
Most "verifiable" agent identity today means: create an account with a vendor, mint a credential in their console, and hope whoever needs to check it also has an account with the same vendor. That's not verification, it's a membership card. The moment the vendor is unreachable, or the checker never signed up, the whole scheme collapses to "trust me." did:web fixes this by making the identity document a normal HTTPS resource at a name you already control — fetchable with curl, verifiable with any JOSE library, no account required on either end. Whisper publishes one automatically for every agent, at the agent's own name, the moment it exists.
The problem it solves
A Decentralized Identifier (DID) is a URI that resolves to a DID document — a small JSON object listing the public keys that speak for that identity and how to use them. The DID Core spec (W3C Recommendation) defines the document shape but deliberately leaves resolution to pluggable "methods": did:key embeds the key in the identifier itself (no resolution needed, but no revocation either), did:ion/did:ethr resolve against a blockchain (durable, but you now depend on a chain), and did:web (W3C CCG did:web method spec) resolves against plain DNS + HTTPS — the identity lives wherever the name already points.
For an agent, that property is exactly what matters: the DID resolves through the same name the agent already has, so there is nothing new to register, host, or trust.
The did:web method, precisely
A did:web identifier is the domain, colon-separated in place of slashes:
did:web:acef2002a323d40d4.demo.agents.whisper.online
Resolution is a fixed, four-step transform from identifier to URL (per the spec):
- Replace
:with/in the method-specific-id to get a path:acef2002a323d40d4.demo.agents.whisper.online. - If the identifier carried no path segment, append
/.well-known. - Append
/did.json. - Percent-decode any
%3A(an encoded port) back to:, prependhttps://, andGETit.
So did:web:acef2002a323d40d4.demo.agents.whisper.online resolves to exactly one URL:
https://acef2002a323d40d4.demo.agents.whisper.online/.well-known/did.json
No registry lookup, no smart contract, no extra round trip — the resolver is an HTTPS GET, over a TLS session anchored the normal way. (A path-form DID, e.g. did:web:example.com:agents:42, maps to https://example.com/agents/42/did.json instead — Whisper always uses the bare-host form so the DID is just the agent's FQDN with a prefix.)
The DID document
The document at that URL is JSON-LD conforming to DID Core §5. The fields that matter:
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1"
],
"id": "did:web:acef2002a323d40d4.demo.agents.whisper.online",
"verificationMethod": [{
"id": "did:web:acef2002a323d40d4.demo.agents.whisper.online#key-1",
"type": "JsonWebKey2020",
"controller": "did:web:acef2002a323d40d4.demo.agents.whisper.online",
"publicKeyJwk": {
"kty": "EC",
"crv": "P-256",
"alg": "ES256",
"use": "sig",
"kid": "key-1",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFGM"
}
}],
"authentication": ["did:web:acef2002a323d40d4.demo.agents.whisper.online#key-1"],
"assertionMethod": ["did:web:acef2002a323d40d4.demo.agents.whisper.online#key-1"],
"alsoKnownAs": [
"urn:x-ip:2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4/128",
"https://rdap.whisper.online/ip/2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"
],
"service": [
{
"id": "did:web:acef2002a323d40d4.demo.agents.whisper.online#whisper-identity",
"type": "LinkedDomains",
"serviceEndpoint": "https://acef2002a323d40d4.demo.agents.whisper.online/.well-known/whisper-identity"
},
{
"id": "did:web:acef2002a323d40d4.demo.agents.whisper.online#rdap",
"type": "LinkedDomains",
"serviceEndpoint": "https://rdap.whisper.online/ip/2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"
}
]
}
verificationMethod— the public key(s) that speak for this DID. Whisper publishes a NIST P-256 (secp256r1) key as aJsonWebKey2020(alg: ES256); the#key-1fragment is the same key id (kid) the agent's apex/.well-known/jwks.jsonpublishes, so a DID consumer and a bare-JWKS consumer resolve to a byte-identical key. P-256/ES256 is the curve used throughout the identity stack — the DANE-pinned leaf and the per-agent CA live on it too — so a verifier's existing ES256 code path handles every door onto the identity.authentication— which verification method the agent uses to authenticate itself in a challenge/response.assertionMethod— which verification method the agent uses to sign statements — this is the one a Verifiable Credential issuer uses.alsoKnownAs— the agent's other public identifiers for the same subject: its/128as aurn:x-ip:…/128and its RDAP object URL. This is what ties the DID to the address — a resolver reading the DID document learns the exact IPv6 identity and where to fetch its registry record, all cross-checkable.service— where to fetch the JWS-signedwhisper-identityassertion and the RDAP object (bothLinkedDomains, both public). A DID-aware verifier follows these to reach the rest of the proof set without any Whisper-specific knowledge.- No private key material ever appears here —
did.jsonis a public document, safe to cache, mirror, or CDN.
Because it's just DNS + HTTPS, the document inherits DNSSEC and TLS's existing trust chain: forging it means either compromising the agent's TLS certificate or its DNS delegation — the same bar as forging the DANE TLSA record or the reverse-DNS PTR that already name the agent. See Identity: the address is the credential for how the FQDN, PTR, TLSA, RDAP, and did:web document all resolve back to the same /128 independently — that redundancy is the point: no single record is a single point of failure.
Dual example: fetch the DID document
With stock tools — a did:web document is nothing but JSON over HTTPS, so any HTTP client resolves it:
curl -s https://acef2002a323d40d4.demo.agents.whisper.online/.well-known/did.json | jq .
# or resolve the DID string itself with a generic resolver, e.g. did-resolver (npm) / did:web py:
python3 -c "
from did_web_resolver import resolve # any spec-compliant resolver
print(resolve('did:web:acef2002a323d40d4.demo.agents.whisper.online'))
"
With Whisper — the did.json key is only worth trusting if the name serving it is provably the agent's. whisper verify --trustless proves exactly that, anchored at the IANA DNSSEC root and trusting no Whisper API — and the key it validates (the DNSSEC-anchored ES256 signing key) is byte-for-byte the one the DID document publishes. So one keyless call establishes the trust the DID document rests on:
whisper verify --trustless acef2002a323d40d4.demo.agents.whisper.online
# cryptographically walks DNSSEC (IANA root) → AAAA/PTR → DANE-EE TLSA →
# the DNSSEC-anchored identity signing key; Whisper's API is never trusted
curl -s https://rdap.whisper.online/verify-identity/2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4 | jq .
# keyless, no account: the full-chain verdict as JSON (this endpoint trusts Whisper to run the chain)
Issuing and verifying a Verifiable Credential
A Verifiable Credential (VC) is a signed statement — "this DID asserts X" — and did:web gives you the one thing a VC verifier needs: a way to fetch the issuer's public key from the DID alone. The common wire form is a compact JWS (a "VC-JWT"): a JSON Web Token whose payload embeds the credential and whose kid header points at a verificationMethod in the issuer's did.json.
header: {"alg":"ES256","kid":"did:web:acef2002a323d40d4.demo.agents.whisper.online#key-1","typ":"JWT"}
payload: {"iss":"did:web:acef2002a323d40d4.demo.agents.whisper.online",
"vc":{"@context":["https://www.w3.org/ns/credentials/v2"],
"type":["VerifiableCredential","AgentCapability"],
"credentialSubject":{"scope":"read:inventory"}},
"nbf":1751328000,"exp":1751414400}
signature: ES256 over header.payload, by key-1
Verification is three steps, and none of them touch Whisper: split the JWT, resolve did:web:…#key-1 to a publicKeyJwk via the algorithm above, and check the ES256 signature against it.
With stock tools — any JOSE library does this without knowing Whisper exists:
import json, urllib.request
from jose import jws
from jose.utils import base64url_decode
fqdn = "acef2002a323d40d4.demo.agents.whisper.online"
doc = json.load(urllib.request.urlopen(f"https://{fqdn}/.well-known/did.json"))
jwk = doc["verificationMethod"][0]["publicKeyJwk"]
payload = jws.verify(credential_jwt, jwk, algorithms=["ES256"])
print(json.loads(payload)) # raises jose.exceptions.JWSError if the signature is bad
# equivalent one-liner with step-cli, no Python needed:
step crypto jwt verify --jwks <(curl -s https://$fqdn/.well-known/did.json | jq '{keys:[.verificationMethod[0].publicKeyJwk]}') --token "$credential_jwt"
With Whisper — checking the credential's signature is a JOSE-library job (above); Whisper's job is the harder half a JOSE library can't do on its own — proving the key you pulled from did.json genuinely belongs to that agent and wasn't swapped by whoever served the document. whisper verify --trustless settles that independently of Whisper's API, so you resolve the issuer key once, confirm it, then verify every credential that issuer signs against it:
# 1. establish trust in the issuer DID's key (DNSSEC-anchored, no Whisper API trusted)
whisper verify --trustless acef2002a323d40d4.demo.agents.whisper.online
# 2. then verify the VC-JWT against the resolved did:web key with any JOSE tool (as above)
step crypto jwt verify \
--jwks <(curl -s https://acef2002a323d40d4.demo.agents.whisper.online/.well-known/did.json \
| jq '{keys:[.verificationMethod[0].publicKeyJwk]}') \
--token "$credential_jwt"
did:web's honest limitation: availability of the document depends on the host staying up, exactly like any HTTPS resource. Whisper's answer is the same one it gives for every other identity record — the document is served from the two independent, active/active authoritative nodes behind agents.whisper.online, not a single origin, so "the host is down" isn't a failure mode you inherit.
Where this fits
did:web is an additional, W3C-native door onto the very identity the seven independent proofs already establish — reverse DNS, forward DNS, DANE, RDAP/WHOIS, and the signable identity document, all resolving back to the same /128. Its alsoKnownAs and service entries link straight back into that set (the /128, the RDAP object, the whisper-identity assertion), so the DID isn't a parallel identity — it's the same one, reshaped for a different ecosystem. And it's the door built specifically for W3C identity/credentials tooling: wallets, verifier SDKs, and agent frameworks that already speak DID/VC can consume a Whisper agent's identity with zero Whisper-specific code. For the full trustless chain-of-custody (DNSSEC → DANE → transparency log) that backs the key in that document, see DANE & DNSSEC; for the CLI and control-plane calls that create the agent this document describes, see Quickstart.
Next: Identity: the address is the credential · DANE & DNSSEC