Signer les e‑mails avec DKIM pour l’authentification et la délivrabilité
DKIM (DomainKeys Identified Mail, RFC 6376) permet à un propriétaire de domaine d’attacher une signature cryptographique aux e‑mails sortants. Les serveurs récepteurs récupèrent la clé publique correspondante depuis le DNS, recomputent le hachage et confirment deux choses à la fois : le corps du message n’a pas été modifié pendant le transport, et le message provient réellement d’un expéditeur autorisé par le domaine. Sans DKIM, les fournisseurs modernes (Gmail, Microsoft 365, Yahoo) marquent le courrier comme indésirable, le suppriment silencieusement ou refusent la remise — DKIM n’est plus optionnel pour le courrier de production.
Ce guide est un tutoriel complet de Aspose.Emailnom d’espace DKIM d’Aspose pour .NET. Chaque bloc de code de cet article provient d’un exemple exécutable dans le DKIMExamples projet et a été vérifié de bout en bout contre la bibliothèque.
Prérequis
Les types DKIM se trouvent dans le Aspose.Email.DKIM espace de noms et ne sont fournis qu’avec les versions .NET Framework 4.0/4.5 de la bibliothèque. Ils sont volontairement exclus des packages .NET Standard 2.0 et .NET 6/8. Pour suivre les exemples ci‑dessous, votre projet doit référencer un assembly Aspose.Email construit pour net45 (cible net48 dans le consommateur .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>
Vous avez également besoin d’une paire de clés RSA. La clé privée reste dans votre application et sert à signer ; la clé publique est publiée sous forme d’enregistrement DNS TXT afin que les récepteurs puissent vérifier. Une clé de 2048 bits est la norme aujourd’hui :
openssl genrsa -out sample-private-key.pem 2048
Le PemReader accepte les deux PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) et PKCS#8 (-----BEGIN PRIVATE KEY-----) fichiers formatés.
| Classe | Rôle | |——-|——| | DKIMSignatureInfo | Décrit la signature : sélecteur, domaine, algorithme de hachage, canonicalisation et quels en-têtes couvrir. | | PemReader | Charge une clé privée RSA depuis un fichier ou flux au format PEM dans un RSACryptoServiceProvider. |
| MailMessage.DKIMSign(rsa, info) | Méthode d’extension/instance qui renvoie une copie signée du message avec le DKIM-Signature en-tête renseigné. | | DkimVerifier | Effectue une vérification DNS d’un message signé — fichier, flux, synchronisé ou asynchrone. |
Le flux complet est toujours : charger la clé → décrire la signature → construire le message → signer → (optionnellement) vérifier.
L’exemple minimal de signature
C’est le plus petit morceau de code qui génère un message signé DKIM valide :
// 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)}...");
Les deux arguments de DKIMSignatureInfo — "dkim" et "example.com" — sont le sélecteur et le domaine. Ensemble, ils indiquent aux récepteurs où chercher votre clé publique : dans l’enregistrement DNS TXT dkim._domainkey.example.com. Choisissez un nom de sélecteur court (souvent default, mail, ou une date comme 2026jan); le sélecteur vous permet de faire tourner les clés sans supprimer l’ancienne.
Un détail crucial : DKIMSign retourne un nouveau MailMessage instance avec le DKIM-Signature en-tête ajouté. Le message original reste inchangé. Enregistrez ou envoyez l’objet retourné, pas l’entrée.
Choisir quels en-têtes signer
Les en-têtes listés dans signInfo.Headers sont les seuls dont les valeurs sont liées à la signature. Tout ce qui n’est pas listé peut être modifié par les relais sans casser la signature, ce qui est parfois souhaitable (p. ex. Received: en‑têtes ajoutés en aval) et parfois dangereux (p. ex. laisser From non signé rend la signature insignifiante).
Règle empirique : signez toujours From, To, Subject, Date. From en particulier est obligatoire pour l’alignement 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);
Si vous répertoriez Date mais ne définissez jamais mailMessage.Date, DKIMSign lance The following headers to be signed do not exist: Date. Remplissez toujours d’abord les en‑têtes, puis signez.
Algorithmes de hachage — RSA‑SHA1 vs RSA‑SHA256
DKIM prend en charge deux algorithmes de signature. Le défaut dans Aspose.Email est RSASha1 pour la compatibilité descendante, mais tout nouveau déploiement devrait utiliser RSASha256. De nombreux fournisseurs marquent désormais les signatures SHA‑1 comme invalides même lorsqu’elles vérifient cryptographiquement.
// 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);
Le résultat DKIM-Signature l’en‑tête annonce l’algorithme via le a= balise (a=rsa-sha1 vs a=rsa-sha256). Les récepteurs consultent cette balise pour décider quel hachage recomputer.
Canonicalisation — Simple vs Relaxed
La canonicalisation est la façon dont DKIM s’accorde sur une forme stable, byte‑exacte du message avant le hachage. Deux algorithmes sont définis pour les en‑têtes et les corps indépendamment :
Simple— presque aucune normalisation. Les lignes vides finales du corps sont supprimées, mais aucun autre espace n’est touché. Un simple espace ajouté par un relais invalidera la signature.Relaxed— replie les espaces blancs à l’intérieur des valeurs d’en‑tête, met les noms d’en‑tête en minuscules et réduit les séquences d’espaces blancs dans le corps. Tolérant aux reformattages triviaux que les agents de transport de courrier ont tendance à faire.
Choisissez Relaxed pour l’en‑tête et le corps sauf si vous avez une raison précise de ne pas le faire. Presque toutes les déploiements en production le font.
// 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);
La canonicalisation de l’en‑tête et du corps sont indépendantes — relaxed/simple est une combinaison parfaitement valide, écrite ainsi dans le c= balise de l’en‑tête DKIM.
Chargement de la clé privée
PemReader gère les deux encodages PEM de manière transparente. Il n’existe pas de séparé Pkcs1Reader / Pkcs8Reader — il suffit de passer le chemin ou le flux :
// 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 renvoie un(e) RSACryptoServiceProvider prêt à être remis à DKIMSign. Il existe également un GetPrivateKey(Stream) surcharge pour les cas où la clé provient d’un coffre, d’un blob chiffré ou d’une ressource incorporée plutôt que du système de fichiers.
Règles de gestion des clés :
- Ne jamais valider les clés privées dans le contrôle de version.
*.pemappartient à.gitignore. - En production, chargez les clés depuis un gestionnaire de secrets (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault), pas depuis le disque.
- Faites pivoter les clés tous les 6 à 12 mois. Publiez d’abord le nouveau sélecteur, changez la signature, puis retirez l’ancien enregistrement DNS après une période de grâce.
Vérification d’un message signé depuis un fichier
La vérification est l’opération symétrique : étant donné un signé .eml, DkimVerifier extrait le DKIM-Signature en‑tête, effectue une recherche DNS contre <selector>._domainkey.<domain> pour la clé publique, et recompute le hachage. Le résultat est un DkimResult avec IsValid et 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");
Vérification d’un message signé avec
d=example.coméchouera toujours avec « clé non présente dans le dictionnaire » ou une erreur similaire de résolution DNS, carexample.comne publie pas d’enregistrement DKIM. Utilisez un domaine que vous contrôlez (ou un message de test réputé fiable) pour les tests de vérification réels.
Vérification depuis un flux
Lorsque le message n’est pas sur disque — il provient d’un fetch IMAP, d’une file d’attente, d’un téléchargement HTTP ou d’un stockage blob — utilisez le Stream surcharge. Elle accepte tout flux de lecture séekable :
// 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) et Verify(string) sont fonctionnellement identiques — les deux bloquent jusqu’à la fin de la résolution DNS. Pour toute interaction utilisateur, utilisez les variantes asynchrones ci‑dessous.
Vérification asynchrone
La vérification DKIM implique un aller‑retour DNS, ce qui peut prendre plusieurs dizaines de millisecondes même sur un réseau rapide. Dans un gestionnaire de requête ASP.NET, une boucle de traitement de messages ou tout thread UI, cela s’accumule rapidement. VerifyAsync renvoie un Task<DkimResult> que vous pouvez 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");
Il existe également un VerifyAsync(Stream) surcharge. Comme avec l’API synchrone, la méthode déclare async Task (pas async void) — les sites d’appel devraient await il, et les points d’entrée d’applications console peuvent faire VerifySignatureAsync().GetAwaiter().GetResult() lorsqu’un async Main n’est pas disponible.
De bout en bout : charger, signer, enregistrer, vérifier
Les éléments ci‑dessus se combinent en un flux de travail unique. Voici le pipeline complet de signature‑et‑vérification que vous intégreriez dans un service d’e‑mail transactionnel :
// 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"]);
Un DKIM-Signature l’en‑tête produit par ce code ressemble à ceci :
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...
Les balises intéressantes :
v=1— version DKIM.a=rsa-sha256— algorithme ; reflèteHashAlgorithm.c=relaxed/relaxed— canonicalisation (en‑tête/corps).d=example.com/s=dkim— sélecteur et domaine ; ensemble ils forment le nom de recherche DNS.h=from:to:subject:date:mime-version— en‑têtes signés, dans l’ordre.bh=— hachage base64 du corps canonisé.b=— la véritable signature RSA sur les en‑têtes canonisés.
Si vous modifiez une quelconque valeur d’en‑tête signée, ou le corps, bh et b ne correspondent plus et la vérification échoue.
Publication de la clé publique dans DNS
Signer localement n’est que la moitié du travail. Pour que les destinataires puissent vérifier, la contre‑partie publique de votre clé RSA doit être publiée comme enregistrement TXT DNS à <selector>._domainkey.<domain>:
Record: dkim._domainkey.example.com
Type: TXT
Value: v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC...
Extrayez la clé publique de votre PEM avec OpenSSL :
openssl rsa -in sample-private-key.pem -pubout -out sample-public-key.pem
Supprimez les lignes d’en‑tête/de pied de page et concaténez le contenu base64 pour le p= valeur. Confirmez ensuite que l’enregistrement est en ligne avant d’y compter :
dig dkim._domainkey.example.com TXT
Des outils en ligne comme Validateur DKIM et Vérificateur DKIM MXToolbox vous dira si un vrai message que vous envoyez est vérifiable de bout en bout. Utilisez‑les avant de basculer le trafic de production.