Menandatangani Email dengan DKIM untuk Otentikasi dan Keandalan Pengiriman
DKIM (DomainKeys Identified Mail, RFC 6376) memungkinkan pemilik domain menambahkan tanda tangan kriptografis pada email keluar. Server penerima mengambil kunci publik yang cocok dari DNS, menghitung ulang hash, dan mengonfirmasi dua hal sekaligus: isi pesan tidak diubah dalam transit, dan pesan memang berasal dari pengirim yang diotorisasi oleh domain. Tanpa DKIM, penyedia modern (Gmail, Microsoft 365, Yahoo) menandai surel sebagai spam, membuangnya secara diam‑diam, atau menolak pengiriman — DKIM kini tidak lagi opsional untuk email produksi.
Panduan ini merupakan langkah‑langkah lengkap dari Aspose.Emailnamespace DKIM milik .NET. Setiap blok kode dalam artikel ini diambil dari contoh yang dapat dijalankan di DKIMExamples proyek dan telah diverifikasi secara menyeluruh terhadap pustaka.
Prasyarat
tipe DKIM berada di Aspose.Email.DKIM namespace dan hanya tersedia dengan build .NET Framework 4.0/4.5 dari pustaka. Mereka sengaja dikecualikan dari paket .NET Standard 2.0 dan .NET 6/8. Untuk mengikuti contoh di bawah, proyek Anda harus merujuk pada assembly Aspose.Email yang dibangun untuk net45 (target net48 di konsumen .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>
Anda juga memerlukan sepasang kunci RSA. Kunci privat disimpan di aplikasi Anda dan digunakan untuk menandatangani; kunci publik dipublikasikan sebagai catatan DNS TXT sehingga penerima dapat memverifikasi. Kunci 2048-bit adalah standar saat ini:
openssl genrsa -out sample-private-key.pem 2048
Aspose’s PemReader menerima keduanya PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) dan PKCS#8 (-----BEGIN PRIVATE KEY-----) berkas yang diformat.
| Kelas | Peran | |——-|——| | DKIMSignatureInfo | Menjelaskan tanda tangan: selector, domain, algoritma hash, kanonisasi, dan header mana yang dicakup. | | PemReader | Memuat kunci privat RSA dari berkas atau aliran berformat PEM ke dalam RSACryptoServiceProvider. |
| MailMessage.DKIMSign(rsa, info) | Metode ekstensi/instance yang mengembalikan salinan pesan yang ditandatangani dengan DKIM-Signature header terisi. | | DkimVerifier | Melakukan verifikasi berbasis DNS pada pesan yang ditandatangani — berkas, aliran, sinkron, atau asinkron. |
Alur lengkap selalu: muat kunci → jelaskan tanda tangan → bangun pesan → tandatangani → (opsional) verifikasi.
Contoh penandatanganan minimal
Inilah jumlah kode terkecil yang menghasilkan pesan yang ditandatangani DKIM secara valid:
// 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)}...");
Dua argumen untuk DKIMSignatureInfo — "dkim" dan "example.com" — adalah selector dan domain. Bersama-sama mereka memberi tahu penerima tempat mencari kunci publik Anda: pada catatan DNS TXT dkim._domainkey.example.com. Pilih nama selector pendek (seringkali default, mail, atau tanggal seperti 2026jan); selector memungkinkan Anda memutar kunci tanpa membuang yang lama.
Detail penting: DKIMSign mengembalikan baru MailMessage instansi dengan DKIM-Signature header ditambahkan. Pesan asli tidak berubah. Simpan atau kirim objek yang dikembalikan, bukan inputnya.
Memilih header mana yang akan ditandatangani
Header yang terdaftar dalam signInfo.Headers adalah satu-satunya yang nilainya terikat pada tanda tangan. Apa pun yang tidak tercantum dapat dimodifikasi oleh relay tanpa merusak tanda tangan, yang terkadang diinginkan (misalnya. Received: header ditambahkan hilir) dan terkadang berbahaya (misalnya meninggalkan From tidak ditandatangani membuat tanda tangan menjadi tidak bermakna).
Aturan praktis: selalu tanda tangani From, To, Subject, Date. From khususnya wajib untuk keselarasan 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);
Jika Anda mencantumkan Date tetapi jangan pernah mengatur mailMessage.Date, DKIMSign melempar The following headers to be signed do not exist: Date. Selalu isi header terlebih dahulu, lalu tanda tangani.
Algoritma hash — RSA‑SHA1 vs RSA‑SHA256
DKIM mendukung dua algoritma penandatanganan. Standar di Aspose.Email adalah RSASha1 untuk kompatibilitas mundur, tetapi setiap penyebaran baru harus menggunakan RSASha256. Banyak penyedia kini menandai tanda tangan SHA‑1 sebagai tidak valid bahkan ketika mereka diverifikasi secara kriptografis.
// 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);
Hasil akhirnya DKIM-Signature header mengiklankan algoritma melalui a= tag (a=rsa-sha1 vs a=rsa-sha256). Penerima memeriksa tag tersebut untuk memutuskan hash mana yang akan dihitung ulang.
Kanonisasi — Simple vs Relaxed
Kanonisasi adalah cara DKIM menyetujui bentuk stabil, byte‑exact dari pesan sebelum hashing. Dua algoritma didefinisikan untuk header dan isi secara independen:
Simple— hampir tidak ada normalisasi. Baris kosong di akhir isi dihapus, tetapi spasi lain tidak diubah. Satu spasi tambahan oleh relay akan membuat tanda tangan tidak valid.Relaxed— melipat spasi dalam nilai header, menurunkan huruf header, dan mengompres rangkaian spasi dalam isi. Toleran terhadap pemformatan ulang trivial yang biasanya dilakukan agen transportasi surat.
Pilih Relaxed untuk header maupun isi kecuali Anda memiliki alasan khusus untuk tidak melakukannya. Hampir setiap penyebaran produksi melakukannya.
// 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);
Kanonisasi header dan isi independen — relaxed/simple adalah kombinasi yang sah, ditulis begitulah dalam c= tag dari header DKIM.
Memuat kunci pribadi
PemReader menangani kedua enkoding PEM secara transparan. Tidak ada yang terpisah Pkcs1Reader / Pkcs8Reader — cukup berikan jalur atau aliran:
// 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 mengembalikan sebuah RSACryptoServiceProvider siap diserahkan ke DKIMSign. Ada juga sebuah GetPrivateKey(Stream) overload untuk kasus di mana kunci berasal dari vault kunci, blob terenkripsi, atau sumber daya yang disematkan alih‑alih sistem file.
Aturan penanganan kunci:
- Jangan pernah meng‑commit kunci pribadi ke kontrol versi.
*.pemtermasuk dalam.gitignore. - Dalam produksi, muat kunci dari manajer rahasia (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault), bukan dari disk.
- Putar kunci setiap 6–12 bulan. Publikasikan selector baru terlebih dahulu, beralih penandatanganan, lalu tarik catatan DNS lama setelah periode tenggang.
Memverifikasi pesan yang ditandatangani dari file
Verifikasi adalah operasi simetris: diberikan pesan yang ditandatangani .eml, DkimVerifier mengekstrak DKIM-Signature header, melakukan pencarian DNS terhadap <selector>._domainkey.<domain> untuk kunci publik, dan menghitung ulang hash. Hasilnya adalah sebuah DkimResult dengan IsValid dan 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");
Memverifikasi pesan yang ditandatangani dengan
d=example.comakan selalu gagal dengan "kunci tidak ada dalam kamus" atau kesalahan pencarian DNS serupa, karenaexample.comtidak mempublikasikan catatan DKIM. Gunakan domain yang Anda kontrol (atau pesan tes yang terpercaya) untuk pengujian verifikasi nyata.
Verifikasi dari aliran
Ketika pesan tidak berada di disk — berasal dari fetch IMAP, antrean, unggahan HTTP, atau penyimpanan blob — gunakan Stream overload. Ia menerima aliran baca yang dapat di‑seek:
// 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) dan Verify(string) berfungsi identik — keduanya memblokir hingga pencarian DNS selesai. Untuk apa pun yang berhadapan dengan pengguna, gunakan varian async di bawah.
Verifikasi Asinkron
Verifikasi DKIM melibatkan putaran DNS, yang dapat memakan puluhan milidetik bahkan pada jaringan cepat. Di dalam handler permintaan ASP.NET, loop pemrosesan pesan, atau thread UI mana pun, itu cepat menumpuk. VerifyAsync mengembalikan sebuah Task<DkimResult> bahwa Anda dapat 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");
Ada juga sebuah VerifyAsync(Stream) overload. Seperti API sinkron, metode ini menyatakan async Task (bukan async void) — lokasi panggilan harus await itu, dan entrypoint console-app dapat melakukan VerifySignatureAsync().GetAwaiter().GetResult() ketika sebuah async Main tidak tersedia.
End-to-end: muat, tandatangani, simpan, verifikasi
Bagian-bagian di atas digabung menjadi satu alur kerja. Ini adalah pipeline tanda tangan-dan-verifikasi lengkap yang dapat Anda sematkan di dalam layanan email transaksional:
// 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"]);
Sebuah DKIM-Signature header yang dihasilkan oleh kode ini terlihat seperti ini:
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...
Tag menarik:
v=1— versi DKIM.a=rsa-sha256— algoritma; mencerminkanHashAlgorithm.c=relaxed/relaxed— kanonisasi (header/isi).d=example.com/s=dkim— selector dan domain; bersama-sama membentuk nama pencarian DNS.h=from:to:subject:date:mime-version— header yang ditandatangani, dalam urutan.bh=— hash base64 dari isi yang dikanonisasi.b=— tanda tangan RSA aktual atas header yang dikanonisasi.
Jika Anda mengubah nilai header yang ditandatangani, atau memodifikasi isi, bh dan b tidak lagi cocok dan verifikasi gagal.
Mempublikasikan kunci publik di DNS
Menandatangani secara lokal hanya setengah pekerjaan. Agar penerima dapat memverifikasi, pasangan publik kunci RSA Anda harus dipublikasikan sebagai catatan DNS TXT di <selector>._domainkey.<domain>:
Record: dkim._domainkey.example.com
Type: TXT
Value: v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC...
Ekstrak kunci publik dari PEM Anda dengan OpenSSL:
openssl rsa -in sample-private-key.pem -pubout -out sample-public-key.pem
Hapus baris header/footer dan gabungkan konten base64 untuk p= nilai. Kemudian konfirmasi catatan tersebut aktif sebelum mengandalkannya:
dig dkim._domainkey.example.com TXT
Alat daring seperti Validator DKIM dan Pemeriksa DKIM MXToolbox akan memberi tahu Anda apakah pesan nyata yang Anda kirim dapat diverifikasi end-to-end. Gunakan sebelum mengalihkan lalu lintas produksi.