امضای ایمیل‌ها با DKIM برای اعتبارسنجی و تحویل‌پذیری

DKIM (DomainKeys Identified Mail، RFC 6376) به مالک دامنه اجازه می‌دهد امضای رمزنگاری‌شده‌ای به ایمیل خروجی اضافه کند. سرورهای دریافت‌کننده کلید عمومی متناظر را از DNS می‌گیرند، هش را محاسبه می‌شوند و دو مورد را همزمان تأیید می‌کنند: بدنه پیام در مسیر تغییر نیافته و پیام واقعاً از فرستنده‌ای مجاز توسط دامنه آمده است. بدون DKIM، ارائه‌دهندگان مدرن (Gmail، Microsoft 365، Yahoo) ایمیل را به عنوان هرزنامه نشان می‌دهند، به‌صورت ساکت حذف می‌کنند یا تحویل را رد می‌کنند — DKIM دیگر برای ایمیل تولیدی اختیاری نیست.

این راهنما یک راهنمای کامل برای Aspose.Emailفضای نام DKIM برای .NET. هر بلوک کد در این مقاله از یک مثال اجرایی در DKIMExamples پروژه و به‌صورت انتها به انتها در برابر کتابخانه تأیید شده است.

پیش‌نیازها

نوع‌های DKIM در Aspose.Email.DKIM فضای نام و فقط با نسخه‌های .NET Framework 4.0/4.5 این کتابخانه ارائه می‌شوند. آن‌ها عمداً از بسته‌های .NET Standard 2.0 و .NET 6/8 حذف شده‌اند. برای پیروی از مثال‌های زیر، پروژه شما باید یک اسمبلی Aspose.Email ساخته‌شده برای 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 نیاز دارید. کلید خصوصی در برنامه شما نگهداری می‌شود و برای امضا استفاده می‌شود؛ کلید عمومی به عنوان یک رکورد TXT DNS منتشر می‌شود تا دریافت‌کنندگان بتوانند تأیید کنند. کلید 2048‑بیتی امروز استاندارد است:

openssl genrsa -out sample-private-key.pem 2048

از Aspose PemReader هر دو PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) و PKCS#8 (-----BEGIN PRIVATE KEY-----) فایل‌های قالب‌بندی‌شده.

| کلاس | نقش | |——-|——| | DKIMSignatureInfo | امضای پیام را توصیف می‌کند: انتخابگر، دامنه، الگوریتم هش، قانون‌گذاری و سرآیندهای پوشش‌داده‌شده. | | PemReader | کلید خصوصی RSA را از فایل یا جریان PEM‑فرمت شده به یک 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)}...");

دو آرگومان برای DKIMSignatureInfo"dkim" و "example.com" — این‌ها انتخابگر و دامنه هستند. با هم به دریافت‌کنندگان می‌گویند کلید عمومی شما را کجا جستجو کنند: در رکورد TXT DNS dkim._domainkey.example.com. یک نام کوتاه برای انتخابگر انتخاب کنید (معمولاً default, mail، یا تاریخ مشابه 2026jan); انتخابگر به شما اجازه می‌دهد کلیدها را بدون حذف کلید قبلی چرخانید.

یک جزئیات حیاتی: DKIMSign یک جدید برمی‌گرداند MailMessage instance با 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 دو الگوریتم امضا را پشتیبانی می‌کند. پیش‌فرض در 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 در مقابل a=rsa-sha256). دریافت‌کنندگان از این برچسب برای تصمیم‌گیری دربارهٔ هش مورد محاسبه مجدد استفاده می‌کنند.

استانداردسازی — ساده در مقابل منعطف

استانداردسازی نحوهٔ توافق DKIM بر یک شکل بایت‑دقیق ثابت پیام قبل از هش کردن است. دو الگوریتم به‌صورت مستقل برای هدرها و بدنه‌ها تعریف شده‌اند:

  • 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) بارگذاری بیش برای مواردی که کلید از یک مخزن کلید، BLOB رمزنگاری‌شده یا منبع تعبیه‌شده می‌آید، نه از سیستم فایل.

قواعد مدیریت کلید:

  1. هرگز کلیدهای خصوصی را به کنترل نسخه کامیت نکنید. *.pem در .gitignore.
  2. در محیط تولید، کلیدها را از یک مدیر رمز (Azure Key Vault، AWS Secrets Manager، HashiCorp Vault) بارگذاری کنید، نه از دیسک.
  3. کلیدها را هر 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 همواره با خطای «کلید در فرهنگ‌نامه نیست» یا خطای مشابه جستجوی 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 شما باید به‌عنوان رکورد TXT DNS در <selector>._domainkey.<domain>:

Record: dkim._domainkey.example.com
Type:   TXT
Value:  v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC...

کلید عمومی را از PEM با OpenSSL استخراج کنید:

openssl rsa -in sample-private-key.pem -pubout -out sample-public-key.pem

سطرهای هدر/فوتر را حذف کنید و محتوای base64 را برای p= مقدار. سپس قبل از تکیه بر آن، تأیید کنید که رکورد فعال است:

dig dkim._domainkey.example.com TXT

ابزارهای آنلاین مانند اعتبارسنج DKIM و بررسی‌کننده DKIM MXToolbox به شما می‌گوید آیا پیام واقعی که ارسال می‌کنید به‌صورت انتها به انتها قابل تأیید است یا نه. قبل از انتقال ترافیک تولیدی از آن‌ها استفاده کنید.