メールの送信者認証と添付ファイル検査
送信元の認証、内容検査、配送後の処理をそれぞれの根拠で評価します。
説明
メールの表示名、Fromのアドレス、SMTPのMAIL FROM、DKIM署名ドメインは役割が異なります。認証に成功しても、本文や添付の内容が安全だという保証にはなりません。
固定メールのヘッダーと添付を開き、送信IPの範囲、DKIMの本文ハッシュと署名、Fromとの対応を確認します。本文や件名を変更したときの検証結果も比較します。
送信元の認証、内容検査、配送後の処理をそれぞれの根拠で評価します。
1表示名とアドレスを分ける
公開試験鍵で署名した固定メールを開きます。
from sp09 import *
raw,pub,signed,sig=make_message()
m=BytesParser(policy=email.policy.default).parsebytes(raw)
print(parseaddr表示名とアドレスを分ける(str(m['From'])))
print(m['Subject'])表示名はSupport Desk、アドレスはnotice@sender.example.testです。表示名だけでは送信元の管理主体を確認できません。
2MAIL FROMのドメインに対するIP条件を調べる
固定のSMTP記録はbounce@vendor.example.test、接続元は192.0.2.7です。SPF試料はip4と-allだけの条件です。
envelope='bounce@vendor.example.test'
spf='v=spf1 ip4:192.0.2.0/24 -all'
print(envelope,spf)
print(ipaddress.ip_addressMAIL FROMのドメインに対するIP条件を調べる('192.0.2.7') in ipaddress.ip_network('192.0.2.0/24'))範囲には含まれますが、評価するドメインはvendor.example.testです。Fromのsender.example.testをこの結果だけで認証したことにはなりません。
3DKIMの署名対象と本文ハッシュを読む
長い署名値を除き、主要タグを表示します。
tagsDKIMの署名対象と本文ハッシュを読む=dict(x.strip().split('=',1) for x in str(m['DKIM-Signature']).split(';'))
print({k:tags[k] for k in ('a','c','d','s','h','bh')})署名はrsa-sha256、正規化はsimple/simple、署名ヘッダーはfromとsubjectです。公開鍵は通常lab._domainkey.sender.example.testから取得する形になります。
4本文ハッシュと公開鍵署名を検証する
固定試料の完全な検査を行います。
body=raw.split(b'\r\n\r\n',1)[1]
print(base64.b64encode(hashlib.sha256(simple_body(body)).digest()).decode()==tags['bh'])
print(verify本文ハッシュと公開鍵署名を検証する(raw,pub))本文ハッシュが一致し、署名もverifiedです。教材の検証関数はこの固定試料のsimple/simpleとヘッダー構成を対象にしています。
5本文と件名の変更を別々に検査する
本文URLと署名対象の件名を変えます。
print(verify本文と件名の変更を別々に検査する(raw.replace(b'docs.example.test',b'other.example.test'),pub))
print(verify(raw.replace(b'Subject: Public report',b'Subject: Changed report'),pub))本文変更はbody_hash_mismatch、件名変更はsignature_mismatchです。検証失敗の箇所を分けて確認できます。
6Fromと認証ドメインの一致を確認する
この例は厳密なドメイン一致で比較します。
from_domainFromと認証ドメインの一致を確認する=parseaddr(str(m['From']))[1].split('@')[1]
print('SPF aligned',envelope.split('@')[1]==from_domain)
print('DKIM aligned',tags['d']==from_domain and verify(raw,pub)=='verified')SPF側は不一致、DKIM側は一致して検証成功です。この例ではDMARCの整合したDKIM経路を満たします。緩和整合では組織ドメインを使うため、この単純な文字列比較とは条件が異なります。
7添付ファイルを復号して内容を取得する
MIMEの転送符号化を外して添付のバイト列を表示します。
for part in m.iter_attachments():
data=part.get_payload添付ファイルを復号して内容を取得する(decode=True)
print(part.get_filename(),part.get_content_type(),len(data),data)
print(hashlib.sha256(data).hexdigest())report.txtの実体はpublic reportの文字列です。署名の成功だけで添付の挙動を評価したことにはなりません。動的検査には、対象ハッシュ、実行環境、観測時間と動作記録が必要です。
8動的解析の報告と添付の実体を照合する
固定の解析報告を、先ほど取り出した添付ハッシュと照合します。
report = {"sha256":hashlib.sha256(data).hexdigest(),"environment":"viewer-disabled","observed_seconds観測時間と実行環境を確認する":30,"result":"no_behavior"}
print(report)
print(report["sha256"]==hashlib.sha256(data).hexdigest())
print(report["sha256"]==hashlib.sha256(data+b"changed").hexdigest())元の添付とは一致し、変更したコピーとは一致しません。no_behaviorは30秒の観測で動作が記録されなかったという固定報告です。viewer-disabledという条件もあるため、ファイルを通常どおり開いて安全性を確認した結果には読み替えません。
9認証結果ヘッダーの出所を確認する
メール自体に受信側の認証結果が付いているか調べます。
print(m.get_all認証結果ヘッダーの出所を確認する('Authentication-Results',[]))空です。今回の結果は自分で行った署名照合です。Authentication-Resultsが付いていても、信頼する受信側が付与した値かを確認し、外部から持ち込まれた自己申告をそのまま使いません。
まとめ
表示名、MAIL FROM、From、署名ドメインを分けて確認します。
署名と本文ハッシュの失敗箇所を照合します。
送信元認証と添付の内容検査は別の判断です。
操作履歴と解説を振り返れます。
Python 3.15 / 公開試験鍵によるDKIM試料 / SQLite
from sp09 import *
raw,pub,signed,sig=make_message()
m=BytesParser(policy=email.policy.default).parsebytes(raw)
print(parseaddr(str(m['From'])))
print(m['Subject'])('Support Desk', 'notice@sender.example.test')
Public reportenvelope='bounce@vendor.example.test'
spf='v=spf1 ip4:192.0.2.0/24 -all'
print(envelope,spf)
print(ipaddress.ip_address('192.0.2.7') in ipaddress.ip_network('192.0.2.0/24'))bounce@vendor.example.test v=spf1 ip4:192.0.2.0/24 -all True
tags=dict(x.strip().split('=',1) for x in str(m['DKIM-Signature']).split(';'))
print({k:tags[k] for k in ('a','c','d','s','h','bh')}){'a': 'rsa-sha256', 'c': 'simple/simple', 'd': 'sender.example.test', 's': 'lab', 'h': 'from:subject', 'bh': 'rqTWF1wquaC14Rr2gecOCduoW0rs0ICJJ64HQGizSAg='}body=raw.split(b'\r\n\r\n',1)[1]
print(base64.b64encode(hashlib.sha256(simple_body(body)).digest()).decode()==tags['bh'])
print(verify(raw,pub))True verified
print(verify(raw.replace(b'docs.example.test',b'other.example.test'),pub))
print(verify(raw.replace(b'Subject: Public report',b'Subject: Changed report'),pub))body_hash_mismatch signature_mismatch
from_domain=parseaddr(str(m['From']))[1].split('@')[1]
print('SPF aligned',envelope.split('@')[1]==from_domain)
print('DKIM aligned',tags['d']==from_domain and verify(raw,pub)=='verified')SPF aligned False DKIM aligned True
for part in m.iter_attachments():
data=part.get_payload(decode=True)
print(part.get_filename(),part.get_content_type(),len(data),data)
print(hashlib.sha256(data).hexdigest())report.txt application/octet-stream 14 b'public report\n' 6c2aa97671fe03216a78d781274355f47af2d36688c9876dc69d41ec87a71bdc
report = {"sha256":hashlib.sha256(data).hexdigest(),"environment":"viewer-disabled","observed_seconds":30,"result":"no_behavior"}
print(report)
print(report["sha256"]==hashlib.sha256(data).hexdigest())
print(report["sha256"]==hashlib.sha256(data+b"changed").hexdigest()){'sha256': '6c2aa97671fe03216a78d781274355f47af2d36688c9876dc69d41ec87a71bdc', 'environment': 'viewer-disabled', 'observed_seconds': 30, 'result': 'no_behavior'}
True
Falseprint(m.get_all('Authentication-Results',[]))[]
演習完了