חתימה על אימיילים עם 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, או אחסון 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 ייתן לך לדעת אם הודעה אמיתית שאתה שולח ניתנת לאימות מקצה לקצה. השתמש בה לפני העברת תנועת ייצור.