توقيع الرسائل باستخدام 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. يُحتفظ بالمفتاح الخاص في تطبيقك ويُستخدم للتوقيع؛ يُنشر المفتاح العام كسجل 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 | يحمل مفتاح 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" — هما المحدد والنطاق. معًا يخبران المستلمين أين يبحثون عن المفتاح العام الخاص بك: في سجل 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 خوارزميتي توقيع. الافتراضي في 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) تجاوز للحالات التي يأتي فيها المفتاح من خزنة مفاتيح، أو كتلة مشفرة، أو مورد مدمج بدلاً من نظام الملفات.

قواعد التعامل مع المفاتيح:

  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 أو تخزين كتل — استخدم 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 أو حلقة معالجة الرسائل أو أي خيط واجهة مستخدم، يتراكم ذلك بسرعة. 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) تجاوز. كما هو الحال مع واجهة برمجة التطبيقات المتزامنة، تُعلن الطريقة عن 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 سوف يخبرك ما إذا كانت الرسالة الحقيقية التي ترسلها قابلة للتحقق من طرف إلى طرف. استخدمها قبل تحويل حركة الإنتاج.