Podpisywanie wiadomości e‑mail przy użyciu DKIM w celu uwierzytelnienia i dostarczalności
DKIM (DomainKeys Identified Mail, RFC 6376) pozwala właścicielowi domeny dołączyć kryptograficzny podpis do wychodzącej poczty e‑mail. Serwery odbierające pobierają pasujący klucz publiczny z DNS, przeliczają skrót i jednocześnie potwierdzają dwie rzeczy: treść wiadomości nie została zmieniona w tranzycie oraz wiadomość naprawdę pochodzi od nadawcy autoryzowanego przez domenę. Bez DKIM nowoczesni dostawcy (Gmail, Microsoft 365, Yahoo) oznaczają pocztę jako spam, cicho ją odrzucają lub odmawiają dostarczenia — DKIM nie jest już opcjonalny dla produkcyjnej poczty.
Ten przewodnik jest kompletnym przeglądem Aspose.Emailprzestrzeń nazw DKIM Aspose dla .NET. Każdy blok kodu w tym artykule pochodzi z uruchamialnego przykładu w DKIMExamples projekt i został zweryfikowany end‑to‑end względem biblioteki.
Wymagania wstępne
Typy DKIM znajdują się w Aspose.Email.DKIM przestrzeń nazw i udostępnia tylko w wersjach .NET Framework 4.0/4.5 biblioteki. Są celowo wyłączone z pakietów .NET Standard 2.0 oraz .NET 6/8. Aby podążać za przykładami poniżej, projekt musi odwoływać się do zestawu Aspose.Email zbudowanego dla net45 (cel net48 w konsumencie .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>
Potrzebujesz również pary kluczy RSA. Klucz prywatny jest przechowywany w aplikacji i używany do podpisywania; klucz publiczny jest publikowany jako rekord DNS TXT, aby odbiorcy mogli zweryfikować. Standardem dziś jest klucz 2048‑bitowy:
openssl genrsa -out sample-private-key.pem 2048
Aspose’owy PemReader akceptuje zarówno PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) i PKCS#8 (-----BEGIN PRIVATE KEY-----) sformatowane pliki.
| Klasa | Rola | |——-|——| | DKIMSignatureInfo | Opisuje podpis: selektor, domena, algorytm skrótu, kanonikalizacja oraz które nagłówki objąć. | | PemReader | Ładuje prywatny klucz RSA z pliku lub strumienia w formacie PEM do RSACryptoServiceProvider. |
| MailMessage.DKIMSign(rsa, info) | Metoda rozszerzenia/instancji zwracająca podpisaną kopię wiadomości z DKIM-Signature nagłówek wypełniony. | | DkimVerifier | Wykonuje weryfikację opartą na DNS podpisanej wiadomości — plik, strumień, synch., lub asynch. |
Pełny przepływ zawsze wygląda tak: wczytaj klucz → opisz podpis → zbuduj wiadomość → podpisz → (opcjonalnie) zweryfikuj.
Minimalny przykład podpisywania
To najmniejszy fragment kodu, który produkuje prawidłową wiadomość podpisaną 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)}...");
Dwa argumenty do DKIMSignatureInfo — "dkim" i "example.com" — to są selektor i domena. Razem mówią odbiorcom, gdzie szukać twojego klucza publicznego: w rekordzie DNS TXT dkim._domainkey.example.com. Wybierz krótką nazwę selektora (często default, mail, lub datę jak 2026jan); selektor pozwala obracać klucze bez wyrzucania starego.
Krytyczny szczegół: DKIMSign zwraca nowy MailMessage instancji z DKIM-Signature nagłówek dodany. Oryginalna wiadomość pozostaje niezmieniona. Zapisz lub wyślij zwrócony obiekt, a nie wejście.
Wybór, które nagłówki podpisać
Nagłówki wymienione w signInfo.Headers to jedyne, których wartości są powiązane z podpisem. Wszystko, co nie jest wymienione, może być modyfikowane przez przekaźniki bez łamania podpisu, co czasem jest pożądane (np. Received: nagłówki dodane później) i czasami niebezpieczne (np. pozostawienie From niepodpisane czyni podpis bezwartościowym).
Reguła praktyczna: zawsze podpisuj From, To, Subject, Date. From szczególnie jest obowiązkowe dla dopasowania 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);
Jeśli wymienisz Date ale nigdy nie ustawiaj mailMessage.Date, DKIMSign rzuca The following headers to be signed do not exist: Date. Zawsze najpierw wypełnij nagłówki, a potem podpisz.
Algorytmy hash — RSA‑SHA1 vs RSA‑SHA256
DKIM obsługuje dwa algorytmy podpisywania. Domyślnie w Aspose.Email jest RSASha1 dla kompatybilności wstecznej, ale każde nowe wdrożenie powinno używać RSASha256. Wielu dostawców teraz oznacza podpisy SHA‑1 jako nieważne, nawet jeśli kryptograficznie się weryfikują.
// 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);
Wynikowy DKIM-Signature nagłówek informuje o algorytmie poprzez a= znacznik (a=rsa-sha1 vs a=rsa-sha256). Odbiorcy odwołują się do tego znacznika, aby zdecydować, który hash przeliczyć ponownie.
Kanonizacja — prosta vs zrelaksowana
Kanonizacja to sposób, w jaki DKIM ustala stabilną, bitową formę wiadomości przed haszowaniem. Dla nagłówków i treści zdefiniowano dwa algorytmy niezależnie:
Simple— prawie brak normalizacji. Puste linie na końcu treści są usuwane, ale żadne inne białe znaki nie są zmieniane. Pojedyncza spacja dodana przez przekaźnik unieważni podpis.Relaxed— zwija białe znaki wewnątrz wartości nagłówków, zamienia nazwy nagłówków na małe litery i redukuje ciągi białych znaków w treści. Toleruje trywialne formatowanie, które wykonują agenty transportu poczty.
Wybierz Relaxed zarówno dla nagłówka, jak i treści, chyba że masz konkretny powód, aby tego nie robić. Prawie każde wdrożenie produkcyjne tak postępuje.
// 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);
Kanonizacja nagłówka i treści są niezależne — relaxed/simple jest w pełni dopuszczalnym połączeniem, napisanym w ten sposób w c= znacznik nagłówka DKIM.
Ładowanie klucza prywatnego
PemReader obsługuje oba kodowania PEM transparentnie. Nie ma osobnego Pkcs1Reader / Pkcs8Reader — po prostu podaj ścieżkę lub strumień:
// 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 zwraca RSACryptoServiceProvider gotowy do przekazania DKIMSign. Istnieje również GetPrivateKey(Stream) przeciążenie dla przypadków, gdy klucz pochodzi z sejfu kluczy, zaszyfrowanego blobu lub zasobu osadzonego, a nie z systemu plików.
Zasady obsługi kluczy:
- Nigdy nie zapisuj kluczy prywatnych w systemie kontroli wersji.
*.pemnależy do.gitignore. - W środowisku produkcyjnym ładuj klucze z menedżera tajemnic (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault), a nie z dysku.
- Obracaj klucze co 6–12 miesięcy. Najpierw opublikuj nowy selektor, przełącz podpisywanie, a potem wycofaj stary rekord DNS po okresie przejściowym.
Weryfikacja podpisanej wiadomości z pliku
Weryfikacja to symetryczna operacja: mając podpisany .eml, DkimVerifier wyodrębnia DKIM-Signature nagłówek, wykonuje zapytanie DNS przeciwko <selector>._domainkey.<domain> dla klucza publicznego i przelicza hash. Wynik to DkimResult z IsValid i 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");
Weryfikacja wiadomości podpisanej przy użyciu
d=example.comzawsze zakończy się niepowodzeniem z błędem „key not in dictionary” lub podobnym błędem DNS, ponieważexample.comnie publikuje rekordu DKIM. Użyj domeny, którą kontrolujesz (lub wiadomości testowej o znanej dobrej reputacji) do rzeczywistych testów weryfikacji.
Weryfikacja ze strumienia
Gdy wiadomość nie znajduje się na dysku — pochodzi z pobrania IMAP, kolejki, uploadu HTTP lub przechowywania blob — użyj Stream przeciążenie. Przyjmuje dowolny strumień odczytu z możliwością przewijania:
// 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) i Verify(string) są funkcjonalnie identyczne — obie blokują do czasu zakończenia zapytania DNS. Dla wszystkiego, co widoczne dla użytkownika, użyj asynchronicznych wariantów poniżej.
Asynchroniczna weryfikacja
Weryfikacja DKIM wymaga rundy DNS, co może zająć dziesiątki milisekund nawet w szybkiej sieci. Wewnątrz obsługi żądania ASP.NET, pętli przetwarzania wiadomości lub dowolnego wątku UI, to szybko się sumuje. VerifyAsync zwraca Task<DkimResult> że możesz 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");
Istnieje także VerifyAsync(Stream) przeciążenie. Podobnie jak w synchronizowanej API, metoda deklaruje async Task (nie async void) — miejsca wywołań powinny await to, a punkty wejścia aplikacji konsolowej mogą robić VerifySignatureAsync().GetAwaiter().GetResult() gdy istnieje async Main jest niedostępny.
End‑to‑end: ładowanie, podpisywanie, zapisywanie, weryfikacja
Powyższe elementy łączą się w jedną sekwencję działań. To pełna linia przetwarzania sign‑and‑verify, którą możesz osadzić w usłudze e‑mail transakcyjnych:
// 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"]);
Kompletny DKIM-Signature nagłówek wygenerowany przez ten kod wygląda tak:
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...
Interesujące znaczniki:
v=1— wersję DKIM.a=rsa-sha256— algorytm; odzwierciedlaHashAlgorithm.c=relaxed/relaxed— kanonizacja (nagłówki/treść).d=example.com/s=dkim— selektor i domena; razem tworzą nazwę wyszukiwania DNS.h=from:to:subject:date:mime-version— podpisane nagłówki, w kolejności.bh=— hash base64 skanonizowanej treści.b=— rzeczywisty podpis RSA nad skanonizowanymi nagłówkami.
Jeśli zmienisz dowolną wartość podpisanego nagłówka lub zmodyfikujesz treść, bh i b już nie pasują i weryfikacja nie powiedzie się.
Publikowanie klucza publicznego w DNS
Podpisywanie lokalne to tylko połowa zadania. Aby odbiorcy mogli zweryfikować, publiczny odpowiednik klucza RSA musi być opublikowany jako rekord TXT DNS pod adresem <selector>._domainkey.<domain>:
Record: dkim._domainkey.example.com
Type: TXT
Value: v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC...
Wyodrębnij klucz publiczny z pliku PEM przy użyciu OpenSSL:
openssl rsa -in sample-private-key.pem -pubout -out sample-public-key.pem
Usuń linie nagłówka/stopki i połącz zawartość base64 dla p= wartość. Następnie potwierdź, że rekord jest aktywny, zanim na nim polegasz:
dig dkim._domainkey.example.com TXT
Narzędzia online takie jak DKIM Validator i MXToolbox DKIM Checker powie Ci, czy rzeczywista wiadomość, którą wysyłasz, jest w pełni weryfikowalna. Używaj ich przed przełączeniem ruchu produkcyjnego.