Sign & prove agent outputs
A report, a commit, an API response — anything your agent produces can carry a signature the recipient checks against a key pinned in DNS, not a "trust me" footer.
An agent finishes a job — a security scan, a generated report, a code change, a JSON payload handed to another service — and hands over the result. How does the recipient know which agent produced it, and that it hasn't been altered since? Today the answer is usually nothing: a filename, a From:-style header, maybe a Slack message saying "done." None of that is checkable by anyone who wasn't in the room. If the output later matters — an audit, a dispute, a downstream agent deciding whether to act on it — "the agent said so" is a rumor with good formatting, not evidence. That gets worse at scale, not better: a fleet of short-lived agents producing outputs continuously means a constant stream of unverifiable claims, with no way to tell a real one from a forged one after the fact.
Whisper doesn't add a new signing product to solve this. It already gives your agent the one thing that makes a signature checkable by a stranger: a DNSSEC-anchored name — the same name its did:web document and DANE TLSA record already resolve identity against. So the idea on this page is not a new key; it's a new use of the anchor you already have. Your agent signs its output with its own signing key, publishes that key's public half at its Whisper name — the same DNS place, DNSSEC-signed, that anchors its identity keys — and the recipient verifies against DNS: no CA, no vendor dashboard, no shared secret. One name, one DNSSEC-rooted chain, now covering both "who is this agent" and "did this agent really produce this." (This is the same shape Whisper already ships for SSH keys — you supply the key, Whisper publishes its fingerprint under your name; see the SSHFP section below.)
The mechanism: a detached JWS over a content hash
The wire format is JSON Web Signature (JWS, RFC 7515) — the same envelope behind every OAuth access token, wrapped around a small manifest instead of the whole file. For anything bigger than a few KB you don't want to base64 the entire artifact into the token; you sign a claim about it instead:
// header (protected)
{ "alg": "ES256", "kid": "did:web:acef2002a323d40d4.demo.agents.whisper.online#sig-1", "typ": "JOSE" }
// payload
{ "iss": "did:web:acef2002a323d40d4.demo.agents.whisper.online",
"sub": "report.md",
"sha256": "86e7530bd0fe9762072958f37b8655872d3fde3e80f5ece15767255c2d7c5fef",
"iat": 1782864000 }
alg: ES256 is ECDSA over P-256 with SHA-256, per RFC 7518 §3.4 — a 64-byte fixed-width R‖S signature, not the DER-encoded signature OpenSSL produces by default. That mismatch is the single most common JWS bug: openssl dgst -sign gives you ASN.1 DER; a JWS verifier expects raw concatenated integers. Use a JOSE library for the envelope (it does the DER↔raw conversion for you) and reserve raw openssl for what it's already good at — hashing the artifact and, separately, checking the DANE pin. The three parts — base64url(header), base64url(payload), base64url(signature) — join with . into the familiar compact form, and the whole thing ships as a sidecar: report.md next to report.md.jws.
kid is the load-bearing field: it's a did:web URL, so "the key that made this signature" isn't an opaque ID you look up in a vendor's dashboard — it's an HTTPS GET away. #sig-1 is your agent's own output-signing key: a verificationMethod your agent publishes at its name, right alongside the #key-1 identity key its DID document already serves (or, if you'd rather not touch the DID doc, a DKIM-style JWK TXT under the same name — see the SSHFP section for that exact publish pattern). Either way it lands in the agent's DNSSEC-signed zone, so resolving the kid yields a key rooted in DNS, bound to the same /128 that produced the output. See Identity: the address is the credential for why that binding — address → name → key — can't be forged from one side alone.
Dual example: sign it, then verify it two ways
With stock tools — sign (any JOSE library; here jwcrypto, pip install jwcrypto):
import hashlib, json, time
from jwcrypto import jwk, jws
from jwcrypto.common import json_encode
digest = hashlib.sha256(open("report.md", "rb").read()).hexdigest()
manifest = json.dumps({
"iss": "did:web:acef2002a323d40d4.demo.agents.whisper.online",
"sub": "report.md", "sha256": digest, "iat": int(time.time()),
}).encode()
key = jwk.JWK.from_pem(open("agent-signing-key.pem", "rb").read()) # the agent's own signing key
token = jws.JWS(manifest)
token.add_signature(key, alg="ES256", protected=json_encode(
{"alg": "ES256", "kid": "did:web:acef2002a323d40d4.demo.agents.whisper.online#sig-1", "typ": "JOSE"}))
open("report.md.jws", "w").write(token.serialize(compact=True))
agent-signing-key.pem is an ordinary P-256 key your agent generates once and keeps for itself — openssl ecparam -genkey -name prime256v1 is the whole ceremony. It's not a Whisper-issued key and not the agent's TLS key; the only Whisper-specific step is publishing its public half at the agent's name (the #sig-1 verificationMethod above), so a recipient can find it by DNS. No CA, no enrollment.
With stock tools — verify (the recipient needs nothing but the JWS and a resolver; no account anywhere):
import hashlib, json, urllib.request
from jwcrypto import jwk, jws
fqdn = "acef2002a323d40d4.demo.agents.whisper.online"
kid = f"did:web:{fqdn}#sig-1" # the key named in the JWS header
doc = json.load(urllib.request.urlopen(f"https://{fqdn}/.well-known/did.json"))
vm = next(m for m in doc["verificationMethod"] if m["id"] == kid) # resolve the exact key
pub = jwk.JWK(**vm["publicKeyJwk"])
token = jws.JWS()
token.deserialize(open("report.md.jws").read())
token.verify(pub, alg="ES256") # raises if the signature is bad
claim = json.loads(token.payload)
assert claim["sha256"] == hashlib.sha256(open("report.md", "rb").read()).hexdigest()
print(f"OK — {claim['sub']} was produced by {claim['iss']}, content unaltered")
Two independent checks, both mandatory: the signature proves the manifest came from the key at sig-1; the hash comparison proves report.md is the exact bytes that manifest describes. Either one failing means reject — a valid signature over a stale hash is exactly as useless as a valid hash with no signature.
With Whisper — the gap that urllib.request.urlopen above leaves open is trust in the name: it fetches did.json over plain HTTPS and believes whoever answered. whisper verify --trustless closes it, re-deriving the whole chain from the DNSSEC root in-process instead of taking the server's word (see Verify an agent), so you prove the name is genuinely this agent's /128 before you trust any key it publishes:
whisper verify --trustless acef2002a323d40d4.demo.agents.whisper.online
# ✓ DNSSEC IANA root -> ... -> demo.agents.whisper.online (RRSIGs valid)
# ✓ DANE-EE served leaf SPKI == TLSA 3 1 1 b653a4ef...fcb82d1d
# ✓ identity_doc ES256 identity key (apex JWKS, DNSSEC-anchored) — claims match
# CRYPTOGRAPHICALLY PROVEN — Whisper API not trusted
The verdict is about the name, and that's exactly the anchor stock tools can't give you: once --trustless proves the DNSSEC chain down to this name, any signing key published under it — the #sig-1 verificationMethod, a JWK TXT — inherits that same anchoring, so a forged or MITM'd did.json fails the chain instead of silently handing you an attacker's key. Feed the exit code into a CI gate — whisper verify --trustless "$fqdn" && python3 verify_output.py — and the whole thing fails closed.
Bonus: signing commits with SSHFP
The same "publish the key at the name, verify against DNS" shape works for git provenance too, with primitives that predate JWS. OpenSSH's native commit signing (git config gpg.format ssh, backed by ssh-keygen -Y sign/-Y verify and OpenSSH's PROTOCOL.sshsig) needs an allowed_signers file mapping a principal to a public key — normally curated by hand, copy-pasted once and trusted forever. SSHFP (RFC 4255) is the DNS record built for exactly this problem — it's what lets ssh's own VerifyHostKeyDNS skip known_hosts for host keys — but note precisely what it carries: an algorithm byte, a fingerprint-type byte, and a hash of the key, not the key itself:
acef2002a323d40d4.demo.agents.whisper.online. IN SSHFP 4 2 <sha256-of-signing-key>
That's enough to confirm a key you already have in hand, not enough to reconstruct one from scratch — so the full public key still needs to travel some other way (a TXT record, same DKIM-style pattern as _whisper-identity's published JWK, or simply alongside the signature itself), and SSHFP is the DNSSEC-anchored cross-check that the key you were handed is the right one:
# fingerprint published under DNSSEC — confirms which key is authoritative
dig +short SSHFP acef2002a323d40d4.demo.agents.whisper.online
# 4 2 <sha256-of-signing-key>
ssh-keygen -lf agent-signing-key.pub # must print the same SHA256 fingerprint
echo "acef2002a323d40d4.demo.agents.whisper.online $(cat agent-signing-key.pub)" > allowed_signers
ssh-keygen -Y verify -f allowed_signers -I "acef2002a323d40d4.demo.agents.whisper.online" \
-n git -s commit.sig < commit_payload
The identity precedent this stands on — a DNSSEC-signed name as the source of truth for a public key — is exactly the DANE pattern the TLSA record already ships for the agent's TLS key; SSHFP is the same idea, RFC 4255 instead of RFC 6698, for a different key use.
Why this beats a "signed by" footer — and what it doesn't do
A text footer is unforgeable only in the sense that forging it takes zero effort; anyone can type "signed by Agent-7." A DNS-anchored signature is unforgeable in the sense that matters: producing a valid one requires the private key an attacker doesn't have, and checking one requires nothing but a resolver and a JOSE library the recipient probably already ships. This is the same idea behind keyless code-signing schemes like Sigstore, minus the OIDC identity provider and the Fulcio CA in the middle — the identity binding here is DNSSEC + DANE, a chain that terminates at the IANA root instead of at a third-party signing service, and it works completely offline from any CI provider.
What signing an output does not prove is when — a signature alone can't stop someone from claiming an output existed earlier than it did. That's a separate, composable proof: see OpenTimestamps & Bitcoin anchoring for pinning the time half onto the same chain, and Transparency log for making the issuance of the signing key itself independently auditable.
Next
did:web & verifiable credentials for the document this key lives in · DANE & DNSSEC for the chain that anchors it.