E‑mails ondertekenen met DKIM voor authenticatie en leverbaarheid

DKIM (DomainKeys Identified Mail, RFC 6376) laat een domeineigenaar een cryptografische handtekening aan uitgaand e‑mail toevoegen. Ontvangende servers halen de bijbehorende publieke sleutel op uit DNS, herberekenen de hash en bevestigen twee zaken tegelijk: de berichtinhoud is niet gewijzigd tijdens transport, en het bericht komt daadwerkelijk van een afzender die bevoegd is door het domein. Zonder DKIM markeren moderne providers (Gmail, Microsoft 365, Yahoo) e‑mail als spam, laten ze stilletjes vallen, of weigeren ze aflevering — DKIM is niet langer optioneel voor productie‑mail.

Deze gids is een volledige walkthrough van Aspose.Email’s DKIM‑namespace voor .NET. Elk code‑blok in dit artikel is afkomstig uit een uitvoerbaar voorbeeld in de DKIMExamples project en is end‑to‑end geverifieerd tegen de bibliotheek.

Voorvereisten

DKIM‑types bevinden zich in de Aspose.Email.DKIM namespace en alleen meegeleverd met de .NET Framework 4.0/4.5 builds van de bibliotheek. Ze zijn bewust uitgesloten van de .NET Standard 2.0‑ en .NET 6/8‑pakketten. Om de onderstaande voorbeelden te volgen, moet je project refereren naar een Aspose.Email‑assembly gebouwd voor net45 (doel net48 in de consumerende .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>

Je hebt ook een RSA‑sleutelpaar nodig. De private‑key wordt in je applicatie bewaard en gebruikt om te ondertekenen; de public‑key wordt gepubliceerd als een DNS TXT‑record zodat ontvangers kunnen verifiëren. Een 2048‑bit sleutel is tegenwoordig de standaard:

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

Aspose’s PemReader accepteert zowel PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) en PKCS#8 (-----BEGIN PRIVATE KEY-----) geformatteerde bestanden.

| Klasse | Rol | |——-|——| | DKIMSignatureInfo | Beschrijft de handtekening: selector, domein, hash‑algoritme, canonicalisatie en welke headers te dekken. | | PemReader | Laadt een RSA‑private‑key vanuit een PEM‑geformatteerd bestand of stream in een RSACryptoServiceProvider. | | MailMessage.DKIMSign(rsa, info) | Extensie/instance‑methode die een ondertekende kopie van het bericht retourneert met de DKIM-Signature header ingevuld. | | DkimVerifier | Voert DNS‑gebaseerde verificatie uit van een ondertekend bericht — bestand, stream, sync of async. |

De volledige stroom is altijd: sleutel laden → handtekening beschrijven → bericht bouwen → ondertekenen → (optioneel) verifiëren.

Het minimale ondertekeningsvoorbeeld

Dit is de kleinste hoeveelheid code die een geldig DKIM-ondertekend bericht produceert:

// 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 twee argumenten voor DKIMSignatureInfo"dkim" en "example.com" — zijn de selector en het domein. Samen vertellen ze ontvangers waar ze je publieke sleutel moeten opzoeken: in het DNS TXT‑record dkim._domainkey.example.com. Kies een korte selectornaam (vaak default, mail, of een datum zoals 2026jan); de selector laat je sleutels roteren zonder de oude weg te gooien.

Een kritisch detail: DKIMSign geeft een nieuw MailMessage instantie met de DKIM-Signature header toegevoegd. Het oorspronkelijke bericht blijft ongewijzigd. Sla het geretourneerde object op of verzend het, niet de invoer.

Kiezen welke headers te ondertekenen

De headers vermeld in signInfo.Headers zijn de enige waarvan de waarden gebonden zijn aan de handtekening. Alles wat niet wordt genoemd kan door relais worden aangepast zonder de handtekening te breken, wat soms gewenst is (bijv. Received: headers toegevoegd stroomafwaarts) en soms gevaarlijk (bijv. het laten staan From ondertekend maakt de handtekening zinloos).

Vuistregel: onderteken altijd From, To, Subject, Date. From is in het bijzonder verplicht voor DMARC‑alignement.

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

Als u een lijst maakt Date maar stel nooit in mailMessage.Date, DKIMSign gooit The following headers to be signed do not exist: Date. Vul eerst de headers in, onderteken daarna.

Hash‑algoritmen — RSA‑SHA1 vs RSA‑SHA256

DKIM ondersteunt twee ondertekenings‑algoritmen. De standaard in Aspose.Email is RSASha1 voor achterwaartse compatibiliteit, maar elke nieuwe implementatie zou moeten gebruiken RSASha256. Veel providers markeren nu SHA‑1‑handtekeningen als ongeldig, zelfs wanneer ze cryptografisch verifieerbaar zijn.

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

Het resulterende DKIM-Signature header adverteert het algoritme via de a= tag (a=rsa-sha1 tegen a=rsa-sha256). Ontvangers raadplegen die tag om te bepalen welke hash ze moeten recomputen.

Canonisering — Simpel vs Versoepeld

Canonisering is hoe DKIM akkoord gaat over een stabiele, byte‑exacte vorm van het bericht vóór het hashen. Twee algoritmen worden gedefinieerd voor respectievelijk headers en bodies:

  • Simple — bijna geen normalisatie. Lege regeleinden aan het einde van de body worden verwijderd, maar andere witruimte blijft onaangeroerd. Een enkele spatie die door een relais wordt toegevoegd, zal de handtekening ongeldig maken.
  • Relaxed — vouwt witruimte binnen header‑waarden, maakt header‑namen kleiner, en reduceert opeenvolgende witruimtes in de body. Tolerant voor de triviale herformattering die mail‑transport‑agents vaak doen.

Kies Relaxed voor zowel header als body tenzij u een specifieke reden hebt om dat niet te doen. Bijna elke productie‑implementatie doet dat.

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

Header‑ en body‑canonisering zijn onafhankelijk — relaxed/simple is een volkomen geldige combinatie, op die manier geschreven in de c= tag van de DKIM‑header.

Het laden van de privésleutel

PemReader handelt beide PEM‑coderingen transparant af. Er is geen aparte Pkcs1Reader / Pkcs8Reader — geef gewoon het pad of de stream op:

// 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 geeft een RSACryptoServiceProvider klaar om af te geven aan DKIMSign. Er is ook een GetPrivateKey(Stream) overload voor gevallen waarin de sleutel afkomstig is uit een sleutelkluis, versleutelde blob, of ingebedde resource in plaats van het bestandssysteem.

Regels voor sleutelhåndtering:

  1. Commit nooit privésleutels naar versiebeheer. *.pem hoort bij .gitignore.
  2. In productie, laad sleutels uit een geheimen‑beheerder (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault), niet vanaf schijf.
  3. Roteer sleutels elke 6–12 maanden. Publiceer eerst de nieuwe selector, schakel de ondertekening over, en trek daarna het oude DNS‑record terug na een respijtperiode.

Een ondertekend bericht verifiëren vanuit een bestand

Verificatie is de symmetrische bewerking: gegeven een ondertekend .eml, DkimVerifier haalt het DKIM-Signature header, voert een DNS‑lookup uit tegen <selector>._domainkey.<domain> voor de openbare sleutel, en recomputeert de hash. Het resultaat is een DkimResult met IsValid en 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");

Verifiëren van een bericht ondertekend met d=example.com zal altijd falen met "key not in dictionary" of een soortgelijke DNS‑lookup‑fout, omdat example.com publiceert geen DKIM‑record. Gebruik een domein dat u beheert (of een bekend goed testbericht) voor echte verificatietests.

Verifiëren vanuit een stream

Wanneer het bericht niet op schijf staat — het kwam van een IMAP‑fetch, een wachtrij, een HTTP‑upload, of blob‑opslag — gebruik de Stream overload. Het accepteert elke zoekbare lees‑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) en Verify(string) zijn functioneel identiek — beide blokkeren tot de DNS‑lookup voltooid is. Voor alles dat de gebruiker ziet, gebruik de async‑varianten hieronder.

Async‑verificatie

DKIM‑verificatie omvat een DNS‑round‑trip, die tientallen milliseconden kan duren, zelfs op een snel netwerk. In een ASP.NET‑request‑handler, berichtverwerkingslus, of welke UI‑thread dan ook, stapelt dat zich snel op. VerifyAsync retourneert een Task<DkimResult> dat u kunt 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");

Er is ook een VerifyAsync(Stream) overload. Net als bij de sync‑API, geeft de methode aan async Task (niet async void) — call sites moeten await het, en console‑app entrypoints kunnen doen VerifySignatureAsync().GetAwaiter().GetResult() wanneer een async Main is niet beschikbaar.

Eind‑tot‑eind: laden, ondertekenen, opslaan, verifiëren

De bovenstaande onderdelen worden samengevoegd in één workflow. Dit is de volledige onderteken‑en‑verifieer‑pipeline die u zou embedden in een transactionele‑e‑mailservice:

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

Een volledige DKIM-Signature header gegenereerd door deze code ziet er als volgt uit:

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 interessante tags:

  • v=1 — DKIM‑versie.
  • a=rsa-sha256 — algoritme; weerspiegelt HashAlgorithm.
  • c=relaxed/relaxed — canonisering (header/body).
  • d=example.com / s=dkim — selector en domein; samen vormen ze de DNS‑opzoekingsnaam.
  • h=from:to:subject:date:mime-version — ondertekende headers, in volgorde.
  • bh= — base64‑hash van de gecanoniseerde body.
  • b= — de feitelijke RSA‑handtekening over de gecanoniseerde headers.

Als u enige ondertekende header‑waarde wijzigt, of de body aanpast, bh en b komen niet meer overeen en verificatie faalt.

Publiceren van de openbare sleutel in DNS

Lokaal ondertekenen is slechts de helft van de taak. Voor ontvangers om te verifieren, moet het publieke tegenhanger van uw RSA‑sleutel gepubliceerd worden als een DNS‑TXT‑record op <selector>._domainkey.<domain>:

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

Extraheer de openbare sleutel uit uw PEM met OpenSSL:

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

Verwijder de header/footer‑regels en concateneer de base64‑inhoud voor de p= waarde. Bevestig vervolgens dat het record live is voordat u er op vertrouwt:

dig dkim._domainkey.example.com TXT

Online‑tools zoals DKIM‑validator en MXToolbox DKIM‑checker zal u vertellen of een echt bericht dat u verzendt van begin tot eind verifieerbaar is. Gebruik ze voordat u het productie‑verkeer omschakelt.