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

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

  1. Step 1.

    Read the raw TXT record at the selector name.

    nslookup -type=TXT selector1._domainkey.github.com 8.8.8.8
    
    Non-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.

  2. Step 2.

    Parse the tags and measure the key.

    node dkim-record.js selector1 github.com
    
    name        : selector1._domainkey.github.com
    records     : 1
    v           : DKIM1
    k           : rsa
    t           : none
    p           : 216 base64 chars, 162 DER bytes
    key         : 1024 bit
    sha256(p)   : 74695ba48e84f277

    1024 bit is the floor RFC 8301 allows, not the size it asks for.

  3. Step 3.

    Read a selector whose key has been withdrawn.

    nslookup -type=TXT 20230601._domainkey.google.com 8.8.8.8
    
    Non-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.

  4. Step 4.

    Ask a domain for a selector nobody would publish.

    node dkim-record.js zq7x4k9n2v example.com
    
    name        : 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.

  5. 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.com
    
    name        : 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)   : cf73ac5d54e4f91a

    Byte 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

1024 bit is the minimum RSA key a DKIM signer may use, and 2048 bit is what the RFC asks for Source: RFC 8301 section 3.2: Signers MUST use RSA keys of at least 1024 bits for all keys. Signers SHOULD use RSA keys of at least 2048 bits.

Common mistakes

Sign: A DKIM checker reports the record as present and mail still fails DKIM.Cause: Presence and usability are different answers. google.com publishes v=DKIM1; k=rsa; p= at selector 20230601: the record resolves, and RFC 6376 section 3.6.1 defines the empty p value as a revoked key. Check the length of p, not the existence of the record.
Sign: Every selector you guess returns a record, including names you invented.Cause: The zone carries a wildcard at *._domainkey. example.com answers any selector with v=DKIM1; p=. Query one name no sender would ever use before you trust a hit, which is what step 4 does.
Sign: You rotate the key in your DNS zone and the signature does not change.Cause: The selector is a CNAME to the provider, as github.com selector1 is to microsoft.onmicrosoft.com. The record you edited is not the one a verifier reads, because the lookup follows the CNAME to the provider's zone.
Sign: Two domains you own publish the identical key under the same selector.Cause: A shared provider selector, not a mistake in your zone. k1._domainkey.github.com and k1._domainkey.cloudflare.com are the same record, so one private key signs for both. Anything that key signs passes DKIM for either domain.

What to check next

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.

basic5 minpublished updated Maks Verny