Signera e‑post med DKIM för autentisering och leveransbarhet

DKIM (DomainKeys Identified Mail, RFC 6376) låter en domänägare bifoga en kryptografisk signatur till utgående e‑post. Mottagande servrar hämtar den matchande offentliga nyckeln från DNS, beräknar hash igen och bekräftar två saker samtidigt: meddelandekroppen har inte ändrats under transport, och meddelandet kom verkligen från en avsändare som är auktoriserad av domänen. Utan DKIM flaggar moderna leverantörer (Gmail, Microsoft 365, Yahoo) e‑post som skräppost, släpper den tyst eller vägrar leverans — DKIM är inte längre valfri för produktionsmail.

Denna guide är en komplett genomgång av Aspose.Emails DKIM‑namnutrymme för .NET. Varje kodblock i den här artikeln är hämtat från ett körbart exempel i DKIMExempel projektet och har verifierats end‑to‑end mot biblioteket.

Förutsättningar

DKIM‑typer finns i Aspose.Email.DKIM namnutrymme och publiceras endast med .NET Framework 4.0/4.5‑byggnader av biblioteket. De är avsiktligt uteslutna från .NET Standard 2.0 och .NET 6/8‑paketen. För att följa exemplen nedan måste ditt projekt referera en Aspose.Email‑samling byggd för net45 (mål net48 i den konsumerande .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>

Du behöver också ett RSA‑nyckelpar. Den privata nyckeln hålls i din applikation och används för att signera; den offentliga nyckeln publiceras som en DNS TXT‑post så att mottagare kan verifiera. En 2048‑bit nyckel är standard idag:

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

Asposes PemReader accepterar både PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) och PKCS#8 (-----BEGIN PRIVATE KEY-----) formaterade filer.

| Klass | Roll | |——-|——| | DKIMSignatureInfo | Beskriver signaturen: selektor, domän, hash‑algoritm, kanonisering och vilka rubriker som ska täckas. | | PemReader | Laddar en RSA‑privatnyckel från en PEM‑formaterad fil eller ström till en RSACryptoServiceProvider. | | MailMessage.DKIMSign(rsa, info) | Utökning/instansmetod som returnerar en signerad kopia av meddelandet med DKIM-Signature rubrik fylld. | | DkimVerifier | Utför DNS‑baserad verifiering av ett signerat meddelande — fil, ström, synk eller async. |

Den fullständiga flödet är alltid: ladda nyckel → beskriva signatur → bygga meddelande → signera → (valfritt) verifiera.

Det minimala signeringsexemplet

Detta är den minsta mängden kod som producerar ett giltigt DKIM‑signerat meddelande:

// 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)}...");

De två argumenten till DKIMSignatureInfo"dkim" och "example.com" — är selektorn och domänen. Tillsammans berättar de för mottagare var de ska slå upp din offentliga nyckel: i DNS TXT-posten dkim._domainkey.example.com. Välj ett kort selektornamn (ofta default, mail, eller ett datum som 2026jan); så låter selektorn dig rotera nycklar utan att kasta bort den gamla.

En kritisk detalj: DKIMSign returnerar en ny MailMessage instans med DKIM-Signature rubrik tillagd. Det ursprungliga meddelandet är oförändrat. Spara eller skicka det returnerade objektet, inte indatan.

Välja vilka rubriker som ska signeras

Rubrikerna som listas i signInfo.Headers är de enda vars värden är bundna till signaturen. Allt som inte listas kan ändras av reläer utan att bryta signaturen, vilket ibland är önskvärt (t.ex. Received: rubriker som läggs till nedströms) och ibland farliga (t.ex. att lämna From osignerade gör signaturen meningslös).

Regel av tumme: signera alltid From, To, Subject, Date. From är särskilt obligatorisk för DMARC‑anpassning.

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);

Om du listar Date men sätt aldrig mailMessage.Date, DKIMSign kastar The following headers to be signed do not exist: Date. Fyll alltid i rubrikerna först, signera sedan.

Hash‑algoritmer — RSA‑SHA1 vs RSA‑SHA256

DKIM stöder två signeringsalgoritmer. Standard i Aspose.Email är RSASha1 för bakåtkompatibilitet, men varje ny distribution bör använda RSASha256. Många leverantörer markerar nu SHA‑1‑signaturer som ogiltiga även när de kryptografiskt verifieras.

// 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);

Det resulterande DKIM-Signature rubrik annonserar algoritmen via a= tagg (a=rsa-sha1 mot a=rsa-sha256). Mottagare konsulterar den taggen för att bestämma vilken hash som ska beräknas om.

Kanonisering — Simple vs Relaxed

Kanonisering är hur DKIM kommer överens om en stabil, byte‑exakt form av meddelandet före hashning. Två algoritmer definieras för rubriker och kroppar var för sig:

  • Simple — nästan ingen normalisering. Tomma avslutande rader i kroppen tas bort, men inget annat blanksteg förändras. Ett enda mellanslag som läggs till av en relä kommer att ogiltigförklara signaturen.
  • Relaxed — vik in blanksteg i rubrikvärden, gör rubriknamn gemena och kollapsar sekvenser av blanksteg i kroppen. Tolerant mot den triviala omformatering som e‑post‑transportagenter ofta utför.

Välj Relaxed för både rubrik och kropp såvida du inte har ett specifikt skäl att undvika det. Nästan varje produktionsimplementation gör det.

// 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);

Rubrik‑ och kroppskanonisering är oberoende — relaxed/simple är en fullt giltig kombination, skriven så i c= tagg för DKIM‑rubriken.

Laddning av den privata nyckeln

PemReader hanterar båda PEM‑kodningarna transparent. Det finns ingen separat Pkcs1Reader / Pkcs8Reader — ange bara sökvägen eller strömmen:

// 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 returnerar ett RSACryptoServiceProvider redo att överlämna till DKIMSign. Det finns också en GetPrivateKey(Stream) överbelastning för fall där nyckeln kommer från ett nyckelvalv, krypterad blob eller inbäddad resurs snarare än filsystemet.

Regler för nyckelhantering:

  1. Lagra aldrig privata nycklar i versionskontroll. *.pem tillhör .gitignore.
  2. I produktion, ladda nycklar från en hemlighets‑hanterare (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault), inte från disk.
  3. Rotera nycklar var 6–12:e månad. Publicera den nya selectorn först, byt signering, och dra sedan tillbaka den gamla DNS‑posten efter en övergångsperiod.

Verifiera ett signerat meddelande från en fil

Verifiering är den symmetriska operationen: givet en signerad .eml, DkimVerifier extraherar DKIM-Signature rubrik, utför ett DNS‑uppslag mot <selector>._domainkey.<domain> för den offentliga nyckeln, och beräknar om hash‑värdet. Resultatet är en DkimResult med IsValid och 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");

Verifiera ett meddelande signerat med d=example.com kommer alltid att misslyckas med "key not in dictionary" eller ett liknande DNS‑uppslagsfel, eftersom example.com publicerar inte en DKIM‑post. Använd en domän du kontrollerar (eller ett känt bra testmeddelande) för verkliga verifieringstester.

Verifiering från en ström

När meddelandet inte finns på disk — det kom från en IMAP‑hämtning, en kö, en HTTP‑uppladdning eller blob‑lagring — använd Stream överbelastning. Den accepterar vilken sökbar läs‑ström som helst:

// 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) och Verify(string) är funktionellt identiska — båda blockerar tills DNS‑uppslag slutförs. För allt användar‑fokuserat, använd de asynkrona varianterna nedan.

Asynkron verifiering

DKIM‑verifiering innebär en DNS‑rundresa, vilket kan ta tiotals millisekunder även på ett snabbt nätverk. Inuti en ASP.NET‑begäranshanterare, meddelande‑bearbetningsloop eller någon UI‑tråd, adderas detta snabbt. VerifyAsync returnerar en Task<DkimResult> att du kan 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");

Det finns också en VerifyAsync(Stream) överbelastning. Precis som med sync‑API:n deklarerar metoden async Task (inte async void) — anropsställen bör await det, och konsol‑app‑ingångspunkter kan göra VerifySignatureAsync().GetAwaiter().GetResult() när en async Main är inte tillgänglig.

End‑to‑end: ladda, signera, spara, verifiera

Delarna ovan kombineras till ett enda arbetsflöde. Detta är den fullständiga signera‑och‑verifiera‑pipeline du skulle bädda in i en transaktions‑e‑posttjänst:

// 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"]);

En komplett DKIM-Signature rubrik producerad av denna kod ser ut så här:

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...

De intressanta taggarna:

  • v=1 — DKIM‑version.
  • a=rsa-sha256 — algoritm; speglar HashAlgorithm.
  • c=relaxed/relaxed — kanonisering (rubrik/kropp).
  • d=example.com / s=dkim — selector och domän; tillsammans bildar de DNS‑uppslagsnamnet.
  • h=from:to:subject:date:mime-version — signerade rubriker, i ordning.
  • bh= — base64‑hash av den kanoniserade kroppen.
  • b= — den faktiska RSA‑signaturen över de kanoniserade rubrikerna.

Om du ändrar något signerat rubrikvärde, eller modifierar kroppen, bh och b matchar inte längre och verifieringen misslyckas.

Publicering av den offentliga nyckeln i DNS

Lokal signering är bara halva jobbet. För att mottagare ska kunna verifiera måste den offentliga motsvarigheten till din RSA‑nyckel publiceras som en DNS‑TXT‑post på <selector>._domainkey.<domain>:

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

Extrahera den offentliga nyckeln från din PEM med OpenSSL:

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

Ta bort rubrik-/fotraderna och sammanfoga base64‑innehållet för p= värde. Bekräfta sedan att posten är aktiv innan du förlitar dig på den:

dig dkim._domainkey.example.com TXT

Online‑verktyg som DKIM‑validerare och MXToolbox DKIM‑kontroll kommer att tala om huruvida ett riktigt meddelande du skickar är verifierbart från början till slut. Använd dem innan du byter över produktionstrafik.