Amazon SESから送信されたメールのDKIM署名を検証してみた

2024.02.05

初めに

これまでメールに関するブログ記事をいくつか執筆していますが実はDKIMについては概念としては知っているもののAmazon SES等の各種メールサービスを使っているとレコードも自動生成、サーバサイドの処理も意識することなくという感じで実のところあまり詳細な仕様は把握していませんでした。 (自分の過去の記事よく見るとDKIMに関してはかなり端折ってたりします)

とはいえここ最近は大手メールサービスの受信側のポリシーの変更で各種DNSによるメールの認証技術周りへの対応を行うような話もあり流石にふんわりした知識でいるのも微妙な感じ...という思いが出てきました。

理解するには実際に手を動かしてみることが一番、ということで受信サーバサイドでやるDKIMの署名検証を実際のメールヘッダやDNSレコードを見ながら順を追ってやって理解を深めてみようと思います。

いっそのこと送信側の署名も自前でやってしまうのが一番良いのですが一気に0からやろうとすると流石に時間がかってしまうので、今回はそちら側には手をつけず受信側の視点でのみ確認を行ってみます。

なお本記事では電子署名の正当性を把握することを主目的としており実際に受信サーバ側でDKIM署名の検証を行う際に必要なフローを全て満たしていない可能性があるためご了承ください。

また必要なパラメータのみに触れ全てのパラメータに触れるわけではないのでより詳細な仕様はRFC 6376を、もう少しライト目かつ細かな値に触れているページとしては以下が良い感じかと思います。

DKIMとは

RFC 6376で標準化されたメールの認証技術であり、ざっくりと理解するので会えれば公開鍵暗号方式によるを利用した電子署名とDNSを組み合わせたメールの認証技術となります。

公開暗号化方式に関する技術詳細はここでは割愛しますが、大枠としてはメールの内容をハッシュ化し秘密鍵で暗号化した電子署名を付与、その対になる公開鍵をそのドメインのDNSレコードに紐付けておき受信者側で届いたメールのハッシュとその電子署名を復号し得られたハッシュの同一性を評価します。

※ 画像はあくまで大枠のイメージです。

秘密鍵は送信者しか保持し得ない秘匿情報のため署名の偽装は基本的には難しく正当な送信者を示すことができます。また本文やメールヘッダの一部のデータを署名データに利用するため署名されたメールの内容に改竄があってもそれを検知することができます。

DKIM署名の復号

抜き出して利用するため特に記事自体は関連しないのですが、今回検証では以下の記事の時にテスト送信したメールを利用しています(個人検証から送られたメールでメールボックスの一番上にあった)。

3.5. The DKIM-Signature Header Field The signature of the email is stored in the DKIM-Signature header field. This header field contains all of the signature and key-fetching data. The DKIM-Signature value is a tag-list as described in Section 3.2.

RFC 6376のセクション3.5に記載されている通りメールに記載された電子署名情報はDKIM-Signatureヘッダに保持されているためこちらを抽出します。

DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;
	s=eogp72tvvmnugysm4vjttzghjnmzww36; d=example.com; t=1703217202;
	h=From:To:Subject:Content-Transfer-Encoding:Content-Type:MIME-Version:Message-ID:Date:List-Unsubscribe:List-Unsubscribe-Post;
	bh=kZ6oKi+Vld5K0ecDbHdAIUN17TdkoBgqNKMvjF6gg9c=;
	b=MKxQkipup5q71Qi+hZWqBSGyKgJKys2YquVA/VD77vgL6Qk0BehKu3QdSUN+Drzn
	S7xsWnEAyTPnASSTW0kshevMhXFSfI87zPJ5t/8XSeKpnQ1yHy4Q9Zdjva/lHPV+Yh2
	m1mhj7l9TpMKcPtj+XhGfz2gWugROOf+UYjPDQYX6/9AocsriGFjMoAVUYEvEWCh000
	YQWdGNPbXTBnGARRYy1b13aFqhhyBUFjZUbfUF+PuRHCnA1rJVHxRmDhplOiVZWO4DA
	hsusgGzEW3l7Pd3VupT6h0MI4xkEKrF/xp32EtREsq+90C8YVeBFjBJoNh9pCvIBP1g
	KkZuA39zbQ==
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;
	s=zh4gjftm6etwoq6afzugpky45synznly; d=amazonses.com; t=1703217202;
	h=From:To:Subject:Content-Transfer-Encoding:Content-Type:MIME-Version:Message-ID:Date:List-Unsubscribe:List-Unsubscribe-Post:Feedback-ID;
	bh=kZ6oKi+Vld5K0ecDbHdAIUN17TdkoBgqNKMvjF6gg9c=;
	b=EnBqMsngWNWlORsdb6rl5qM+Jifpd5W91qLR9iNATlKHrPsMGh7rZOTNKLaa9FMW
	9wAKQALMcPEbs7Zzkq4XnS4dIZFqNSripLzeVlX7Yhuu5v0xz/E9na0N07tCn8/w+RV
	h6xYepuzcXHJ8HVa1lfAjMkz3Dpv5aY6ppTeusOk=

し確認としては検証(openssl pkeyutl -verify)でも十分ですが、今回は目で確かめるために復号openssl pkeyutl -verifyrecover)まで行います。

公開鍵の取得

公開鍵が指定されているレコードは{{セレクタ(s=の値)}}._domainkey.{{ドメイン(d=の値)}}となるので以下のように対応を行い抽出します。

Amazon SESの場合、直接上記のレコードに値が含まれるのではなく一旦CNAMEで別のレコードを経由した上でその先に指定されているためご注意ください。

% dig eogp72tvvmnugysm4vjttzghjnmzww36._domainkey.example.com CNAME +short
eogp72tvvmnugysm4vjttzghjnmzww36.dkim.amazonses.com.
# 本当はバージョン等の情報も付与可能みたいだが省略されており鍵データ(p=の値)しか含まれていない
% dig eogp72tvvmnugysm4vjttzghjnmzww36.dkim.amazonses.com txt +short
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkKkQS3kmd+xp8t36utAX0dEMdlMFYVyQl3MYfffhhksvJUY/cbWATl2bcwAaNq+EHS5bpgy52alkBLd3uiSUOpp7eC/el+W3ffoOBZzDuO7pjEWcLZCEK1MY+HTGTEZHjZ/tfIKc3GPXR3ii4QNsf955+ADmnqt7z34tI164qsG92OJbAn6sdOtnDyQWt4SK5P6lwzBkJVEX06EDp" "1lePA8V8HtiPZkYCPNBtR2J8jdBOvUisKNsi9XM7oHS1nh4I2IjQrLlLOfi/vadGsCtuPXWBb85wLrYhVhiWHzBZcWaKT38m6PQ3gDPAq9TgEQIIqIE49YhvCh2U8zIWJBngwIDAQAB"
# 取得した値が本当に公開鍵であっているかの確認作業(明らかな誤りがない程度にはなるが)
# 上記の2値を連結して`-----BEGIN PUBLIC KEY-----`と`-----END PUBLIC KEY-----`と合わせてpubファイルに格納
% openssl rsa -pubin -text < mydomain.pub
Public-Key: (2048 bit)
Modulus:
    00:90:a9:10:4b:79:26:77:ec:69:f2:dd:fa:ba:d0:
    17:d1:d1:0c:76:53:05:61:5c:90:97:73:18:7d:f7:
    e1:86:4b:2f:25:46:3f:71:b5:80:4e:5d:9b:73:00:
    1a:36:af:84:1d:2e:5b:a6:0c:b9:d9:a9:64:04:b7:
    77:ba:24:94:3a:9a:7b:78:2f:de:97:e5:b7:7d:fa:
    0e:05:9c:c3:b8:ee:e9:8c:45:9c:2d:90:84:2b:53:
    18:f8:74:c6:4c:46:47:8d:9f:ed:7c:82:9c:dc:63:
    d7:47:78:a2:e1:03:6c:7f:de:79:f8:00:e6:9e:ab:
    7b:cf:7e:2d:23:5e:b8:aa:c1:bd:d8:e2:5b:02:7e:
    ac:74:eb:67:0f:24:16:b7:84:8a:e4:fe:a5:c3:30:
    64:25:51:17:d3:a1:03:a7:59:5e:3c:0f:15:f0:7b:
    62:3d:99:18:08:f3:41:b5:1d:89:f2:37:41:3a:f5:
    22:b0:a3:6c:8b:d5:cc:ee:81:d2:d6:78:78:23:62:
    23:42:b2:e5:2c:e7:e2:fe:f6:9d:1a:c0:ad:b8:f5:
    d6:05:bf:39:c0:ba:d8:85:58:62:58:7c:c1:65:c5:
    9a:29:3d:fc:9b:a3:d0:de:00:cf:02:af:53:80:44:
    08:22:a2:04:e3:d6:21:bc:28:76:53:cc:c8:58:90:
    67:83
Exponent: 65537 (0x10001)
writing RSA key
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkKkQS3kmd+xp8t36utAX
0dEMdlMFYVyQl3MYfffhhksvJUY/cbWATl2bcwAaNq+EHS5bpgy52alkBLd3uiSU
Opp7eC/el+W3ffoOBZzDuO7pjEWcLZCEK1MY+HTGTEZHjZ/tfIKc3GPXR3ii4QNs
f955+ADmnqt7z34tI164qsG92OJbAn6sdOtnDyQWt4SK5P6lwzBkJVEX06EDp1le
PA8V8HtiPZkYCPNBtR2J8jdBOvUisKNsi9XM7oHS1nh4I2IjQrLlLOfi/vadGsCt
uPXWBb85wLrYhVhiWHzBZcWaKT38m6PQ3gDPAq9TgEQIIqIE49YhvCh2U8zIWJBn
gwIDAQAB
-----END PUBLIC KEY-----

# 同様の手段でamazonses.com側の公開鍵も取得
% dig zh4gjftm6etwoq6afzugpky45synznly._domainkey.amazonses.com CNAME +short
zh4gjftm6etwoq6afzugpky45synznly.dkim.amazonses.com.
% dig zh4gjftm6etwoq6afzugpky45synznly.dkim.amazonses.com txt +short
"p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqN74VC9fwh4vtMX4fQM/n6GYucSeF/yNDg/cn5UC0KC0ntVMxoF1zWSK8Js5FrCwg4pge2zAGyxn9ijfoA0gPtHqqUI0hNE609SEeRk76UnLHGKiCA19iW6k+MrgGU4chYy4eitBqH6qF4zk7fNxDXxgM4WyU+2DnSdlAXgl5wwIDAQAB"
% openssl rsa -pubin -text < amazonses.pub
Public-Key: (1024 bit)
Modulus:
    00:aa:37:be:15:0b:d7:f0:87:8b:ed:31:7e:1f:40:
    cf:e7:e8:66:2e:71:27:85:ff:23:43:83:f7:27:e5:
    40:b4:28:2d:27:b5:53:31:a0:5d:73:59:22:bc:26:
    ce:45:ac:2c:20:e2:98:1e:db:30:06:cb:19:fd:8a:
    37:e8:03:48:0f:b4:7a:aa:50:8d:21:34:4e:b4:f5:
    21:1e:46:4e:fa:52:72:c7:18:a8:82:03:5f:62:5b:
    a9:3e:32:b8:06:53:87:21:63:2e:1e:8a:d0:6a:1f:
    aa:85:e3:39:3b:7c:dc:43:5f:18:0c:e1:6c:94:fb:
    60:e7:49:d9:40:5e:09:79:c3
Exponent: 65537 (0x10001)
writing RSA key
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqN74VC9fwh4vtMX4fQM/n6GYu
cSeF/yNDg/cn5UC0KC0ntVMxoF1zWSK8Js5FrCwg4pge2zAGyxn9ijfoA0gPtHqq
UI0hNE609SEeRk76UnLHGKiCA19iW6k+MrgGU4chYy4eitBqH6qF4zk7fNxDXxgM
4WyU+2DnSdlAXgl5wwIDAQAB
-----END PUBLIC KEY-----

bタグの元のハッシュ値の取得

取得された公開鍵および、電子署名の値であるDKIM-Signatureb=の値からハッシュ値を復号し取得します。
署名はa=の値からrsa-sha256で行われていることがわかりますのでアルゴリズムはそちらを指定します。

# echoの値は`b=`の値
# 流石に復号したした値そのままだと視認性が悪すぎるのでxxdコマンドを利用して16進数ダンプを出力
% echo "MKxQkipup5q71Qi+hZWqBSGyKgJKys2YquVA/VD77vgL6Qk0BehKu3QdSUN+Drzn S7xsWnEAyTPnASSTW0kshevMhXFSfI87zPJ5t/8XSeKpnQ1yHy4Q9Zdjva/lHPV+Yh2 m1mhj7l9TpMKcPtj+XhGfz2gWugROOf+UYjPDQYX6/9AocsriGFjMoAVUYEvEWCh000 YQWdGNPbXTBnGARRYy1b13aFqhhyBUFjZUbfUF+PuRHCnA1rJVHxRmDhplOiVZWO4DA hsusgGzEW3l7Pd3VupT6h0MI4xkEKrF/xp32EtREsq+90C8YVeBFjBJoNh9pCvIBP1g KkZuA39zbQ==" | base64 -d | openssl pkeyutl -verifyrecover -pkeyopt digest:sha256 -pubin -inkey mydomain.pub |xxd -p
940f27d69b8d8a3b838a26fc2bfa4ad0f37b013706fde9c70e0e936aaaf8
64b1

% echo "EnBqMsngWNWlORsdb6rl5qM+Jifpd5W91qLR9iNATlKHrPsMGh7rZOTNKLaa9FMW 9wAKQALMcPEbs7Zzkq4XnS4dIZFqNSripLzeVlX7Yhuu5v0xz/E9na0N07tCn8/w+RV h6xYepuzcXHJ8HVa1lfAjMkz3Dpv5aY6ppTeusOk=" | base64 -d | openssl pkeyutl -verifyrecover -pkeyopt digest:sha256 -pubin -inkey amazonses.pub | xxd -p
775464be287995332445a8b5d23d82afffbf2e981b3cfa0500e6b7426ba0
5693

ここで2つの電子署名の値は一致していませんが、今回はそれぞれのDKIM-Signatureh=の値(電子署名を生成する際に利用する対象のメールヘッダ)が異なっている関係でハッシュを生成する元の値が異なっているためです。

メールデータからハッシュ値の作成

上記で送信サーバ側で生成されたハッシュが確認できたためクライアント側でも実際のメールの値を元にハッシュ値を生成します。

具体的な大きな流れは同RFCセクション3.7及び6.1を参考にします。

正規化

以降の処理を行うにあたり指定によっては一部正規化が必要な場合がありますので際に概要だけ触れておきます。

正規の方法はc=で指定され現時点ではrelaxed/simpleもしくはsimple/simpleが存在するようです。

simple/simpleはメールヘッダの値をそのまま利用しますが、relaxed/simpleについては同RFCセクション3.4に記載される方式に基づき値を加工します。なおこの加工は電子署名のために算出する値であり実際のメールを変更しません。

厳格なのはsimple/simpleですがrelaxed/simpleを利用することにより各種メールサーバを経由する際により発生する可能性のある大文字小文字の変動、空白文字の変動といった細かな違いを吸収することができます。

今回はc=relaxed/simpleが指定されているので所定のアルゴリズムに基づき加工を行いますが、行われる処理についてはメールヘッダおよび本文で異なるため実際のステップにて補足します。

本文の正規化とハッシュ化

本文のみの改竄を検出するために本文の正規化した値のハッシュを求め、DKIM-Signature内のbh=の値と一致しているかを確認します。

メッセージ本文の正規化はボディ末尾の空行を全て削除(ただし行末の最後の改行のみ残す)、各行の末尾の空白文字を削除、連続する空白文字(半角スペースおよびタブ文字)を一つにまとめる、といった加工を行います。

原文でいうところのwhitespaceはかなり意訳しているので正確な定義は同RFCの2.8および3.4.4を参照してください。

今回のメールの内容は以下のとおりとなっており特に正規化が必要な部分はなかったのですが、改行コードはCRLFである必要があるのでその部分だけ変換をかけてハッシュ化します。
(memo: サービスからダウンロードしたメールはLFだったが一致せず、各種メール系の標準仕様としてCRLFが推奨されているためもしやと思いやったところうまくいった)

% cat mail-body.txt
im from ses

https://ap-northeast-1.user-subscription.com/uw/xxxxxxxx
% openssl dgst -sha256 -binary mail-body.txt | base64
kZ6oKi+Vld5K0ecDbHdAIUN17TdkoBgqNKMvjF6gg9c=

kZ6oKi+Vld5K0ecDbHdAIUN17TdkoBgqNKMvjF6gg9c=という値が得られ最初に記述したDKIM-Signatureヘッダに含まれるbh=の値と一致が確認できました。

秘匿情報を含まない処理に基づきますので、この時点では本文のみの改竄が検知可能でbh=の値と本文がセットで改竄されてしまった場合は改竄を検出できない点には注意しましょう。

メールヘッダの抽出・正規化

署名にはメールヘッダの値が利用されます。

ヘッダは全てを利用するわけではなくh=で指定された値に加えて算出途中のDKIM-Signatureを取り扱います。

今回の場合example.comの場合はh=From:To:Subject:Content-Transfer-Encoding:Content-Type:MIME-Version:Message-ID:Date:List-Unsubscribe:List-Unsubscribe-Post;となっており署名に用いるデータのヘッダはこの順番で記載する必要があります(DKIM-Signatureは末尾に付与)。

egrepで検索したら順番としては良い感じで並んでいたので先頭三行を削除したものを別のファイルに書き出しておきます。

% egrep "(From:|To:|Subject:|Content-Transfer-Encoding:|Content-Type:|MIME-Version:|Message-ID:|Date:|List-Unsubscribe:|List-Unsubscribe-Post)" from\ ses.eml 
Delivered-To: foo@example.net
        h=From:To:Subject:Content-Transfer-Encoding:Content-Type:MIME-Version:Message-ID:Date:List-Unsubscribe:List-Unsubscribe-Post;
        h=From:To:Subject:Content-Transfer-Encoding:Content-Type:MIME-Version:Message-ID:Date:List-Unsubscribe:List-Unsubscribe-Post:Feedback-ID;
From: optin@example.com
To: foo@example.net
Subject: from ses
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset=us-ascii
MIME-Version: 1.0
Message-ID: <xxxxxxx@ap-northeast-1.amazonses.com>
Date: Fri, 22 Dec 2023 03:53:22 +0000
List-Unsubscribe: xxxxxx
List-Unsubscribe-Post: List-Unsubscribe=One-Click

またメール本文に記載DKIM-Signatureからb=の値を差し引いた値を末尾に合わせて追加しておきます。

example.com側の署名を検証する場合は以下の通りです。

DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;
	s=eogp72tvvmnugysm4vjttzghjnmzww36; d=example.com; t=1703217202;
	h=From:To:Subject:Content-Transfer-Encoding:Content-Type:MIME-Version:Message-ID:Date:List-Unsubscribe:List-Unsubscribe-Post;
	bh=kZ6oKi+Vld5K0ecDbHdAIUN17TdkoBgqNKMvjF6gg9c=;
	b=

この値を元に正規化を行います。

relaxed/simpleの場合は、メールヘッダのキーを全て小文字に変換し(値は変換しない)、キーと値の区切り(:)前後の空白文字を削除し、ヘッダの値が複数にまたがっている場合は一行にまとめる(展開)、連続する空白文字を1文字にまとめる
といった対応が必要になります。

これを元に変換すると以下の通りになります。

sig-master.txt

from:optin@example.com
to:foo@example.net
subject:from ses
content-transfer-encoding:7bit
content-type:text/plain; charset=us-ascii
mime-version:1.0
message-id:<xxxxxxx@ap-northeast-1.amazonses.com>
date:Fri, 22 Dec 2023 03:53:22 +0000
list-unsubscribe:xxxxxx
list-unsubscribe-post:List-Unsubscribe=One-Click
dkim-signature:v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; s=eogp72tvvmnugysm4vjttzghjnmzww36; d=example.com; t=1703217202; h=From:To:Subject:Content-Transfer-Encoding:Content-Type:MIME-Version:Message-ID:Date:List-Unsubscribe:List-Unsubscribe-Post; bh=kZ6oKi+Vld5K0ecDbHdAIUN17TdkoBgqNKMvjF6gg9c=; b=

これが送信サーバ側では電子署名を行うための元の値、受信サーバ側では電子署名を検証する為の元の値となります。

with the value of the "b=" tag (including all surrounding whitespace) deleted (i.e., treated as the empty string), canonicalized using the header canonicalization algorithm specified in the "c=" tag, and without a trailing CRLF.

なお注意点としては本文同様に改行コードがCRLF以外の場合はCRLFに変換が必要であり、それに加え末尾には改行コードを付与してはいけないという点です。

ファイル末尾の改行コードについて

自分はあまりテキストファイルの使用については詳しくないのですが、調べてみるとPOSIXの定義として行の定義として末尾に改行が含まれてる(厳密には0文字以上の文字+改行)となっているようです。最後に改行=空行があるものと勘違いしていました。

ファイル末尾の改行コードテキストエディタで保存すると自動でつくことが多いようで、自分はvimで編集していましたが編集後バイナリを確認すると以下のように0x0d 0x0a(CRLF)がb=の後に入っていることが確認できます。

000004d0: 4955 4e31 3754 646b 6f42 6771 4e4b 4d76  IUN17TdkoBgqNKMv
000004e0: 6a46 3667 6739 633d 3b20 623d 0d0a       jF6gg9c=; b=..

なお末尾に空行が付いている場合は0x0d 0x0a 0x0d 0x0a(CRLF CRLF)となるようです。

000004d0: 4955 4e31 3754 646b 6f42 6771 4e4b 4d76  IUN17TdkoBgqNKMv
000004e0: 6a46 3667 6739 633d 3b20 623d 0d0a 0d0a  jF6gg9c=; b=....

vimの場合はバイナリモード(vim -b)で起動すると保存時に末尾にCRLFをつかないようなのでこちらを利用して編集しました。

確認はcat -eで%で終わっていることでも確認できます。

 % cat -e sig-master.txt | tail -n2
list-unsubscribe-post:List-Unsubscribe=One-Click^M$
dkim-signature:v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; s=eogp72tvvmnugysm4vjttzghjnmzww36; d=example.com; t=1703217202; h=From:To:Subject:Content-Transfer-Encoding:Content-Type:MIME-Version:Message-ID:Date:List-Unsubscribe:List-Unsubscribe-Post; bh=kZ6oKi+Vld5K0ecDbHdAIUN17TdkoBgqNKMvjF6gg9c=; b=%

ハッシュ化と検証

サーバサイドではこれをハッシュ化したものを秘密鍵で署名しb=の値を算出するわけですが、クライアントでは当然秘密鍵を保持していないためハッシュまでは算出できますがb=自体の算出はできないものとなります。

そのため先ほどの正規化を済ませDKIM-Signatureを付与したヘッダをハッシュ化したものと、最初のステップでb=の値から復号した値(ハッシュ値)を比較し一致していることを確認します。

% openssl dgst -sha256 -binary sig-master.txt | xxd -p
940f27d69b8d8a3b838a26fc2bfa4ad0f37b013706fde9c70e0e936aaaf8
64b1

example.comについて先に算出した公開鍵でb=の値を復号した値と一致していることを確認できましたので、同様の処理を行いamazonses.com側も検証を行います。

dkim-signatureの値はamazonses.com側の値に差し替え、さらにこちら側にはFeedback-IDヘッダが要求されているためそちらを付与してハッシュ化します。

% cat -e sig-master-ses.txt
from:optin@example.com^M$
to:foo@example.net^M$
subject:from ses^M$
content-transfer-encoding:7bit^M$
content-type:text/plain; charset=us-ascii^M$
mime-version:1.0^M$
message-id:<xxxxx@ap-northeast-1.amazonses.com>^M$
date:Fri, 22 Dec 2023 03:53:22 +0000^M$
list-unsubscribe:xxxxx^M$
list-unsubscribe-post:List-Unsubscribe=One-Click^M$
feedback-id:1.ap-northeast-1.l/xxxxxxxx:AmazonSES^M$
dkim-signature:v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; s=zh4gjftm6etwoq6afzugpky45synznly; d=amazonses.com; t=1703217202; h=From:To:Subject:Content-Transfer-Encoding:Content-Type:MIME-Version:Message-ID:Date:List-Unsubscribe:List-Unsubscribe-Post:Feedback-ID; bh=kZ6oKi+Vld5K0ecDbHdAIUN17TdkoBgqNKMvjF6gg9c=; b=%

% openssl dgst -sha256 -binary sig-master.txt | xxd -p
775464be287995332445a8b5d23d82afffbf2e981b3cfa0500e6b7426ba0
5693

こちらも復号したb=の値と一致してることができましたので公開鍵のついになる秘密鍵で署名されたメールかつ改竄されていないメールであることが確認できました。

メールヘッダ自体には直接メール本文は含まれていませんがこの段階あればメール本文の改竄がされていないということを示すことができます。

  • 本文及びbhタグの改竄、もしくはbhタグのみ改竄
    • bタグを除いたDKIM-Signature自体も電子署名に用いるたためbhの値が送信時から変動し最終的に検証を行うヘッダのハッシュが一致しない
  • 本文のみの改竄(bhは改竄しない)
    • bタグ算出前に本文ハッシュとbhタグとの比較時の不一致で検出可能
  • 本文、bhタグ、bタグ自体の改竄
    • bタグには送信しているメールドメインで公開されている公開鍵と対になる秘密鍵で署名が必要で準備が困難
    • 仮に異なる秘密鍵で署名した場合は公開鍵の対になってないので復号に失敗(もしくは検証の失敗)

終わりに

受信者視点のみとなりますが実際にDKIMによる署名を検証することでより正確に理解を得ることができました。

本文やメールヘッダの改竄検知も可能でSPFと比べても非常に強力な認証技術とは思いますが、DNSを登録するだけのSPFとは違い送信サーバ側にミドルウェアを入れたりする必要もあったり、そもそもサービス仕様上り用が難しかったり壁はありそうです。

とは言え最近ではp=noneで良いのでDMARCの導入を必須にするようなポリシーもあるためDKIMもどこかで強制とされるような未来があるのでしょうか。

Amazon SESで提供されているDKIM周りのレコードはかなりシンプルとなっており、実際にはメールの本文の何文字目までを利用するか等さまざまなオプションが存在します。

メールサービスを利用しているとそういった部分はなかなか触ることはありませんがぜひ知識としてでも一通り眺めておくと良いかもしれません。
(Amazon SESもCNAMEでamazonses.com側のドメインに実態があり変更できない)