メインコンテンツまでスキップ

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...

タグの説明

タグ名称説明必須
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'); // 末尾の改行を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などのより高度な電子メールセキュリティメカニズムの基礎を築くことができます。