RFC 6376 - DKIM署名
基本情報
- RFC番号: 6376
- タイトル: DomainKeys Identified Mail (DKIM) Signatures
- 日本語タイトル: ドメインキー識別メール署名
- 発行日: 2011年9月
- ステータス: 提案標準 (PROPOSED STANDARD)
- 著者: D. Crocker, T. Hansen, M. Kucherawy
概要 (Abstract)
DomainKeys Identified Mail (DKIM) は、ドメインレベルの電子メール認証方式です。組織が暗号署名を使用して電子メールメッセージの責任を負うことを可能にします。受信者は署名を検証して、メールが実際に主張されたドメインから送信され、内容が改ざんされていないことを確認できます。
DKIM概要
DKIMとは?
定義:
DKIM = DomainKeys Identified Mail
機能: 電子メールデジタル署名
目的:
✓ 送信者ドメインの検証
✓ メール改ざんの検出
✓ フィッシングと偽造の防止
核心概念:
送信者が秘密鍵で署名 → 受信者が公開鍵で検証
電子メールセキュリティの問題:
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'); // 末尾の改行を1つだけ保持
}
// ヘッダーを正規化 (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(`✓ ${result.domain}からの有効なDKIM署名`);
} else {
console.log(`✗ 無効なDKIM署名: ${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などのより高度な電子メールセキュリティメカニズムの基礎を築くことができます。