Whisper · Docs
Standards

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):

  1. Replace : with / in the method-specific-id to get a path: acef2002a323d40d4.demo.agents.whisper.online.
  2. If the identifier carried no path segment, append /.well-known.
  3. Append /did.json.
  4. Percent-decode any %3A (an encoded port) back to :, prepend https://, and GET it.

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"
    }
  ]
}

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