E‑Mails mit DKIM zur Authentifizierung und Zustellbarkeit signieren
DKIM (DomainKeys Identified Mail, RFC 6376) ermöglicht es einem Domain‑Besitzer, eine kryptografische Signatur an ausgehende E‑Mails anzuhängen. Empfangende Server holen den passenden öffentlichen Schlüssel aus dem DNS, berechnen den Hash neu und bestätigen gleichzeitig zwei Dinge: Der Nachrichtentext wurde während der Übertragung nicht geändert und die Nachricht stammt wirklich von einem vom Domain autorisierten Absender. Ohne DKIM kennzeichnen moderne Anbieter (Gmail, Microsoft 365, Yahoo) Mails als Spam, verwerfen sie stillschweigend oder lehnen die Zustellung ab — DKIM ist für produktiven Mailverkehr nicht mehr optional.
Dieser Leitfaden ist ein vollständiger Durchlauf von Aspose.Emails DKIM‑Namespace für .NET. Jeder Code‑Block in diesem Artikel stammt aus einem ausführbaren Beispiel im DKIMBeispiele Projekt und wurde End‑zu‑End‑gegen die Bibliothek verifiziert.
Voraussetzungen
DKIM‑Typen befinden sich im Aspose.Email.DKIM Namespace und nur mit den .NET Framework 4.0/4.5‑Builds der Bibliothek ausliefern. Sie sind absichtlich aus den .NET Standard 2.0‑ und .NET 6/8‑Paketen ausgeschlossen. Um den unten stehenden Beispielen zu folgen, muss Ihr Projekt eine Aspose.Email‑Assembly referenzieren, die für net45 (Ziel net48 im konsumierenden .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>
Sie benötigen außerdem ein RSA‑Schlüsselpaar. Der private Schlüssel wird in Ihrer Anwendung gehalten und zum Signieren verwendet; der öffentliche Schlüssel wird als DNS‑TXT‑Eintrag veröffentlicht, damit Empfänger verifizieren können. Ein 2048‑Bit‑Schlüssel ist heute Standard:
openssl genrsa -out sample-private-key.pem 2048
Asposes PemReader akzeptiert sowohl PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) und PKCS#8 (-----BEGIN PRIVATE KEY-----) formatierte Dateien.
| Klasse | Rolle | |——-|——| | DKIMSignatureInfo | Beschreibt die Signatur: Selektor, Domain, Hash‑Algorithmus, Kanonisierung und welche Header abgedeckt werden. | | PemReader | Lädt einen RSA‑Privat‑Schlüssel aus einer PEM‑formatierten Datei oder einem Stream in einen RSACryptoServiceProvider. |
| MailMessage.DKIMSign(rsa, info) | Erweiterungs‑/Instanz‑Methode, die eine signierte Kopie der Nachricht mit dem zurückgibt DKIM-Signature Header befüllt. | | DkimVerifier | Führt DNS‑basierte Verifizierung einer signierten Nachricht durch — Datei, Stream, sync oder async. |
Der gesamte Ablauf ist immer: Schlüssel laden → Signatur beschreiben → Nachricht bauen → signieren → (optional) verifizieren.
Das minimale Signatur‑Beispiel
Dies ist die kleinste Menge an Code, die eine gültige DKIM‑signierte Nachricht erzeugt:
// 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)}...");
Die beiden Argumente für DKIMSignatureInfo — "dkim" und "example.com" — sind der Selektor und die Domain. Zusammen sagen sie Empfängern, wo Ihr öffentlicher Schlüssel zu finden ist: im DNS‑TXT‑Eintrag dkim._domainkey.example.com. Wählen Sie einen kurzen Selektornamen (oft default, mail, oder ein Datum wie 2026jan); der Selektor ermöglicht das Rotieren von Schlüsseln, ohne den alten zu verwerfen.
Ein kritisches Detail: DKIMSign gibt ein neues zurück MailMessage Instanz mit dem DKIM-Signature Header hinzugefügt. Die Originalnachricht bleibt unverändert. Speichern oder senden Sie das zurückgegebene Objekt, nicht die Eingabe.
Auswahl, welche Header signiert werden sollen
Die aufgelisteten Header in signInfo.Headers sind die einzigen, deren Werte an die Signatur gebunden sind. Alles, was nicht aufgeführt ist, kann von Relays geändert werden, ohne die Signatur zu brechen, was manchmal erwünscht ist (z. B. Received: Header, die stromabwärts hinzugefügt werden) und manchmal gefährlich (z. B. das Belassen von From ungerichtet macht die Signatur bedeutungslos).
Faustregel: immer signieren From, To, Subject, Date. From ist insbesondere für DMARC‑Abstimmung zwingend erforderlich.
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);
Wenn Sie Date aber setzen Sie nie mailMessage.Date, DKIMSign wirft The following headers to be signed do not exist: Date. Befüllen Sie immer zuerst die Header und signieren Sie dann.
Hash‑Algorithmen — RSA‑SHA1 vs RSA‑SHA256
DKIM unterstützt zwei Signatur‑Algorithmen. Der Standard in Aspose.Email ist RSASha1 für Abwärtskompatibilität, aber jede neue Bereitstellung sollte RSASha256. Viele Anbieter kennzeichnen SHA‑1‑Signaturen jetzt als ungültig, selbst wenn sie kryptografisch verifiziert werden können.
// 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);
Das resultierende DKIM-Signature Header gibt den Algorithmus über das a= Tag (a=rsa-sha1 gegen a=rsa-sha256). Empfänger konsultieren dieses Tag, um zu entscheiden, welchen Hash sie neu berechnen sollen.
Kanonisierung — Simple vs Relaxed
Kanonisierung ist das Verfahren, mit dem DKIM vor dem Hashen eine stabile, byte‑exakte Form der Nachricht definiert. Zwei Algorithmen werden unabhängig für Header und Bodies definiert:
Simple— fast keine Normalisierung. Entfernt abschließende leere Zeilen im Body, aber kein anderes Leerzeichen wird berührt. Ein einzelnes Leerzeichen, das von einem Relay hinzugefügt wird, macht die Signatur ungültig.Relaxed— faltet Leerzeichen innerhalb von Header‑Werten, schreibt Header‑Namen klein und reduziert Leerzeichen‑Folgen im Body. Tolerant gegenüber der trivialen Neuformatierung, die Mail‑Transport‑Agenten häufig durchführen.
Wählen Sie Relaxed für sowohl Header als auch Body, es sei denn, Sie haben einen speziellen Grund, dies nicht zu tun. Fast jede Produktionsumgebung macht das.
// 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‑ und Body‑Kanonisierung sind unabhängig — relaxed/simple ist eine vollkommen gültige Kombination, so geschrieben im c= Tag des DKIM‑Headers.
Laden des privaten Schlüssels
PemReader handhabt beide PEM‑Codierungen transparent. Es gibt kein separates Pkcs1Reader / Pkcs8Reader — geben Sie einfach den Pfad oder Stream an:
// 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 liefert ein RSACryptoServiceProvider bereit für die Übergabe an DKIMSign. Es gibt außerdem einen GetPrivateKey(Stream) Überladung für Fälle, in denen der Schlüssel aus einem Schlüsseltresor, einem verschlüsselten Blob oder einer eingebetteten Ressource stammt, anstatt aus dem Dateisystem.
Regeln für den Umgang mit Schlüsseln:
- Committen Sie niemals private Schlüssel in die Versionskontrolle.
*.pemgehört zu.gitignore. - In der Produktion laden Sie Schlüssel aus einem Secrets‑Manager (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault), nicht von der Festplatte.
- Drehen Sie Schlüssel alle 6–12 Monate. Veröffentlichen Sie zuerst den neuen Selektor, wechseln Sie die Signierung und entfernen Sie den alten DNS‑Eintrag nach einer Übergangsfrist.
Verifizierung einer signierten Nachricht aus einer Datei
Verifizierung ist der symmetrische Vorgang: gegeben ein signiertes .eml, DkimVerifier extrahiert den DKIM-Signature Header, führt einen DNS‑Lookup gegen <selector>._domainkey.<domain> für den öffentlichen Schlüssel, und berechnet den Hash neu. Das Ergebnis ist ein DkimResult mit IsValid und 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");
Verifizierung einer mit
d=example.comwird immer mit „key not in dictionary“ oder einem ähnlichen DNS‑Lookup‑Fehler fehlschlagen, weilexample.comveröffentlicht keinen DKIM‑Eintrag. Verwenden Sie eine Domain, die Sie kontrollieren (oder eine bekannte Testnachricht) für reale Verifizierungstests.
Verifizierung aus einem Stream
Wenn die Nachricht nicht auf der Festplatte liegt — sie stammt von einem IMAP‑Fetch, einer Queue, einem HTTP‑Upload oder Blob‑Speicher — verwenden Sie den Stream Überladung. Sie akzeptiert jeden suchbaren Lesestream:
// 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) und Verify(string) sind funktional identisch — beide blockieren, bis der DNS‑Lookup abgeschlossen ist. Für alles, das Nutzer*innen sieht, verwenden Sie die asynchronen Varianten unten.
Asynchrone Verifizierung
DKIM‑Verifizierung beinhaltet einen DNS‑Round‑Trip, der selbst in einem schnellen Netzwerk Zehntausende Millisekunden dauern kann. In einem ASP.NET‑Request‑Handler, einer Nachrichtenverarbeitungsschleife oder jedem UI‑Thread summiert sich das schnell. VerifyAsync gibt einen Task<DkimResult> dass Sie 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");
Es gibt auch ein VerifyAsync(Stream) Überladung. Wie bei der synchronen API deklariert die Methode async Task (nicht async void) — Aufrufstellen sollten await und Konsolen‑App‑Einstiegspunkte können VerifySignatureAsync().GetAwaiter().GetResult() wenn ein async Main ist nicht verfügbar.
End‑zu‑Ende: Laden, Signieren, Speichern, Verifizieren
Die oben genannten Teile werden zu einem einzigen Workflow kombiniert. Dies ist die komplette Sign‑und‑Verifizierungs‑Pipeline, die Sie in einen transaktionalen E‑Mail‑Service einbetten würden:
// 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"]);
Ein vollständiges DKIM-Signature Der von diesem Code erzeugte Header sieht folgendermaßen aus:
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...
Die interessanten Tags:
v=1— DKIM‑Version.a=rsa-sha256— Algorithmus; spiegeltHashAlgorithm.c=relaxed/relaxed— Kanonisierung (Header/Body).d=example.com/s=dkim— Selektor und Domain; zusammen bilden sie den DNS‑Lookup‑Namen.h=from:to:subject:date:mime-version— signierte Header in Reihenfolge.bh=— Base64‑Hash des kanonisierten Bodys.b=— die eigentliche RSA‑Signatur über die kanonisierten Header.
Wenn Sie einen signierten Header‑Wert ändern oder den Body modifizieren, bh und b passen nicht mehr zusammen und die Verifizierung schlägt fehl.
Veröffentlichung des öffentlichen Schlüssels im DNS
Lokales Signieren ist nur die halbe Arbeit. Damit Empfänger verifizieren können, muss der öffentliche Gegenpart Ihres RSA‑Schlüssels als DNS‑TXT‑Eintrag veröffentlicht werden unter <selector>._domainkey.<domain>:
Record: dkim._domainkey.example.com
Type: TXT
Value: v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC...
Extrahieren Sie den öffentlichen Schlüssel aus Ihrem PEM mit OpenSSL:
openssl rsa -in sample-private-key.pem -pubout -out sample-public-key.pem
Entfernen Sie die Header‑/Footer‑Zeilen und verketten Sie den Base64‑Inhalt für das p= Wert. Bestätigen Sie dann, dass der Eintrag aktiv ist, bevor Sie sich darauf verlassen:
dig dkim._domainkey.example.com TXT
Online‑Tools wie DKIM‑Validator und MXToolbox DKIM‑Checker zeigt Ihnen, ob eine von Ihnen gesendete echte Nachricht durchgängig verifizierbar ist. Verwenden Sie sie, bevor Sie den Produktionsverkehr umstellen.