RFC 6376 - Signatures DKIM
Informations de base
- Numéro RFC : 6376
- Titre : DomainKeys Identified Mail (DKIM) Signatures
- Titre français : Signatures de courrier identifié par clés de domaine
- Date de publication : Septembre 2011
- Statut : NORME PROPOSÉE (PROPOSED STANDARD)
- Auteurs : D. Crocker, T. Hansen, M. Kucherawy
Résumé (Abstract)
DomainKeys Identified Mail (DKIM) est une méthode d'authentification de courrier électronique au niveau du domaine. Elle permet aux organisations d'assumer la responsabilité des messages électroniques en utilisant des signatures cryptographiques. Les destinataires peuvent vérifier la signature pour confirmer que le courrier provient effectivement du domaine revendiqué et que son contenu n'a pas été altéré.
Aperçu de DKIM
Qu'est-ce que DKIM ?
Définition :
DKIM = DomainKeys Identified Mail
Fonction : Signature numérique de courrier électronique
Objectif :
✓ Vérifier le domaine de l'expéditeur
✓ Détecter l'altération du courrier
✓ Prévenir l'hameçonnage et la falsification
Concept fondamental :
L'expéditeur signe avec une clé privée → Le destinataire vérifie avec une clé publique
Problèmes de sécurité du courrier électronique :
Problèmes du protocole SMTP :
❌ Peut falsifier l'adresse de l'expéditeur
❌ Ne peut pas vérifier l'authenticité du courrier
❌ Facilement exploité pour l'hameçonnage
Exemple d'hameçonnage :
From: [email protected] (falsifié)
En réalité : Serveur de l'attaquant
Solution DKIM :
✓ Le propriétaire du domaine signe le courrier
✓ Publier la clé publique dans DNS
✓ Le destinataire vérifie la signature
✓ Échec de la signature → rejeter ou marquer
Flux de travail DKIM
Côté envoi (example.com) :
1. Le serveur de messagerie génère une signature
- Signe les en-têtes et le corps du courrier
- Utilise une clé privée (secrète)
2. Ajouter la signature à l'en-tête du courrier
DKIM-Signature: v=1; a=rsa-sha256; d=example.com; ...
3. Envoyer le courrier normalement
Côté réception :
1. Extraire l'en-tête DKIM-Signature
2. Récupérer la clé publique depuis DNS
Requête : default._domainkey.example.com
3. Vérifier la signature
- Recalculer le hachage du courrier
- Déchiffrer la signature avec la clé publique
- Comparer les valeurs de hachage
4. Résultat :
✓ Signature valide → Provient d'example.com, non altéré
✗ Signature invalide → Falsifié ou modifié
Détails de l'en-tête DKIM-Signature
Format de base
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=example.com; s=default; t=1234567890;
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
h=From:To:Subject:Date;
b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD08b9/0i
+=hB...
Description des balises
| Balise | Nom | Description | Exemple | Requis |
|---|---|---|---|---|
v | Version | Numéro de version | v=1 | Oui |
a | Algorithm (Algorithme) | Algorithme de signature | a=rsa-sha256 | Oui |
b | Signature | Signature réelle (Base64) | b=dzdVy... | Oui |
bh | Body Hash (Hachage du corps) | Hachage du corps du courrier | bh=2jUSO... | Oui |
c | Canonicalization (Canonisation) | Algorithme de canonisation | c=relaxed/relaxed | Non |
d | Domain (Domaine) | Domaine de signature | d=example.com | Oui |
h | Headers (En-têtes) | Champs d'en-tête signés | h=From:To:Subject | Oui |
i | Identity (Identité) | Identité du signataire | [email protected] | Non |
l | Body Length (Longueur du corps) | Longueur du corps signée | l=1000 | Non |
q | Query Method (Méthode de requête) | Méthode de requête de clé publique | q=dns/txt | Non |
s | Selector (Sélecteur) | Sélecteur | s=default | Oui |
t | Timestamp (Horodatage) | Horodatage de la signature | t=1234567890 | Non |
x | Expire Time (Expiration) | Temps d'expiration | x=1234657890 | Non |
z | Copied Headers (En-têtes copiés) | Copie des en-têtes originaux | z=From:... | Non |
Détails des paramètres clés :
a (Algorithm - Algorithme) :
Algorithmes pris en charge :
rsa-sha256 (recommandé)
rsa-sha1 (obsolète, non sécurisé)
Exemple :
a=rsa-sha256
c (Canonicalization - Canonisation) :
Format : c=<header>/<body>
Algorithmes :
- simple : Correspondance stricte
- relaxed : Correspondance souple (ignore les espaces, la casse, etc.)
Exemples :
c=relaxed/relaxed (recommandé, tolère les modifications du serveur de messagerie)
c=simple/simple (strict, toute modification provoque un échec de vérification)
d (Domain - Domaine) :
Domaine de signature (doit être le domaine de l'expéditeur ou un domaine parent)
Valide :
From: [email protected]
d=example.com ✓ (domaine parent)
Non valide :
From: [email protected]
d=other.com ✗ (domaine non lié)
s (Selector - Sélecteur) :
Sélecteur : Utilisé pour les requêtes DNS
Objectif :
- Prendre en charge plusieurs clés
- Rotation des clés
- Différents services utilisent différentes clés
Requête DNS :
s=default, d=example.com
→ Requête : default._domainkey.example.com
h (Headers - En-têtes) :
Champs d'en-tête signés (séparés par des deux-points)
Recommandé de signer :
- From (requis !)
- To
- Subject
- Date
- Message-ID
Exemple :
h=From:To:Subject:Date:Message-ID
Note :
Le champ From doit être inclus !
Enregistrement de clé publique DNS
Format d'enregistrement TXT
Requête : <selector>._domainkey.<domain>
Exemple : default._domainkey.example.com
Contenu de l'enregistrement TXT :
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...
Description des balises :
v=DKIM1 Version
k=rsa Type de clé
p=<clé publique> Clé publique encodée en Base64
Détails des balises de clé publique
| Balise | Description | Exemple | Requis |
|---|---|---|---|
v | Version | v=DKIM1 | Non |
k | Type de clé | k=rsa | Non |
p | Clé publique (Base64) | p=MIGfMA0... | Oui |
h | Algorithmes de hachage acceptables | h=sha256 | Non |
s | Type de service | s=email | Non |
t | Drapeaux | t=y (mode test) | Non |
n | Notes | n=Notes | Non |
Exemple complet :
default._domainkey.example.com. IN TXT (
"v=DKIM1; k=rsa; "
"p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC3QEKyU1fSma0axspqYK5iAj+54lsAg4qRRCnpKK68hawSIhv5TGT4j"
"..."
)
Notes :
- Peut être divisé en plusieurs chaînes (limite DNS 255 octets)
- Les espaces sont ignorés
- Balise p vide → clé révoquée
Exemples d'implémentation
Générer une paire de clés
# Générer une clé privée RSA de 2048 bits
openssl genrsa -out dkim_private.pem 2048
# Extraire la clé publique
openssl rsa -in dkim_private.pem -pubout -out dkim_public.pem
# Convertir au format DNS (supprimer les en-têtes et les sauts de ligne)
cat dkim_public.pem | grep -v "BEGIN\|END" | tr -d '\n'
Sortie :
Clé privée (dkim_private.pem) :
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA...
-----END RSA PRIVATE KEY-----
Clé publique (dkim_public.pem) :
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
Format DNS :
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
Implémentation de la signature Node.js
const crypto = require('crypto');
const dns = require('dns').promises;
class DKIMSigner {
constructor(domain, selector, privateKey) {
this.domain = domain;
this.selector = selector;
this.privateKey = privateKey;
}
// Signer le courrier électronique
signEmail(headers, body) {
// 1. Canoniser le corps
const canonicalBody = this.canonicalizeBody(body);
// 2. Calculer le hachage du corps
const bodyHash = crypto
.createHash('sha256')
.update(canonicalBody)
.digest('base64');
// 3. Construire l'en-tête DKIM (sans balise b)
const timestamp = Math.floor(Date.now() / 1000);
const dkimHeader = [
'v=1',
'a=rsa-sha256',
'd=' + this.domain,
's=' + this.selector,
't=' + timestamp,
'c=relaxed/relaxed',
'h=From:To:Subject:Date',
'bh=' + bodyHash,
'b=' // Signature vide, à remplir plus tard
].join('; ');
// 4. Canoniser les en-têtes
const canonicalHeaders = this.canonicalizeHeaders(headers, dkimHeader);
// 5. Générer la signature
const sign = crypto.createSign('RSA-SHA256');
sign.update(canonicalHeaders);
const signature = sign.sign(this.privateKey, 'base64');
// 6. DKIM-Signature complet
return dkimHeader.replace('b=', 'b=' + signature);
}
// Canoniser le corps (relaxed)
canonicalizeBody(body) {
return body
.replace(/[ \t]+(\r\n)/g, '$1') // Supprimer les espaces de fin
.replace(/(\r\n)+$/, '\r\n'); // Ne conserver qu'un seul saut de ligne final
}
// Canoniser les en-têtes (relaxed)
canonicalizeHeaders(headers, dkimHeader) {
const headersToSign = ['from', 'to', 'subject', 'date'];
let canonical = '';
// Ajouter les en-têtes à signer
for (const name of headersToSign) {
const value = headers[name];
if (value) {
canonical += name + ':' + value.trim() + '\r\n';
}
}
// Ajouter l'en-tête DKIM-Signature (sans la valeur de la balise b)
canonical += 'dkim-signature:' +
dkimHeader.split('b=')[0] + 'b=';
return canonical;
}
}
// Utilisation
const privateKey = fs.readFileSync('dkim_private.pem', 'utf8');
const signer = new DKIMSigner('example.com', 'default', privateKey);
const headers = {
from: '[email protected]',
to: '[email protected]',
subject: 'Test Email',
date: new Date().toUTCString()
};
const body = 'This is the email body.\r\n';
const dkimSignature = signer.signEmail(headers, body);
console.log('DKIM-Signature:', dkimSignature);
Implémentation de la vérification
class DKIMVerifier {
async verifyEmail(dkimSignature, headers, body) {
// 1. Analyser DKIM-Signature
const tags = this.parseDKIMSignature(dkimSignature);
// 2. Obtenir la clé publique
const publicKey = await this.getPublicKey(tags.d, tags.s);
// 3. Vérifier le hachage du corps
const bodyValid = this.verifyBodyHash(body, tags.bh, tags.c);
if (!bodyValid) {
return { valid: false, reason: 'Body hash mismatch' };
}
// 4. Vérifier la signature
const signatureValid = this.verifySignature(
headers,
dkimSignature,
publicKey,
tags
);
return {
valid: signatureValid,
domain: tags.d,
selector: tags.s
};
}
parseDKIMSignature(signature) {
const tags = {};
signature.split(';').forEach(part => {
const [key, value] = part.trim().split('=');
if (key && value) {
tags[key] = value.trim();
}
});
return tags;
}
async getPublicKey(domain, selector) {
const dnsName = `${selector}._domainkey.${domain}`;
const records = await dns.resolveTxt(dnsName);
// Analyser l'enregistrement TXT
const dkimRecord = records[0].join('');
const match = dkimRecord.match(/p=([^;]+)/);
if (!match) {
throw new Error('Public key not found');
}
const publicKeyB64 = match[1];
return `-----BEGIN PUBLIC KEY-----\n${publicKeyB64}\n-----END PUBLIC KEY-----`;
}
verifyBodyHash(body, expectedHash, canonicalization) {
const canonical = this.canonicalizeBody(body);
const hash = crypto
.createHash('sha256')
.update(canonical)
.digest('base64');
return hash === expectedHash;
}
verifySignature(headers, dkimSignature, publicKey, tags) {
// Extraire la valeur de la signature
const signature = tags.b;
// Reconstruire les données de signature
const canonicalHeaders = this.canonicalizeHeaders(
headers,
dkimSignature.split('b=')[0] + 'b='
);
// Vérifier
const verify = crypto.createVerify('RSA-SHA256');
verify.update(canonicalHeaders);
return verify.verify(publicKey, signature, 'base64');
}
}
// Utilisation
const verifier = new DKIMVerifier();
const result = await verifier.verifyEmail(dkimSignature, headers, body);
if (result.valid) {
console.log(`✓ Signature DKIM valide de ${result.domain}`);
} else {
console.log(`✗ Signature DKIM invalide : ${result.reason}`);
}
Meilleures pratiques
1. Gestion des clés
Longueur de clé :
✓ Au moins 2048 bits
✓ Recommandé 2048 ou 3072 bits
✗ Éviter 1024 bits (non sécurisé)
Rotation des clés :
- Changer les clés tous les 1-2 ans
- Publier simultanément les nouvelles et anciennes clés (utiliser différents sélecteurs)
- Supprimer les anciennes clés après la période de transition
Nommage des sélecteurs :
✓ Par date : 2024-01
✓ Par objectif : mailserver1
✓ Par rotation : key1, key2
2. Sélection des en-têtes
// En-têtes recommandés à signer
const recommendedHeaders = [
'From', // Requis !
'To',
'Subject',
'Date',
'Message-ID',
'Reply-To',
'Cc',
'MIME-Version',
'Content-Type'
];
// Éviter de signer les en-têtes volatils
const avoidHeaders = [
'Received', // Ajouté par chaque relais
'Return-Path', // Peut changer en transit
'X-*' // Les en-têtes personnalisés peuvent être modifiés
];
3. Mode test
Ajouter le drapeau t=y à l'enregistrement DNS :
v=DKIM1; k=rsa; t=y; p=MIGfMA...
Objectif :
- Indique le mode test
- L'échec de la vérification ne rejettera pas le courrier
- Utilisé pour les tests de déploiement initial
Références
RFC liés à DKIM :
- [RFC 6376] DKIM Signatures ← Ce document
- [RFC 8301] Cryptographic Algorithm and Key Usage Update to DKIM
- [RFC 8463] A New Cryptographic Signature Method For DKIM
Normes connexes :
- [RFC 7489] DMARC (Basé sur DKIM et SPF)
- [RFC 7208] SPF (Sender Policy Framework)
Résumé : DKIM fournit une authentification au niveau du domaine pour le courrier électronique grâce à la technologie de signature numérique, formant la base de la sécurité moderne du courrier électronique. Une mise en œuvre correcte de DKIM peut considérablement améliorer la délivrabilité du courrier, réduire les faux positifs comme spam et jeter les bases de mécanismes de sécurité du courrier électronique plus avancés comme DMARC.