私のDKIMチェッカーは、1024ビットのRSA鍵を「1280ビット」と表示していました。2048ビットの鍵なら「2304ビット」と出ていたはずです。
DKIMの鍵長は、RFC 8301 で基準が決まっている数字です。署名者は1024ビット以上の鍵を使わなければならず(MUST)、2048ビット以上が推奨(SHOULD)です。標準的なサイズの鍵なら、1280と出ても「推奨に届かない」という判定自体は変わりません。それでも、鍵長を表示するツールが鍵長を間違えていたら、ほかの表示も信用されなくなります。
症状: 数字が少しだけ大きい
例として microsoft.com の selector2 を見ます。2026年10月30日に dns.google で引いた結果です。
selector2._domainkey.microsoft.com
CNAME selector2-microsoft-com._domainkey.microsoft.onmicrosoft.com
TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ...QIDAQAB"
p= の値は216文字のbase64で、パディングの = はありません。デコードすると 216 × 3 / 4 = 162バイトです。中身は1024ビットのRSA鍵ですが、チェッカーは1280ビットと表示していました。
ずれ方が中途半端です。倍でも半分でもなく、1.25倍。最初に疑いたくなるのは、TXTレコードの255バイト分割のつなぎ間違いや、base64の文字数をバイト数と取り違えるといった文字列処理の失敗です。ただ、文字数を取り違えたなら 216 × 8 = 1728 になり、もっと大きく外れます。
原因: ブロブ全体を鍵として数えていた
原因は、デコードした162バイト全体を鍵の大きさとして扱っていたことです。162 × 8 = 1296ビット。表示の1280は、このラッパー込みのサイズを何らかの形で丸めた数字でした。旧コードの丸め方は残っていないので、1296から1280になった経緯までは説明できません。
DKIMの鍵長はモジュラスのビット数
鍵長として意味があるのは、RSAのモジュラス n のビット数だけです。DKIMの p= によく入っているのは SubjectPublicKeyInfo(SPKI, RFC 5280)というASN.1 DERの入れ物で、モジュラスはその奥にあります。
先頭のbase64 MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ をデコードすると、構造がそのまま見えます。
30 81 9f SEQUENCE, 159バイト (SubjectPublicKeyInfo)
30 0d SEQUENCE, 13バイト (AlgorithmIdentifier)
06 09 2a864886f70d010101 OID 1.2.840.113549.1.1.1 (rsaEncryption)
05 00 NULL
03 81 8d 00 BIT STRING, 141バイト(先頭1バイトは未使用ビット数)
30 81 89 SEQUENCE, 137バイト (RSAPublicKey)
02 81 81 00 ... INTEGER, 129バイト = 0x00 + モジュラス 128バイト
02 03 01 00 01 INTEGER, 65537 (公開指数)
足すと 3 + 15 + 4 + 3 + 132 + 5 = 162 バイトです。そのうちモジュラスは128バイト、つまり1024ビットです。残りの34バイトは、アルゴリズムを示すOID、DERのタグと長さ、符号のための 0x00、公開指数の65537 です。base64が IDAQAB で終わるのは、この 02 03 01 00 01 の部分です。
鍵が長くなっても、ラッパーの大きさはほぼ一定です。だから「全体 × 8」は常に少し大きく出ます。Node.js 22 で各サイズの鍵を作り、SPKIのDERを書き出して測った表です。
| モジュラス | DERのバイト数 | base64の文字数 | バイト数 × 8 |
|---|---|---|---|
| 512 | 94 | 128 | 752 |
| 1024 | 162 | 216 | 1296 |
| 2048 | 294 | 392 | 2352 |
| 3072 | 422 | 564 | 3376 |
| 4096 | 550 | 736 | 4400 |
もう一つのバグ: 空の p= を「見つかった」と数えていた
同じチェッカーには、もう一つ問題がありました。p= が空のレコードを、鍵が見つかったものとして表示していたことです。
RFC 6376 の 3.6.1節は p= について「An empty value means that this public key has been revoked.」と定めています。空は「鍵がない」ではなく「この鍵は失効させた」という意味です。
例は gmail.com の 20230601 セレクタです。2026年10月30日の時点で、TXTは次の通りでした。
20230601._domainkey.gmail.com TXT "v=DKIM1; k=rsa; p="
古い鍵を引退させたときの、ごく普通の状態です。これを「DKIM鍵: 見つかりました」と出すのは誤りで、鍵長の計算に回せば0バイトから数字を作ることになります。状態は「見つかった」「失効」「レコードが壊れている」の3つに分けて扱います。
いまのチェッカーと、より確実な方法
正直に書いておくと、MyIPScanのチェッカーは現在、公開されている鍵のサイズを上の表のような対応表で鍵長に変換しています。162バイトなら1024ビット、という形です。標準的なSPKIの鍵ならこれで正しく出ますが、表にないサイズは推定になります。
もう一つ、表では拾えない形式があります。RFC 6376 の本文は p= を「DER-encoded RSAPublicKey」と書いていますが、上の microsoft.com のように、SPKIで包まれた鍵が普通に使われています。この食い違いは Errata 3017 で指摘されていて、ステータスは「Held for Document Update」です。包まれていない素の RSAPublicKey だと、1024ビット鍵のDERは140バイト、2048ビットは270バイトで、SPKI前提の表とは一致しません。
より確実なのは、DERをパースしてモジュラスのビット長を直接取る方法です。依存なしで書くと、これくらいの量です。
// DERのタグと長さを1つ読む(範囲外なら例外)
function readTLV(buf, pos) {
if (pos + 2 > buf.length) throw new Error('DERが途中で切れています');
const tag = buf[pos++];
let len = buf[pos++];
if (len & 0x80) { // 長形式: 下位7ビットが長さのバイト数
const n = len & 0x7f;
if (n === 0 || n > 4 || pos + n > buf.length) throw new Error('長さが不正です');
len = 0;
for (let i = 0; i < n; i++) len = (len * 256) + buf[pos++];
}
if (pos + len > buf.length) throw new Error('長さがデータを超えています');
return { tag, start: pos, end: pos + len };
}
// INTEGER のビット長(符号用の先頭 0x00 は数えない)
function bitLength(buf, int) {
let i = int.start;
while (i < int.end && buf[i] === 0) i++;
if (i === int.end) return 0;
return (int.end - i - 1) * 8 + (32 - Math.clz32(buf[i]));
}
function rsaModulusBits(der) {
const outer = readTLV(der, 0);
if (outer.tag !== 0x30) throw new Error('SEQUENCE ではありません');
const first = readTLV(der, outer.start);
if (first.tag === 0x02) return bitLength(der, first); // 素の RSAPublicKey
if (first.tag !== 0x30) throw new Error('想定外の構造です');
const bits = readTLV(der, first.end); // BIT STRING
if (bits.tag !== 0x03) throw new Error('BIT STRING がありません');
const rsaKey = readTLV(der, bits.start + 1); // 未使用ビット数の1バイトを飛ばす
const modulus = readTLV(der, rsaKey.start);
if (modulus.tag !== 0x02) throw new Error('モジュラスがありません');
return bitLength(der, modulus);
}
TXTレコード全体からは、次のように呼び出します。失効の判定はデコードより前に置き、壊れたbase64やDERは例外ではなく invalid として返します。
function dkimKeyInfo(txt) {
const tags = Object.fromEntries(
txt.split(';').map(s => s.trim()).filter(s => s.includes('=')).map(s => {
const i = s.indexOf('=');
return [s.slice(0, i).trim(), s.slice(i + 1).replace(/\s+/g, '')];
})
);
const k = tags.k || 'rsa'; // k= の既定値は rsa
if (!('p' in tags)) return { status: 'invalid' };
if (tags.p === '') return { status: 'revoked', k };
try {
const der = Uint8Array.from(atob(tags.p), c => c.charCodeAt(0));
if (k !== 'rsa') return { status: 'found', k, bytes: der.length };
return { status: 'found', k, bytes: der.length, bits: rsaModulusBits(der) };
} catch {
return { status: 'invalid', k };
}
}
k=ed25519 の鍵(RFC 8463)は別に扱う必要があります。RFC 8463 では p= はEd25519の公開鍵をそのままbase64にしたもので、例のレコードは44文字です。RSAのパーサに通すものではありません。
自前でパースしたくない場合は、WebCrypto に任せる方法もあります。
const key = await crypto.subtle.importKey(
'spki', der, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']
);
console.log(key.algorithm.modulusLength); // 1024
ただし importKey('spki', ...) は名前の通りSPKI用で、素の RSAPublicKey はそのままでは読めません。両方の形式を受けたいなら、上のパーサの方が扱いやすいです。
記事のコードの検証
Node.js 22.23 で、512、1024、1536、2048、3072、4096ビットのRSA鍵を生成し、それぞれSPKI形式と素の RSAPublicKey 形式で書き出し、TXTレコードの形にして dkimKeyInfo に通しました。12パターンすべてで、生成時のビット数と一致しました。WebCrypto の modulusLength も、SPKIの5サイズで同じ値を返しました。
実在のレコードでは、microsoft.com の selector2 が162バイトのSPKIで1024ビット、gmail.com の 20230601 が空の p= で revoked です。
これはこの記事のコードの検証で、MyIPScanに載っているチェッカーの検証ではありません。
ルール
鍵の長さは、入れ物の長さではなくモジュラスの長さです。base64をデコードしたバイト数を8倍しても、鍵長にはなりません。ASN.1で包まれた値から数字を出すときは、中のフィールドまでパースしてから数えます。空の値も「なし」ではなく、意味のある状態として扱います。
チェッカーの現状は MyIPScan で見られます。よく使われる40のセレクタをブラウザから引いて、鍵の種類と長さ、失効した鍵を表示します。MyIPScanは私が作ったサイトです。
みなさんは鍵長をどう取っていますか。サイズの対応表で済ませていますか、それともパースしていますか。
参考:
- RFC 6376 3.6.1節(
p=が空なら失効): https://www.rfc-editor.org/rfc/rfc6376.html#section-3.6.1 - RFC 8301 3.2節(鍵長: 1024ビット以上必須、2048ビット以上推奨): https://www.rfc-editor.org/rfc/rfc8301.html#section-3.2
- RFC 6376 Errata 3017(
p=のSPKI形式): https://www.rfc-editor.org/errata/eid3017 - RFC 5280 4.1節(SubjectPublicKeyInfo): https://www.rfc-editor.org/rfc/rfc5280.html#section-4.1
- RFC 8463(Ed25519 の DKIM 鍵): https://www.rfc-editor.org/rfc/rfc8463.html