인증 및 전송 가능성을 위한 DKIM 이메일 서명
DKIM (DomainKeys Identified Mail, RFC 6376) 도메인 소유자가 발신 이메일에 암호 서명을 첨부하도록 합니다. 수신 서버는 DNS에서 해당 공개 키를 가져와 해시를 재계산하고 두 가지를 동시에 확인합니다: 전송 중에 메시지 본문이 수정되지 않았으며, 메시지가 도메인에 의해 승인된 발신자에서 실제로 발송되었는지. DKIM이 없으면 최신 제공업체(Gmail, Microsoft 365, Yahoo)는 메일을 스팸으로 표시하거나 조용히 삭제하거나 배달을 거부합니다 — DKIM은 이제 프로덕션 메일에 선택 사항이 아닙니다.
이 가이드는 전체 walkthrough입니다 Aspose.Email의 .NET용 DKIM 네임스페이스입니다. 이 기사에 있는 모든 코드 블록은 DKIMExamples 프로젝트에 포함되어 라이브러리와 전체 엔드투엔드 검증을 거쳤습니다.
전제 조건
DKIM 유형이 위치한 Aspose.Email.DKIM 네임스페이스이며 라이브러리의 .NET Framework 4.0/4.5 빌드와만 함께 배포됩니다. .NET Standard 2.0 및 .NET 6/8 패키지에서는 의도적으로 제외되었습니다. 아래 예제를 따르려면 프로젝트가 net45 (대상 net48 소비하는 .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>
RSA 키 쌍도 필요합니다. 개인 키는 애플리케이션에 보관되어 서명에 사용되고, 공개 키는 DNS TXT 레코드로 게시되어 수신자가 검증할 수 있습니다. 오늘날 표준은 2048비트 키입니다:
openssl genrsa -out sample-private-key.pem 2048
Aspose의 PemReader PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) 및 PKCS#8 (-----BEGIN PRIVATE KEY-----) 형식 파일.
| 클래스 | 역할 | |——-|——| | DKIMSignatureInfo | 서명을 설명합니다: 선택자, 도메인, 해시 알고리즘, 정규화, 그리고 포함할 헤더. | | PemReader | PEM 형식 파일 또는 스트림에서 RSA 개인 키를 로드하여 RSACryptoServiceProvider. |
| MailMessage.DKIMSign(rsa, info) | 확장/인스턴스 메서드로, 다음을 사용해 메시지의 서명된 복사본을 반환합니다 DKIM-Signature 헤더가 채워졌습니다. | | DkimVerifier | 서명된 메시지의 DNS 기반 검증 수행 — 파일, 스트림, 동기식 또는 비동기식. |
전체 흐름은 항상 다음과 같습니다: 키 로드 → 서명 설명 → 메시지 구성 → 서명 → (선택적으로) 검증.
최소 서명 예제
유효한 DKIM 서명 메시지를 생성하는 가장 작은 코드량은 다음과 같습니다:
// 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)}...");
에 대한 두 인수 DKIMSignatureInfo — "dkim" 및 "example.com" — 선택자와 도메인입니다. 이 둘은 수신자가 DNS TXT 레코드에서 공개 키를 찾을 수 있도록 알려줍니다: dkim._domainkey.example.com. 짧은 선택자 이름을 선택하십시오(종종 default, mail, 또는 다음과 같은 날짜 2026jan); 선택자를 사용하면 이전 키를 버리지 않고 키를 교체할 수 있습니다.
중요한 세부 사항: DKIMSign 새로운 새 MailMessage 인스턴스를 사용하여 파일을 로드합니다. DKIM-Signature 헤더가 추가되었습니다. 원본 메시지는 변경되지 않습니다. 입력이 아닌 반환된 객체를 저장하거나 전송하십시오.
서명할 헤더 선택하기
헤더가 나열된 곳 signInfo.Headers are the only ones whose values are bound to the signature. Anything not listed can be modified by relays without breaking the signature, which is sometimes desirable (e.g. Received: headers added downstream) and sometimes dangerous (e.g. leaving From unsigned makes the signature meaningless).
경험법칙: 항상 서명하십시오 From, To, Subject, Date. From in particular is mandatory for DMARC alignment.
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);
If you list Date but never set mailMessage.Date, DKIMSign throws The following headers to be signed do not exist: Date. 항상 헤더를 먼저 채운 다음 서명하십시오.
해시 알고리즘 — RSA-SHA1 vs RSA-SHA256
DKIM은 두 가지 서명 알고리즘을 지원합니다. Aspose.Email의 기본값은 RSASha1 호환성을 위해 유지되지만, 새 배포에서는 반드시 사용해야 합니다 RSASha256**. 많은 공급자가 이제 SHA-1 서명을 암호학적으로 검증되더라도 무효로 표시합니다.
// 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);
결과적인 DKIM-Signature 헤더는 알고리즘을 태그를 통해 광고합니다 a= 태그 (a=rsa-sha1 vs a=rsa-sha256). 수신자는 해당 태그를 참조하여 어떤 해시를 재계산할지 결정합니다.
정규화 — Simple vs Relaxed
정규화는 해시하기 전에 메시지의 안정적인 바이트 정확형을 정의하는 방법입니다. 헤더와 본문에 대해 각각 두 가지 알고리즘이 정의됩니다:
Simple— 거의 정규화가 없습니다. 본문의 끝 빈 줄은 제거되지만 다른 공백은 그대로 유지됩니다. 릴레이가 추가한 한 칸의 공백도 서명을 무효화합니다.Relaxed— 헤더 값 내부의 공백을 접고, 헤더 이름을 소문자로 변환하며, 본문의 연속 공백을 축소합니다. 메일 전송 에이전트가 수행하는 사소한 재포맷을 허용합니다.
선택 Relaxed 특별한 이유가 없는 한 헤더와 본문 모두에 적용됩니다. 거의 모든 프로덕션 배포가 이렇게 합니다.
// 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);
헤더와 본문 정규화는 독립적입니다 — relaxed/simple 완전히 유효한 조합이며, 해당 파일에 그렇게 작성되어 있습니다 c= DKIM 헤더의 태그.
개인 키 로드
PemReader PEM 인코딩을 투명하게 처리합니다. 별도의 Pkcs1Reader / Pkcs8Reader — 경로나 스트림만 전달하십시오:
// 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 다음을 반환합니다 RSACryptoServiceProvider hand to DKIMSign. 또한 GetPrivateKey(Stream) 키가 파일 시스템이 아닌 키 볼트, 암호화된 Blob 또는 임베디드 리소스에서 오는 경우에 대한 overload.
키 처리 규칙:
- 절대 개인 키를 버전 관리에 커밋하지 마십시오.
*.pembelongs in.gitignore. - 운영 환경에서는 디스크가 아닌 비밀 관리 서비스(Azure Key Vault, AWS Secrets Manager, HashiCorp Vault)에서 키를 로드하십시오.
- 키를 6–12개월마다 교체하십시오. 새 선택자를 먼저 게시하고, 서명을 전환한 뒤, 유예 기간 후에 오래된 DNS 레코드를 제거합니다.
파일에서 서명된 메시지 검증
검증은 대칭 연산입니다: 서명된 .eml, DkimVerifier 추출합니다 DKIM-Signature 헤더이며, DNS 조회를 수행합니다 <selector>._domainkey.<domain> 공개 키를 가져와 해시를 재계산합니다. 결과는 DkimResult 와 함께 IsValid 및 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");
서명된 메시지 검증
d=example.com"키가 사전(dict)에 없습니다" 또는 유사한 DNS 조회 오류와 함께 항상 실패합니다, 왜냐하면example.comDKIM 레코드를 게시하지 않습니다. 실제 검증 테스트를 위해서는 제어 가능한 도메인(또는 알려진 테스트 메시지)을 사용하십시오.
스트림에서 검증
메시지가 디스크에 없을 때 — IMAP 가져오기, 큐, HTTP 업로드 또는 Blob 스토리지에서 온 경우 — Stream overload. It accepts any seekable read stream:
// 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) 및 Verify(string) 기능적으로 동일합니다 — 둘 다 DNS 조회가 완료될 때까지 차단합니다. 사용자와 직접 마주하는 경우 아래 비동기 버전을 사용하십시오.
비동기 검증
DKIM 검증에는 DNS 왕복이 포함되며, 빠른 네트워크에서도 수십 밀리초가 걸릴 수 있습니다. ASP.NET 요청 핸들러, 메시지 처리 루프 또는 UI 스레드 내에서 사용하면 빠르게 누적됩니다. VerifyAsync returns a Task<DkimResult> that you can 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");
또한 VerifyAsync(Stream) overload. As with the sync API, the method declares async Task (not async void) — call sites should await it, and console-app entrypoints can do VerifySignatureAsync().GetAwaiter().GetResult() when an async Main 사용할 수 없습니다.
엔드 투 엔드: 로드, 서명, 저장, 검증
위의 조각들이 하나의 워크플로우로 결합됩니다. 이것이 거래형 이메일 서비스에 삽입할 전체 서명 및 검증 파이프라인입니다:
// 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"]);
완전한 DKIM-Signature 이 코드가 생성한 헤더는 다음과 같습니다:
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...
흥미로운 태그:
v=1— DKIM 버전.a=rsa-sha256— 알고리즘; 반영됨HashAlgorithm.c=relaxed/relaxed— 정규화(헤더/본문).d=example.com/s=dkim— 선택자와 도메인; 함께 DNS 조회 이름을 형성합니다.h=from:to:subject:date:mime-version— 순서대로 서명된 헤더.bh=— 정규화된 본문의 base64 해시.b=— 정규화된 헤더에 대한 실제 RSA 서명.
서명된 헤더 값을 변경하거나 본문을 수정하면 bh 및 b 더 이상 일치하지 않아 검증이 실패합니다.
DNS에 공개 키 게시
로컬 서명은 절반에 불과합니다. 수신자가 검증하려면 RSA 키의 공개 부분을 DNS TXT 레코드에 게시해야 합니다: <selector>._domainkey.<domain>:
Record: dkim._domainkey.example.com
Type: TXT
Value: v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC...
OpenSSL을 사용하여 PEM에서 공개 키를 추출:
openssl rsa -in sample-private-key.pem -pubout -out sample-public-key.pem
헤더/푸터 라인을 제거하고 base64 콘텐츠를 연결하여 p= 값을 입력하십시오. 그런 다음 레코드가 실시간인지 확인한 후 사용하십시오:
dig dkim._domainkey.example.com TXT
온라인 도구 예: DKIM 검증기 및 MXToolbox DKIM 검사기 실제 전송한 메시지가 끝까지 검증 가능한지 알려줍니다. 프로덕션 트래픽을 전환하기 전에 이를 사용하십시오.