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...
标签说明
| 标签 | 名称 | 说明 | 示例 | 必需 |
|---|---|---|---|---|
v | Version | 版本号 | v=1 | 是 |
a | Algorithm | 签名算法 | a=rsa-sha256 | 是 |
b | Signature | 实际签名 (Base64) | b=dzdVy... | 是 |
bh | Body Hash | 邮件正文哈希 | bh=2jUSO... | 是 |
c | Canonicalization | 规范化算法 | c=relaxed/relaxed | 否 |
d | Domain | 签名域名 | d=example.com | 是 |
h | Headers | 签名的头部字段 | h=From:To:Subject | 是 |
i | Identity | 签名者标识 | [email protected] | 否 |
l | Body Length | 签名的正文长度 | l=1000 | 否 |
q | Query Method | 公钥查询方法 | q=dns/txt | 否 |
s | Selector | 选择器 | s=default | 是 |
t | Timestamp | 签名时间戳 | t=1234567890 | 否 |
x | Expire Time | 过期时间 | x=1234657890 | 否 |
z | Copied 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等更高级的邮件安全机制打下基础。