電子メールは、ビジネスやプライベートで広く利用されていますが、その通信は様々なセキュリティリスクにさらされています。SMTP はもともと 1980 年代前半に設計されたプロトコルで、送信者が「誰であるか」を検証する仕組みを一切持ちません。封筒に好きな差出人を書けるのと同じで、MAIL FROM や From: ヘッダは送信側が自由に名乗れる自己申告に過ぎません。この構造的な欠陥を補うために、通信経路の暗号化・送信者認証・内容の完全性保護という異なるレイヤーで、様々な技術が積み重ねられてきました。本記事ではそれぞれの技術が具体的にどういう攻撃を防ぐために、どういう仕組みで動くのかを、実際に動く Python コードで検証しながら解説します。

1. 通信経路の暗号化
SMTP (Simple Mail Transfer Protocol) over TLS
SMTPはメール送信に使われるプロトコルですが、そのままでは通信内容が平文で送信されます。TLS (Transport Layer Security) を利用することで、SMTP通信を暗号化し、通信経路上の第三者による盗聴や改ざんを防ぐことができます。実際には STARTTLS コマンドで平文の SMTP セッションを TLS に昇格させる方式(opportunistic TLS)が主流です。
- 注意点: TLSによる暗号化は通信経路の保護に限定されます。メールが最終的に到達するメールサーバーに保存された段階では復号されているため、メールサーバー自体への不正アクセスには対応できません。
- STARTTLSストリッピング攻撃:
STARTTLSは「対応していれば使う」という日和見的(opportunistic)な仕組みのため、中間者攻撃者がSTARTTLSコマンドや応答を通信経路上で削除・改変してしまうと、双方が気づかないまま平文通信にダウングレードしてしまう危険があります。これに対処するため、MTA-STS (RFC 8461) は送信側が「相手ドメインは常に TLS 必須」というポリシーを HTTPS 経由で事前に取得・キャッシュする仕組みを、DANE for SMTP (RFC 7672) は DNSSEC で保護された TLSA レコードで正しい証明書を検証する仕組みを提供し、ダウングレードや中間者攻撃を防ぎます。
2. 送信者メールサーバーが送信者を認証
SMTP Auth (SMTP Authentication)
SMTP Authは、メール送信時に、送信者(ユーザー)が正当なユーザーであることをメールサーバーが認証する仕組みです。ユーザー名とパスワードを用いて認証を行い、不正なユーザーによるメールサーバーの利用を防ぎます。
- 認証方式: チャレンジレスポンス方式など、パスワードを直接送信しない安全な認証方式が用いられることが多いです。
- 前提: SMTP Auth の認証情報は平文で流れる方式も多いため、送信用ポート(587番、submission)では TLS 化(1. の STARTTLS)とセットで運用するのが必須です。TLS なしで認証情報だけ保護しても、経路上の盗聴でパスワードが漏洩します。
3. 受信側のメールサーバーが送信側のメールサーバーを認証
これらの技術は、送信元ドメインの詐称(なりすましメール)を防ぐことを目的としています。SMTP 自体には送信者の身元を検証する手段がないため、DNS という別の信頼できるチャネルを使って「このドメインを名乗るメールは、本当にこの経路・この鍵で送られたものか」を後付けで検証します。
SPF (Sender Policy Framework)
SPFは、メールの送信元IPアドレスが、そのドメインの正規の送信サーバーからのものであるかを検証する仕組みです。
- 仕組み: 送信側ドメインのDNSサーバーに、メール送信を許可するIPアドレスを記述したSPFレコード(
v=spf1から始まるDNS TXTレコード)を登録します。受信側のメールサーバーは、SMTP のMAIL FROM(envelope-from、Return-Path とも呼ばれる)に書かれたドメインのSPFレコードをDNSに問い合わせ、実際に接続してきたIPアドレスがそのレコードに記載された範囲と一致するかを確認します。 - なぜこれでなりすましを防げるか: 攻撃者がドメイン名を詐称して
MAIL FROM: attacker@example.comのようなメールを送っても、攻撃者は example.com の DNS を書き換える権限を持っていないため、自分の送信元IPを example.com の SPF レコードに登録できません。したがって受信側の照合は必ず不一致(fail/softfail)になり、正規の送信者だけがpassを得られます。これは「DNSの管理権限=ドメインの管理権限」という別の信頼基盤に、送信元認証を委譲していると言い換えられます。 - mechanism(記述要素):
ip4:/ip6:(IPレンジを直接列挙)、a/mx(当該ドメインのAレコード・MXレコードが指すIP)、include:(他ドメインのSPFレコードを再帰的に取り込む。委託しているメール配信サービスなどで使う)、all(残り全てにマッチする最終トークン)。 - qualifier(判定の強さ): 各mechanismの前に付く記号で結果の強さが変わります。
+(pass、省略時のデフォルト)、-(fail、拒否推奨のハードフェイル)、~(softfail、疑わしいが即座には拒否しないソフトフェイル)、?(neutral、判定なし)。特にallに付ける qualifier が重要で、-allは「ここに列挙した以外は拒否」という強いポリシー、~allは「一致しないものは疑わしいとマークするが受信は許容」という緩やかな移行期向けポリシーです。+allは事実上SPFを無効化してしまう危険な設定です。 - 限界1: 検証対象がenvelope-fromであり、表示上のFromヘッダではない: SPFが検証するのはSMTPプロトコルレベルの
MAIL FROMのドメインであり、メールクライアントが表示するFrom:ヘッダのドメインとは別物です。攻撃者は自分が正規にSPFをpassできる別ドメインを envelope-from に使いつつ、表示上のFrom:だけを詐称できてしまいます(“friendly-from spoofing”)。この隙間を埋めるのが後述のDMARCです。 - 限界2: メール転送で壊れる: 受信者がメールを別アドレスに自動転送すると、転送サーバーのIPは元のSPFレコードに登録されていないため、転送先での再検証は
fail/softfailになってしまいます。SMTPの構造上、転送はメールの送信元IPを転送サーバーに置き換えてしまうため、これはSPFの設計そのものに起因する限界です(後述のPython検証で再現します)。 - 限界3: DNSルックアップ回数の上限:
include:の再帰的な展開などでDNS問い合わせが10回を超えると、SPF検証はpermerrorとなり評価不能になります(RFC 7208)。委託先サービスを増やしすぎるとこの上限に抵触することがあります。
DKIM (DomainKeys Identified Mail)
DKIMは、メールにデジタル署名を付与することで、メールの送信元ドメインが正当であることと、メールの内容が改ざんされていないことを検証する仕組みです。これは デジタル署名の一般原理 のメール応用そのもので、秘密鍵で作った署名を公開鍵で誰でも検証できるという非対称性を使います。
- 仕組み: 送信側は、署名対象に選んだヘッダー群と本文をハッシュ化し、自身の秘密鍵で署名して
DKIM-Signatureヘッダーとしてメールに付与します。送信側ドメインのDNSサーバーには、セレクタ._domainkey.ドメイン名というTXTレコードとして対応する公開鍵が登録されています(セレクタはキーローテーションのための識別子で、同じドメインが複数の鍵を並行運用できます)。受信側のメールサーバーは、DKIM-Signatureヘッダーのd=(署名ドメイン)とs=(セレクタ)からDNSの公開鍵を取得し、署名を検証します。 - 署名対象の選択(h= タグ)が改ざん耐性を左右する:
DKIM-Signatureヘッダーのh=タグには、署名対象に含めるヘッダー名(from,to,subject,dateなど)を列挙します。ここに列挙されていないヘッダーは署名の保護範囲外であり、署名を無効化せずに書き換えられてしまいます。たとえばh=from:to:dateのようにsubjectを含めない設定にすると、署名自体は正当なままSubject:だけを「至急支払え」のような文言に改ざんできてしまいます。逆に本文はbh=(body hash)で常にハッシュが取られるため、本文の改ざんはh=の設定に関わらず必ず検知されます。この非対称性は後述のPython実験で実際に確認します。 - canonicalization(正規化): メール転送の過程で改行コードや末尾空白が変わることがあるため、DKIMは署名前に
simple(変更にほぼ非寛容)とrelaxed(空白の畳み込みなど軽微な変更を許容)という2種類の正規化方式を用意し、転送によるフォーマットの揺れで署名検証が壊れないようにしています。
DMARC (Domain-based Message Authentication, Reporting & Conformance)
SPFとDKIMには共通の弱点があります。どちらも「検証しているドメイン」と「人間が実際に見るFromヘッダーのドメイン」が一致している保証がないという点です。SPFはenvelope-fromのドメインを、DKIMは d= タグのドメインを検証しますが、この2つは表示上の From: ヘッダーとは別のフィールドです。攻撃者が自分の管理する evil-mailer.example というドメインで正規にSPF・DKIMをpassさせつつ、表示上の From: だけ有名企業のドメインに詐称すれば、SPF・DKIMは個別には「正当」と判定してしまいます。
DMARCはこの隙間を埋めるために、アラインメント (alignment) という追加のチェックを導入します。
- アラインメントの判定: DMARCは、SPFで検証されたドメイン(envelope-fromのドメイン)およびDKIMで検証されたドメイン(
d=タグのドメイン)を、それぞれFrom:ヘッダーのドメインと突き合わせます。一致モードには2種類あり、strictは完全一致を要求し、relaxed(デフォルト)は組織ドメイン(Organizational Domain)が一致すればサブドメインの違いを許容します。 - DMARC pass の条件: 「SPFがpassし、かつFromヘッダーとアラインメントする」または「DKIMがpassし、かつFromヘッダーとアラインメントする」のどちらか一方が成立すればDMARCはpassします(AND ではなく OR)。つまりSPFとDKIMが個別にpassしていても、どちらもアラインメントしていなければDMARCはfailになります。これが前述の「friendly-from spoofing」を検知できる核心的な仕組みです。
- ポリシー: ドメイン管理者はDNSにDMARCレコード(
_dmarc.ドメイン名のTXTレコード)を公開し、p=none(監視のみ、何もしない)、p=quarantine(迷惑メール扱い)、p=reject(拒否)というポリシーと、適用割合を指定するpct=を宣言できます。多くの組織はまずp=noneでレポート(rua=で指定する集計レポート)を収集し、正規の送信経路をすべて洗い出してから段階的にquarantine→rejectへ引き上げる運用を取ります。
以下の図は、正規メールとfriendly-from spoofingそれぞれについて、SPF/DKIMの個別判定とDMARCアラインメント判定がどう分岐するかを示したものです。

Python実行検証: SPF照合ロジック
SPFレコードをパースし、送信元IPとの照合を実装して、正規送信・委託先送信・詐称・転送によるSPF破綻の4パターンを確認します。
import ipaddress
# --- 簡易 SPF レコードパーサ・照合ロジック ---
# 実際の SPF (RFC 7208) は ip4/ip6/a/mx/include/exists など多数の mechanism と
# +/-/~/? の qualifier を持つが、ここでは実務で最頻出の ip4 / include / all を実装する。
QUALIFIER_RESULT = {
"+": "pass", # 明示的 pass(省略時のデフォルトも + 扱い)
"-": "fail", # ハードフェイル: 拒否推奨
"~": "softfail", # ソフトフェイル: 疑わしいが受理されうる(弱い失敗)
"?": "neutral", # 判定なし(許可も否定もしない)
}
def parse_spf(record, resolver):
"""SPF レコード文字列をトークンに分解する。
resolver: include: で参照するドメイン名 -> SPF レコード文字列 の辞書(DNS 問い合わせの代替)
"""
assert record.startswith("v=spf1"), "SPF レコードは v=spf1 から始まる必要がある"
tokens = record.split()[1:]
return tokens
def check_spf(record, source_ip, resolver, _depth=0):
"""送信元 IP が SPF レコードに一致するかを判定し、qualifier に応じた結果文字列を返す。
include: は再帰的に解決する(無限ループ防止のため深さ制限を設ける)。
"""
if _depth > 10:
return "permerror" # ループ・過度な再帰は RFC 7208 でも permerror 扱い
ip = ipaddress.ip_address(source_ip)
tokens = parse_spf(record, resolver)
for tok in tokens:
qualifier = "+"
if tok[0] in "+-~?":
qualifier, tok = tok[0], tok[1:]
if tok == "all":
return QUALIFIER_RESULT[qualifier]
if tok.startswith("ip4:") or tok.startswith("ip6:"):
network = tok.split(":", 1)[1]
if ip in ipaddress.ip_network(network, strict=False):
return QUALIFIER_RESULT[qualifier]
elif tok.startswith("include:"):
included_domain = tok.split(":", 1)[1]
included_record = resolver.get(included_domain)
if included_record is None:
continue # 解決できない include は無視して次のトークンへ
sub_result = check_spf(included_record, source_ip, resolver, _depth + 1)
# include は「その中で all にヒットして pass したら」初めて一致とみなす
if sub_result == "pass":
return QUALIFIER_RESULT[qualifier]
# sub_result が fail/softfail/neutral の場合は include 自体は不一致として次へ
return "neutral" # どの mechanism にも一致せず all も無い場合(RFC 上は neutral)
# --- シナリオ設定 ---
# example.com の SPF レコード: 自社IPレンジ + 委託しているメール配信サービス(_spf.mailer.example) を許可
resolver = {
"_spf.mailer.example": "v=spf1 ip4:198.51.100.0/24 -all",
}
spf_record_example_com = "v=spf1 ip4:203.0.113.0/24 include:_spf.mailer.example ~all"
test_cases = [
("203.0.113.10", "自社メールサーバー(正規IP)"),
("198.51.100.5", "委託先メール配信サービス(正規IP・includeで許可)"),
("192.0.2.99", "スパム業者が詐称して送信(許可外IP)"),
]
print("SPFレコード:", spf_record_example_com)
print()
for ip, label in test_cases:
result = check_spf(spf_record_example_com, ip, resolver)
print(f"送信元IP {ip:15s} ({label:35s}) -> SPF結果: {result}")
print()
print("--- 転送メールでSPFが壊れるケースのシミュレーション ---")
direct_ip = "203.0.113.10"
print(
f"直接受信時 (接続元={direct_ip}): "
f"{check_spf(spf_record_example_com, direct_ip, resolver)}"
)
forwarder_ip = "192.0.2.55" # 第三者の転送サーバーのIP(SPFレコードには未登録)
print(
f"転送経由受信時 (接続元={forwarder_ip}): "
f"{check_spf(spf_record_example_com, forwarder_ip, resolver)}"
)
実行結果:
SPFレコード: v=spf1 ip4:203.0.113.0/24 include:_spf.mailer.example ~all
送信元IP 203.0.113.10 (自社メールサーバー(正規IP) ) -> SPF結果: pass
送信元IP 198.51.100.5 (委託先メール配信サービス(正規IP・includeで許可) ) -> SPF結果: pass
送信元IP 192.0.2.99 (スパム業者が詐称して送信(許可外IP) ) -> SPF結果: softfail
--- 転送メールでSPFが壊れるケースのシミュレーション ---
直接受信時 (接続元=203.0.113.10): pass
転送経由受信時 (接続元=192.0.2.55): softfail
正規の自社IP・委託先IPはいずれも pass になり、許可外IPからの詐称は ~all に従って softfail(拒否ではなく警告扱い)となりました。さらに転送シミュレーションでは、同じ正規メールでも接続元IPが変わるだけでSPFの結果が pass から softfail に転落することが確認できます。これがSPF単体の構造的な限界であり、実運用ではDKIM(本文・ヘッダーに対する署名なので転送されても壊れにくい)と併用することが推奨される理由です。
Python実行検証: DKIM署名と改ざん検知(h=タグの効果)
RSA署名でメールヘッダーと本文のハッシュを署名し、h= タグに含めるヘッダーの違いで改ざん耐性がどう変わるかを検証します。
import hashlib
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
# --- DKIM の簡易実装: RFC 6376 の hash-and-sign を再現する ---
# 実際の DKIM は "rsa-sha256" (RSA + PKCS#1 v1.5 padding) を使い、
# DKIM-Signature ヘッダの h= タグに列挙されたヘッダだけを署名対象にする。
# ここでは canonicalize を簡略化し、本質である「どのヘッダを署名対象にするか」の効果を再現する。
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
def canonicalize(headers: dict, signed_header_names: list[str], body: str) -> bytes:
"""署名対象ヘッダ(h=で指定されたものだけ)と本文ハッシュを連結した署名対象文字列を作る。"""
body_hash = hashlib.sha256(body.encode()).hexdigest()
parts = [f"{name}:{headers[name]}" for name in signed_header_names]
parts.append(f"body-hash:{body_hash}")
return "\n".join(parts).encode()
def dkim_sign(headers: dict, signed_header_names: list[str], body: str) -> bytes:
data = canonicalize(headers, signed_header_names, body)
return private_key.sign(data, padding.PKCS1v15(), hashes.SHA256())
def dkim_verify(headers: dict, signed_header_names: list[str], body: str, signature: bytes) -> bool:
data = canonicalize(headers, signed_header_names, body)
try:
public_key.verify(signature, data, padding.PKCS1v15(), hashes.SHA256())
return True
except Exception:
return False
# --- 元のメール ---
original_headers = {
"from": "billing@example.com",
"to": "user@example.net",
"subject": "Your invoice #1024",
"date": "Fri, 17 Jul 2026 09:00:00 +0900",
}
original_body = "Please find attached your invoice for July. Total: $50."
print("=== ケースA: h= に From/To/Date のみを含め、Subject を含めない場合 ===")
h_narrow = ["from", "to", "date"] # subject を意図的に含めない (弱い設定)
sig_narrow = dkim_sign(original_headers, h_narrow, original_body)
print("署名直後の検証:", dkim_verify(original_headers, h_narrow, original_body, sig_narrow))
tampered_headers = dict(original_headers)
tampered_headers["subject"] = "URGENT: Your invoice #1024 - pay immediately via gift card"
print(
"Subjectを書き換えた後の検証 (署名対象外のため検知できない):",
dkim_verify(tampered_headers, h_narrow, original_body, sig_narrow),
)
print()
print("=== ケースB: h= に Subject も含める場合(推奨設定)===")
h_wide = ["from", "to", "subject", "date"]
sig_wide = dkim_sign(original_headers, h_wide, original_body)
print("署名直後の検証:", dkim_verify(original_headers, h_wide, original_body, sig_wide))
print(
"Subjectを書き換えた後の検証 (署名対象に含むため改ざん検知できる):",
dkim_verify(tampered_headers, h_wide, original_body, sig_wide),
)
print()
print("=== 参考: 本文改ざんの検知(body-hashは常に対象) ===")
tampered_body = original_body.replace("$50", "$50000")
print(
"本文を書き換えた後の検証 (ケースAの署名でも本文ハッシュ不一致で棄却):",
dkim_verify(original_headers, h_narrow, tampered_body, sig_narrow),
)
実行結果:
=== ケースA: h= に From/To/Date のみを含め、Subject を含めない場合 ===
署名直後の検証: True
Subjectを書き換えた後の検証 (署名対象外のため検知できない): True
=== ケースB: h= に Subject も含める場合(推奨設定)===
署名直後の検証: True
Subjectを書き換えた後の検証 (署名対象に含むため改ざん検知できる): False
=== 参考: 本文改ざんの検知(body-hashは常に対象) ===
本文を書き換えた後の検証 (ケースAの署名でも本文ハッシュ不一致で棄却): False
ケースAでは、Subject を署名対象に含めなかったため、件名を「至急ギフトカードで支払え」という詐欺文言に書き換えても署名検証は True のままでした。これは実際のフィッシング詐欺で悪用されうる典型的な設定ミスです。ケースBのように Subject を h= に含めれば、同じ改ざんが確実に検知(False)されます。一方で本文の改ざんは、h= の設定によらず body-hash の不一致で常に検知されることも確認できました。実務でDKIMを設定する際は、From / Subject / Date / To など、なりすまし・改ざんの実害に直結するヘッダーを漏れなく h= に含めることが重要です。
Python実行検証: DMARCアラインメント判定
Fromドメイン・SPFドメイン・DKIMドメインの一致確認ロジックを実装し、正規メール・relaxedアラインメントが効くケース・friendly-from spoofingの3パターンで判定します。
def registrable_domain(domain: str) -> str:
"""relaxed alignment 用の簡易 Organizational Domain 抽出(本来は Public Suffix List を使うべき)。
ここでは 'a.b.example.com' -> 'example.com' のように末尾2ラベルを返す簡略版とする。
"""
labels = domain.split(".")
return ".".join(labels[-2:]) if len(labels) >= 2 else domain
def is_aligned(from_domain: str, auth_domain: str, mode: str) -> bool:
"""DMARC アラインメント判定。
strict: From ドメインと認証ドメインが完全一致する必要がある。
relaxed: Organizational Domain (登録ドメイン) が一致すればよい(サブドメインの差を許容)。
"""
if mode == "strict":
return from_domain == auth_domain
return registrable_domain(from_domain) == registrable_domain(auth_domain)
def dmarc_evaluate(from_domain, spf_domain, spf_result, dkim_domain, dkim_result, mode="relaxed"):
"""DMARC 判定: SPFまたはDKIMの少なくとも一方が (1)検証結果pass かつ (2)Fromドメインとアラインメント
していれば DMARC pass。RFC 7489 の '少なくとも一方で十分' というOR構造をそのまま実装する。
"""
spf_pass_aligned = (spf_result == "pass") and is_aligned(from_domain, spf_domain, mode)
dkim_pass_aligned = (dkim_result == "pass") and is_aligned(from_domain, dkim_domain, mode)
dmarc_pass = spf_pass_aligned or dkim_pass_aligned
return {
"spf_aligned": spf_pass_aligned,
"dkim_aligned": dkim_pass_aligned,
"dmarc_result": "pass" if dmarc_pass else "fail",
}
scenarios = [
dict(
label="正規メール: 自社ドメインから自社経由で送信",
from_domain="example.com",
spf_domain="example.com", # envelope-from (Return-Path) のドメイン
spf_result="pass",
dkim_domain="example.com", # DKIM-Signature の d= タグ
dkim_result="pass",
),
dict(
label="正規メール: メルマガ配信サービス経由(サブドメインでrelaxed許容)",
from_domain="news.example.com",
spf_domain="bounce.mailer-service.example", # 配信代行の envelope-from(別ドメイン)
spf_result="pass",
dkim_domain="example.com", # DKIM は委託元ドメインの鍵で署名(アラインメントする)
dkim_result="pass",
),
dict(
label="なりすまし: friendly-from spoofing(攻撃者自身のドメインではSPF/DKIMとも正規にpassする)",
from_domain="example.com", # 表示上のFromは詐称された正規ドメイン
spf_domain="evil-mailer.attacker-domain.com", # 実際の送信元(envelope-from)は攻撃者ドメイン
spf_result="pass", # 攻撃者ドメイン自身のSPFとしては正当にpass
dkim_domain="evil-mailer.attacker-domain.com",
dkim_result="pass", # 攻撃者ドメイン自身の鍵で正当に署名 = DKIMもpass
),
]
for sc in scenarios:
label = sc.pop("label")
result = dmarc_evaluate(**sc)
print(f"[{label}]")
print(f" From: {sc['from_domain']} / SPFドメイン: {sc['spf_domain']} (SPF={sc['spf_result']})"
f" / DKIMドメイン: {sc['dkim_domain']} (DKIM={sc['dkim_result']})")
print(f" -> SPFアラインメント: {result['spf_aligned']}, DKIMアラインメント: {result['dkim_aligned']}"
f", DMARC判定: {result['dmarc_result']}")
print()
実行結果:
[正規メール: 自社ドメインから自社経由で送信]
From: example.com / SPFドメイン: example.com (SPF=pass) / DKIMドメイン: example.com (DKIM=pass)
-> SPFアラインメント: True, DKIMアラインメント: True, DMARC判定: pass
[正規メール: メルマガ配信サービス経由(サブドメインでrelaxed許容)]
From: news.example.com / SPFドメイン: bounce.mailer-service.example (SPF=pass) / DKIMドメイン: example.com (DKIM=pass)
-> SPFアラインメント: False, DKIMアラインメント: True, DMARC判定: pass
[なりすまし: friendly-from spoofing(攻撃者自身のドメインではSPF/DKIMとも正規にpassする)]
From: example.com / SPFドメイン: evil-mailer.attacker-domain.com (SPF=pass) / DKIMドメイン: evil-mailer.attacker-domain.com (DKIM=pass)
-> SPFアラインメント: False, DKIMアラインメント: False, DMARC判定: fail
2番目のシナリオでは、SPFのドメイン(配信代行会社のバウンスドメイン)はFromドメイン(news.example.com)と完全一致しないため strict では不一致ですが、DKIMのドメイン(委託元が自社の鍵で署名しているため example.com)は組織ドメインが一致するため relaxed アラインメントが成立し、DMARCはpassします。これはメルマガ配信サービスなど第三者委託がある正規の運用でDMARCが誤ってfailしないための現実的な設計です。一方で3番目のfriendly-from spoofingでは、SPF・DKIMとも攻撃者自身のドメインとしては正当にpassしているにもかかわらず、Fromヘッダーとのアラインメントが取れないためDMARC判定はfailになります。これがSPF・DKIM単体では防げなかったなりすましを、DMARCが追加でブロックできる理由です。
4. 受信者が送信者を認証・メール内容を保護
これらの技術は、メールの送信者認証に加え、メールの内容の機密性や完全性を保護することを目的としています。SPF/DKIM/DMARCが「メール配送の途中経路」を保護するのに対し、こちらはメールの内容そのものをエンドツーエンドで保護する点が異なります。
S/MIME (Secure / Multipurpose Internet Mail Extensions)
S/MIMEは、公開鍵暗号方式を用いてメールのデジタル署名と暗号化を行う標準的な技術です。
- デジタル署名: 送信者の身元を証明し、メールの改ざんを検知できます。
- 暗号化: 受信者の公開鍵でメール本文を暗号化し、対応する秘密鍵を持つ受信者以外には読めないようにします(実際には本文をAESなどの共通鍵で暗号化し、その共通鍵だけをRSA/ECDHで受信者の公開鍵によって包む、ハイブリッド暗号方式が使われます)。
S/MIMEを利用するには、信頼できる認証局(CA)が発行したデジタル証明書(X.509証明書)が必要です。
- 証明書チェーンによる信頼の検証: X.509証明書は「エンドエンティティ証明書(利用者の公開鍵とメールアドレスを結びつける)→ 中間CA証明書 → ルートCA証明書」という階層(チェーン)を成します。受信側のメールクライアントは、署名に添付された証明書から親の証明書へと署名検証をたどり、最終的にOSやクライアントに事前installされた信頼済みルート証明書にたどり着けるかを確認します。途中の証明書のどれか1つでも検証に失敗すれば、チェーン全体が信頼できないと判断されます。加えて、証明書の有効期限や失効状態(CRL・OCSPによる失効確認)もチェックする必要があります。ルートCAという単一の頂点に信頼を集約する、中央集権的な信頼モデルです。
- 限界: メール本文は暗号化されても、
Subject:やTo:/From:などのヘッダー(メタデータ)は通常平文のまま配送されるため、誰が誰にいつメールを送ったかという通信の存在自体は秘匿できません。また、正規のCAから証明書を取得・更新し続ける運用コストもPKI共通の課題です。
PGP (Pretty Good Privacy) / OpenPGP
PGPは、S/MIMEと同様に、公開鍵暗号方式を用いてメールのデジタル署名と暗号化を行うソフトウェア/規格(OpenPGP, RFC 4880)です。
- Web of Trust vs PKI: S/MIMEが認証局(CA)を頂点とする階層的な信頼モデル(PKI)を採用するのに対し、PGPは「信頼の輪(Web of Trust)」という分散型の信頼モデルを採用しています。中央集権的な認証局を置かず、ユーザー同士が互いの公開鍵に署名し合うことで信頼のネットワークを築きます。ある鍵の正当性は、「自分が直接検証した鍵」または「自分が信頼する人が署名した鍵」をたどることで判断します。これはCAという単一障害点・単一の信頼対象を必要としない代わりに、鍵の正当性の判断を分散した個々のユーザーの社会的な検証行為(key-signing partyなど)に委ねる設計思想であり、初期のセットアップや鍵の信頼確立に運用上の手間がかかるという実務上の課題も指摘されてきました。
- 利用: 送信者と受信者が事前に公開鍵を交換しておく必要があります。
- 実装上の注意(EFAIL, 2018): 2018年に公表された EFAIL は、PGPおよびS/MIMEの多くのメールクライアント実装を対象にした脆弱性で、暗号化に使われるブロック暗号モード(CBC/CFBなど)の展性(マレアビリティ)を悪用し、HTMLメールとして暗号文の一部を書き換えることで、メールクライアントが自動的にレンダリングする外部リソース読み込み(画像タグなど)を通じて復号後の平文をバックチャネルで攻撃者に送信させる、という攻撃でした。これはPGP/S/MIMEというプロトコル自体の暗号設計の欠陥というより実装側での完全性検証の不備が原因であり、対策として本文の暗号化と併せてMDC(Modification Detection Code)や認証付き暗号(AEAD)の使用、HTMLメールでの外部リソース読み込みの無効化が重要であることが再確認されました。
近年の動向(2023年以降)
BIMI (Brand Indicators for Message Identification)
BIMIは、DMARCで十分な保護レベル(多くの場合 p=quarantine または p=reject の強制適用)を達成しているドメインに対し、受信箱でブランドロゴを表示できるようにする仕組みです。ロゴの商標所有権を証明する VMC (Verified Mark Certificate) を取得するのが従来の主流でしたが、2025年にはGoogleが商標登録を必須としない CMC (Common Mark Certificate)(1年以上のロゴ使用実績があれば取得可能)の採用を発表するなど、参入障壁を下げる動きが進んでいます。調査によれば上位1,000万ドメインのうちBIMIレコードを公開しているドメインは2023年6月時点で14,305件、2024年6月時点で21,222件に増加した一方、依然として9割超のドメインはBIMIレコード自体を公開していないという報告もあり、DMARCのエンドースメントとしての普及はまだ発展途上です。
AI/機械学習を使ったフィッシング検知
2024年以降、大規模言語モデル (LLM) を使ったフィッシングメール検知の研究が活発化しています。LLMはメール本文の意味内容を理解できるため、キーワードやシグネチャベースの検知をすり抜けるように巧妙に書き換えられた文面にも頑健であるとされ、LLMと従来の機械学習モデルを組み合わせるハイブリッド手法や、検索拡張生成(RAG)を用いた個別化検知の研究が報告されています。一方で、LLMを単体の分類器として使うと正規メールを誤ってフィッシングと判定する誤検知率が高くなるという課題も指摘されており、まだ実務での標準的な解法が確立した段階ではありません。加えて、同じ生成AIの能力がより自然で説得力のあるフィッシングメールの自動生成にも使われうるという「攻撃・防御双方でのAI利用」という新しい構図も、この分野の重要な論点になっています。
これらの技術を組み合わせることで、メール通信のセキュリティを多層的に強化し、フィッシング詐欺やスパム、情報漏洩などのリスクを低減することができます。
まとめ
- SMTPには送信者を検証する仕組みが元々なく、通信経路の暗号化(TLS/STARTTLS)・送信者認証(SPF/DKIM/DMARC)・内容保護(S/MIME/PGP)という異なるレイヤーで防御を積み重ねる必要がある。
- SPFはenvelope-fromドメインのDNSレコードと接続元IPを照合する仕組みで、DNS管理権限という別の信頼基盤に依拠する。転送で壊れる、表示上のFromヘッダーを検証しないという限界がある。
- DKIMは秘密鍵署名・公開鍵検証というデジタル署名の一般原理をメールに適用したもので、
h=タグで選んだヘッダーだけが改ざん検知の対象になる(実験で確認済み)。 - DMARCはSPF/DKIMが個別に検証するドメインと表示上のFromヘッダーとのアラインメントを追加でチェックすることで、SPF/DKIMが単体では防げないfriendly-from spoofingを検知できる(実験で確認済み)。
- S/MIMEはCAを頂点とするPKIの証明書チェーンで、PGPは分散的なWeb of Trustで、それぞれ異なる信頼モデルによりエンドツーエンドの署名・暗号化を実現する。実装不備(EFAIL)にも注意が必要。
- 近年はBIMIによるブランド表示の高度化と、LLMを使ったフィッシング検知の研究が進む一方、AI自体が攻撃・防御双方に使われる新しい局面に入っている。
関連記事
- デジタル署名(ECDSA / EdDSA / RSA-PSS)の理論と Python 実装 - DKIMが使う「秘密鍵で署名・公開鍵で検証」というデジタル署名の一般原理と安全性の考え方を詳解しています。
- ゼロトラストセキュリティの概要と導入 - メール認証をゼロトラストの構成要素の一つとして位置づけた文脈を解説しています。
- OAuth 2.0/OIDC 入門 - メールとは別のなりすまし対策として、Webの認証・認可の仕組みを解説しています。
関連ツール
- Base64エンコード/デコード(DevToolBox) - 文字列のBase64エンコード・デコード
- ハッシュ生成ツール(DevToolBox) - DKIMの本文ハッシュ計算などに使われるSHA-256等のハッシュ生成
参考文献
- 2023 情報処理安全確保支援士「専門知識+午後問題」の重点対策」(Amazon.co.jp)
- Wong, M., & Schlitt, W. (2014). “Sender Policy Framework (SPF) for Authorizing Use of Domains in Email”. RFC 7208.
- Crocker, D., Hansen, T., & Kucherawy, M. (2011). “DomainKeys Identified Mail (DKIM) Signatures”. RFC 6376.
- Kucherawy, M., & Zwicky, E. (2015). “Domain-based Message Authentication, Reporting, and Conformance (DMARC)”. RFC 7489.
- Margolis, D., et al. (2018). “SMTP MTA Strict Transport Security (MTA-STS)”. RFC 8461.
- Dukhovni, V., & Hardaker, W. (2015). “SMTP Security via Opportunistic DNS-Based Authentication of Named Entities (DANE)”. RFC 7672.
- Ramsdell, B., & Turner, S. (2010). “Secure/Multipurpose Internet Mail Extensions (S/MIME) Version 3.2 Message Specification”. RFC 5751.
- Callas, J., et al. (2007). “OpenPGP Message Format”. RFC 4880.
- Poddebniak, D., et al. (2018). “Efail: Breaking S/MIME and OpenPGP Email Encryption using Exfiltration Channels”. USENIX Security Symposium 2018.
- BIMI Group. “Brand Indicators for Message Identification — Supporting Documents”. bimigroup.org.
- SPFとDKIMとDMARCとは?メール認証技術の仕組みを解説(SendGrid Blog)
- S/MIMEとPGPとは?メールの暗号化と署名について解説(SendGrid Blog)