認証と配信可能性のための 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) 鍵がキーボールト、暗号化ブロブ、埋め込みリソースから取得されるケース用のオーバーロードです。

鍵取り扱いルール:

  1. 秘密鍵をバージョン管理システムにコミットしないでください。 *.pem に属します .gitignore.
  2. 本番環境では、ディスクからではなくシークレットマネージャ(Azure Key Vault、AWS Secrets Manager、HashiCorp Vault など)から鍵をロードします。
  3. キーは 6〜12 ヶ月ごとにローテーションしてください。新しいセレクタを先に公開し、署名を切り替え、猶予期間後に古い DNS レコードを削除します。

ファイルから署名済みメッセージを検証する

検証は対称操作です: 署名されたデータが与えられたときに .eml, DkimVerifier を抽出します DKIM-Signature ヘッダーで、次の DNS 参照を行います <selector>._domainkey.<domain> 公開鍵を取得し、ハッシュを再計算します。その結果は DkimResultIsValid および 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.com DKIM レコードを公開しません。実際の検証テストには管理下のドメイン(または既知のテストメッセージ)を使用してください。

ストリームからの検証

メッセージがディスク上にない場合 — 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 チェッカー 実際に送信したメッセージがエンドツーエンドで検証可能かどうかを教えてくれます。運用トラフィックに切り替える前に使用してください。