Amazon SESで自前のキーペアを利用して署名したメールを送ってみた(BYODKIM)

2024.02.20

初めに

Amazon SESでのDKIMの利用に関してとりあえず使えればOKという方はDKIMにはEasyDKIMをご利用されている場合が多いかと思います。

EasyDKIMは良くも悪くもマネージドな管理される領域が広く署名や検証に利用するキーペアの管理を利用者で行う必要はありませんが、組織のポリシーとして所定期間で鍵のローテーションが必要であったり特定のキーペアを利用が強制される場合はこちらでは要件を満たせないものとなります。

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/send-email-authentication-dkim-easy.html 既に設定されているキーの長さと同じキーの長さに切り替えることはできません。
24 時間の間に複数回異なるキーの長さに切り替えることはできません(ただし、その間の1024への最初のダウングレードでない限り)。

一応鍵長や有無効を変更することで鍵の変更自体は可能ですが、2048bitの場合は鍵長を一旦短くする必要があるのであまり良さそうではありません。

キーペアを持ち込むBYODKIMを利用することで任意のタイミングでローテーションが可能ですのでこちらを使って署名や検証に用いる鍵を設定し切り替えてみます。

キーペアの作成

DKIMで利用するキーペアは信頼される認証局に依存しないことが特徴となっており所謂自己証明書の利用で問題ないとされています。
ただし技術標準としての話ですので100%全ての受信サーバがチェックしているとここでは断言できない点はご注意ください。
(感覚的には保険的な意味合いで実際問題やっている様なことはないと個人的には思ってはいるのですが...)

Amazon SESでは1024bit〜2048bitの証明書が利用可能です。

o The security constraint that keys smaller than 1024 bits are subject to off-line attacks
o The practical constraint that large (e.g., 4096-bit) keys might not fit within a 512-byte DNS UDP response packet

なおこの鍵長についてですがRFC 6376にて署名者側で最低限サポートすべき鍵長となっております。
実際問題それ未満の場合はセキュリティリスクによる問題があり、4096bitを超えるとEDNSを利用しない名前解決のUDPの1パケットに収まる512byteを超えてしまい機器によっては取り扱いができないという問題があります。
(512 Byteはペイロードのみのサイズで実際にはヘッダを含む576Byteが上限。4096bitでも鍵自体は512Byteだがp=の文字列など直接の鍵データ以外も含むため上限を超える)

今回はローテーションも行うので2048bitの鍵を2つ生成します。

# 最初に設定する予定のキーペア(pemが秘密鍵、pubが公開鍵)
% openssl genrsa 2048 > base.pem
% openssl rsa -in base.pem -pubout > base.pub
writing RSA key
# ローテーション後に利用する予定のキーペア
% openssl genrsa 2048 > rotate.pem
% openssl rsa -in rotate.pem -pubout > rotate.pub
writing RSA key

DKIMレコードの設定

秘密鍵の登録はマネジメントコンソールで行いますが、公開鍵を登録するレコードに関しては検証済IDに指定するドメイン配下のサブドメインのTXTレコードとして登録します。

登録先としては{{任意の文字列(セレクタ)}}._domainkey.example.comのTXTレコードとなります。セレクタの文字列には識別子をつけても良いしランダムな値にしてもよく同RFC上特に命名の指定はありません。

以前別件の記事で少し触れましたがEasyDKIMを利用している場合、DKIMレコード本体は以下のようにpタグ(鍵の指定)のみとなっております。

% dig eogp72tvvmnugysm4vjttzghjnmzww36.dkim.amazonses.com txt +short
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkKkQS3kmd+xp8t36utAX0dEMdlMFYVyQl3MYfffhhksvJUY/cbWATl2bcwAaNq+EHS5bpgy52alkBLd3uiSUOpp7eC/el+W3ffoOBZzDuO7pjEWcLZCEK1MY+HTGTEZHjZ/tfIKc3GPXR3ii4QNsf955+ADmnqt7z34tI164qsG92OJbAn6sdOtnDyQWt4SK5P6lwzBkJVEX06EDp" "1lePA8V8HtiPZkYCPNBtR2J8jdBOvUisKNsi9XM7oHS1nh4I2IjQrLlLOfi/vadGsCtuPXWBb85wLrYhVhiWHzBZcWaKT38m6PQ3gDPAq9TgEQIIqIE49YhvCh2U8zIWJBngwIDAQAB"

BYODKIMを利用することでDKIMレコード側にこの辺りのオプションを利用できるのもメリットの一つではありますが、現状では実質固定値に近いオプションがほとんどでこの辺りはの利用のため...というのはあまりなさそうな気はします。
利用可能なメールアドレスを制限するgタグが面白そうでしたがRFC 6376時点で既に利用非推奨となっておりました。残念。

DKIMのレコードに設定する公開鍵情報は鍵本体の部分の改行を削除して一つに繋げたものとなりますので例えば以下のような形で抽出が可能です。

 % cat base.pub |  tail -n +2 | sed '$d' | tr -d '\n'
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkdSVFllnwRxl9lS1lwxbsQzPXSEVJuTj97IvWi2ZVbR48pJuDISkUnKKdbsObhkYzh8akwPL16JBiJtEXXTzCwTQ4873Ay9hW1rBblJxjvB/aONL33OF/vzLm/nUzbcaU0H2EMzHAhe1Twf2eqK4gA3fqvJkXRQ8yqnyIimtEIN6fixA0qvnTCUCFW+bHgaognLJuoBtO3fMKdRx38hq0qhLMRJtFN5gURWomMf/CYRSmaHoKz9RffI7g5yKfdmPb+3yyrBWc6N95+399810+FvkU2YlHNsVns2Sgl1fp8kWGdOzcL2urhsVcw+T1dK2S28RdG84p6Q65jFs/wp0ZwIDAQAB

% cat rotate.pub |  tail -n +2 | sed '$d' | tr -d '\n'
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1xiDaohHawKnN9gFXz3u66jMbwPDUrIgRlO//3M6lKdNPuC2+r+IKl2nXLGfNObEvQMdB6j56IhWAkN3xUvbMDg3G2d5YPVSu7ifMStYvrvxs8LkzGwswqNXT3+wKKJ2B7PGNT/S0TsHo9kqP86USAsX9g1wv+eoIbY0xG1CoajKA6B5wtx9LWRUnyMiwSOMZ6rOrpPaJY5n4+R/eL4JlRJwNq59gNojRtdThRONOqrKusG0eR1CnpgUueefxJh9tjI9KIq41Wh1m5DukMZzQ7mDdH7/aE/Qoq9rr1aJSOiuVFLVh0ZJJxpENMyyqmruGm7Zw0a/CFpPfPpqi5nPkQIDAQAB

ただしTXTレコードは仕様として1コード4000文字まで入るようですが、ダブルクォートで囲まれる一つの文字列の上限は254文字となるためそれを超える場合は2つの文字列に分解して格納する必要があります(RFC7208のセクション3.3も参照)

実際には公開鍵のみではなく各種タグの値もあるためそれらの値を含め一旦文字列を作成し254文字目あたりで区切るようにして2つの文字列に分解して設定する必要があります。
今回の場合設定したレコードの名前解決の結果は以下のとおりです。

% dig rotate-before-dkim-key._domainkey.example.com txt +short
"v=DKIMv1;t=y;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkdSVFllnwRxl9lS1lwxbsQzPXSEVJuTj97IvWi2ZVbR48pJuDISkUnKKdbsObhkYzh8akwPL16JBiJtEXXTzCwTQ4873Ay9hW1rBblJxjvB/aONL33OF/vzLm/nUzbcaU0H2EMzHAhe1Twf2eqK4gA3fqvJkXRQ8yqnyIimtEIN6fixA0qvnTCUCFW+bHgaognLJ" "uoBtO3fMKdRx38hq0qhLMRJtFN5gURWomMf/CYRSmaHoKz9RffI7g5yKfdmPb+3yyrBWc6N95+399810+FvkU2YlHNsVns2Sgl1fp8kWGdOzcL2urhsVcw+T1dK2S28RdG84p6Q65jFs/wp0ZwIDAQAB"

% dig rotate-after-dkim-key._domainkey.example.com txt +short
"v=DKIMv1;t=y;MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1xiDaohHawKnN9gFXz3u66jMbwPDUrIgRlO//3M6lKdNPuC2+r+IKl2nXLGfNObEvQMdB6j56IhWAkN3xUvbMDg3G2d5YPVSu7ifMStYvrvxs8LkzGwswqNXT3+wKKJ2B7PGNT/S0TsHo9kqP86USAsX9g1wv+eoIbY0xG1CoajKA6B5wtx9LWRUnyMiwSOMZ6rOrp" "PaJY5n4+R/eL4JlRJwNq59gNojRtdThRONOqrKusG0eR1CnpgUueefxJh9tjI9KIq41Wh1m5DukMZzQ7mDdH7/aE/Qoq9rr1aJSOiuVFLVh0ZJJxpENMyyqmruGm7Zw0a/CFpPfPpqi5nPkQIDAQAB"

y This domain is testing DKIM. Verifiers MUST NOT treat messages from Signers in testing mode differently from unsigned email, even should the signature fail to verify. Verifiers MAY wish to track testing mode results to assist the Signer.

念の為t=yを設定しテストモードで試行します。
こちらを設定することでDKIMの検証の成否を受信の判定に利用しない、すなわちDKIM署名が付与されていないメールと同等の取り扱いを受けることになります。

ちなみに自分のドメインはCloudflareのネームサーバを利用しているのですが、こちらの場合はTXTレコードの値は分割せず格納することで上記のように良い感じに最大調でで分割してくれるようです。 むしろダブルクォートで囲うと以下のようにエスケープされてしまいましたが、この辺りについては利用しているネームサーバの仕様によるかと思いますので事前にご確認ください。

# 誤った登録をしてしまった時の結果
$ dig rotate-before-dkim-key._domainkey.example.com txt +short
"\"v=DKIMv1;t=y;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkdSVFllnwRxl9lS1lwxbsQzPXSEVJuTj97IvWi2ZVbR48pJuDISkUnKKdbsObhkYzh8akwPL16JBiJtEXXTzCwTQ4873Ay9hW1rBblJxjvB/aONL33OF/vzLm/nUzbcaU0H2EMzHAhe1Twf2eqK4gA3fqvJkXRQ8yqny\" \"IimtEIN6fixA0qvnTCUCFW+bHgao" "gnLJuoBtO3fMKdRx38hq0qhLMRJtFN5gURWomMf/CYRSmaHoKz9RffI7g5yKfdmPb+3yyrBWc6N95+399810+FvkU2YlHNsVns2Sgl1fp8kWGdOzcL2urhsVcw+T1dK2S28RdG84p6Q65jFs/wp0ZwIDAQAB\""

Amazon SES側の設定

Amazon SESの新規の検証ID発行のDKIMセクションの詳細設定もしくは作成後のDKIMの設定の「編集」からEasyDKIMとBYODKIMを切り替えることができます。

こちらの画面で切り替え秘密鍵に先程の秘密鍵の情報(base.pem)と対応するセレクタ(rotate-before-dkim-key)を指定します。

生成されたパブリックキーの最初と最後の行 (それぞれ -----BEGIN PUBLIC KEY----- と -----END PUBLIC KEY-----) を削除する必要があります。さらに、生成されたパブリックキーの改行を削除する必要があります。結果の値は、スペースや改行を含まない文字列になります。

登録の際は先程公開鍵に施した処理の様に先頭末尾の行と改行を削除する必要があります。
pemエンコードについてはに関しては先のopensslコマンドで発行した形式がすでにその形式ですので別途加工する必要はありません。

この後検証中となるので完了まで待ちます。
どこまで見られているかは不明ですが一定のレコードの正当性は検証されるようでv=DKIM1ではなくv=DKIMv1と誤って設定したところ検証エラーで「検証保留中」となりました。

こちらを修正しDKIMの検証が成功していることを確認します。

送信・確認

実際にメールを送信して確認してみます。
メール送信自体はEasyDKIMを利用する方式と変わらないので具体的なフローはここでは省略します。

届いたメールを確認するとexample.com側に指定されたDKIMの検証が先程指定されたセレクタのレコードでpassになっていることが確認できます。
passの右側に(test mode)の記載もありt=yもしっかり判別されています。

Authentication-Results: mx.google.com;
       dkim=pass (test mode) header.i=@example.com header.s=rotate-before-dkim-key header.b=iIzUUf6r;
       dkim=pass header.i=@amazonses.com header.s=zh4gjftm6etwoq6afzugpky45synznly header.b=ex0VlCy7;
       spf=pass (google.com: domain of xxxxxxx@ses.example.com designates 23.251.234.12 as permitted sender) smtp.mailfrom=xxxxx@ses.example.com;
       dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; s=rotate-before-dkim-key; d=example.com; t=1708425479; h=FROM:TO:SUBJECT:Message-Id:Date; bh=+qK+xkYRkWOX1w3SSkqyBh1NFCwnIPfFECIp/kKpH+c=; b=iIzUUf6r1etvp/ci2SRaYK4RfXKlfCo5eqq2fwz9FaNpx5hyzOSi5pnYVoM7axbI yUOY4M3amx8mCXRz6BPJaQ3Pisk2g0ntUUJw3gTFuhYOdg0A0dNu4a2zvzdNfz2R8NV NdiLuiUILqQhla5w1QP7e6fteU0OVPf/xCO6+qbZaMM9ymb4Zhkmvxvt0Gz+b3Berui aoRx6JY8r65u5kA60q4SfCnZ9azufCMFtPPtV1sLt8bdth4JMwy7QJGJ99NF7Vpi8QJ RuMc1/hMskhqXD3WTMjwOGNlWm2jv2OzY1zc25sLsKB1SU3otEBz9LwT++dAQOzQjV6 WHau9lsJ4w==
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; s=zh4gjftm6etwoq6afzugpky45synznly; d=amazonses.com; t=1708425479; h=FROM:TO:SUBJECT:Message-Id:Date:Feedback-ID; bh=+qK+xkYRkWOX1w3SSkqyBh1NFCwnIPfFECIp/kKpH+c=; b=ex0VlCy72xXEX2nsLtDW5yqs6k1pbY/NNlLBVl9FxUoucFJdRlklwT57iZB2Xx8O rmdcw+n+/ZilRguhjY1YGmco93oYq5EtYb4mbsAC0bpJK7TeZVlBKi6uzQCgdYXNY+d eUin2U2pesX5/LAnaJaOuZ1vymJT92XFNWIWMTVY=

鍵の差し替え(ローテーション)

同一のレコードを書き換えて鍵を切り替えることも不可能ではないですが、差し替えが全て完了するまで公開鍵・秘密鍵が新旧混在し整合性が取れずDKIM認証に失敗してしまう問題があります。
特に公開鍵に関しては各環境のDNSのキャッシュを受け正確な切り替え期間が読めないためセレクタ自体を別に登録し切り替えることが好ましいと考えておりますのでそちらの方式を利用します。

先程の設定と同様の手順を今度はrotate.pemのキーで行い検証を済ませます。

実際にメールを送信してみるとセレクタの値が入れ替わっていることを確認できました。

Authentication-Results: mx.google.com;
       dkim=pass (test mode) header.i=@example.com header.s=rotate-after-dkim-key header.b=GDh26ibj;
       dkim=pass header.i=@amazonses.com header.s=zh4gjftm6etwoq6afzugpky45synznly header.b=DeEtmhhs;
       spf=pass (google.com: domain of xxxxx@ses.example.com designates 23.251.234.11 as permitted sender) smtp.mailfrom=xxxxx@ses.example.com;
       dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; s=rotate-after-dkim-key; d=example.com; t=1708426486; h=From:To:Subject:Message-Id:Date; bh=+qK+xkYRkWOX1w3SSkqyBh1NFCwnIPfFECIp/kKpH+c=; b=GDh26ibjks2D0C6n8IZZvM4gP1Um2vm8FtnCyBQVwJVPv808pXXkUYf6Htv1s5/6 s5gjkcF/zH84Zl7tRGlqCbF6ouenP0q079eQ2/yhZBxP2vxSSnA/0RMqE83Nu0SwkG1 qezamikM2DtTIvUJ0ZkyCtoNWhhz5vOJCN+ZQPnwOMtlKyRm5uBVHHj/SqveLqgumJX w8lApUFsXXOxUyeJZfRhUvL6MDTMB/8NJmlU6Q2FVhCQFVLAWJC+I4lq8RDGxp0RCgL DvyyXkVO/mmlHjIT7iazSt6GEJRVW/MOkxWd/shCElnLYEgJBsCJyvGY4i7JBVkD7XP ktzWkDQ8IA==
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; s=zh4gjftm6etwoq6afzugpky45synznly; d=amazonses.com; t=1708426486; h=From:To:Subject:Message-Id:Date:Feedback-ID; bh=+qK+xkYRkWOX1w3SSkqyBh1NFCwnIPfFECIp/kKpH+c=; b=DeEtmhhsGjQUpJoepNhCGfJQRuUKT0SNS+9BfS6V23x5pn1Y8TEzCxNzQmVfsi2K ZceblPbtOl2sQCzTu+6KgMFSXQvkxQgtdLAiKxACKY2tvlB7ryDKux7CkYwIUeG62X8 jr72tEKPp39eFmBsWpTxQBBaDYcXdAAiY2ZgcvZk=

実際に切り替わっている正確なタイミングは把握できませんが、切り替え前のrotate-before-dkim-keyセレクタに対応するDKIMのレコードは残っておりますので仮にこの時点で切り替えが行われていなくともDKIMの検証については悪影響はございません。

強いていうのであれば切り替え前のbase.pemの秘密鍵で署名が行われDKIM-Signatureの値としてs=rotate-after-dkim-keyが指定されると流石にキーがペアになっておらず失敗しますがそんな不整合を取るような仕組みにはなっていないとは思います。

終わりに

今回はAmazon SESに独自のキーペアを持ち込みメールを送信していみました。

機密性の高い秘密鍵の管理や署名処理部分をAWS側に任せマネージドに管理しつつ、任意タイミングでのローテーションやDKIMレコードで指定可能な範囲のオプション的な指定が可能で多少の自由はあるので悪くはなさそうです。

ただ署名処理自体には手を入れられず署名アルゴリズムの指定等の対応はできませんのでそういった部分に手を入れる必要がある...となった場合は難しくなってしまいます。

BYODKIMが選択肢となる時点で一定の要件は定まっているかと思いますので仕様を満たすか十分ご確認の上ご利用いただければと思います。