Podepisování e‑mailů pomocí DKIM pro ověřování a doručitelnost

DKIM (DomainKeys Identified Mail, RFC 6376) umožňuje vlastníkovi domény připojit kryptografický podpis k odchozímu emailu. Přijímací servery načtou odpovídající veřejný klíč z DNS, přepočítají hash a potvrdí dvě věci najednou: tělo zprávy nebylo během přenosu změněno a zpráva opravdu pochází od odesílatele oprávněného doménou. Bez DKIM moderní poskytovatelé (Gmail, Microsoft 365, Yahoo) označují poštu jako spam, tiše ji zahazují nebo odmítají doručení — DKIM již není volitelný pro produkční poštu.

Tento průvodce je kompletní návod na Aspose.Email’s DKIM jmenný prostor pro .NET. Každý blok kódu v tomto článku je převzat z spustitelného příkladu v DKIMExamples projekt a byl ověřen end‑to‑end proti knihovně.

Předpoklady

DKIM typy jsou umístěny v Aspose.Email.DKIM jmenný prostor a jsou k dispozici pouze s .NET Framework 4.0/4.5 sestavami knihovny. Jsou úmyslně vyloučeny z .NET Standard 2.0 a .NET 6/8 balíčků. Pro následování níže uvedených příkladů musí váš projekt odkazovat na sestavení Aspose.Email pro net45 (cíl net48 v konzumujícím .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>

Také potřebujete dvojici RSA klíčů. Soukromý klíč je držen ve vaší aplikaci a používá se k podepisování; veřejný klíč je publikován jako DNS TXT záznam, aby jej příjemci mohli ověřit. 2048‑bitový klíč je dnes standardem:

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

Aspose’s PemReader přijímá jak PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) a PKCS#8 (-----BEGIN PRIVATE KEY-----) formátované soubory.

| Třída | Role | |——-|——| | DKIMSignatureInfo | Popisuje podpis: selektor, doména, hash algoritmus, kanonikalizaci a které hlavičky zahrnout. | | PemReader | Načte soukromý RSA klíč z PEM‑formátovaného souboru nebo proudu do RSACryptoServiceProvider. | | MailMessage.DKIMSign(rsa, info) | Rozšíření/metoda instance, která vrací podepsanou kopii zprávy s DKIM-Signature hlavička vyplněna. | | DkimVerifier | Provádí DNS‑založené ověření podepsané zprávy — soubor, proud, synchronní nebo asynchronní. |

Celý tok je vždy: načíst klíč → popsat podpis → vytvořit zprávu → podepsat → (volitelně) ověřit.

Minimální příklad podepisování

Toto je nejmenší množství kódu, které vytvoří platnou DKIM podepsanou zprávu:

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

Dva argumenty pro DKIMSignatureInfo"dkim" a "example.com" — jsou selektor a doména. Společně říkají příjemcům, kde najít váš veřejný klíč: v DNS TXT záznamu dkim._domainkey.example.com. Vyberte krátký název selektoru (často default, mail, nebo datum jako 2026jan); selektor vám umožní otáčet klíče, aniž byste vyhazovali starý.

Kritický detail: DKIMSign vrací nový MailMessage instance s DKIM-Signature hlavička přidána. Původní zpráva zůstává nezměněna. Uložte nebo odešlete vrácený objekt, nikoli vstup.

Výběr, které hlavičky podepsat

Hlavičky uvedené v signInfo.Headers jsou jediné, jejichž hodnoty jsou svázány s podpisem. Cokoliv, co není uvedeno, může být změněno přeposílači, aniž by se podpis porušil, což je někdy žádoucí (např. Received: hlavičky přidané v níže řetězci) a někdy nebezpečné (např. ponechání From nepodepsáno dává podpisu smysl).

Obecné pravidlo: vždy podepisujte From, To, Subject, Date. From je zvláště povinné pro DMARC zarovnání.

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

Pokud uvedete Date ale nikdy nenastavujte mailMessage.Date, DKIMSign vyvolá The following headers to be signed do not exist: Date. Vždy nejprve naplňte hlavičky, poté podepište.

Hashovací algoritmy — RSA‑SHA1 vs RSA‑SHA256

DKIM podporuje dva podepisovací algoritmy. Výchozí v Aspose.Email je RSASha1 pro zpětnou kompatibilitu, ale každé nové nasazení by mělo použít RSASha256. Mnoho poskytovatelů nyní označuje SHA‑1 podpisy jako neplatné i když kryptograficky ověřují.

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

Výsledný DKIM-Signature hlavička oznamuje algoritmus pomocí a= značka (a=rsa-sha1 vs a=rsa-sha256). Příjemci používají tuto značku k rozhodnutí, který hash přepočítat.

Kanonizace — Simple vs Relaxed

Kanonizace určuje, jak se DKIM shoduje na stabilní, bajtově přesnou podobu zprávy před hashováním. Dva algoritmy jsou definovány nezávisle pro hlavičky a těla:

  • Simple — téměř žádná normalizace. Koncové prázdné řádky v těle jsou odstraněny, ale žádná další bílá místa nejsou dotčena. Jedna mezera přidaná přeposílačem zneplatní podpis.
  • Relaxed — sbírá bílá místa uvnitř hodnot hlaviček, převádí názvy hlaviček na malá písmena a sloučení sekvencí bílých míst v těle. Tolerantní k drobným přiformátováním, které provádějí poštovní transportní agenty.

Vyberte Relaxed pro obojí, hlavičku i tělo, pokud nemáte konkrétní důvod to nedělat. Téměř každé produkční nasazení to tak činí.

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

Kanonizace hlavičky a těla jsou nezávislé — relaxed/simple je naprosto platná kombinace, zapsaná tímto způsobem v c= značka DKIM hlavičky.

Načítání soukromého klíče

PemReader transparentně zpracovává oba PEM kódování. Neexistuje samostatné Pkcs1Reader / Pkcs8Reader — stačí předat cestu nebo stream:

// 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 vrací RSACryptoServiceProvider připravené k předání DKIMSign. Existuje také GetPrivateKey(Stream) přetížení pro případy, kdy klíč pochází z trezoru klíčů, šifrovaného blobu nebo vloženého zdroje místo souborového systému.

Pravidla pro zacházení s klíči:

  1. Nikdy neukládejte soukromé klíče do verzovacího systému. *.pem patří do .gitignore.
  2. V produkci načítejte klíče ze správce tajemství (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault), ne z disku.
  3. Klíče otáčejte každých 6–12 měsíců. Nejprve publikujte nový selektor, přepněte podepisování, pak po přechodném období stáhněte starý DNS záznam.

Ověřování podepsané zprávy ze souboru

Ověření je symetrická operace: z daného podepsaného .eml, DkimVerifier extrahuje DKIM-Signature hlavička, provádí DNS lookup proti <selector>._domainkey.<domain> pro veřejný klíč a přepočítává hash. Výsledkem je DkimResult s IsValid a 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");

Ověřování zprávy podepsané s d=example.com vždy selže s „klíč není ve slovníku“ nebo podobnou chybou při DNS lookupu, protože example.com nepublikuje DKIM záznam. Použijte doménu, kterou ovládáte (nebo známý testovací e‑mail) pro reálné ověřovací testy.

Ověřování ze streamu

Když zpráva není na disku — přišla z IMAP fetch, fronty, HTTP uploadu nebo blob úložiště — použijte Stream přetížení. Přijímá libovolný čitelný seekovatelný 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) a Verify(string) jsou funkčně identické — oba blokují, dokud DNS dotaz nedokončí. Pro vše, co je uživatelsky viditelné, použijte níže uvedené asynchronní varianty.

Asynchronní ověření

DKIM ověření zahrnuje DNS dotaz, který může trvat desítky milisekund i na rychlé síti. V rámci ASP.NET request handleru, smyčky zpracování zpráv nebo jakéhokoli UI vlákna to rychle narůstá. VerifyAsync vrací Task<DkimResult> že můžete 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");

Existuje také VerifyAsync(Stream) přetížení. Stejně jako u synchronního API metoda deklaruje async Task (ne async void) — volací místa by měla await on a vstupní body konzolových aplikací mohou VerifySignatureAsync().GetAwaiter().GetResult() když je async Main není k dispozici.

Od konce do konce: načíst, podepsat, uložit, ověřit

Výše uvedené části se spojí do jednoho pracovního postupu. Toto je kompletní pipeline podepisování a ověřování, kterou byste integrovali do služby transakčního e‑mailu:

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

Kompletní DKIM-Signature hlavička vygenerovaná tímto kódem vypadá takto:

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

Zajímavé značky:

  • v=1 — verzi DKIM.
  • a=rsa-sha256 — algoritmus; odráží HashAlgorithm.
  • c=relaxed/relaxed — kanonizace (hlavička/tělo).
  • d=example.com / s=dkim — selektor a doména; dohromady tvoří DNS dotazovací název.
  • h=from:to:subject:date:mime-version — podepsané hlavičky v pořadí.
  • bh= — base64 hash kanonizovaného těla.
  • b= — skutečný RSA podpis nad kanonizovanými hlavičkami.

Pokud změníte jakoukoli podepsanou hodnotu hlavičky nebo upravíte tělo, bh a b již neodpovídá a ověření selže.

Publikování veřejného klíče v DNS

Lokální podpis je jen polovinou úkolu. Aby příjemci mohli ověřovat, musí být veřejná část vašeho RSA klíče publikována jako DNS TXT záznam na <selector>._domainkey.<domain>:

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

Extrahujte veřejný klíč z vašeho PEM pomocí OpenSSL:

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

Odeberte řádky záhlaví/patičky a spojte base64 obsah pro p= hodnota. Poté potvrďte, že záznam je aktivní, než na něj spolehnete:

dig dkim._domainkey.example.com TXT

Online nástroje jako DKIM Validátor a MXToolbox DKIM Kontrola řekne vám, zda je skutečná zpráva, kterou odešlete, ověřitelná od konce do konce. Použijte je před nasazením do produkce.