Skip to main content

RFC 6376 - DKIM邮件签名

基本信息

  • RFC编号: 6376
  • 标题: DomainKeys Identified Mail (DKIM) Signatures
  • 中文标题: 域名密钥识别邮件签名
  • 发布日期: 2011年9月
  • 状态: PROPOSED STANDARD (提案标准)
  • 作者: D. Crocker, et al.

摘要 (Abstract)

DomainKeys Identified Mail (DKIM)是一种域名级的邮件认证方法。它允许组织为邮件消息承担责任,使用加密签名。接收方可以验证该签名,确认邮件确实来自声称的域名,并且内容未被篡改。

DKIM概述

什么是DKIM?

定义:

DKIM = DomainKeys Identified Mail
作用: 邮件数字签名
目的:
✓ 验证发件人域名
✓ 检测邮件篡改
✓ 防止钓鱼和伪造

核心思想:
发件人用私钥签名 → 接收方用公钥验证

Email安全问题:

SMTP协议问题:
❌ 可以伪造发件人地址
❌ 无法验证邮件真实性
❌ 容易被钓鱼利用

示例钓鱼:
From: [email protected] (伪造)
实际: 攻击者的服务器

DKIM解决方案:
✓ 域名所有者签名邮件
✓ DNS发布公钥
✓ 接收方验证签名
✓ 签名失败→拒收或标记

DKIM工作流程

发送端 (example.com):
1. 邮件服务器生成签名
- 对邮件头部和正文签名
- 使用私钥 (保密)

2. 将签名添加到邮件头部
DKIM-Signature: v=1; a=rsa-sha256; d=example.com; ...

3. 正常发送邮件

接收端:
1. 提取DKIM-Signature头部

2. 从DNS获取公钥
查询: default._domainkey.example.com

3. 验证签名
- 重新计算邮件哈希
- 用公钥解密签名
- 比较哈希值

4. 结果:
✓ 签名有效 → 来自example.com,未篡改
✗ 签名无效 → 伪造或被修改

DKIM-Signature头部详解

基本格式

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=example.com; s=default; t=1234567890;
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
h=From:To:Subject:Date;
b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD08b9/0i
+=hB...

标签说明

标签名称说明示例必需
vVersion版本号v=1
aAlgorithm签名算法a=rsa-sha256
bSignature实际签名 (Base64)b=dzdVy...
bhBody Hash邮件正文哈希bh=2jUSO...
cCanonicalization规范化算法c=relaxed/relaxed
dDomain签名域名d=example.com
hHeaders签名的头部字段h=From:To:Subject
iIdentity签名者标识[email protected]
lBody Length签名的正文长度l=1000
qQuery Method公钥查询方法q=dns/txt
sSelector选择器s=default
tTimestamp签名时间戳t=1234567890
xExpire Time过期时间x=1234657890
zCopied Headers原始头部副本z=From:...

关键参数详解:

a (Algorithm):

支持的算法:
rsa-sha256 (推荐)
rsa-sha1 (已弃用,不安全)

示例:
a=rsa-sha256

c (Canonicalization):

格式: c=<header>/<body>

算法:
- simple: 严格匹配
- relaxed: 宽松匹配 (忽略空格、大小写等)

示例:
c=relaxed/relaxed (推荐,容忍邮件服务器修改)
c=simple/simple (严格,任何修改都会导致验证失败)

d (Domain):

签名域名 (必须是发件人域名或其父域)

有效:
From: [email protected]
d=example.com ✓ (父域)

无效:
From: [email protected]
d=other.com ✗ (不相关域名)

s (Selector):

选择器: 用于DNS查询

目的:
- 支持多个密钥
- 密钥轮换
- 不同服务使用不同密钥

DNS查询:
s=default, d=example.com
→ 查询: default._domainkey.example.com

h (Headers):

签名的头部字段 (冒号分隔)

推荐签名:
- From (必需!)
- To
- Subject
- Date
- Message-ID

示例:
h=From:To:Subject:Date:Message-ID

注意:
From字段必须包含!

DNS公钥记录

TXT记录格式

查询: <selector>._domainkey.<domain>
示例: default._domainkey.example.com

TXT记录内容:
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...

标签说明:
v=DKIM1 版本
k=rsa 密钥类型
p=<公钥> Base64编码的公钥

公钥标签详解

标签说明示例必需
v版本v=DKIM1
k密钥类型k=rsa
p公钥 (Base64)p=MIGfMA0...
h可接受的哈希算法h=sha256
s服务类型s=email
t标志t=y (测试模式)
n注释n=Notes

完整示例:

default._domainkey.example.com. IN TXT (
"v=DKIM1; k=rsa; "
"p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC3QEKyU1fSma0axspqYK5iAj+54lsAg4qRRCnpKK68hawSIhv5TGT4j"
"..."
)

说明:
- 可以分成多个字符串 (DNS限制255字节)
- 空格会被忽略
- p标签为空 → 表示密钥已撤销

实现示例

生成密钥对

# 生成2048位RSA私钥
openssl genrsa -out dkim_private.pem 2048

# 提取公钥
openssl rsa -in dkim_private.pem -pubout -out dkim_public.pem

# 转换为DNS格式 (去除头尾和换行)
cat dkim_public.pem | grep -v "BEGIN\|END" | tr -d '\n'

输出:

私钥 (dkim_private.pem):
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA...
-----END RSA PRIVATE KEY-----

公钥 (dkim_public.pem):
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----

DNS格式:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...

Node.js签名实现

const crypto = require('crypto');
const dns = require('dns').promises;

class DKIMSigner {
constructor(domain, selector, privateKey) {
this.domain = domain;
this.selector = selector;
this.privateKey = privateKey;
}

// 签名邮件
signEmail(headers, body) {
// 1. 规范化正文
const canonicalBody = this.canonicalizeBody(body);

// 2. 计算正文哈希
const bodyHash = crypto
.createHash('sha256')
.update(canonicalBody)
.digest('base64');

// 3. 构建DKIM头部 (不含b标签)
const timestamp = Math.floor(Date.now() / 1000);
const dkimHeader = [
'v=1',
'a=rsa-sha256',
'd=' + this.domain,
's=' + this.selector,
't=' + timestamp,
'c=relaxed/relaxed',
'h=From:To:Subject:Date',
'bh=' + bodyHash,
'b=' // 空签名,稍后填充
].join('; ');

// 4. 规范化头部
const canonicalHeaders = this.canonicalizeHeaders(headers, dkimHeader);

// 5. 生成签名
const sign = crypto.createSign('RSA-SHA256');
sign.update(canonicalHeaders);
const signature = sign.sign(this.privateKey, 'base64');

// 6. 完整的DKIM-Signature
return dkimHeader.replace('b=', 'b=' + signature);
}

// 规范化正文 (relaxed)
canonicalizeBody(body) {
return body
.replace(/[ \t]+(\r\n)/g, '$1') // 移除行尾空格
.replace(/(\r\n)+$/, '\r\n'); // 只保留一个尾部换行
}

// 规范化头部 (relaxed)
canonicalizeHeaders(headers, dkimHeader) {
const headersToSign = ['from', 'to', 'subject', 'date'];
let canonical = '';

// 添加要签名的头部
for (const name of headersToSign) {
const value = headers[name];
if (value) {
canonical += name + ':' + value.trim() + '\r\n';
}
}

// 添加DKIM-Signature头部 (不含b标签的值)
canonical += 'dkim-signature:' +
dkimHeader.split('b=')[0] + 'b=';

return canonical;
}
}

// 使用
const privateKey = fs.readFileSync('dkim_private.pem', 'utf8');
const signer = new DKIMSigner('example.com', 'default', privateKey);

const headers = {
from: '[email protected]',
to: '[email protected]',
subject: 'Test Email',
date: new Date().toUTCString()
};

const body = 'This is the email body.\r\n';

const dkimSignature = signer.signEmail(headers, body);
console.log('DKIM-Signature:', dkimSignature);

验证实现

class DKIMVerifier {
async verifyEmail(dkimSignature, headers, body) {
// 1. 解析DKIM-Signature
const tags = this.parseDKIMSignature(dkimSignature);

// 2. 获取公钥
const publicKey = await this.getPublicKey(tags.d, tags.s);

// 3. 验证正文哈希
const bodyValid = this.verifyBodyHash(body, tags.bh, tags.c);
if (!bodyValid) {
return { valid: false, reason: 'Body hash mismatch' };
}

// 4. 验证签名
const signatureValid = this.verifySignature(
headers,
dkimSignature,
publicKey,
tags
);

return {
valid: signatureValid,
domain: tags.d,
selector: tags.s
};
}

parseDKIMSignature(signature) {
const tags = {};
signature.split(';').forEach(part => {
const [key, value] = part.trim().split('=');
if (key && value) {
tags[key] = value.trim();
}
});
return tags;
}

async getPublicKey(domain, selector) {
const dnsName = `${selector}._domainkey.${domain}`;
const records = await dns.resolveTxt(dnsName);

// 解析TXT记录
const dkimRecord = records[0].join('');
const match = dkimRecord.match(/p=([^;]+)/);

if (!match) {
throw new Error('Public key not found');
}

const publicKeyB64 = match[1];
return `-----BEGIN PUBLIC KEY-----\n${publicKeyB64}\n-----END PUBLIC KEY-----`;
}

verifyBodyHash(body, expectedHash, canonicalization) {
const canonical = this.canonicalizeBody(body);
const hash = crypto
.createHash('sha256')
.update(canonical)
.digest('base64');

return hash === expectedHash;
}

verifySignature(headers, dkimSignature, publicKey, tags) {
// 提取签名值
const signature = tags.b;

// 重建签名数据
const canonicalHeaders = this.canonicalizeHeaders(
headers,
dkimSignature.split('b=')[0] + 'b='
);

// 验证
const verify = crypto.createVerify('RSA-SHA256');
verify.update(canonicalHeaders);

return verify.verify(publicKey, signature, 'base64');
}
}

// 使用
const verifier = new DKIMVerifier();
const result = await verifier.verifyEmail(dkimSignature, headers, body);

if (result.valid) {
console.log(`✓ Valid DKIM signature from ${result.domain}`);
} else {
console.log(`✗ Invalid DKIM signature: ${result.reason}`);
}

最佳实践

1. 密钥管理

密钥长度:
✓ 至少2048位
✓ 推荐2048或3072位
✗ 避免1024位 (已不安全)

密钥轮换:
- 每1-2年更换密钥
- 同时发布新旧密钥 (使用不同selector)
- 过渡期后移除旧密钥

选择器命名:
✓ 按日期: 2024-01
✓ 按用途: mailserver1
✓ 按轮换: key1, key2

2. 头部选择

// 推荐签名的头部
const recommendedHeaders = [
'From', // 必需!
'To',
'Subject',
'Date',
'Message-ID',
'Reply-To',
'Cc',
'MIME-Version',
'Content-Type'
];

// 避免签名易变的头部
const avoidHeaders = [
'Received', // 每个中继都会添加
'Return-Path', // 传输中可能改变
'X-*' // 自定义头部可能被修改
];

3. 测试模式

DNS记录添加 t=y 标志:
v=DKIM1; k=rsa; t=y; p=MIGfMA...

作用:
- 表示测试模式
- 验证失败不会拒绝邮件
- 用于部署初期测试

参考文献

DKIM相关RFC:

  • [RFC 6376] DKIM Signatures ← 本文档
  • [RFC 8301] Cryptographic Algorithm and Key Usage Update to DKIM
  • [RFC 8463] A New Cryptographic Signature Method For DKIM

相关标准:

  • [RFC 7489] DMARC (基于DKIM和SPF)
  • [RFC 7208] SPF (Sender Policy Framework)

总结: DKIM通过数字签名技术为邮件提供域名级认证,是现代邮件安全的基础。正确实现DKIM可以显著提高邮件送达率,减少被误判为垃圾邮件的概率,并为DMARC等更高级的邮件安全机制打下基础。