本文へスキップ
ecdsa.com
← すべての記事

DKIM が失敗する原因:本文ハッシュ、不正な署名、鍵、DNS

DKIM は、署名後にメッセージが変更されたとき、または検証者が使用可能な鍵を取得できないときに失敗します。以下の失敗パターンはすべて dkimpy と mailauth で再現し、それぞれが出力する判定をそのまま掲載しています。Authentication-Results に表示される内容と照らし合わせて確認できます。

要点:DKIM 署名は 2 つの要素で構成されます。メッセージ本文のハッシュ(bh=)と、選択したヘッダーとそのハッシュに対する署名(b=)です。body hash did not verify は、署名後に本文が変更されたことを意味します。たとえば、フッター、免責事項、ウイルス対策ソフトのバナーの追加や、再エンコードです。bad signature / signature verification failed は、署名対象のヘッダーが変更された(Subject へのタグの追加や From の書き換えなど)か、鍵が一致しないことを意味します。no key / key not found と permerror は DNS の問題を示しています。セレクターの誤り、失効したレコード、形式が不正なレコードです。relaxed 正規化(c=relaxed/relaxed)が許容するのは、空白の変更とヘッダー名の大文字・小文字の変更だけで、それ以外は許容しません。Authentication-Results ヘッダーと DKIM-Signature のタグを比較して原因を診断し、署名後にメールを変更しているコンポーネントを修正するか、パイプラインのより後の段階で署名してください。

DKIM が実際に署名する対象

DKIM(RFC 6376)は DKIM-Signature ヘッダーを追加します。次のヘッダーは、dkimpy 1.1.8 がテスト用のメッセージに対して生成したものです。

DKIM-Signature ヘッダー(dkimpy 1.1.8、生成されたとおりに折り返し)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com;
 i=@example.com; q=dns/txt; s=sel; t=1792500885; h=from : to : subject
 : date : message-id; bh=eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE=;
 b=FVDVEZSc7Whs7waF1W5ImDSxpa/czkhdpeUBefQ3/daFclzCdP0x9EQh6NOxBCa6oWph4
 WtntuKENSuqQGtjzcVlLdT+itgDCj5PaKMHI6S6JmQPBo2yy2EzPpdcDPeuiORtNbJMRHdZ
 0MhMgg7k11sPD9ts0pE6i+nzWmUXyJ2jEKWw68oG59kSO3B9jns

ここでは署名の値を短縮しています。デバッグで重要なタグは次のとおりです。

タグ意味よくある失敗
d=、s=署名ドメインとセレクター。鍵の取得元は s._domainkey.dセレクターの誤り、レコードの欠落または失効
c=ヘッダーと本文の正規化。simple または relaxed。デフォルトは simple/simple空白の変更で simple が失敗
h=署名対象のヘッダーフィールド(記載順)そのいずれかの変更
bh=正規化した本文の Base64 ハッシュ本文の変更
b=署名対象のヘッダーと DKIM-Signature 自体(b= を空にしたもの)に対する署名ヘッダーの変更、または鍵の誤り
l=省略可能。本文の先頭 l バイトだけをハッシュ誰でも内容を追加できてしまう(後述)
a=アルゴリズム:rsa-sha256 または ed25519-sha256rsa-sha1 はすでに無効(RFC 8301)

検証者は決まった順序で確認を行います。まず鍵(s._domainkey.d の TXT)を取得します。次に本文を正規化し、そのハッシュを bh= と比較します。そのあとで初めて、ヘッダーに対する b= を検証します。判定結果はこの順序で説明できます。本文が変更されていると、ヘッダーも変更されていた場合でも、本文ハッシュの問題として報告されます。

正規化:「relaxed」が許容する変更

正規化は、ハッシュを計算する前にメッセージを標準的な形式に変換する処理で、配送中の無害な変更によって署名が壊れないようにします。RFC 6376 §3.4 は 2 つの形式を定義しています。

  • simple のヘッダー:一切の変更を許容しません。ヘッダー名の大文字・小文字が変わることさえ認められません。
  • relaxed のヘッダー:名前を小文字にし、継続行の折り返しを解除し、連続する空白を 1 つのスペースにまとめ、値の末尾とコロンの前後にある空白を削除します。
  • simple の本文:末尾の空行だけが無視されます。
  • relaxed の本文:これに加えて、行末の空白が無視され、行内で連続するスペースとタブが 1 つのスペースになります。

どちらの形式も、単語の変更、行の追加、MIME パートの再エンコード、改行コードの違いは許容しません。relaxed が許容するのは、空白とヘッダー名の大文字・小文字の違いだけです。そのため、通常は c=relaxed/relaxed が選ばれます。RFC のデフォルトは simple/simple なので、c= のない DKIM-Signature では厳格な形式が使われます。

失敗の再現

このテストでは、短いメッセージに 2048 ビットの RSA 鍵で署名し(c=relaxed/relaxed、h=from:to:subject:date:message-id)、実際の中継者と同じ方法でメッセージを変更してから、dkimpy 1.1.8 で検証します。DNS ルックアップはプロセス内の関数に置き換えているため、ケースごとに鍵のレコードを変更できます。

dkimpy 1.1.8、Python 3.13.5(dkimpy のログ出力、base64 の値は省略なし)
--- 1. unchanged message, relaxed/relaxed
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: True
    verify: True
--- 2. list appends a footer to the body
    log: bh: b'cL30gL2flFNCvVcW5T6gPIkNZAhq6yYRaXnavNmMAJ8='
    exception: ValidationError: body hash mismatch (got b'cL30gL2flFNCvVcW5T6gPIkNZAhq6yYRaXnavNmMAJ8=', expected b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE=')
--- 3. list adds a [team] tag to the Subject
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: False
    verify: False
--- 4a. whitespace changed in transit, relaxed/relaxed
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: True
    verify: True
--- 4b. same whitespace change, simple/simple
    log: bh: b'CdTP1x6zZ/fF6DwHq354s/aG/L7f72HAMBorT94XVmk='
    exception: ValidationError: body hash mismatch (got b'CdTP1x6zZ/fF6DwHq354s/aG/L7f72HAMBorT94XVmk=', expected b'bxvAnpxX30DaKPhGagkY2hEaFkHH+Q8kAXms32JaSNY=')
--- 4c. simple/simple, only the Subject whitespace changed
    log: bh: b'bxvAnpxX30DaKPhGagkY2hEaFkHH+Q8kAXms32JaSNY='
    log: b'DKIM-Signature' valid: False
    verify: False

ケースごとに見ていきます。

  1. 変更なしのメッセージ:本文ハッシュが一致するため、署名の検証に成功します。
  2. フッターの追加:メーリングリストが配信停止用のフッターを追加します。計算した本文ハッシュ(got)が bh=(expected)と異なるため、検証は署名の確認に進む前に止まります。
  3. Subject へのタグ追加:メーリングリストが Subject の先頭に [team] を付けます。本文ハッシュは一致したままなので本文は無傷ですが、ヘッダーの署名の検証に失敗します。「本文ハッシュは一致するのに署名が失敗する」という結果は、ヘッダーの変更か鍵の誤りを示しています。
  4. 空白:テストでは、Subject 内のスペースを 2 つに増やし、本文内の連続した 2 つのスペースを 1 つにまとめ、行末にスペースを追加します。relaxed/relaxed では、署名の検証に引き続き成功します。simple/simple では、本文の変更によって本文ハッシュが一致しなくなり(4b)、ヘッダーの変更だけでも署名の検証に失敗します(4c)。

受信サーバーは結果を Authentication-Results に書き込みます。次に示すのは、同じケースを mailauth 8.0.1(Node.js 24.22)で検証した結果で、受信したメッセージで目にするのと同じ形式です。

mailauth 8.0.1:Authentication-Results の dkim 部分
unchanged                          dkim=pass header.i=@example.com header.s=sel header.a=rsa-sha256 header.b=P1HuE+RF;
footer appended                    dkim=neutral (body hash did not verify) header.i=@example.com header.s=sel header.a=rsa-sha256 header.b=P1HuE+RF;
[team] added to Subject            dkim=fail (bad signature) header.i=@example.com header.s=sel header.a=rsa-sha256 header.b=P1HuE+RF;
key revoked (empty p=)             dkim=neutral (invalid public key) header.i=@example.com header.s=sel header.a=rsa-sha256 header.b=P1HuE+RF;
no key in DNS                      dkim=neutral (no key) header.i=@example.com header.s=sel header.a=rsa-sha256 header.b=P1HuE+RF;

結果の表現は検証者によって異なります。mailauth は、本文ハッシュの不一致をデフォルトでは neutral、strict モードでは dkim=fail (body hash did not verify) として報告します。OpenDKIM は、本文の変更にもヘッダーの変更にも dkim=fail reason="signature verification failed" と記録します。表現が違っても原因は同じです。表現のバリエーションは、body hash did not verify、signature verification failed、key not found in DNS、permerror の各エラーページにまとめています。

配送中にメッセージを変更するもの

症状と原因の対応は次のとおりです。

症状よくある原因
特定の送信者からのすべてのメッセージで本文ハッシュが不一致DKIM の署名処理の後に動作するコンテンツフィルター、または免責事項やメール署名を付加するツール。送信者自身の側にあることが多い
メーリングリスト経由のメッセージでのみ本文ハッシュが不一致メーリングリストのソフトウェアによるフッターの追加や MIME パートの書き換え
非 ASCII テキストを含むメッセージでのみ本文ハッシュが不一致中継サーバーによる 8 ビットのコンテンツから quoted-printable への変換。RFC 6376 §5.3 は、署名の前に 7 ビットの MIME へ変換するよう署名者に指示
スクリプトで生成したメッセージで本文ハッシュが不一致LF だけの改行コードで署名し、送信時は CRLF(またはその逆)
署名が不正、本文ハッシュは一致Subject へのタグ追加、From の書き換え(メーリングリストの DMARC 対策)、ゲートウェイによるヘッダーの再折り返しや再エンコード、転送による Reply-To や To の変更
すべてのメッセージで署名が不正秘密鍵と DNS レコードの不一致:セレクターの誤り、ローテーション済みの鍵、貼り付け時に誤りが入った鍵

単純な .forward やエイリアスによる転送では、通常、本文と署名対象のヘッダーがそのまま保たれるため、SPF が失敗する場面でも DKIM は成功します。メーリングリストは、メッセージを変更すること自体が目的であるため、DKIM を壊す典型例です。最近のメーリングリストのソフトウェアは、本文と Subject を変更しないか、From をリストのドメインに書き換えてリスト自身の鍵で署名することで、この問題を回避できます。ARC(RFC 8617、実験的な仕様)に対応した受信側は、メーリングリストの ARC チェーンを信頼することもできます。ARC チェーンには、変更前の元の DKIM の結果が記録されています。

l= タグ:壊れにくさが偽造を招く

l= は、正規化した本文の先頭 l バイトだけをハッシュするよう検証者に指示します。メーリングリストのフッターが付いても署名が壊れないようにするためのものでした。テストではその欠点が明らかになります。l=35 を指定して署名した同じメッセージは、追加されたのがフッターであれ、第三者が書いたまったく新しい段落であれ、内容を追加した後でも検証に成功します。

dkimpy 1.1.8:l= を指定した署名
--- 5. signed with l= (body length limit)
    l=35
--- 5a. footer appended to an l= signature
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: True
    verify: True
--- 5b. a whole new part appended to an l= signature
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: True
    verify: True

5b で追加したテキストは Please pay invoice 4711 to IBAN XX00 0000 0000. でしたが、署名の結果は依然として pass です。RFC 6376 の §8.2 は、l= について「悪意のある中継者がメッセージを改変できる攻撃を可能にする」と警告し、「署名者はこのタグの使用にきわめて慎重になるべき」であり、「評価者はこのタグを使う署名を無視したいと考えるかもしれない」と述べています。MIME や HTML では、追加された内容によって、受信者に表示される内容を完全に置き換えることができます。l= を付けて署名しないでください。送信メールにこのタグが含まれている場合は、署名者の設定から削除してください。OpenDKIM では、l= は BodyLengthDB と MaximumSignedBytes の設定によって付加されます。

鍵と DNS

検証者は selector._domainkey.domain を TXT レコードとして照会します。まずデモのケースを、続いて実際の DNS での結果を示します。

dkimpy 1.1.8:鍵の問題
--- 6. key revoked in DNS (empty p=)
    log: could not parse RSA public key (b''): Unparsable public key; SubjectPublicKeyInfo: Data truncated at byte 0; RSAPublicKey: Data truncated at byte 0
    verify: False
--- 7. no key record in DNS
    log: missing public key: b'sel._domainkey.example.com.'
    verify: False
--- 8. 512-bit RSA key
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: True
    exception: KeyFormatError: public key too small: 512

ドメインは、p= を空にすることでセレクターを失効させます。ケース 8 から分かるとおり、512 ビットの署名でも RSA の計算そのものは成立しますが、検証者はそれでも鍵を拒否します。RFC 8301 は、検証者は「1024 ビット未満の RSA 鍵を使った署名を有効な署名とみなしてはならない(MUST NOT)」と定めています。署名者は「1024 ビット以上の RSA 鍵を使わなければならず(MUST)」、「2048 ビット以上の RSA 鍵を使うべき(SHOULD)」であり、rsa-sha1 は「署名にも検証にも使用してはならない(MUST NOT)」とされています。Ed25519(a=ed25519-sha256、RFC 8463)も定義されています。この RFC 自体に掲載されている例のメッセージには RSA 署名と Ed25519 署名の両方が付いており、実際の運用もこの形で、Ed25519 に対応していない検証者のために RSA 署名を併用します。

実際の DNS に対しては、OpenDKIM の opendkim-testkey を使うと、検証者と同じ方法でセレクターを確認できます。

opendkim-testkey(OpenDKIM 2.11.0~beta2、Debian 13)、2026年10月20日
$ opendkim-testkey -d github.com -s google -vvv
opendkim-testkey: checking key 'google._domainkey.github.com'
opendkim-testkey: key not secure
opendkim-testkey: key OK
$ opendkim-testkey -d gmail.com -s 20230601 -vvv
opendkim-testkey: checking key '20230601._domainkey.gmail.com'
opendkim-testkey: Revoked key
$ opendkim-testkey -d gmail.com -s nosuchselector -vvv
opendkim-testkey: checking key 'nosuchselector._domainkey.gmail.com'
opendkim-testkey: 'nosuchselector._domainkey.gmail.com' record not found

「key not secure」は、応答が DNSSEC で検証されていないことを意味するだけです。gmail.com の 20230601 セレクターは、p= が空になった、実際に使用を終えた鍵です。この鍵で署名されたメッセージは現在では検証に失敗します。鍵のローテーション後に、古いメッセージが DKIM の pass を「失う」ことがあるのはこのためです。

2048 ビットの鍵は 255 バイトの TXT 文字列 1 つには収まらないため、複数の文字列として公開され、検証者はそれらを区切り文字なしで連結します。

dig +short TXT google._domainkey.github.com(BIND 9.20.10、2026年10月20日)
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj6T5sl/RwdSqGoYWaWaFbS2UAeyPrEmd0gogocmRfS441qwR8/0KB81Hw89P0l4YiFRrXYk7NVIGfyCRHAYYZUzCkGeOysI2EjgzLFhd/NEsbRzOEc/kWkK/RO6JFq/5lOn6M9AZw/ap9tds4JG9ApgNNdSpPxp9DmvpsOSgNMVflRxQFrk3kdS4RNAPKu/" "OPoA7dlR/A/pECryjRoYgENtDXzdnK70HgCekems6UDzxDj61cjyoKoXtEMF/QsaHEQ1Gjfv014rDJBsubk/kT5VqHkWHa/ia68Z5r228Ety/wFfQNjXTx/J7KGZ9GkZlKED659eiJcLnWcKDSiQlhwIDAQAB"

DNS でよくある間違いには、次のようなものがあります。手作業で分割するときに、引用符やスペースを鍵の中に貼り付けてしまうこと。DNS プロバイダーが 1 つの長い文字列を、2 つの文字列を持つ 1 つのレコードではなく、2 つのレコードにしてしまうこと。ゾーンがオリジンを末尾に付加するために、sel._domainkey.example.com.example.com のような誤った名前で鍵を公開してしまうこと。そして、レコードが反映される前に新しいセレクターを使い始めてしまうことです。DKIM レコード生成ツールは、公開鍵から正しく分割されたレコードを生成します。

診断の手順

  1. 受信したままの生のメッセージを入手する:すべてのヘッダーを含めてください。Gmail の「メッセージのソースを表示」、Outlook の「ソースの表示」、または .eml ファイルを使います。メッセージを自分宛てに転送すると、内容が変わってしまいます。
  2. Authentication-Results を読む:自分の受信サーバーが付けたもの、つまりいちばん上にあるものを読みます。DKIM の判定とその理由(本文ハッシュ、不正な署名、鍵なし、permerror)を控えておきます。
  3. DKIM-Signature を読む:d= と s=(どの鍵か)、c=(どれだけ厳格か)、h=(どのヘッダーか)、l=(付いていないことが望ましい)を確認します。
  4. 鍵を確認する:dig +short TXT s._domainkey.d または opendkim-testkey -d d -s s -vvv を実行します。鍵が存在し、失効しておらず、1024 ビット以上で、欠けていないかを確認してください。
  5. 本文ハッシュが不一致の場合:送信した内容と受信した内容の間で、追加されたものや再エンコードされたもの(フッター、バナー、Content-Transfer-Encoding の変更)を探します。送信側を管理している場合は、送信済みフォルダーまたは MTA のログにあるコピーと、受信したコピーを比較してください。
  6. 本文ハッシュは一致するのに署名が不正な場合:h= に列挙された各ヘッダーを、送信時と受信時のコピーで比較します。どれも変わっていなければ、鍵が使用中の秘密鍵と一致していません。ローカルで署名して検証するメッセージでテストしてください。
  7. 断続的に失敗する場合:送信経路が複数(2 台の MTA、クラウドの中継サービスなど)あり、正しく署名しているのはそのうち 1 つだけ、という構成になっていないかを確認します。ローテーション済みのセレクターをまだ使っている経路がないかも確認してください。

この作業を手作業ではなく、貼り付けたメッセージで行うには、DKIM デバッガーを使ってください。正規化の各ステップ、計算した bh= とヘッダーの値の比較、DNS から取得した鍵、最も可能性の高い変更箇所が表示されます。メッセージはブラウザー内にとどまります。SPF、DMARC、ARC を含めた全体像を確認するには、メール認証アナライザーを使ってください。

恒久的な対策

  • 最後に署名する:自分の送信経路では、免責事項、フッター、リンクの書き換え、ウイルス対策を DKIM の署名処理より前に実行してください。Postfix では、smtpd_milters または content_filter の順序がこれにあたります。
  • c=relaxed/relaxed を使う:コストはかからず、ヘッダーの再折り返しや空白の変更があっても署名が壊れません。
  • 署名の前に 7 ビット MIME に変換する:RFC 6376 §5.3 が推奨するとおり quoted-printable か base64 に変換しておけば、8BITMIME に対応していない中継サーバーが本文を再エンコードすることはありません。
  • l= を使わない:また、中継者が正当に変更するヘッダーは、署名が必要でない限り h= に含めないでください。
  • 期間を重ねて鍵をローテーションする:新しいセレクターを公開し、DNS キャッシュが期限切れになるのを待ってから署名を切り替えます。配送中のメールが引き続き検証に成功するよう古いレコードを数日間残し、その後 p= を空にして失効させてください。
  • 2048 ビットの RSA 鍵:1024 ビットが最低限です。署名に使うソフトウェアが対応していれば、Ed25519 署名も併せて付けてください。
  • 自分で運用するメーリングリストでは:本文と Subject を変更しないか、From を書き換えてリストのドメインで署名し、さらに ARC 署名を付けてください。

出典

  • RFC 6376:DKIM 署名。§3.4(正規化)、§3.5(タグ)、§5.3(伝送時の変換)、§8.2(l=)。
  • RFC 8301:暗号アルゴリズムと鍵の用途に関する更新(SHA-1 の廃止、最小 1024 ビット)。
  • RFC 8463:DKIM 向けの Ed25519。
  • RFC 8617:Authenticated Received Chain(実験的な仕様)。
  • dkimpy 1.1.8、mailauth 8.0.1、OpenDKIM の opendkim-testkey。

dkimpy のケースは node:24.22.0-trixie(Debian の Python 3.13.5 と venv)で、mailauth は node:24.22.0-bookworm-slim で、opendkim-testkey は debian:trixie-20261005-slim で、dig は nicolaka/netshoot:v0.14 で、いずれも 2026年10月20日に実行しました。