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:

  1. Nigdy nie zapisuj kluczy prywatnych w systemie kontroli wersji. *.pem należy do .gitignore.
  2. W środowisku produkcyjnym ładuj klucze z menedżera tajemnic (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault), a nie z dysku.
  3. 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.com zawsze zakończy się niepowodzeniem z błędem „key not in dictionary” lub podobnym błędem DNS, ponieważ example.com nie 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; odzwierciedla HashAlgorithm.
  • 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.