認証と配信可能性のための DKIM 署名メール
DKIM(DomainKeys Identified Mail、 RFC 6376) はドメイン所有者が送信メールに暗号署名を付与できるようにします。受信サーバーは DNS から対応する公開鍵を取得し、ハッシュを再計算して、メッセージ本文が転送中に改ざんされていないこと、送信者がドメインに許可されたものであることの 2 点を同時に確認します。DKIM がないと、Gmail、Microsoft 365、Yahoo などの最新プロバイダーはメールをスパムとしてフラグ付けしたり、黙って削除したり、配信を拒否したりします。DKIM は本番メールではもはやオプションではありません。
このガイドは Aspose.Emailの .NET 用 DKIM 名前空間です。本記事のすべてのコードブロックは、 DKIMExamples プロジェクトに含まれ、ライブラリ全体でエンドツーエンドに検証されています。
前提条件
DKIM タイプは Aspose.Email.DKIM 名前空間で、.NET Framework 4.0/4.5 ビルドのライブラリのみと共に出荷されます。.NET Standard 2.0 および .NET 6/8 パッケージからは意図的に除外されています。以下の例を実行するには、プロジェクトが net45 (対象 net48 利用側で .csproj).
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Reference Include="Aspose.Email">
<HintPath>..\path\to\net45\Aspose.Email.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
RSA 鍵ペアも必要です。秘密鍵はアプリケーション内に保持され署名に使用され、公開鍵は DNS TXT レコードとして公開され受信側が検証できるようになります。現在の標準は 2048 ビット鍵です:
openssl genrsa -out sample-private-key.pem 2048
Aspose の PemReader PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) と PKCS#8 (-----BEGIN PRIVATE KEY-----) 形式のファイル。
| クラス | 役割 | |——-|——| | DKIMSignatureInfo | 署名を記述します: セレクター、ドメイン、ハッシュアルゴリズム、正規化、カバーするヘッダー。 | | PemReader | PEM 形式のファイルまたはストリームから RSA 秘密鍵を読み込み、 RSACryptoServiceProvider. |
| MailMessage.DKIMSign(rsa, info) | 拡張/インスタンス メソッドで、ヘッダー付きの署名済みコピーを返します DKIM-Signature ヘッダーが設定されました。 | | DkimVerifier | 署名されたメッセージの DNS ベース検証を実行します — ファイル、ストリーム、同期、または非同期。 |
全体のフローは常に: キーのロード → 署名の記述 → メッセージの構築 → 署名 → (オプションで)検証。
最小限の署名例
これは有効な DKIM 署名メッセージを生成する最小限のコードです:
// Load private key from PEM file
var rsa = PemReader.GetPrivateKey(PrivateKeyPath);
// Create DKIM signature information
var signInfo = new DKIMSignatureInfo("dkim", "example.com")
{
// Default: Relaxed/Relaxed canonicalization
// Default: RSASha1 hash algorithm
};
// Add headers to sign
signInfo.Headers.Add("From");
signInfo.Headers.Add("To");
signInfo.Headers.Add("Subject");
// Create a test email
var mailMessage = new MailMessage(
"sender@example.com",
"recipient@example.com",
"Test Subject",
"This is a test email body signed with DKIM.")
{
From = new MailAddress("sender@example.com", "Sender Name")
};
// Sign the message
var signedMsg = mailMessage.DKIMSign(rsa, signInfo);
// Save the signed message
string outputPath = Path.Combine(DataDir, "basic-signed.eml");
signedMsg.Save(outputPath);
Console.WriteLine($"DKIM-Signature header: {signedMsg.Headers["DKIM-Signature"]?.Substring(0, 50)}...");
2 つの引数は DKIMSignatureInfo — "dkim" および "example.com" — それらは セレクター と ドメイン です。これらが組み合わさって受信側に公開鍵の検索先(DNS TXT レコード)を示します。 dkim._domainkey.example.com. 短いセレクター名を選んでください(通常は default, mail、または次のような日付 2026jan); セレクターを使用すると、古いキーを破棄せずにキーをローテーションできます。
重要な詳細: DKIMSign 新しい を返します MailMessage インスタンスで DKIM-Signature ヘッダーが追加されました。元のメッセージは変更されていません。入力ではなく、返されたオブジェクトを保存または送信してください。
署名するヘッダーの選択
ヘッダーが列挙されている場所 signInfo.Headers は署名に結びつく唯一のヘッダーです。リストに無いものはリレーで変更可能で、署名を壊さずに済む場合があります(例: Received: 下流で追加されたヘッダー) であり、時に危険です(例: 何も残さないこと) From 未署名は署名が意味を持たなくなります)。
経験則: 常に署名してください From, To, Subject, Date. From は特に DMARC 整合性のために必須です。
var signInfo = new DKIMSignatureInfo("dkim2", "example.com")
{
Headers =
{
"From",
"To",
"Subject",
"Date",
"MIME-Version"
}
};
// Create email with additional headers
var mailMessage = new MailMessage(
"sender@example.com",
"recipient@example.com",
"Custom Headers Test",
"This message includes custom headers in the DKIM signature.")
{
From = new MailAddress("sender@example.com", "Sender"),
Date = DateTime.UtcNow
};
// Ensure headers listed in DKIM signature are present on the message
mailMessage.Headers.Add("MIME-Version", "1.0");
mailMessage.Headers.Add("X-Custom-Header", "CustomValue");
// Sign with custom headers
var signedMsg = mailMessage.DKIMSign(rsa, signInfo);
string outputPath = Path.Combine(DataDir, "custom-headers-signed.eml");
signedMsg.Save(outputPath);
リストに含める場合は Date しかし決して設定しないでください mailMessage.Date, DKIMSign 例外を投げます The following headers to be signed do not exist: Date. 常に最初にヘッダーを設定し、その後で署名してください。
ハッシュアルゴリズム — RSA‑SHA1 と RSA‑SHA256
DKIM は 2 つの署名アルゴリズムをサポートしています。Aspose.Email のデフォルトは RSASha1 下位互換性のために残していますが、新規展開は必ず RSASha256. 多くのプロバイダーは SHA-1 署名を暗号的に検証できても無効として扱います。
// Example with RSASha1 (older, less secure but widely supported)
var signInfoSha1 = new DKIMSignatureInfo("dkim-sha1", "example.com")
{
HashAlgorithm = DKIMHashAlgorithm.RSASha1,
Headers = { "From", "To", "Subject" }
};
var mailMessage1 = new MailMessage(
"sender@example.com",
"recipient@example.com",
"SHA1 Test",
"Signed with RSASha1 algorithm.");
mailMessage1.From = new MailAddress("sender@example.com");
var signedMsgSha1 = mailMessage1.DKIMSign(rsa, signInfoSha1);
string pathSha1 = Path.Combine(DataDir, "sha1-signed.eml");
signedMsgSha1.Save(pathSha1);
// Example with RSASha256 (newer, more secure)
var signInfoSha256 = new DKIMSignatureInfo("dkim-sha256", "example.com")
{
HashAlgorithm = DKIMHashAlgorithm.RSASha256,
Headers = { "From", "To", "Subject" }
};
var mailMessage2 = new MailMessage(
"sender@example.com",
"recipient@example.com",
"SHA256 Test",
"Signed with RSASha256 algorithm.");
mailMessage2.From = new MailAddress("sender@example.com");
var signedMsgSha256 = mailMessage2.DKIMSign(rsa, signInfoSha256);
string pathSha256 = Path.Combine(DataDir, "sha256-signed.eml");
signedMsgSha256.Save(pathSha256);
結果として得られる DKIM-Signature ヘッダーはアルゴリズムを次のように広告します: a= タグ (a=rsa-sha1 vs a=rsa-sha256). 受信側はこのタグを参照して、どのハッシュを再計算すべきか判断します。
正規化 — Simple と Relaxed の比較
正規化は、ハッシュ化前にメッセージのバイト単位で安定した形に合意するための DKIM の手法です。ヘッダーと本文に対してそれぞれ独立した 2 つのアルゴリズムが定義されています:
Simple— ほぼ正規化なし。本文の末尾空行は除去されますが、他の空白は変更されません。リレーによる単一スペースの追加でも署名は無効になります。Relaxed— ヘッダー値内の空白を折りたたみ、ヘッダー名を小文字化し、本文中の連続空白を圧縮します。メール転送エージェントが行う些細な再フォーマットに寛容です。
選択してください Relaxed 特別な理由がない限り、ヘッダーと本文の両方に適用すべきです。ほとんどすべての本番展開がこの方法を採用しています。
// Simple canonicalization - almost no changes allowed
var signInfoSimple = new DKIMSignatureInfo("dkim-simple", "example.com")
{
HeaderCanonicalization = CanonicalizationType.Simple,
BodyCanonicalization = CanonicalizationType.Simple,
Headers = { "From", "To", "Subject" }
};
var mailMessage1 = new MailMessage(
"sender@example.com",
"recipient@example.com",
"Simple Canonicalization",
"This message uses Simple canonicalization.");
mailMessage1.From = new MailAddress("sender@example.com");
var signedSimple = mailMessage1.DKIMSign(rsa, signInfoSimple);
string pathSimple = Path.Combine(DataDir, "simple-canonical.eml");
signedSimple.Save(pathSimple);
// Relaxed canonicalization - minor changes allowed (whitespace normalization)
var signInfoRelaxed = new DKIMSignatureInfo("dkim-relaxed", "example.com")
{
HeaderCanonicalization = CanonicalizationType.Relaxed,
BodyCanonicalization = CanonicalizationType.Relaxed,
Headers = { "From", "To", "Subject" }
};
var mailMessage2 = new MailMessage(
"sender@example.com",
"recipient@example.com",
"Relaxed Canonicalization",
"This message uses Relaxed canonicalization.");
mailMessage2.From = new MailAddress("sender@example.com");
var signedRelaxed = mailMessage2.DKIMSign(rsa, signInfoRelaxed);
string pathRelaxed = Path.Combine(DataDir, "relaxed-canonical.eml");
signedRelaxed.Save(pathRelaxed);
ヘッダーと本文の正規化は独立しています — relaxed/simple は完全に有効な組み合わせで、上記のように記述されています c= DKIM ヘッダーのタグです。
秘密鍵のロード
PemReader PEM のエンコーディングを透過的に処理します。別個の Pkcs1Reader / Pkcs8Reader — パスまたはストリームを渡すだけです:
// Check if private key file exists
if (!File.Exists(PrivateKeyPath))
{
Console.WriteLine($"Private key file not found: {PrivateKeyPath}");
Console.WriteLine("Creating a sample private key for demonstration...");
// Create a sample RSA key (for testing only - in production, use real keys)
using var rsa = new System.Security.Cryptography.RSACryptoServiceProvider(2048);
var privateKeyParams = rsa.ExportParameters(true);
Console.WriteLine("Sample RSA key created (2048 bits).");
Console.WriteLine("In production, load your actual private key from a secure location.");
Console.WriteLine("PEM file format should contain either:");
Console.WriteLine(" - BEGIN RSA PRIVATE KEY (PKCS#1)");
Console.WriteLine(" - BEGIN PRIVATE KEY (PKCS#8)");
}
else
{
// Load the private key
var rsa = PemReader.GetPrivateKey(PrivateKeyPath);
Console.WriteLine($"Private key loaded successfully from: {PrivateKeyPath}");
Console.WriteLine("Key size: 2048 bits (typical for DKIM)");
}
Console.WriteLine("\nPEM File Format Examples:");
Console.WriteLine("--------------------------");
Console.WriteLine("PKCS#1 format:");
Console.WriteLine("-----BEGIN RSA PRIVATE KEY-----");
Console.WriteLine("MIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy...");
Console.WriteLine("-----END RSA PRIVATE KEY-----");
Console.WriteLine();
Console.WriteLine("PKCS#8 format:");
Console.WriteLine("-----BEGIN PRIVATE KEY-----");
Console.WriteLine("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...");
Console.WriteLine("-----END PRIVATE KEY-----");
PemReader.GetPrivateKey 戻り値は RSACryptoServiceProvider すぐに渡せる形で DKIMSign. さらに GetPrivateKey(Stream) 鍵がキーボールト、暗号化ブロブ、埋め込みリソースから取得されるケース用のオーバーロードです。
鍵取り扱いルール:
- 秘密鍵をバージョン管理システムにコミットしないでください。
*.pemに属します.gitignore. - 本番環境では、ディスクからではなくシークレットマネージャ(Azure Key Vault、AWS Secrets Manager、HashiCorp Vault など)から鍵をロードします。
- キーは 6〜12 ヶ月ごとにローテーションしてください。新しいセレクタを先に公開し、署名を切り替え、猶予期間後に古い DNS レコードを削除します。
ファイルから署名済みメッセージを検証する
検証は対称操作です: 署名されたデータが与えられたときに .eml, DkimVerifier を抽出します DKIM-Signature ヘッダーで、次の DNS 参照を行います <selector>._domainkey.<domain> 公開鍵を取得し、ハッシュを再計算します。その結果は DkimResult と IsValid および ErrorMessage.
// For this example, we need a signed message
if (!File.Exists(signedMessagePath))
{
Console.WriteLine($"Signed message file not found: {signedMessagePath}");
Console.WriteLine("Please provide a DKIM-signed .eml file for verification.");
return;
}
// Verify the signature
DkimResult result = DkimVerifier.Verify(signedMessagePath);
// Check the result
if (result.IsValid)
{
Console.WriteLine("[OK] DKIM signature verification successful!");
Console.WriteLine("The message was signed by the claimed domain.");
}
else
{
Console.WriteLine("[FAIL] DKIM signature verification failed!");
Console.WriteLine($"Error: {result.ErrorMessage}");
}
Console.WriteLine("\nVerification Process:");
Console.WriteLine("1. Extracts DKIM-Signature header from the message");
Console.WriteLine("2. Queries DNS for the public key (selector._domainkey.domain)");
Console.WriteLine("3. Verifies the signature using the public key");
Console.WriteLine("4. Compares computed body hash with the signed body hash");
署名されたメッセージの検証
d=example.com必ず "key not in dictionary" などの DNS 参照エラーで失敗します。なぜならexample.comDKIM レコードを公開しません。実際の検証テストには管理下のドメイン(または既知のテストメッセージ)を使用してください。
ストリームからの検証
メッセージがディスク上にない場合 — IMAP から取得したり、キュー、HTTP アップロード、Blob ストレージから来た場合 — は Stream オーバーロードです。シーク可能な読み取りストリームを受け取ります:
// Load the signed message into a stream
using (var stream = File.OpenRead(signedMessagePath))
{
DkimResult result = DkimVerifier.Verify(stream);
if (result.IsValid)
{
Console.WriteLine("[OK] DKIM signature verified successfully from stream!");
}
else
{
Console.WriteLine("[FAIL] Verification failed!");
Console.WriteLine($"Error: {result.ErrorMessage}");
}
}
Console.WriteLine("\nStream-based verification is useful when:");
Console.WriteLine(" - Working with email from IMAP/POP3 clients");
Console.WriteLine(" - Processing emails from databases or cloud storage");
Console.WriteLine(" - Building web applications that receive emails via HTTP");
Verify(Stream) および Verify(string) 機能的には同一です — どちらも DNS 参照が完了するまでブロックします。ユーザー向けの処理には下記の非同期バリアントを使用してください。
非同期検証
DKIM 検証は DNS の往復が必要で、速いネットワークでも数十ミリ秒かかります。ASP.NET のリクエストハンドラやメッセージ処理ループ、UI スレッド内で実行するとすぐに蓄積します。 VerifyAsync 戻り値は Task<DkimResult> できることを宣言します await:
if (!File.Exists(signedMessagePath))
{
Console.WriteLine("Signed message file not found.");
return;
}
// Async verification
DkimResult result = await DkimVerifier.VerifyAsync(signedMessagePath);
if (result.IsValid)
{
Console.WriteLine("[OK] Async verification successful!");
}
else
{
Console.WriteLine("[FAIL] Async verification failed!");
Console.WriteLine($"Error: {result.ErrorMessage}");
}
Console.WriteLine("\nAsync verification is recommended for:");
Console.WriteLine(" - Web applications (ASP.NET, etc.)");
Console.WriteLine(" - Services that process multiple emails");
Console.WriteLine(" - Any application where you want to avoid blocking the main thread");
また、 VerifyAsync(Stream) オーバーロードです。同期 API と同様に、このメソッドは async Task (非 async void) — 呼び出し側は await それと、コンソールアプリのエントリポイントは VerifySignatureAsync().GetAwaiter().GetResult() があるときに async Main 利用できません。
エンドツーエンド: ロード、署名、保存、検証
上記の要素を組み合わせると単一のワークフローになります。これはトランザクションメールサービスに組み込む完全な署名・検証パイプラインです:
// Step 1: Load private key
var rsa = PemReader.GetPrivateKey(PrivateKeyPath);
// Step 2: Create signature information
var signInfo = new DKIMSignatureInfo("dkim", "example.com")
{
HeaderCanonicalization = CanonicalizationType.Relaxed,
BodyCanonicalization = CanonicalizationType.Relaxed,
HashAlgorithm = DKIMHashAlgorithm.RSASha256,
Headers =
{
"From",
"To",
"Subject",
"Date",
"MIME-Version"
}
};
// Step 3: Create email message
var mailMessage = new MailMessage(
"john.doe@example.com",
"jane.smith@example.org",
"Important Document - DKIM Signed",
"Please find the attached document.\n\nBest regards,\nJohn Doe");
mailMessage.From = new MailAddress("john.doe@example.com", "John Doe");
mailMessage.To.Add(new MailAddress("jane.smith@example.org", "Jane Smith"));
mailMessage.Date = DateTime.UtcNow;
mailMessage.Headers.Add("MIME-Version", "1.0");
// Step 4: Sign the message
var signedMessage = mailMessage.DKIMSign(rsa, signInfo);
// Step 5: Save the signed message
string signedPath = Path.Combine(DataDir, "end-to-end-signed.eml");
signedMessage.Save(signedPath);
// Step 6: Verify the signature
var result = DkimVerifier.Verify(signedPath);
if (result.IsValid)
{
Console.WriteLine(" [OK] Signature verification successful!");
Console.WriteLine(" [OK] The message integrity is confirmed.");
Console.WriteLine(" [OK] The sender domain is authenticated.");
}
else
{
Console.WriteLine($" [FAIL] Verification failed: {result.ErrorMessage}");
}
// Step 7: Display DKIM-Signature header
Console.WriteLine("\nStep 7: DKIM-Signature header:");
Console.WriteLine(" " + signedMessage.Headers["DKIM-Signature"]);
完全な DKIM-Signature このコードが生成するヘッダーは次のようになります:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com;
h=from:to:subject:date:mime-version; q=dns/txt; s=dkim; t=1780252880;
bh=e2RkwEm0k0yLcVmhi3KeXpt14Ppx3Xx49S33/+J8OGc=;
b=Zd3sfCGGD9YdOLw1o3YHLFs7DO6V9cc7anye5E3kQ/MEdkU14daTru0sXTZCtCryvJubCs...
興味深いタグ:
v=1— DKIM バージョン。a=rsa-sha256— アルゴリズム;これはHashAlgorithm.c=relaxed/relaxed— 正規化(ヘッダー/本文)。d=example.com/s=dkim— セレクタとドメイン;これらが組み合わさって DNS 参照名を構成します。h=from:to:subject:date:mime-version— 順序どおりの署名ヘッダー。bh=— 正規化された本文の base64 ハッシュ。b=— 正規化されたヘッダー全体に対する実際の RSA 署名。
署名されたヘッダー値を変更したり、本文を改変した場合、 bh および b 一致しなくなり、検証が失敗します。
DNS に公開鍵を公開する
ローカルで署名するだけでは不十分です。受信側が検証できるように、RSA 鍵の公開側を DNS TXT レコードとして次の場所に公開する必要があります: <selector>._domainkey.<domain>:
Record: dkim._domainkey.example.com
Type: TXT
Value: v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC...
OpenSSL を使用して PEM から公開鍵を抽出:
openssl rsa -in sample-private-key.pem -pubout -out sample-public-key.pem
ヘッダー/フッター行を除去し、ベース64 コンテンツを連結して p= 値。レコードが有効になることを確認してから使用してください:
dig dkim._domainkey.example.com TXT
Online tools like DKIM バリデータ および MXToolbox DKIM チェッカー 実際に送信したメッセージがエンドツーエンドで検証可能かどうかを教えてくれます。運用トラフィックに切り替える前に使用してください。