Aller au contenu principal

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

BaliseNomDescriptionExempleRequis
vVersionNuméro de versionv=1Oui
aAlgorithm (Algorithme)Algorithme de signaturea=rsa-sha256Oui
bSignatureSignature réelle (Base64)b=dzdVy...Oui
bhBody Hash (Hachage du corps)Hachage du corps du courrierbh=2jUSO...Oui
cCanonicalization (Canonisation)Algorithme de canonisationc=relaxed/relaxedNon
dDomain (Domaine)Domaine de signatured=example.comOui
hHeaders (En-têtes)Champs d'en-tête signésh=From:To:SubjectOui
iIdentity (Identité)Identité du signataire[email protected]Non
lBody Length (Longueur du corps)Longueur du corps signéel=1000Non
qQuery Method (Méthode de requête)Méthode de requête de clé publiqueq=dns/txtNon
sSelector (Sélecteur)Sélecteurs=defaultOui
tTimestamp (Horodatage)Horodatage de la signaturet=1234567890Non
xExpire Time (Expiration)Temps d'expirationx=1234657890Non
zCopied Headers (En-têtes copiés)Copie des en-têtes originauxz=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

BaliseDescriptionExempleRequis
vVersionv=DKIM1Non
kType de clék=rsaNon
pClé publique (Base64)p=MIGfMA0...Oui
hAlgorithmes de hachage acceptablesh=sha256Non
sType de services=emailNon
tDrapeauxt=y (mode test)Non
nNotesn=NotesNon

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.