How to check if dnssec is enabled
A zone is signed when its parent publishes a DS record and the zone itself publishes matching DNSKEY records. Windows nslookup cannot ask for either type. Query a DNS over HTTPS resolver with curl instead, type=DS, and read the Answer array: one DS for example.com, key tag 2371, algorithm 13.
Why check this
Run this before a DNS cutover, after a registrar transfer, and after a signing key change. The failure it prevents is a domain that resolves on your machine and fails for part of the internet: the parent still publishes a DS for a key the new provider does not hold, so validating resolvers return SERVFAIL while the rest keep answering. A tester on a resolver that does not validate sees a working site.
Reading a domain you do not own is safe. Every command asks a resolver about a name, and no packet reaches the domain in the query.
Prerequisites
- curl with TLS. A DoH JSON endpoint answers a plain GET, so the build needs no HTTP/2. Version here: 8.21.0.
- Node 22 for
show.jsandds-match.js. Neither makes a network call. - A JSON DoH endpoint: Google Public DNS or Cloudflare.
- RFC 4034 for the DS and DNSKEY formats, appendix B for the key tag arithmetic.
Steps
- Step 1.
Ask nslookup for the DS record of a signed zone.
nslookup -type=DS example.com 8.8.8.8Non-authoritative answer: Server: dns.google Address: 8.8.8.8 Name: example.com Addresses: 2606:4700:10::6814:179a 2606:4700:10::ac42:93f3 104.20.23.154 172.66.147.243 unknown query type: DSRead the last line first. This nslookup has no DS type, so it sent a type A query and printed four addresses that say nothing about signing. It exits 0.
- Step 2.
Ask a DoH resolver for the same record and keep the response.
curl -sS -H 'accept: application/dns-json' 'https://dns.google/resolve?name=example.com&type=DS' | tee ds.json{"Status":0,"TC":false,"RD":true,"RA":true,"AD":true,"CD":false,"Question":[{"name":"example.com.","type":43}],"Answer":[{"name":"example.com.","type":43,"TTL":21013,"data":"2371 13 2 C988EC423E3880EB8DD8A46FE06CA230EE23F35B578D64E78B29C3E1C83D245A"}]}Status0 is NOERROR.Answerholds one record oftype43, which is DS. Its fields are the key tag 2371, algorithm 13, digest type 2 for SHA-256, and the digest. The parent zonecomtrusts a key inexample.com. - Step 3.
Read the keys the zone publishes. Save this as
show.jsand runcurl -sS -H 'accept: application/dns-json' 'https://dns.google/resolve?name=example.com&type=DNSKEY' | tee dnskey.json | node show.js.let s = ''; process.stdin.on('data', (d) => (s += d)).on('end', () => { const j = JSON.parse(s); console.log('Status ' + j.Status + ' AD ' + j.AD + ' CD ' + j.CD); const line = (sec, r) => sec.padEnd(10) + r.name + ' type ' + r.type + ' ttl ' + r.TTL + ' ' + (r.data.length > 48 ? r.data.slice(0, 48) + '...' : r.data); for (const r of j.Answer || []) console.log(line('ANSWER', r)); for (const r of j.Authority || []) console.log(line('AUTHORITY', r)); });Status 0 AD true CD false ANSWER example.com. type 48 ttl 2663 256 3 13 MjyZielP0GqniI1+j+wAG/3t0ImDDIlj1CxR0oo... ANSWER example.com. type 48 ttl 2663 256 3 13 oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8X... ANSWER example.com. type 48 ttl 2663 256 3 13 kxipjoIbNZDsWqEKaYaGq6fM/XThrRp1ue6AV9R... ANSWER example.com. type 48 ttl 2663 257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnop...Four keys, all algorithm 13. The first number is the flags field: 256 is a zone signing key, 257 a key signing key. Only the 257 key can be the one the DS names. The script cuts each key to 48 characters.
- Step 4.
Prove that the DS in the parent belongs to one of those keys. Save this as
ds-match.jsand runnode ds-match.js example.com dnskey.json ds.json.const { createHash } = require('node:crypto'); const fs = require('node:fs'); function wireName(name) { const parts = name.replace(/\.$/, '').split('.'); const out = []; for (const p of parts) { out.push(Buffer.from([p.length]), Buffer.from(p, 'ascii')); } out.push(Buffer.from([0])); return Buffer.concat(out); } function keyTag(rdata) { let ac = 0; for (let i = 0; i < rdata.length; i++) ac += (i & 1) ? rdata[i] : rdata[i] << 8; ac += (ac >> 16) & 0xffff; return ac & 0xffff; } const name = process.argv[2]; const keys = JSON.parse(fs.readFileSync(process.argv[3], 'utf8')).Answer || []; const ds = JSON.parse(fs.readFileSync(process.argv[4], 'utf8')).Answer || []; for (const k of keys) { const [flags, proto, alg, ...b64] = k.data.split(' '); const rdata = Buffer.concat([ Buffer.from([flags >> 8, flags & 0xff, Number(proto), Number(alg)]), Buffer.from(b64.join(''), 'base64'), ]); const digest = createHash('sha256').update(Buffer.concat([wireName(name), rdata])).digest('hex'); const tag = keyTag(rdata); const hit = ds.find((d) => d.data.toLowerCase() === `${tag} ${alg} 2 ${digest}`); console.log(`flags ${flags} alg ${alg} tag ${tag} ${hit ? 'matches the DS in the parent' : 'no DS for this key'}`); }flags 256 alg 13 tag 36315 no DS for this key flags 256 alg 13 tag 34505 no DS for this key flags 256 alg 13 tag 9776 no DS for this key flags 257 alg 13 tag 2371 matches the DS in the parentThe digest is recomputed locally, from the owner name in wire form followed by the key record, and it equals the one the parent publishes. The three zone signing keys have no DS and need none. This is the link a validator checks: a DS existing is not the same as the DS pointing at a key this zone still serves.
- Step 5.
Ask for a DS record that is not there.
curl -sS -H 'accept: application/dns-json' 'https://dns.google/resolve?name=github.com&type=DS'{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"github.com.","type":43}],"Authority":[{"name":"com.","type":6,"TTL":784,"data":"a.gtld-servers.net. nstld.verisign-grs.com. 1789161743 1800 900 604800 900"}]}There is no
Answerkey in this response, andStatusis still 0.Authorityholds the SOA ofcom: the parent zone answered and has no DS for the name. The delegation is unsigned. - Step 6.
Ask for an address in the signed zone, with validation turned off by the client.
curl -sS -H 'accept: application/dns-json' 'https://dns.google/resolve?name=example.com&type=A&cd=1'{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":true,"Question":[{"name":"example.com.","type":1}],"Answer":[{"name":"example.com.","type":1,"TTL":299,"data":"172.66.147.243"},{"name":"example.com.","type":1,"TTL":299,"data":"104.20.23.154"}]}CDis true because the query asked for checking to be disabled, andADis now false. This is the zone that returnedADtrue in step 2, minutes earlier. The flag describes the resolver, not the domain. - Step 7.
Ask a second resolver for the record from step 2.
curl -sS -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=example.com&type=DS'{"Status":0,"TC":false,"RD":true,"RA":true,"AD":true,"CD":false,"Question":[{"name":"example.com","type":43}],"Answer":[{"name":"example.com","type":43,"TTL":86400,"data":"2371 13 2 c988ec423e3880eb8dd8a46fe06ca230ee23f35b578d64e78b29c3e1c83d245a"}]}Same key tag, algorithm, digest type and digest. The text around them differs: lower case here and upper case at the other resolver, no trailing dot on the owner name, a TTL of 86400 against 21013.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Answer holds a record of type 43 | The parent publishes a DS, so the delegation is signed | Run step 3 and step 4 and confirm a key in the zone still matches that digest |
| Status 0, no Answer, an SOA in Authority | The parent has no DS for the name, so the delegation is unsigned | Nothing to fix unless signing was expected. Compare the delegation with How to check ns record |
| A DS in the parent and no matching key in step 4 | The parent trusts a key the zone no longer serves | Stop the cutover. Restore the key or remove the DS at the registrar, then wait out the DS TTL |
| AD true | This resolver validated this answer | Read it as a resolver verdict, not as a zone property. Step 6 flips it without touching the zone |
| AD false on a zone with a DS | This resolver did not validate, or was asked not to | Repeat without cd=1, then at a second resolver, before calling it a fault |
What a validation failure would look like
This page has no capture of one: every domain it may query is either correctly signed or unsigned. In the fields above, a resolver that cannot build the chain answers SERVFAIL with no Answer array, while the same name with cd=1 returns the records. That pair is a signing fault, not an outage.
Common mistakes
Thresholds
What to check next
- How to check dns records: every type at one name.
- How to check ns record: the delegation the DS sits beside.
- How to check dns ttl: how long a stale DS survives.
- How to check dns propagation: one record, several resolvers.
- How to check txt record of a domain: another resolver-side record.
- DNS migration checklist: the order these run in.
FAQ
How to check dnssec for a domain?
Query the registrable domain for type=DS, as in step 2. A record of type 43 in Answer means the delegation is signed. A subdomain carries no DS of its own.
How to check dnssec validation?
Send the query twice, plain and with cd=1. The resolver validated when the plain answer has AD true, as in step 2, and was told not to when CD is true, as in step 6.
Can I check if domains are dnssec protected programmatically?
The DoH endpoints answer a plain GET with JSON, so a script reads Answer and looks for type 43. Do not shell out to nslookup: step 1 shows it exiting 0 on a type it does not support.
Verified
Verified by Maks Vernycurl 8.21.0node 22.23.2nslookup Windows 11 build 22631
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate8 minpublished updated Maks Verny