第十章:域名密钥识别邮件规范化DKIM记录DKIM签名DKIM验证和失败DKIM对齐对出站邮件进行签名创建密钥发布DKIM密钥密钥表签名表dkim_signing将Postfix附加到Rspamd测试签名多个DKIM选择器密钥寿命和轮换
DomainKeys Identified Mail —— 域名密钥识别邮件 —— DKIM
电子邮件没有统一的身份验证系统。像OpenPGP这样的协议旨在对人进行身份验证,但需要一定程度的技术专长。提交服务器对用户进行身份验证,但不会通过任何程度的完整性验证将该身份验证转发给其他主机,甚至本地软件栈的其他部分。IMAP服务器对用户进行身份验证,但该身份验证仍然是主机本地的。
DomainKeys Identified Mail(DKIM)允许MTA向来自其域的邮件添加身份验证信息。
DKIM建立在标准公钥密码学之上。公钥在DNS记录中提供。MTA使用私钥对所有出站邮件进行签名,并将签名添加到邮件头中。SPF验证 MAIL FROM
和 HELO
标头时,DKIM专门处理 FROM
、其他标头和正文。DKIM降低了垃圾邮件发送者伪造发件人用户可见电子邮件地址的能力。
电子邮件帝国认为DKIM签名的消息比未签名的消息更可靠。如果你想让大型提供商接受你的电子邮件,DKIM是必不可少的。只要您正在为传出消息部署DKIM,您就可以在传入消息上对其进行验证。
我们将研究有关签名消息、DKIM记录、验证消息以及最终签名我们自己的消息的问题。
canonicalization —— 规范化
如果你使用过OpenPGP等签名协议,那么你对数字签名很熟悉。您获取一块数据并对其进行签名。对文件的任何更改都会使签名无效。你可能会期望DKIM以同样的方式工作。它可以,但不能。
如果程序员认为有必要或正确,传统的邮件软件可以自由地重新排列消息。这可能包括添加或重新排列标题、用一种空白替换另一种空白、修剪尾随空白、转换换行等。任何这些更改都会使数字签名无效。复杂的邮件系统可能会在邮件到达目的地之前通过多个MTA传递邮件。这些系统通常来自不同的供应商,他们各自对标准有独特的解释。一些旧的所谓“电子邮件防火墙”破坏了消息,以实现他们所谓的“安全”,其中一些系统仍在使用中。
DKIM通过一种方式处理消息,在签名之前对消息进行规范化,另一种方式分别对消息头和消息体进行签名。规范化邮件意味着删除邮件中无关的部分,如空行、电子邮件地址中的引号以及大于和小于的符号,将所有标题放在小写,并将所有连续的空格替换为单个空格。用户永远看不到规范化的消息;它仅用于签名目的。这些转换的规则非常正式,必须按顺序遵守。消息规范化和正文规范化有不同的规则,但不幸的是,它们的名字分别是 relaxed 和 simple 。简单的头部规范化与简单的邮件体规范化非常不同。
对于标头签名,签名者可以选择签名哪些标头。也许你的服务器签署了最低限度的 From
标头。您还可以签署 Subject
、 To
、 Message-ID
或更多。签名者在邮件中列出了这些特征。有些标头不应该签名——每个在传输过程中接触邮件的MTA都会添加一个 Received-by
标头,因此原始发件人不应该尝试对这些标头进行签名。simple的头部规范化更严格;标题必须完全保持不变。relaxed的头部规范化将头部名称转换为小写,使所有头部成为一行,将空格聚集到一个空格中,并删除任何尾随空格。简单的标头规范化不太可能在通过多个MTA时幸存下来。始终使用relaxed(宽松)的头部规范化。
在body签名中,simple的规范化条带尾随空白行。relaxed邮件体规范化会去除这些空白行,并用单个空格替换任何空白行。虽然将制表符转换为空格的MTA可能会破坏程序员,但它不会破坏数字签名。
发送方必须描述它在创建签名时使用了什么样的规范化。规范化方法被写成头方法、斜线和体方法。如果你想对头部进行宽松的(relaxed)规范化,但对正文进行简单的(simple)规范化,这将被写成 relaxed/simple 。这两种简单的规范化方法都很脆弱,几乎总是应该避免。我们大多数人应该总是使用 relaxed/relaxed。你无法控制远程MTA可能会犯下的愚蠢行为。
始终指定这两种规范化。如果你只放一个单词,它适用于header方法。body方法将默认为simple。你可能不想那样。
DNS记录是DKIM实现的公共面。与SPF记录一样,DKIM记录是一种特殊格式的TXT记录:
selector._domainkey.domainname TXT "v=DKIM1; k=algorithm;" "p=publickey"
selector 是 “key name”的花俏的词。一个域可以有多个key。每个DKIM签名都标识了创建签名时使用的密钥的名称。DKIM验证器提取该名称,并检查发送域的DNS中是否有该名称的公钥。选择器是一个任意的文本字符串。许多教程建议使用单词默认值,但该选择器声明您部署了DKIM并且从未旋转过按键。35其他人建议使用日期字符串或包含日期的字符串,但有些人不想向公众提供这些信息。就像服务器名称一样,为选择器选择什么并不重要。选择器还允许多个主机为您的域签署电子邮件。
_domainkey
声明这是一条DKIM记录。
DKIM记录位于您的域名下,就像A或MX记录一样。
剩下的就是一个键值存储。将记录值括在括号中,并用分号分隔组件。
第一个标签 v
是DKIM版本。DKIM今天是版本1,所以你不必显式声明它,但有一天他们会发布版本2,你会希望你已经声明了它。版本必须先出现。
k
标签给出了关键算法和编码。默认值是 rsa ,它声明此记录中的密钥是具有SHA256哈希的rsa公钥,采用ASN.1格式,以DER编码,并进一步以base64编码。虽然标准允许使用EdDSA等其他算法,但大型提供商尚未部署验证支持。
p
值是实际的公钥。这通常是一个base64字符串,使用由 k
值定义的编码。密钥生成器同时提供 k
和 p
,您必须在不做更改的情况下包含它们。钥匙很长,必须放在同一行上。您可能需要用引号将其分解为255字节的块,但大多数密钥生成器都会为您完成此操作。
DKIM支持其他字段,例如 n
表示注释。然而,DNS限制了记录大小,因此必须严格限制这种使用。在记录本身之外保留任何笔记。如果没有其他内容,请记录部署每个选择器的时间及其用途。
DKIM记录不适用于子域。用于对来自的邮件进行签名的DKIM记录 mwl@ratoperatedvehicle.com
不适用于 mwl@mail.ratoperatedvehicle.com
,即使他们是同一个主人。每个子域都必须有自己的DKIM记录。
有了这个,我们现在可以理解验证器的作用了。
DKIM签名者将 DKIM-Signature
标头添加到消息中。这些标题看起来很复杂,但当你分解它们时,它们变得平易近人。与DKIM DNS记录非常相似, DKIM-Signature
标头由几个键值对组成。DNS记录和标头尽可能使用相同的键名。这是我域名中的邮件签名:
xxxxxxxxxx
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=mwl.io; s=mwl;
t=1702932921; bh=PHvggxbkGSIzdlkBmP+jU+86V9altffxu//x/ntKa4g=;
h=Date:From:To:Subject;
b=WvWODED5cYETtcPLmrMCHDTNBx5OzvRqvMba+X9Ws7QSMt70hK028kWhmuDu2ZW5j
ATUhu5Cw4J0bcdJOYlwowqOKRul79pLVeMxz3JBkUySoxT+asa/LD4V88LxdkzyUsF
…
如果你查看谷歌或微软的签名邮件,你会看到更多的密钥。
v
字段是DKIM版本。
a
字段是公钥算法。RSA-SHA256是默认算法,也是最受支持的算法。
规范化出现在 c
中。此消息使用 relaxed/simple
,表示可以调整标头,但不得触摸消息正文本身。
签名服务器的域出现在 d
中。DKIM验证器将检查此域是否有DKIM记录。此域是 mwl.io
。如果用户可见的 From
标头与 d
值中的域匹配,则称消息处于DKIM对齐状态( DKIM alignment )。
选择器(selector)显示在 s
中。此消息中的选择器是 mwl
。结合域名,这告诉验证器(validator)在 mwl._domainkey.mwl.io
的DKIM TXT记录中检查DNS中的公钥 。
t
字段以纪元时间为单位对签名创建进行时间戳。
bh
字段给出规范化消息体的哈希值。此消息使用relaxed/simple规范化,这意味着除了修剪尾随的空白行外,正文不能以任何方式进行转换。如果主体足够长,签名者只计算了前几千个字节的签名,则字节数以 l
表示。
h
字段列出了此签名中包含的标头。
最后一个字段 b
给出了完整的数字签名。
您可能还会看到 x
,即签名的到期日期(以纪元时间表示)。默认情况下,签名不会过期,但一些组织声明签名仅在一周内有效。“过期”并不意味着邮件在收件箱中未读时变得不真实,而是发件人希望目标MTA在此日期之前接收并验证邮件。过期不是对重播攻击的防御。
i
键指示是否有其他实体为发件人签署了消息,如果是,则指示是谁。
q
(DKIM密钥检索方法)和 z
(复制的标头)密钥是有效的,但很少使用。
您偶尔会在签名中看到其他密钥。它们是邮件提供商用来测试DKIM改进的实验值。
当验证器检查消息时,它会查看DKIM签名中包含的标头( h
字段)。它规范化这些标头并计算它们的哈希值。如果哈希与数字签名中的哈希匹配,则消息具有有效的DKIM。否则,消息将无法通过DKIM。如果验证器可以收集到验证签名所需的所有信息,但签名无效,则是永久性的失败。如果验证器在收集所有内容时遇到问题,例如,如果DNS查询DKIM记录超时,则故障是暂时的。
验证成功后,rspamd会分配 R_DKIM_ALLOW
符号。
为权重为 1
的 R_DKIM_REJECT
符号分配了无效签名。它也可能得到 R_DKIM_TEMPFAIL
或 R_DKIM_PERMFAIL
。
如果消息完全缺少DKIM,则分配 R_DKIM_NA
。
许多验证器使用 Authentication-Results
标头,就像SPF一样。一些验证器将SPF和DKIM结果组合在一个标头中。
xxxxxxxxxx
Authentication-Results: mail.ratoperatedvehicle.com;
dkim=pass (2048-bit key; unprotected) header.d=mwl.io header.i=@mwl.io header.b=WvWODED5
这些列表列出了执行身份验证的主机、验证尝试的结果以及测试的一些标头。
DKIM alignment (对齐)度量DKIM记录中的域与 From
标头中的域之间的关系。与SPF比对一样,DKIM比对是一个DMARC概念,在这里讨论是有意义的。与SPF比对一样,DKIM比对可以是严格的(exact ,精确匹配)或宽松的(relaxed ,域的子级)。考虑这段标头:
xxxxxxxxxx
DKIM-Signature: v=1; d=ratoperatedvehicle.com;
…
From: mwl@ratoperatedvehicle.com
签名 d
标记中的域与 From
标头中的域完全匹配。此消息符合 strict 的DKIM对齐。将其与另一封电子邮件中的片段进行比较:
xxxxxxxxxx
DKIM-Signature: v=1; d=tiltedwindmillpress.com;
…
From: sales@www.tiltedwindmillpress.com
From
标头声明此消息是由主机 www.tiltedwindmilpress.com
发送的。签名的 d
标签表示这是针对该域的,但不是针对该主机的。此消息实现了轻松的DKIM对齐( relaxed DKIM alignment )。
邮件列表存在DKIM对齐问题。他们接收消息,更改标题,添加页脚,然后重新发送。所有这些都超出了规范化所允许的转换范围。如果邮件列表转发了原始DKIM签名,则不会生效。大多数邮件列表都会在发送到其列表的邮件中添加自己的DKIM签名。然而,其中许多保留了发件人的原始 From
标头。签名具有邮件列表的域,但 From
地址位于完全不同的域中,因此消息不能有任何类型的DKIM对齐。许多邮件列表使用DMARC认证接收链保留了以前的签名(第11章)。
现在我们可以验证签名了,让我们对自己的邮件进行签名。
Rspamd的 dkim_signing 模块将为您对出站邮件进行签名。它可以通过几种不同的方式存储和配置DKIM密钥。您可以使用商业密钥存储,或将密钥填充到Redis中。我们将使用与OpenDKIM兼容的密钥配置来提供到OpenDKIM的简单迁移路径,或者与使用OpenDKIM签名的独立邮件发送者进行互操作,正如我们将在第15章中讨论的那样。
要对邮件进行签名,您必须创建密钥,发布这些密钥,构建密钥表和签名表以告诉rspamd在哪里找到这些密钥,并将rspamd的milter附加到Postfix的submission服务。
每个域都需要自己的密钥对(keypair),一个域可能有多个密钥,所以从一开始就组织起来。我建议为每个域创建一个存储密钥的目录和一个子目录。在这些目录中,在key的选择器之后为给定的keypair命名文件。(记住,selector 只是“name”的一个花哨的词。)我们使用OpenDKIM风格的配置来实现广泛的兼容性,因此我们将把密钥放在OpenDKIM的位置:FreeBSD上的 /var/db/OpenDKIM 和Debian上的 /etc/dkimkeys 。创建目录并使rspamd成为所有者。在该目录中,为要签名的域之一创建一个子目录。
Rspamd通过提供一个不那么可怕的 rspamadm(1)
子命令来为您调用OpenSSL,从而保护您免受可怕的OpenSSL命令的攻击:dkim_keygen
。
# rspamadm dkim_keygen -s 'selector' -d domain -b 2048 -k selector.private > selector.txt
-s
选项设置选择器, -d
选项设置域。使用 -b
可以设置密钥中的位。默认值 1024
现在被认为很弱。 -k
选项允许您提供一个文件来放置私钥。该命令通常会输出此密钥的DNS记录,但我们都知道,在您需要之前,它会滚动到视图之外,因此也将其写入文件。在这里,我使用选择器 wookie
为域名 ratoperatedvehicle.com
创建了一个密钥:
xxxxxxxxxx
wookie.private 文件包含私钥。
在 wookie.txt 中,您将找到需要添加到域DNS记录中的DKIM记录。这是两行,包裹着。
DKIM记录格式是挑剔和脆弱的。永远不要试图手动创建这些记录。密钥创建命令以您需要的确切格式提供该记录。使用它。该文件将位于密钥目录中,以选择器命名,以 .txt 结尾。我为 ratoperatedvehicle.com
创建了密钥 wookie
,所以我需要 /etc/dkimkeys/ratoperatedvewicle.com/wookie.txt :
xxxxxxxxxx
wookie._domainkey IN TXT ( "v=DKIM1; k=rsa; "
"p=MIGfMA0GCSqGSIb3DQEB…" )
它在两行上。就像许多SOA记录一样,使用括号将它们包含在一行中。第二行很长。密钥可能会被分割成碎片,以适应TXT记录限制。将此条目复制到您的区域并重新加载该区域。然后仔细检查您的名称服务器是否确实发布了它:
xxxxxxxxxx
$ dig wookie._domainkey.ratoperatedvehicle.com txt @localhost
如果您的密钥在DNS中,请继续。
OpenDKIM风格的密钥表是一个将文件映射到DNS条目和选择器的文件。除非操作系统包管理器另有声明,否则我将其放在 /etc/mail/dkim.keytable 中。
使用的每个key都需要一个key表条目。每个条目在一行中包含四个值:密钥的DNS条目(包括选择器)、域、选择器本身以及包含相关私钥的文件。DNS条目后有一个空格,但其余的都是冒号分隔的:
DNS domain:selector:keyfile
这是我们刚刚为 ratoperatedvehicle.com 创建的密钥条目。(由于物理原因,它被包装在你的书中,但在文件中只有一行。)
xxxxxxxxxx
wookie._domainkey.ratoperatedvehicle.com ratoperatedvehicle.com:wookie:/etc/dkimkeys/ratoperatedvehicle.com/wookie.private
此条目使用选择器 wookie 作为 ratoperatedvehicle.com
的DKIM密钥。它指定了域 ratoperatedvehicle.com
,然后使用冒号表示私钥的路径。
如果您有多个域,或者一个域有多个key,请在单独的行中列出每个键:
xxxxxxxxxx
wookie._domainkey.ratoperatedvehicle.com ratoperatedvehicle.com:wookie:/etc/dkimkeys/ratoperatedvehicle.com/wookie.private
fuzzball._domainkey.ratoperatedvehicle.com ratoperatedvehicle.com:swamp:/etc/dkimkeys/ratoperatedvehicle.com/swamp.private
zamwessel._domainkey.immortalclay.com immortalclay.com:zamwessel:/etc/dkimkeys/immortalclay.com/zamwessel.private
您可以在某些地方使用百分号(%)来代替明显的域名,但这会导致混淆。您还可以将DER编码的私钥直接放入此文件中,而不是指向密钥文件,但这会很快变得很长。坚持使用关键文件和完整域名。
哈希标记(#)表示注释。如果您没有留下其他评论,请记录密钥创建日期和密钥用于的主机。当您需要执行密钥滚动或审计时,这些信息将非常宝贵。
签名表告诉rspamd哪些发件人电子邮件地址应该用哪个密钥签名。除非包管理人员有其他想法,否则我会把它放在 /etc/mail/dkim.signingtable 中。OpenDKIM格式要求每个域rspamd签名都有一个签名表条目。每个条目都是一行,将电子邮件帐户与DKIM记录相匹配。使用星号表示“此域的所有用户”。在这里,我将我的两个域中的所有用户与其密钥进行匹配:
xxxxxxxxxx
*@ratoperatedvehicle.com wookie._domainkey.ratoperatedvehicle.com
*@immortalclay.com zamwessel._domainkey.immortalclay.com
来自每个域的所有用户的电子邮件将由分配的密钥签名。是的,你可以为特定的用户设置特定的密钥,但第一个,先让基础功能正常工作,然后才是第二个,为什么?
现在告诉模块 dkim_signing
使用这些表。主要配置在 /etc/rspamd/modules.d/dkim_signing.conf 中。您将看到通过指定路径变量启用模块的引用,但我们没有使用这种方法。创建 /etc/rspamd/local.d/dkim_signing.conf 并设置 signing_table
和 key_table
:
xxxxxxxxxx
signing_table = "/etc/mail/dkim.signingtable";
key_table = "/etc/mail/dkim.keytable";
重新加载rspamd。
对于来自外部MTA的邮件,我们已经将Postfix连接到rspamd,但这与通过提交服务来自经过身份验证的用户的邮件是分开的。Rspamd对待经过身份验证的邮件与处理来自本地地址的邮件相同,并禁用灰名单和SPF等检查。在 /etc/postfix/master.cf 中找到提交条目,并添加milter选项。
xxxxxxxxxx
smtpds inet n - n - - smtpd
-o syslog_name=postfix-submission
-o smtpd_tls_wrappermode=yes
…
-o smtpd_milters=inet:localhost:11332
重启Postfix并测试。
像其他事情一样,在依赖签名之前,先测试一下它们。验证您的两个测试主机都在运行rspamd,并从您的主机向另一个主机发送带有DKIM密钥的邮件。接收MTA应添加 R_DKIM_ALLOW
符号。如果没有,请找到它添加的 R_DKIM
符号,看看缺少什么。检查Postfix和rspamd错误日志。
一个组织可能拥有数十或数千个MTA,通常由不同的实体控制。在所有这些之间共享DKIM密钥是不切实际的。处理这些问题的唯一明智方法是允许每个MTA拥有自己的DKIM密钥。
那很好。
我的主MTA有一个DKIM密钥。我的邮件列表提供商也是如此。它们的所有密钥都用不同的选择器标识。外部MTA不关心我的域有多少DKIM密钥;他们只关心是否能找到他们正在寻找的特定DKIM密钥。这就是为什么我们使用选择器。
如果你的一个MTA根本不签名怎么办?外部MTA将尊重其未签名的邮件。DMARC允许您指示外部MTA拒绝未签名的邮件,但这是您的选择。
公钥加密被破坏。你的组织越有价值,就有越多的人试图破解你的密钥。破解钥匙需要时间。避免入侵者暴力破解公钥的标准方法是定期轮换它们。但是你应该轮换你的钥匙吗?
现代DKIM使用2048位密钥。根据目前的数学理解,在宇宙预期的热死亡之前,它们不是暴力破解的。然而,现代加密算法不会屈服于暴力。数学家们慢慢地研究它们,发现一个又一个弱点,直到最终有人想出如何在合理的时间内打破它们。计算机速度可能不会像几十年前那样加快,但处理能力比以往任何时候都更容易获得。每年,“合理时间”的任何定义都包含越来越多的处理能力。
你的密钥会被破解吗?可能不会。
你的组织是入侵者的目标吗?你处理金钱、战斗站计划或个人身份信息吗?合法的冲锋队可能会笼罩你的世界,把它炸成十亿块碎片吗?如果是这样的话,每年左右轮换你的密钥是你为说服审计员你正在采取明智的预防措施而做的事情清单上值得尊敬的一项。创建一个新密钥,将其添加到DNS,让它传播,切换您的邮件系统以使用它,并从DNS中删除旧密钥。
现在您的域提供了DKIM和SPF记录,您可以建立DMARC。