問い合わせフォームからの通知メールが届かない。届いても迷惑メールフォルダに入っている。サイト運用の定番の悩みですが、では何がそのメールを迷惑メールだと決めているのか。判定しているのは受信側のサーバーで、見ているのはドメインの DNS に置かれた SPF・DKIM・DMARC の3枚の書類です。この判定の裏で、本編で作った部品たちが毎日働いています。番外編は、その現場を見に行きます。

OpenSSL で手を動かして学ぶ暗号の基礎、今回は番外編です。本編を読んでいなくても進められますが、署名の仕組みは第4回、信頼の名簿の考え方は第5回が下敷きになっています。

メールは、名乗りがタダ

前提をひとつ。メールの差出人(From)は自己申告です。仕組みの上では、誰でも info@ から始まる好きなアドレスを名乗って送信できます。この緩さが迷惑メールとなりすましの土壌で、受信サーバーは「名乗りどおりの送り主か」を自力で検品するしかありません。

差出人は、自己申告。
だれでも 書ける From: alice@torigoedesign.com From は自己申告 受信サーバー ? だれでも 書ける From: alice@torigoedesign.com From は自己申告 受信サーバー ?

名乗りはタダ。受け取る側が、自力で検品するしかない。

もうひとつ前提があります。メールの差出人は、実は2つあります。

メールには、差出人が2つある。
封筒の差出人(MAIL FROM) alice@torigoedesign.com 中身 手紙の差出人(From) alice@torigoedesign.com SPF が照合するのは、封筒の差出人 画面には出ない。Return-Path に残る 人が見るのは、手紙の差出人 DMARC が守るのも、こちら 封筒の差出人(MAIL FROM) alice@torigoedesign.com SPF が照合するのは、封筒の差出人 画面には出ない。Return-Path に残る 中身 手紙の差出人(From) alice@torigoedesign.com 人が見るのは、手紙の差出人 DMARC が守るのも、こちら

封筒と手紙で差出人が違っていても、メールは届く。なりすましが狙うのは手紙のほうで、3枚の書類はそこを守るためにある。

ひとつは封筒に書かれた差出人です。送信サーバーが受信サーバーに接続して「この差出人から預かりました」と名乗るときの名前で、SMTP の MAIL FROM コマンドで渡されます。届いたメールには Return-Path: ヘッダとして残りますが、メールソフトの画面には出てきません。配送に失敗したときのエラーメールの戻り先です。

もうひとつは手紙に書かれた差出人、つまり From: ヘッダです。メールソフトが名札として表示するのはこちらで、人が「誰からのメールか」を判断する材料もこちらです。

この2つが違っていても、メールは普通に届きます。配信サービスを使えば封筒は業者のドメイン、手紙は自社のドメインになりますし、メーリングリストを経由すれば封筒はリストの名前に書き換わります。そして、なりすましが狙うのは手紙のほうです。人が見るのが手紙だからです。

検品の道具が SPF、DKIM、DMARC の3点セットです。封筒と手紙のどちらを見ているかが、3つの違いになります。

3枚の書類は、先に DNS に置いてある

3点セットはすべて、ドメインの持ち主が DNS に置いた公開情報です。受信サーバーは検品のたびに、差出人のドメインの DNS を引きに来ます。

先に、持ち主が3枚を DNS に置く。
ドメインの持ち主 torigoedesign.com 書く DNS(ドメインの掲示板) 名簿(SPF) v=spf1 include:… ~all 錠前(DKIM の公開鍵) default._domainkey p=MIIB… 方針書(DMARC) _dmarc p=reject rua=mailto:… ここに書けるのは、ドメインの持ち主だけ ドメインの持ち主 torigoedesign.com 書く DNS(ドメインの掲示板) 名簿(SPF) v=spf1 include:… ~all 錠前(DKIM の公開鍵) default._domainkey p=MIIB… 方針書(DMARC) _dmarc p=reject rua=mailto:… ここに書けるのは、ドメインの持ち主だけ

受け取る側が、あとで見に来る掲示板。書けるのは持ち主だけ、という前提が保証人の代わりになる。

第5回で、公開鍵の身元保証には名簿(ルート証明書ストア)が要るという話をしました。メールの世界はその名簿の役を DNS に任せています。ドメインの DNS を書き換えられるのはドメインの持ち主だけ、という前提が保証人の代わりです。公開情報なので、dig コマンドで誰でも読めます。題材は本編に続き、当ブログのドメインです。

SPF は、送信元の名簿

dig +short TXT torigoedesign.com
# "google-site-verification=gF-9-il9UbfZaPFgO2ZTimBzAgI0W4qLrigLqkUF-Cw"
# "v=spf1 +a:sv14530.xserver.jp include:spf.sender.xserver.jp ~all"

TXT レコードは雑居ビルなので、関係ない住人(サイト認証用の文字列)も同居しています。探しているのは v=spf1 で始まる行です。

封筒の差出人を名乗れる送信元を、TXT に並べる

「このドメインを封筒の差出人として名乗ってよい送信元は、ここだけです」という一覧です。ドメインの TXT レコードに置きます。署名もハッシュも出てこない、ただの名簿です。

受信サーバーは、送信元 IP を左から照合する

受信サーバーは、接続してきた送信サーバーの IP アドレスと、封筒の差出人(MAIL FROM)のドメインを手に取ります。そのドメインの SPF レコードを引き、左から順に照合して、最初に当たった項目で結果が決まります。このドメインのレコードは、こう読みます。

  • +a:sv14530.xserver.jp は「このホスト名の A レコードの IP からなら OK」。先頭の + は「当たったら pass」の意味で、省略しても同じです
  • include:spf.sender.xserver.jp は「Xserver 側が管理している名簿も、まるごと参照する」。レンタルサーバーの送信サーバーが増減しても、利用者側のレコードを直さなくて済みます
  • ~all は「ここまでに当たらなかった残り全部は softfail(疑ってよい)」。-all なら fail(はっきり不合格)、?all なら中立です

結果は pass、fail、softfail、neutral などで受信サーバーに渡ります。include や a のように DNS を引き直す項目は、合わせて 10 回までという上限があり、名簿を継ぎ足しすぎると permerror で丸ごと無効になります。

SPF は、送信元の名簿。
DNS の TXT(SPF) 送ってよい送信元 Xserver のサーバー それ以外は疑う(~all) 照合 Xserver から ✓ 通す 知らない送信元から ✕ 疑う DNS の TXT(SPF) 送ってよい送信元 Xserver のサーバー それ以外は疑う(~all) 照合 Xserver から ✓ 通す 知らない送信元から ✕ 疑う

暗号はひとつも出てこない。ただの名簿。

名簿が見ているのは封筒の差出人と送信元の IP だけで、手紙の From: も本文も見ていません。だから SPF 単体では名札のなりすましは止められませんし、転送を経由したメールは転送サーバーの IP が名簿にないので落ちます。SPF の担当は、封筒までです。3点セットのうち、暗号が働いているのは次の DKIM だけです。

DKIM は、メールに付くデジタル署名

DKIM-Signature ヘッダの a=、d=、s= を読む

送信サーバーが、メールのヘッダと本文に付けるデジタル署名です。第4回でやった dgst -sign と同じ構図で、本文のハッシュを取り、秘密鍵で署名する。受信メールのヘッダには、こういう形の行が入っています(手元の受信メールで「ソースを表示」すると本物が見られます)。

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=torigoedesign.com; s=default;
        h=from:subject:date; bh=(本文のハッシュ); b=(署名本体)

a=rsa-sha256 に第1回と第3回の名前が並んでいます。d= が署名したドメイン、s= はセレクタで、鍵に付けた名前です。鍵を複数持って入れ替えられるように、鍵の置き場所をこの名前で分けています。

署名の手順をたどり、公開鍵を DNS から引く

署名する側の手順はこうです。

  1. 本文を整形(c= の正規化。空白の揺れで署名が壊れないようにする)してハッシュを取り、bh= に入れる
  2. h= に並べたヘッダ(From、Subject、Date……)と、b= を空にした DKIM-Signature ヘッダ自身をつないでハッシュを取る
  3. そのハッシュを秘密鍵で署名して b= に入れる

受信側は逆をたどります。本文のハッシュを取り直して bh= と比べ、ヘッダのハッシュを取り直して b= を公開鍵で検証する。では、検証に使う公開鍵はどこにあるのか。第4回の答えは「アリスから受け取る」でしたが、メールの世界は DNS に置きます。s= のセレクタ名と d= のドメインをつないだ場所です。

dig +short TXT default._domainkey.torigoedesign.com
# "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt0h/Dm6jPSc6Ci0eJjff..." "Y4HgUwooa2R/JaNAnV+CYQL8ljRnDb253jRu27I9Q3r..."

p= の後ろの塊が公開鍵です。本物かどうか、openssl で開いて確かめます。

dig +short TXT default._domainkey.torigoedesign.com | tr -d '" ' | sed 's/.*p=//' | base64 -d > dkim.der
openssl pkey -pubin -inform DER -in dkim.der -text -noout | head -3
# Public-Key: (2048 bit)
# Modulus:
#     00:b7:48:7f:0e:6e:a3:3d:27:3a:0a:2d:1e:26:37:

2048 ビットの RSA 公開鍵。第3回で genpkey から取り出した alice.pub と同じ種類の鍵が、DNS のレコードとして世界に公開されていました。受信サーバーはこれを引いて署名を検証し、「このメールは確かに torigoedesign.com のサーバーを通って出発し、途中で書き換えられていない」を確かめます。

DKIM は、配送に押す実印。
送信サーバーが署名 DKIM-Signature 署名 メール+署名 受信サーバー 署名 OK DNS default._domainkey p=MIIBIjANBgkqhkiG… (公開鍵=錠前) 公開鍵を引く 送信サーバーが署名 DKIM-Signature 署名 メール+署名 署名 OK 受信サーバー DNS default._domainkey p=MIIBIjANBgkqhkiG… (公開鍵=錠前) 公開鍵を引く

公開鍵の置き場が、アリスの手渡しではなく DNS。ドメインの持ち主しか DNS を触れない、という前提が保証人の代わり。

署名はメールそのものに付いているので、転送されても残ります。h= に含めたヘッダと本文が変わらない限り、転送先でも検証は通ります。逆に、メーリングリストが件名に [リスト名] を足したり本文の末尾にフッターを付けたりすると、ハッシュが合わなくなって落ちます。

もうひとつ、署名が証明しているのは「d= のドメインの鍵で署名された」ことまでです。d= が手紙の From: と同じドメインだとは限りません。配信サービスが自社ドメインの鍵で署名することもあります。この「名札と同じ名前か」を見るのが、次の DMARC の仕事です。

DMARC は、名札の照合と、落ちたときの方針書

dig +short TXT _dmarc.torigoedesign.com
# "v=DMARC1; p=none; rua=mailto:postmaster@torigoedesign.com; ruf=mailto:postmaster@torigoedesign.com; adkim=s; aspf=s; fo=1;"

From: の持ち主が、_dmarc に方針を置く

手紙の差出人(From:)のドメインの持ち主が、「SPF と DKIM の結果を私の名札と照らし合わせ、落ちたときはこう扱い、結果はここに報告してほしい」と宣言する方針書です。置き場所は _dmarc. の下と決まっています。SPF が封筒、DKIM が配送を見るのに対して、DMARC だけが人の見る名札を守っています。

名札の照合、合否、報告の3つを受け持つ

受信サーバーは、SPF と DKIM の結果が出そろってから DMARC に取りかかります。仕事は3つあります。

1つめは名札の照合です。SPF が pass したドメイン(封筒の差出人)が From: のドメインと同じか。DKIM が pass した署名の d= が From: のドメインと同じか。この「同じ」の判定を adkim= と aspf= で指定します。r(relaxed)なら組織ドメインが同じならよく、mail.torigoedesign.com の署名でも torigoedesign.com の名札に対して合格です。s(strict)なら完全一致が必要で、このドメインは両方 strict にしてあります。

2つめは合否です。SPF か DKIM のどちらか一方でも「pass かつ名札と同じ」なら、DMARC は合格です。両方揃っている必要はありません。片方でよい理由は後で出てきます。

3つめが、落ちたときの方針と報告です。

  • p= が方針の中心です。none は「落ちても配送は止めず、報告だけを送ってほしい」という監視モードです。quarantine なら迷惑メールへ、reject なら受信拒否です
  • sp= はサブドメイン用の方針、pct= は方針を適用する割合で、段階的に厳しくするときに使います
  • rua= は集計レポートの送り先です。誰がこのドメインを名乗って送信し、どれだけ通ってどれだけ落ちたかが、XML で(たいてい1日1通)届きます。誰かがこのドメインを騙って送信すると、その事実がここに現れます
  • ruf= は落ちた個別のメールについての失敗レポートの送り先で、fo=1 は「SPF か DKIM のどちらか一方でも落ちたら送ってほしい」という指定です(既定の fo=0 は両方落ちたときだけ)
DMARC は、落ちたときの方針書。
検品に落ちたメール DMARC(方針書) p=none p=quarantine p=reject 届ける。報告だけ送る 迷惑メールへ 受信拒否 検品に落ちたメール DMARC(方針書) p=none p=quarantine p=reject 届ける。 報告だけ送る 迷惑メールへ 受信拒否

名簿(SPF)、署名(DKIM)、方針書(DMARC)。3枚揃って「From は信用してよい」になる。

ここにも暗号はありません。SPF が名簿、DKIM が署名、DMARC が名札の照合と方針書。役割の違う3枚が揃って、はじめて「From の名乗りは信用してよい」が成り立ちます。冒頭の「フォームのメールが迷惑メール扱いされる」の調査が、この3つの dig から始まる理由でもあります。

3枚がかかる順番と、なりすましが落ちる場所

1通のメールが届くまでに、3枚の書類がどの順番でかかるのかを通しで見ます。

1通のメールが、検品ラインを上から順に通る。
実印(秘密鍵) 送信サーバー(Xserver)が署名して送る From: alice@torigoedesign.com 署名 名札(From)に、実印の署名が付いた 受信サーバーの検品ライン 1 名簿にある?(SPF) 名簿(SPF) v=spf1 include:… Xserver のサーバーなら OK 照合 送ってきたのは Xserver ✓ 名簿にある 2 実印は本物?(DKIM) 錠前(DKIM 公開鍵) default._domainkey p=MIIB…(錠前) 署名を開く 署名 署名が錠前で開いた ✓ 本物の実印 3 名札と同じ名前?(DMARC) From の torigoedesign.com と、①②で確かめた名前が同じか 方針書(DMARC) _dmarc p=reject 落ちたときの扱い 落ちたら見る From: torigoedesign.com ✓ 同じ名前 ①か②が通り、③で名前が同じなら受信箱へ。落ちたら方針書どおり。 実印(秘密鍵) 送信サーバー(Xserver)が署名して送る From: alice@torigoedesign.com 署名 名札(From)に、実印の署名が付いた 受信サーバーの検品ライン 1 名簿にある?(SPF) 名簿(SPF) v=spf1 include:… Xserver のサーバーなら OK 照合 送ってきたのは Xserver ✓ 名簿にある 2 実印は本物?(DKIM) 錠前(DKIM 公開鍵) default._domainkey p=MIIB…(錠前) 署名を開く 署名 署名が錠前で開いた ✓ 本物の実印 3 名札と同じ名前?(DMARC) From の torigoedesign.com と、 ①②で確かめた名前が同じか 方針書(DMARC) _dmarc p=reject 落ちたときの扱い 落ちたら見る From: torigoedesign.com ✓ 同じ名前 ①か②が通り、③で名前が同じなら 受信箱へ。落ちたら方針書どおり。

名簿(SPF)と実印(DKIM)はどちらかが通ればよい。最後に名札(From)と照らし合わせるのが DMARC の仕事。

この順番は、受信サーバーに情報が届く順です。封筒の差出人を名乗った時点で判定できる SPF は、本文を受け取る前に済みます。DKIM は本文まで受け取らないとハッシュが取れません。DMARC は両方の結果が出てから、名札と照らし合わせます。

SPF と DKIM が「どちらか一方でよい」設計になっているのは、転送のためです。メーリングリストや自動転送を経由すると、送信元のサーバーが変わるので SPF はほぼ確実に落ちます。一方、DKIM の署名はメールに付いたまま運ばれるので、中身が変わっていなければ転送先でも通ります。正規のメールが転送されただけで不合格になることを避けるために、片方が通れば合格とし、方針書の出番は両方落ちたときだけに限っています。

では、なりすましはどこで落ちるのか。

なりすましは、どこで落ちる?
知らないサーバーから 名札だけ alice を名乗る From: alice@torigoedesign.com 受信サーバー 1 ✕ 名簿にない(SPF) 2 ✕ 実印がない(DKIM) 3 ✕ 名札を証明できない(DMARC) p=reject → 受信拒否 知らないサーバーから 名札だけ alice を名乗る From: alice@torigoedesign.com 受信サーバー 1 ✕ 名簿にない(SPF) 2 ✕ 実印がない(DKIM) 3 ✕ 名札を証明できない(DMARC) p=reject → 受信拒否

名簿にも載っていない、実印も押せない。名札だけ本物に似せても、3つ目で落ちる。

知らないサーバーから送れば名簿にありませんし、秘密鍵を持っていないので本物の署名も作れません。名札だけ本物に似せても、それを裏付ける書類がひとつも通らないので、方針書どおりの扱いになります。p=reject なら受信拒否、p=none なら届いたうえで持ち主にレポートが飛びます。

自分のメールがどう判定されたかは、受け取った側で確かめられます。受信メールの「ソースを表示」(Gmail なら「元のメッセージを表示」)で、Authentication-Results: というヘッダを探してください。受信サーバーが検品の結果を書き残した行で、こういう形をしています。

Authentication-Results: mx.example.com;
        spf=pass smtp.mailfrom=torigoedesign.com;
        dkim=pass header.d=torigoedesign.com header.s=default;
        dmarc=pass header.from=torigoedesign.com

spf= の横にあるのが封筒の差出人のドメイン、dkim= の横が署名の d=、dmarc= の横が名札のドメインです。3つのドメインを見比べると、名札の照合がどう通ったのかまで読み取れます。

S/MIME は、本文そのものに署名する

DKIM が守るのは配送です。差出人個人が本文そのものを署名・暗号化する仕組み(S/MIME)も存在していて、openssl に専用コマンドがあります。第5回の証明書を流用して試せます。

printf '打ち合わせは 15:00 からに変更です。\n' > mail.txt
openssl cms -sign -in mail.txt -signer server.crt -inkey server.key -out mail.signed -text

head -4 mail.signed
# MIME-Version: 1.0
# Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg="sha-256"; boundary="----30E3234AA3157086B2A2C881CA85C503"
#
# This is an S/MIME signed message

openssl cms -verify -in mail.signed -CAfile server.crt
# Content-Type: text/plain
#
# 打ち合わせは 15:00 からに変更です。
# CMS Verification successful

署名付きメールの実体は、本文と署名を束ねた MIME 文書でした。ただし現実の世界は、個人が本文に署名する S/MIME より、サーバーが配送に署名する DKIM と、経路を暗号化する TLS(第6回)の組み合わせを選びました。設定が個人任せにならず、送る側の運用だけで完結するからです。

注意点

dig の結果が2つに割れていても壊れていない

DKIM の公開鍵が "..." "..." と2つの文字列に分かれて返ってきました。TXT レコード1文字列の上限が255バイトで、RSA 2048 の公開鍵は収まらないためです。読むときは連結します。さきほどのコマンドで tr -d '" ' を挟んでいたのはこの処理です。

p= が空のレコードは、鍵の失効

有名ドメインの DKIM レコードを覗くと、"v=DKIM1; k=rsa; p=" のように鍵が空のものが見つかります。壊れているのではなく、そのセレクタの鍵を失効させた印です。DKIM は鍵の交換(ローテーション)を前提にした設計で、古いセレクタは空にして捨てられます。

p=none は「守られていない」ではない

DMARC を reject にしないと無意味、という説明を見かけますが、いきなり reject にするのは危険です。転送やメーリングリストを経由した正規のメールは、SPF が落ちたうえに DKIM まで壊れることがあり、そうなると方針書どおりに捨てられます。レポート(rua)で実態を監視してから pct= で少しずつ強める、というのが設計どおりの運用です。none はその第一段階で、手抜きではありません。

封筒と手紙の差出人が違うのは、異常ではない

配信サービスやフォームのプラグインを使うと、封筒は業者のドメイン、手紙は自分のドメインという組み合わせが普通に起こります。このとき SPF は業者のドメインで pass しますが、名札と一致しないので DMARC では数えられません。頼みの綱は DKIM です。自分のドメインの鍵で署名してもらえるよう、サービス側の DKIM 設定(CNAME や TXT の追加)を済ませておくのが、届くメールを作る近道です。

自分のドメインの3点セットを見ておく

今日の3つの dig は、ドメイン名を差し替えればそのまま健康診断になります。フォームのメールが届かないときに見るべき場所が、SPF の名簿に送信サーバーが載っているか、DKIM の鍵が引けて名札と同じドメインか、DMARC の方針が厳しすぎないか、の3箇所に絞れます。

封筒と手紙、2つの差出人。名簿と、署名と、方針書。メールが届くかどうかは、暗号理論ではなくこの書類で決まっています。そしてその1枚に、本編で手作りした署名の仕組みが、DNS を名簿代わりにして埋め込まれていました。部品はどれも、もうあなたの手元で一度動いたものです。

本編は第0回からどうぞ。