Ký Email bằng DKIM để Xác thực và Độ tin cậy
DKIM (DomainKeys Identified Mail, RFC 6376) cho phép chủ sở hữu miền đính kèm chữ ký mật mã vào email gửi đi. Các máy chủ nhận sẽ lấy khóa công khai tương ứng từ DNS, tính lại hàm băm và xác nhận đồng thời hai điều: nội dung tin nhắn không bị thay đổi trong quá trình truyền, và tin nhắn thực sự xuất phát từ người gửi được ủy quyền bởi miền. Không có DKIM, các nhà cung cấp hiện đại (Gmail, Microsoft 365, Yahoo) sẽ đánh dấu email là spam, loại bỏ nó im lặng hoặc từ chối giao nhận — DKIM không còn là tùy chọn cho email sản xuất.
Hướng dẫn này là một bài hướng dẫn đầy đủ về Aspose.Emailkhông gian tên DKIM của .NET. Mỗi khối mã trong bài viết này được lấy từ một ví dụ có thể chạy được trong DKIMExamples dự án và đã được xác minh đầu cuối so với thư viện.
Điều kiện tiên quyết
Các kiểu DKIM nằm trong Aspose.Email.DKIM không gian tên và chỉ ship với các bản dựng .NET Framework 4.0/4.5 của thư viện. Chúng được cố ý loại bỏ khỏi các gói .NET Standard 2.0 và .NET 6/8. Để làm theo các ví dụ dưới đây, dự án của bạn phải tham chiếu một assembly Aspose.Email được xây dựng cho net45 (mục tiêu net48 trong phần tiêu thụ .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>
Bạn cũng cần một cặp khóa RSA. Khóa riêng được giữ trong ứng dụng của bạn và dùng để ký; khóa công khai được công bố dưới dạng bản ghi DNS TXT để người nhận có thể xác minh. Khóa 2048-bit là tiêu chuẩn hiện nay:
openssl genrsa -out sample-private-key.pem 2048
Của Aspose PemReader chấp nhận cả PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) và PKCS#8 (-----BEGIN PRIVATE KEY-----) các tệp đã định dạng.
| Lớp | Vai trò | |——-|——| | DKIMSignatureInfo | Mô tả chữ ký: bộ chọn, miền, thuật toán băm, chuẩn hoá, và các tiêu đề cần bao phủ. | | PemReader | Tải một khóa RSA riêng từ tệp hoặc luồng định dạng PEM vào RSACryptoServiceProvider. |
| MailMessage.DKIMSign(rsa, info) | Phương thức mở rộng/instance trả về một bản sao đã ký của tin nhắn với DKIM-Signature tiêu đề đã được điền. | | DkimVerifier | Thực hiện xác minh dựa trên DNS cho một tin nhắn đã ký — tệp, luồng, đồng bộ hoặc bất đồng bộ. |
Quy trình đầy đủ luôn luôn: tải khóa → mô tả chữ ký → xây dựng tin nhắn → ký → (tùy chọn) xác minh.
Ví dụ ký tối thiểu
Đây là đoạn mã ngắn nhất tạo ra một tin nhắn đã ký DKIM hợp lệ:
// 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)}...");
Hai đối số cho DKIMSignatureInfo — "dkim" và "example.com" — là bộ chọn và miền. Cùng nhau chúng cho người nhận biết nơi tra cứu khóa công khai của bạn: tại bản ghi DNS TXT dkim._domainkey.example.com. Chọn một tên bộ chọn ngắn (thường là default, mail, hoặc một ngày như 2026jan); bộ chọn cho phép bạn xoay khóa mà không cần loại bỏ khóa cũ.
Một chi tiết quan trọng: DKIMSign trả về một mới MailMessage đối tượng với DKIM-Signature tiêu đề được thêm. Tin nhắn gốc không thay đổi. Lưu hoặc gửi đối tượng trả về, không phải đầu vào.
Chọn các tiêu đề để ký
Các tiêu đề được liệt kê trong signInfo.Headers là những tiêu đề duy nhất có giá trị bị ràng buộc với chữ ký. Bất kỳ tiêu đề nào không được liệt kê có thể được các relay sửa đổi mà không phá vỡ chữ ký, điều này đôi khi muốn có (ví dụ. Received: tiêu đề được thêm ở hạ nguồn) và đôi khi nguy hiểm (ví dụ để lại From không ký làm cho chữ ký mất ý nghĩa).
Quy tắc chung: luôn ký From, To, Subject, Date. From đặc biệt là bắt buộc cho sự đồng bộ 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);
Nếu bạn liệt kê Date nhưng không bao giờ đặt mailMessage.Date, DKIMSign ném The following headers to be signed do not exist: Date. Luôn điền các tiêu đề trước, sau đó ký.
Thuật toán băm — RSA‑SHA1 vs RSA‑SHA256
DKIM hỗ trợ hai thuật toán ký. Mặc định trong Aspose.Email là RSASha1 đối với khả năng tương thích ngược, nhưng mọi triển khai mới nên sử dụng RSASha256. Nhiều nhà cung cấp hiện đánh dấu chữ ký SHA‑1 là không hợp lệ ngay cả khi chúng xác minh được về mặt mật mã.
// 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);
Kết quả là DKIM-Signature tiêu đề quảng cáo thuật toán qua a= thẻ (a=rsa-sha1 vs a=rsa-sha256). Người nhận tham khảo thẻ này để quyết định thuật toán băm nào sẽ được tính lại.
Chuẩn hoá — Simple vs Relaxed
Chuẩn hoá là cách DKIM đồng ý về một dạng ổn định, byte‑exact của tin nhắn trước khi băm. Hai thuật toán được định nghĩa riêng cho tiêu đề và nội dung:
Simple— hầu như không chuẩn hoá. Các dòng trống cuối trong nội dung bị xóa, nhưng không có whitespace nào khác bị chạm. Một khoảng trắng đơn được thêm bởi một relay sẽ làm mất hiệu lực chữ ký.Relaxed— gập whitespace bên trong giá trị tiêu đề, chuyển các tên tiêu đề sang chữ thường, và gộp các chuỗi whitespace trong nội dung. Chịu được việc định dạng lại đơn giản mà các mail transport agent thường làm.
Chọn Relaxed đối với cả tiêu đề và nội dung trừ khi bạn có lý do cụ thể để không làm vậy. Hầu hết các triển khai sản xuất đều làm như vậy.
// 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);
Tiêu đề và nội dung chuẩn hoá là độc lập — relaxed/simple là một sự kết hợp hoàn toàn hợp lệ, được viết như vậy trong c= thẻ của tiêu đề DKIM.
Tải khóa riêng
PemReader xử lý cả hai mã hoá PEM một cách trong suốt. Không có Pkcs1Reader / Pkcs8Reader — chỉ truyền đường dẫn hoặc luồng:
// 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 trả về một RSACryptoServiceProvider sẵn sàng giao cho DKIMSign. Cũng có một GetPrivateKey(Stream) quá tải cho các trường hợp khóa đến từ vault, blob được mã hoá, hoặc tài nguyên nhúng thay vì hệ thống tệp.
Quy tắc xử lý khóa:
- Không bao giờ commit khóa riêng tư vào hệ thống kiểm soát phiên bản.
*.pemthuộc về.gitignore. - Trong môi trường sản xuất, tải khóa từ trình quản lý bí mật (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault), không phải từ đĩa.
- Quay vòng khóa mỗi 6–12 tháng. Đầu tiên công bố bộ chọn mới, chuyển sang ký, sau đó rút lại bản ghi DNS cũ sau một thời gian ân hạn.
Xác minh một tin nhắn đã ký từ tệp
Xác minh là hoạt động đối xứng: cho một nội dung đã ký .eml, DkimVerifier trích xuất DKIM-Signature tiêu đề, thực hiện tra cứu DNS chống lại <selector>._domainkey.<domain> đối với khóa công khai, và tính lại hàm băm. Kết quả là một DkimResult với IsValid và 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");
Xác minh một tin nhắn đã ký bằng
d=example.comsẽ luôn thất bại với "khóa không có trong từ điển" hoặc lỗi tra cứu DNS tương tự, vìexample.comkhông công bố bản ghi DKIM. Sử dụng một miền bạn kiểm soát (hoặc một tin nhắn kiểm tra đáng tin cậy) cho các thử nghiệm xác minh thực tế.
Xác minh từ luồng
Khi tin nhắn không có trên đĩa — nó đến từ một fetch IMAP, một hàng đợi, một tải lên HTTP, hoặc lưu trữ blob — hãy dùng Stream quá tải. Nó chấp nhận bất kỳ luồng đọc có thể tìm kiếm nào:
// 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) và Verify(string) cùng chức năng — cả hai đều chặn cho đến khi tra cứu DNS hoàn tất. Đối với mọi thứ hướng tới người dùng, hãy dùng các biến thể bất đồng bộ bên dưới.
Xác minh bất đồng bộ
Xác minh DKIM bao gồm một vòng quay DNS, có thể mất vài chục mili giây ngay cả trên mạng nhanh. Trong một handler yêu cầu ASP.NET, vòng lặp xử lý tin nhắn, hoặc bất kỳ luồng UI nào, điều này tích lũy nhanh chóng. VerifyAsync trả về một Task<DkimResult> rằng bạn có thể 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");
Cũng có một VerifyAsync(Stream) quá tải. Giống như API đồng bộ, phương thức khai báo async Task (không async void) — các vị trí gọi nên await nó, và các điểm vào console‑app có thể thực hiện VerifySignatureAsync().GetAwaiter().GetResult() khi một async Main không khả dụng.
Đầu‑cuối: tải, ký, lưu, xác minh
Các phần ở trên kết hợp thành một quy trình làm việc duy nhất. Đây là quy trình ký‑và‑xác minh đầy đủ mà bạn sẽ nhúng vào dịch vụ email giao dịch:
// 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"]);
Một bộ DKIM-Signature tiêu đề được tạo bởi đoạn mã này trông như sau:
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...
Các thẻ thú vị:
v=1— phiên bản DKIM.a=rsa-sha256— thuật toán; phản ánhHashAlgorithm.c=relaxed/relaxed— chuẩn hoá (tiêu đề/nội dung).d=example.com/s=dkim— bộ chọn và miền; chúng cùng nhau tạo thành tên tra cứu DNS.h=from:to:subject:date:mime-version— các tiêu đề đã ký, theo thứ tự.bh=— hàm băm base64 của nội dung đã chuẩn hoá.b=— chữ ký RSA thực tế trên các tiêu đề đã chuẩn hoá.
Nếu bạn thay đổi bất kỳ giá trị tiêu đề đã ký nào, hoặc sửa đổi nội dung, bh và b không còn khớp và việc xác minh sẽ thất bại.
Xuất bản khóa công khai trong DNS
Ký cục bộ chỉ là một nửa công việc. Để người nhận xác minh, phần công khai của khóa RSA của bạn phải được xuất bản dưới dạng bản ghi DNS TXT tại <selector>._domainkey.<domain>:
Record: dkim._domainkey.example.com
Type: TXT
Value: v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC...
Trích xuất khóa công khai từ PEM của bạn bằng OpenSSL:
openssl rsa -in sample-private-key.pem -pubout -out sample-public-key.pem
Xóa các dòng tiêu đề/chân và nối nội dung base64 cho p= giá trị. Sau đó xác nhận bản ghi đang hoạt động trước khi dựa vào nó:
dig dkim._domainkey.example.com TXT
Các công cụ trực tuyến như DKIM Validator và MXToolbox DKIM Checker sẽ cho bạn biết liệu một tin nhắn thực tế bạn gửi có thể xác minh được từ đầu đến cuối hay không. Hãy sử dụng chúng trước khi chuyển lưu lượng sản xuất.