is it DNS? wiki/Security/DNS path attestation ← live monitor
Security

DNS path attestation

DNSSEC proves the data is authentic: this RRset is the one the zone owner published and signed. It does not prove that your query reached a genuine authoritative server, over an unmodified path, at a particular moment. Those are properties of the path, not the data. Path attestation closes that gap with an ordinary DNS query and an offline signature check.

The gap#

A validating resolver can tell you an answer was signed by the right zone. It cannot answer any of these:

  • Did my query actually reach an authoritative server for this name, or did something on the path answer for it?
  • Did the response come back unmodified?
  • Is there a transparent forwarder between me and the resolver I configured, faithfully returning correct answers while logging every one?

The obvious candidate for the first two is the ad bit, and it is not up to the job. RFC 4035, section 4.9.3 is explicit: ad is an assertion a validating resolver makes to its client, over the last hop, and it is meaningful only if that last hop is itself secured. RFC 4035 puts it plainly: a security-aware stub resolver "MUST NOT place any reliance on signature validation allegedly performed on its behalf, except when the security-aware stub resolver obtained the data in question from a trusted security-aware recursive name server via a secure channel." Anything on an unsecured hop can set or clear the bit at will, and consumer routers and captive portals do exactly that (see dnssec for the wider ad-versus-RRSIG story).

So the two mechanisms answer different questions, and neither substitutes for the other:

DNSSECPath attestation
Provesthe data is what the zone publishedthe path carried this query and answer intact
Verified bya validating resolver, chain to the rootthe client, offline, against a published key
Trust rootthe IANA root KSKa pre-distributed Ed25519 public key
Scopethat RRset, for its signature lifetimethis query, this zone, this moment
Detectsforged or modified recordsan answer that did not come from the real authority

Scope, up front. A valid attestation covers this query, to this zone, at this moment. It is a strong data point in an argument about whether DNS was at fault. It is not a verdict on your network, and it is not a claim that nothing is intercepting you. The limits section is not a footnote; it is the reason the result is worth anything.

The mechanism#

Ask for a name that has never existed and never will.

The client mints a fresh 128-bit random nonce, encodes it as 26 lowercase base32 characters (RFC 4648, section 6), and queries a name built from it under a zone whose contents are synthesized per query. Nothing about that name is in any cache, and nothing can precompute an answer for it, because the namespace is unbounded and the client picked the point in it. Getting an answer at all means the query transited the real delegation hierarchy to a real authority.

The authority answers with a single TXT record. The RDATA is base64url(payload || signature) (RFC 4648, section 5, no padding): a 48-octet binary payload followed by a 64-octet Ed25519 signature (RFC 8032; DNSSEC algorithm 15, RFC 8080, is the same primitive). The whole response fits inside 512 octets (RFC 1035), so no EDNS buffer negotiation is needed to carry it. Note that a client with no EDNS still cannot obtain it over UDP: DNS Cookies are an EDNS(0) option, and the anti-amplification gate below truncates cookieless UDP queries, so such a client is told to retry over TCP.

The payload is what the server saw:

FieldOctetsWhat it is
version1payload format version
key id4which signing key; a hint, confirm the full key from your key store
timestamp4the server's clock, seconds since the epoch
qname hash16first 16 octets of SHA-256 over the canonical lowercase queried name
transport10 = UDP/53, 1 = TCP/53, 2 = DoT, 3 = DoH
flags2EDNS, DO, CD, DNS Cookie, 0x20 mixed case seen, qname-minimization seen, ECS present, report channel
ecs4ECS family and prefix lengths only, never address bits (RFC 7871)
egress16the source address the query arrived from: your resolver's egress

The signature is computed over a domain-separated input, so an attestation signature can never be valid in any other context:

signature = Ed25519(key, "isitdns/opshal/attest" || 0x00 || payload)

The qname hash is the binding that makes the whole thing work. It covers the exact name you asked, nonce included, so a signature over someone else's query cannot be replayed as an answer to yours.

Verification is offline. You need the public key and nothing else: no CA, no callback to the server, no reliance on any assertion your resolver made, including ad.

Why the trust zone is an insecure delegation#

This is the part that looks wrong and is load-bearing.

The deployment uses two zones. A signed parent (probe.isitdns.net) is conventionally DNSSEC-signed with a DS lodged in its own parent, up to the root. The trust zone (t.probe.isitdns.net) is a child of it, delegated insecurely: the parent publishes NS records for it and publishes no DS.

The trust zone cannot be signed, for three separate reasons:

  1. There is no static RRset to pre-sign. Every owner name is a fresh nonce that appears once.
  2. A signed wildcard would be replayable. An RRSIG whose label count is less than the owner name's label count is a wildcard signature (RFC 4034, section 3.1.3), so one capture would validate for every future nonce.
  3. A signed wildcard is worse than useless here: an aggressive-NSEC cache (RFC 8198) could synthesize the answer locally and never contact the authority at all, which destroys the one property the design depends on.

Unsigned data inside a signed parent is normally bogus, and a validating resolver returns SERVFAIL. The insecure delegation is what prevents that. The parent's authenticated denial of a DS record (RFC 4035, section 5.2) tells validators the child is provably unsigned, so they accept its unsigned answers instead of rejecting them. You can see the denial:

dig @1.1.1.1 +dnssec DS t.probe.isitdns.net
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42058
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 4, ADDITIONAL: 1

probe.isitdns.net.       IN SOA   pns1.isitdns.net. hostmaster.isitdns.net. ...
probe.isitdns.net.       IN RRSIG SOA 13 3 3600 ... 51770 probe.isitdns.net. ...
t.probe.isitdns.net.     IN NSEC  \000.t.probe.isitdns.net. A NS HINFO TXT ...
t.probe.isitdns.net.     IN RRSIG NSEC 13 4 3600 ... 51770 probe.isitdns.net. ...

NOERROR with no answer, and a signed NSEC proving no DS exists at that name. That signed "there is no DS here" is the entire trick. Get this wrong (serve the trust zone from somewhere that is not delegated inside the signed parent) and every validating resolver on the internet SERVFAILs every attestation. The reference deployment confirmed that failure mode the hard way.

Two consequences worth internalising:

  • The signed parent's chain must reach the root. The authenticated denial of DS is only as trustworthy as the parent's own chain, so probe.isitdns.net needs a DS at isitdns.net, which needs a DS in .net, which chains to the IANA root.
  • Attestation answers arrive with ad unset, and that is correct. See it below.

Try it#

The zone is live and the commands below are real. Nothing here needs an account, a key of your own, or anything installed beyond dig.

1. Mint a nonce and ask for a name that has never existed.

N=$(head -c16 /dev/urandom | base32 | tr 'A-Z' 'a-z' | tr -d '=' | cut -c1-26)
dig @1.1.1.1 +short TXT x${N}.t.probe.isitdns.net
"AnBuczFqaNnetULbZo45N-KnxVqvvDNp3AEAhQAAAAAAAAAAAAAAAAAA__-sRUVQHnFGjZhM5F3V
ctdgJ1NYcuhSTsk6OKbkY9HjN3yT7ylXuYI4uecG2uo-MXw5bTEgZN-WDJGiE64i1S9ICg1gCg"

(One character-string on the wire; wrapped here to fit the page. Yours will differ in every byte: different nonce, different timestamp, different signature.)

2. Notice what the DNSSEC flags say. Ask the same question over DoH, where nothing on the path can strip a flag:

curl -s -H 'accept: application/dns-json' \
  "https://cloudflare-dns.com/dns-query?name=x${N}.t.probe.isitdns.net&type=TXT"
{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false, ... }

"AD":false, from a validating resolver, on a healthy answer. Compare a name in the signed parent:

curl -s -H 'accept: application/dns-json' \
  'https://cloudflare-dns.com/dns-query?name=canary.probe.isitdns.net&type=A'
{"Status":0,"TC":false,"RD":true,"RA":true,"AD":true,"CD":false,
 "Answer":[{"name":"canary.probe.isitdns.net","type":1,"TTL":5,"data":"192.0.2.111"}]}

"AD":true in the signed zone, "AD":false one level down in the insecurely delegated child. That is the design working exactly as intended, and it is also the point: the DNSSEC machinery is contributing nothing to the attestation. The proof is in the RDATA, not in a flag.

(canary.probe.isitdns.net is a static tripwire in the signed parent. It answers 192.0.2.111, a documentation address from RFC 5737. Anything else means the answer was rewritten.)

3. Verify it offline. Fetch the published keys:

curl -s https://isitdns.net/api/attest/keys

Each entry is a key id, an Ed25519 public key, and the authority it belongs to. Then check the signature yourself. This needs PyNaCl, which wraps libsodium's crypto_sign_open and performs the strict RFC 8032 checks (canonical S, small-order point rejection) that not every Ed25519 implementation does:

pip install 'pynacl>=1.5.0'
import base64, hashlib, socket, struct, sys
from nacl.signing import VerifyKey

# from https://isitdns.net/api/attest/keys
KEYS = {
    "pns1": "7f52f24997a9365879f7915347442dba36317a74855b3268c8efce78b9afe977",
    "pns2": "760096025815bfaece769dd2d53b39ed7c05340e853aadf83fa4087f7f7a6733",
    "pns3": "28111cd033b014dee4183a9fe25420a08995fa9ec01d928fee832ecc1ee683a9",
}

txt, qname = sys.argv[1], sys.argv[2].lower().rstrip(".") + "."
raw = base64.urlsafe_b64decode(txt + "=" * (-len(txt) % 4))
if len(raw) != 112:
    sys.exit("wrong length: not an attestation")
payload, sig = raw[:48], raw[48:]

kid = payload[1:5].decode("ascii", "replace")
try:
    VerifyKey(bytes.fromhex(KEYS[kid])).verify(b"isitdns/opshal/attest\x00" + payload, sig)
except Exception:
    sys.exit("TAMPERED: signature did not verify")

if payload[9:25] != hashlib.sha256(qname.encode()).digest()[:16]:
    sys.exit("TAMPERED: signed, but bound to a different name")

print("PROVEN  this query reached a genuine authority, answer unmodified")
print("  signed by  ", kid)
print("  at         ", struct.unpack("!I", payload[5:9])[0])
print("  arrived on ", ["UDP/53", "TCP/53", "DoT", "DoH"][payload[25]])
print("  egress seen", socket.inet_ntop(socket.AF_INET6, payload[32:48]))
python3 verify.py '<the TXT string, without the quotes>' "x${N}.t.probe.isitdns.net"
PROVEN  this query reached a genuine authority, answer unmodified
  signed by   pns1
  at          1785256414
  arrived on  TCP/53
  egress seen ::ffff:172.69.69.x

Four facts fall out of that. pns1 is which authority answered, proven by the signed key id (not by NSID, which is an unauthenticated hint). TCP/53 is the transport the query really arrived on, which is not always the one you sent from. The timestamp is the server's clock. And the egress is the address your resolver actually used to reach the authority, which is where most of the diagnostic value lives.

The last octet of the egress is redacted in the sample output above, deliberately. The full address is delivered in-band to whoever made the query and nowhere else; publishing resolver egress at full precision is exactly the thing the deployment does not do.

4. Break it on purpose. Change one character of the TXT string and run the verifier again:

TAMPERED: signature did not verify

Or pass a different name for the same attestation:

TAMPERED: signed, but bound to a different name

That second case is the qname binding doing its job: a valid signature attached to the wrong question is not evidence, it is a substitution attempt.

The verdicts, and what each is worth#

The taxonomy is enforced, not decorative. Nothing is allowed to render as proof that is not proof.

VerdictGranted whenWhat it is worth
PROVENa valid strict-Ed25519 signature over your fresh nonce and the exact name you askedCryptographic proof, scoped to this query, this zone, this moment. Independent of ad, of your resolver's honesty, and of any CA.
TAMPEREDthe signature fails, or the payload is bound to a different nameCryptographic proof of in-flight modification. The one alarm you can act on without further argument.
INFERREDeverything else: no answer, an egress that does not match your configured resolver, timing differences, cache behaviourEvidence. Sometimes strong evidence. Never proof, and never presented as proof.
CAN'T SAYthe observed facts are insufficientThe honest answer, and the one that makes the other three trustworthy. A tool that never says this is guessing somewhere.

Two rules sit under that table, and they are the ones people want to break:

Absence is never proof. A dropped query, a server outage, a rate limit, and active interception all produce exactly the same nothing. "No attestation came back" is INFERRED, always.

A matching egress proves nothing. Comparing the egress in your signed payload against the resolver you configured is the transparent-interception check, and it is asymmetric. A mismatch is evidence that something is re-forwarding your queries (INFERRED). A match rules nothing out, because an interceptor that re-forwards through your expected resolver produces a perfectly matching egress. Egress-match is never a clean bill of health.

What it does not prove#

Stated plainly, because a proof that is oversold is worth less than no proof at all.

  • It does not detect a faithful transparent relay or logger. Something that re-forwards your query through the expected resolver, modifies nothing, and logs everything produces a genuine, valid attestation. Path attestation proves non-modification and reachability. It can never prove on-path absence.
  • It says nothing about other domains. The namespace is fixed and public, so a resolver can serve it faithfully while rewriting everything else. A spotless attestation battery is scoped to the one zone measured.
  • It provides no confidentiality. On UDP/53 the nonce and the name are in the clear. The nonce defeats caching and precomputation; it is not a secret. If you want the exchange private, use DoT or DoH.
  • It does not prove liveness against a delaying adversary. Replaying an identical attestation inside its freshness window is idempotent and harmless. The receipt attests reachability and integrity at the attested time.
  • Where authorities are anycast, it proves "some authorized member of the authority set answered." Never per-host attribution from a signature alone.
  • Verification in a browser is not proof. Code an adversary can rewrite cannot verify a signature on your behalf. Anything running in a page whose trust store and content an attacker controls has to be capped below PROVEN.
  • Every PROVEN reduces to holding the genuine public key. First acquisition of that key is the real boundary. Fetching it over TLS on a device with an attacker-installed root CA gets you an attacker's key. The published anchor is cross-checkable over DNSSEC precisely so the check does not depend on the Web PKI:
delv +root TXT _anchor.probe.isitdns.net

delv carries its own root trust anchor and validates the chain on your machine, so the answer is authenticated through the IANA root key rather than any CA. It ships with the BIND utilities; on macOS the bundled build has no crypto support, so install the full package first (brew install bind). A client that cannot authenticate the anchor must not report PROVEN.

One property makes all of this survivable: every failure mode denies proof, none forges it. Strip the delegation, block encrypted transports, suppress the key manifest, drop the responses, and the client fails closed. A determined on-path adversary can always deny you the proof. It can never fabricate one.

Operational notes#

Anti-amplification is mandatory. Every attestation name is unique, so every response is unique, so classic Response Rate Limiting (which groups identical responses) never triggers. A UDP/53 attesting server without a countermeasure is an amplifier: a small spoofed query elicits a larger signed response. The countermeasure is DNS Cookies (RFC 7873, RFC 9018): a UDP query without a valid server cookie gets a truncated (TC=1) response and no attestation, forcing a TCP retry. A spoofed source cannot complete the handshake. The presence of a cookie option is not validity; it has to verify against the client address and a server secret.

Query-name minimization needs care. A minimizing resolver (RFC 9156) walks the intermediate labels on the way to the full name. Those ancestors must answer NODATA (NOERROR with an SOA), never NXDOMAIN, because an NXDOMAIN at any cut denies the whole subtree (RFC 8020) and aborts resolution before the real query is ever sent. Names outside the grammar get NXDOMAIN with an Extended DNS Error (RFC 8914) explaining why.

Signing keys are online keys. The nonce is chosen by the client at query time, so attestations cannot be signed offline. Exposure is inherent, and containment is the goal: one key per authority per purpose, so a compromise is attributable by key id and revocable without taking the others down, published in a short-lived signed manifest with a revocation list. Superseded entries stay verifiable, because a receipt you cannot check next month is not evidence.

Where this runs#

The mechanism is deployed as an authoritative-server plugin plus an offline command-line verifier. As of 2026-07-28 it runs on three attesting authorities on two continents: New York, Phoenix, and Frankfurt, across two independent providers (two DigitalOcean regions, AS14061, plus Oracle, AS31898). Each proves every other over the same protocol on a schedule, so a broken delegation or a dead authority shows up as a failed cross-check rather than as silence. 1,692 attestations proven as of 2026-07-28.

It defines no new resource record type and needs no protocol extension: an insecure delegation, TXT records, DNS Cookies, and offline Ed25519 signatures, all of which have been in the DNS for years. The composition is the new part. A specification of the mechanism is drafted for the IETF dnsop working group as an Experimental Internet-Draft; it has not been submitted, so treat everything above as a running deployment rather than a standard.

See also#