How to check DKIM record
Query TXT at the selector name, not at the domain: nslookup -type=TXT selector1._domainkey.github.com 8.8.8.8. A usable record carries v=DKIM1 and a p= tag holding base64 key data. An answer with p= and nothing after it is a published record whose key is revoked.
Why check this
Run this after a mail provider change, a key rotation, or a DNS cutover, and before any release that moves transactional mail to a new sending path. The failure it prevents is narrow and common: the provider signs every message with a selector whose record was never copied into the new zone, every receiver fails the DKIM check, and the application logs a clean send while the mail lands in spam.
The read settles three things: whether a record exists at that selector, whether the key is present or revoked, and how large the key is. It does not tell you that the signature on any particular message verifies against it. That is How to verify DKIM signature. It also cannot tell you which selectors a domain uses, because DNS has no query that answers that question. See How to check DKIM selector.
Prerequisites
- A selector name. Take it from the
s=tag of aDKIM-Signatureheader on a message the domain sent, or from the provider's setup screen. - RFC 6376 section 3.6.1 for the meaning of each tag in the key record.
- Node 22 for step 2. Save this as
dkim-record.js:
const { Resolver } = require('dns');
const crypto = require('crypto');
const r = new Resolver();
r.setServers(['8.8.8.8']); // Node's default resolver is not always usable
const [selector, domain] = process.argv.slice(2);
const name = selector + '._domainkey.' + domain;
r.resolveTxt(name, (err, recs) => {
if (err) return console.log(name + '\n lookup failed: ' + err.code);
const rec = recs[0].join(''); // RFC 6376 section 3.6.2.2: concatenate, no separator
const tags = {};
for (const part of rec.split(';')) {
const i = part.indexOf('=');
if (i > 0) tags[part.slice(0, i).trim()] = part.slice(i + 1).trim();
}
console.log('name : ' + name);
console.log('records : ' + recs.length);
console.log('v : ' + (tags.v ?? 'absent, defaults to DKIM1'));
console.log('k : ' + (tags.k ?? 'absent, defaults to rsa'));
console.log('t : ' + (tags.t ?? 'none'));
if (!tags.p) {
console.log('p : EMPTY. RFC 6376 section 3.6.1: the key is revoked.');
return;
}
const der = Buffer.from(tags.p, 'base64');
const key = crypto.createPublicKey({ key: der, format: 'der', type: 'spki' });
console.log('p : ' + tags.p.length + ' base64 chars, ' + der.length + ' DER bytes');
console.log('key : ' + key.asymmetricKeyDetails.modulusLength + ' bit');
console.log('sha256(p) : ' + crypto.createHash('sha256').update(tags.p).digest('hex').slice(0, 16));
});
Steps
- Step 1.
Read the raw TXT record at the selector name.
nslookup -type=TXT selector1._domainkey.github.com 8.8.8.8Non-authoritative answer: Server: dns.google Address: 8.8.8.8 selector1._domainkey.github.com canonical name = selector1-github-com._domainkey.microsoft.onmicrosoft.com selector1-github-com._domainkey.microsoft.onmicrosoft.com text = "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxZC/z2cK+2s1f/ktzSDSeFzkfIHrjwtGFsfKMAYvKaXjPVNzKykpbXBkX5nB7dVUTFttda7aROr2iSrIseQ27Ui+4rUZVzgFanE8RaXhYM9n5wPKNv8GtLJk7JgcsYZ7ErgID4uW2sEYyV/dnRYw6rDzOaeRKkKELTUfJI5ebLQIDAQAB;"Read the second line before the record. The selector is a CNAME into
microsoft.onmicrosoft.com, so the key material belongs to the mail provider and the zone owner holds a pointer to it. - Step 2.
Parse the tags and measure the key.
node dkim-record.js selector1 github.comname : selector1._domainkey.github.com records : 1 v : DKIM1 k : rsa t : none p : 216 base64 chars, 162 DER bytes key : 1024 bit sha256(p) : 74695ba48e84f2771024 bit is the floor RFC 8301 allows, not the size it asks for.
- Step 3.
Read a selector whose key has been withdrawn.
nslookup -type=TXT 20230601._domainkey.google.com 8.8.8.8Non-authoritative answer: Server: dns.google Address: 8.8.8.8 20230601._domainkey.google.com text = "v=DKIM1; k=rsa; p="The record resolves, so a tool that checks for presence reports success. There is no key after
p=, so no signature can verify against it. - Step 4.
Ask a domain for a selector nobody would publish.
node dkim-record.js zq7x4k9n2v example.comname : zq7x4k9n2v._domainkey.example.com records : 1 v : DKIM1 k : absent, defaults to rsa t : none p : EMPTY. RFC 6376 section 3.6.1: the key is revoked.A wildcard sits at
*._domainkey.example.com, so every selector you invent answers. Run this control query on any domain before you trust a hit. - Step 5.
Compare the key published under the same selector on two unrelated domains.
node dkim-record.js k1 github.com && node dkim-record.js k1 cloudflare.comname : k1._domainkey.github.com records : 1 v : absent, defaults to DKIM1 k : rsa t : none p : 216 base64 chars, 162 DER bytes key : 1024 bit sha256(p) : cf73ac5d54e4f91a name : k1._domainkey.cloudflare.com records : 1 v : absent, defaults to DKIM1 k : rsa t : none p : 216 base64 chars, 162 DER bytes key : 1024 bit sha256(p) : cf73ac5d54e4f91aByte for byte the same record. One provider signs for both domains with one key pair.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| v=DKIM1; k=rsa; p=<base64> | A key is published and parses | Move on to verifying a real signature. |
| p= with nothing after it | The key is revoked, RFC 6376 3.6.1 | Signatures with this selector fail. Rotate to a live selector. |
| lookup failed: ENOTFOUND | No record at that name | The selector is wrong, or the record was never copied to this zone. |
| key : 1024 bit | The RFC 8301 minimum | Ask the provider for 2048. Some receivers downgrade on 1024. |
| A CNAME above the record | The provider owns the key | Rotation is their action. Your zone only holds the pointer. |
| Two records at one selector | Verifiers pick one and may pick the wrong one | Publish exactly one TXT record per selector. |
Thresholds
Common mistakes
What to check next
- How to check DKIM selector: where the selector in step 1 comes from, since no lookup lists them.
- How to verify DKIM signature: the key in this record is only useful once a signature verifies against it.
- How to check SPF record: the other authentication path, and the one that breaks on forwarding.
- How to check DMARC record: the policy that decides what a DKIM result does to the message.
- Email testing checklist: the full set of reads before a release that touches mail.
FAQ
How to check a DKIM record of a domain?
Query TXT at <selector>._domainkey.<domain>. The bare domain holds SPF and verification strings, never a DKIM key. Without a selector there is nothing to query, which is the whole difficulty of the check.
How to check a DKIM record using nslookup?
nslookup -type=TXT selector1._domainkey.github.com 8.8.8.8. Name the resolver in the command. Keep the full answer: a 2048 bit key is over 255 characters, so DNS carries it as two quoted strings that a verifier joins with nothing between them.
How to check if DKIM is enabled?
A published key with a non-empty p= means a key exists. It does not mean mail is signed. Only a message carrying a DKIM-Signature header proves signing is switched on at the sending side.
What is a DKIM selector?
A label that picks one key out of several for the same domain. It appears as s= in the signature and as the first label of the record name, which lets a domain run several keys and rotate one at a time.
Why does the record end with a semicolon?
Trailing semicolons are legal in tag lists and carry no meaning. A parser that treats the empty final tag as an error is the bug, not the record.
Verified
Verified by Maks Vernynslookup Windows 10.0.22631node 22.23.2
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
basic5 minpublished updated Maks Verny