Assinando E‑mails com DKIM para Autenticação e Entregabilidade
DKIM (DomainKeys Identified Mail, RFC 6376) permite que o proprietário de um domínio anexe uma assinatura criptográfica ao e‑mail de saída. Servidores receptores buscam a chave pública correspondente no DNS, recalculam o hash e confirmam duas coisas simultaneamente: o corpo da mensagem não foi alterado no trânsito e a mensagem realmente se originou de um remetente autorizado pelo domínio. Sem DKIM, provedores modernos (Gmail, Microsoft 365, Yahoo) marcam o e‑mail como spam, o descartam silenciosamente ou recusam a entrega — DKIM não é mais opcional para e‑mail de produção.
Este guia é um walkthrough completo de Aspose.Emailnamespace DKIM da .NET da ‘.’. Cada bloco de código neste artigo foi extraído de um exemplo executável no DKIMExamples projeto e foi verificado de ponta a ponta contra a biblioteca.
Pré-requisitos
Os tipos DKIM vivem no Aspose.Email.DKIM namespace e distribua apenas com as compilações .NET Framework 4.0/4.5 da biblioteca. Elas são intencionalmente excluídas dos pacotes .NET Standard 2.0 e .NET 6/8. Para seguir os exemplos abaixo, seu projeto deve referenciar um assembly Aspose.Email compilado para net45 (destino net48 no consumidor .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>
Você também precisa de um par de chaves RSA. A chave privada fica em sua aplicação e é usada para assinar; a chave pública é publicada como registro DNS TXT para que os destinatários possam verificar. Uma chave de 2048 bits é o padrão atualmente:
openssl genrsa -out sample-private-key.pem 2048
Aspose’s PemReader aceita tanto PKCS#1 (-----BEGIN RSA PRIVATE KEY-----) e PKCS#8 (-----BEGIN PRIVATE KEY-----) arquivos formatados.
| Classe | Função | |——-|——| | DKIMSignatureInfo | Descreve a assinatura: selector, domínio, algoritmo de hash, canonização e quais cabeçalhos cobrir. | | PemReader | Carrega uma chave privada RSA de um arquivo ou fluxo formatado em PEM para um RSACryptoServiceProvider. |
| MailMessage.DKIMSign(rsa, info) | Método de extensão/instância que retorna uma cópia assinada da mensagem com o DKIM-Signature cabeçalho preenchido. | | DkimVerifier | Executa verificação baseada em DNS de uma mensagem assinada — arquivo, fluxo, síncrono ou assíncrono. |
O fluxo completo é sempre: carregar chave → descrever assinatura → montar mensagem → assinar → (opcionalmente) verificar.
O exemplo mínimo de assinatura
Este é o menor trecho de código que produz uma mensagem assinada com DKIM válida:
// 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)}...");
Os dois argumentos para DKIMSignatureInfo — "dkim" e "example.com" — são o selector e o domínio. Juntos, eles indicam aos destinatários onde buscar sua chave pública: no registro DNS TXT dkim._domainkey.example.com. Escolha um nome de selector curto (geralmente default, mail, ou uma data como 2026jan); o selector permite girar chaves sem descartar a antiga.
Um detalhe crítico: DKIMSign retorna um novo MailMessage instância com o DKIM-Signature cabeçalho adicionado. A mensagem original permanece inalterada. Salve ou envie o objeto retornado, não a entrada.
Escolhendo quais cabeçalhos assinar
Os cabeçalhos listados em signInfo.Headers são os únicos cujos valores estão vinculados à assinatura. Qualquer coisa não listada pode ser modificada por retransmissores sem quebrar a assinatura, o que às vezes é desejável (ex.: Received: cabeçalhos adicionados a jusante) e às vezes perigoso (ex.: deixar From não assinado torna a assinatura sem sentido).
Regra prática: sempre assine From, To, Subject, Date. From em particular é obrigatório para alinhamento 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);
Se você listar Date mas nunca defina mailMessage.Date, DKIMSign lança The following headers to be signed do not exist: Date. Sempre preencha os cabeçalhos primeiro, então assine.
Algoritmos de hash — RSA‑SHA1 vs RSA‑SHA256
DKIM suporta dois algoritmos de assinatura. O padrão no Aspose.Email é RSASha1 por compatibilidade retroativa, mas toda nova implantação deve usar RSASha256. Muitos provedores agora marcam assinaturas SHA‑1 como inválidas mesmo quando verificam criptograficamente.
// 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);
O resultado DKIM-Signature cabeçalho anuncia o algoritmo via a= tag (a=rsa-sha1 vs a=rsa-sha256). Os receptores consultam essa tag para decidir qual hash recomputar.
Canonicalização — Simples vs Relaxada
Canonicalização é como o DKIM concorda em uma forma estável, byte‑exata da mensagem antes de gerar o hash. Dois algoritmos são definidos para cabeçalhos e corpos independentemente:
Simple— quase nenhuma normalização. Linhas vazias finais no corpo são removidas, mas nenhum outro espaço é alterado. Um único espaço adicionado por um retransmissor invalidará a assinatura.Relaxed— dobra espaços dentro dos valores de cabeçalho, coloca nomes de cabeçalho em minúsculas e colapsa sequências de espaços no corpo. Tolerante à reformatação trivial que agentes de transporte de e‑mail tendem a fazer.
Escolha Relaxed para ambos, cabeçalho e corpo, a menos que você tenha um motivo específico para não fazê‑lo. Quase toda implantação em produção faz isso.
// 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);
Canonicalização de cabeçalho e corpo são independentes — relaxed/simple é uma combinação perfeitamente válida, escrita dessa forma no c= tag do cabeçalho DKIM.
Carregando a chave privada
PemReader lida com ambas as codificações PEM de forma transparente. Não há um Pkcs1Reader / Pkcs8Reader — basta passar o caminho ou stream:
// 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 retorna um RSACryptoServiceProvider pronto para ser entregue a DKIMSign. Há também um GetPrivateKey(Stream) sobrecarga para casos em que a chave vem de um cofre de chaves, blob criptografado ou recurso incorporado ao invés do sistema de arquivos.
Regras de manipulação de chaves:
- Nunca commit chaves privadas ao controle de versão.
*.pempertence a.gitignore. - Em produção, carregue chaves de um gerenciador de segredos (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault), não do disco.
- Gire as chaves a cada 6–12 meses. Publique primeiro o novo selector, mude a assinatura e então remova o registro DNS antigo após um período de carência.
Verificando uma mensagem assinada a partir de um arquivo
A verificação é a operação simétrica: dado um assinante .eml, DkimVerifier extrai o DKIM-Signature cabeçalho, executa uma consulta DNS contra <selector>._domainkey.<domain> para a chave pública e recalcula o hash. O resultado é um DkimResult com IsValid e 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");
Verificando uma mensagem assinada com
d=example.comsempre falhará com "chave não encontrada no dicionário" ou erro semelhante de consulta DNS, porqueexample.comnão publica um registro DKIM. Use um domínio que você controla (ou uma mensagem de teste conhecida) para testes reais de verificação.
Verificando a partir de um stream
Quando a mensagem não está em disco — veio de uma busca IMAP, fila, upload HTTP ou armazenamento de blobs — use o Stream sobrecarga. Aceita qualquer stream de leitura seekable:
// 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) e Verify(string) são funcionalmente idênticas — ambas bloqueiam até que a consulta DNS seja concluída. Para qualquer coisa voltada ao usuário, use as variantes assíncronas abaixo.
Verificação assíncrona
A verificação DKIM envolve uma ida e volta ao DNS, o que pode levar dezenas de milissegundos mesmo em rede rápida. Dentro de um manipulador de requisição ASP.NET, loop de processamento de mensagens ou qualquer thread de UI, isso se acumula rapidamente. VerifyAsync retorna um Task<DkimResult> que você pode 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");
Há também um VerifyAsync(Stream) sobrecarga. Como na API síncrona, o método declara async Task (não async void) — os locais de chamada devem await ele, e pontos de entrada de aplicativos console podem fazer VerifySignatureAsync().GetAwaiter().GetResult() quando um async Main não está disponível.
Fim a fim: carregar, assinar, salvar, verificar
As peças acima se combinam em um único fluxo de trabalho. Este é o pipeline completo de assinatura e verificação que você incorporaria dentro de um serviço de e‑mail transacional:
// 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"]);
Um DKIM-Signature cabeçalho produzido por este código se parece com isto:
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...
As tags interessantes:
v=1— versão DKIM.a=rsa-sha256— algoritmo; refleteHashAlgorithm.c=relaxed/relaxed— canonicalização (cabeçalho/corpo).d=example.com/s=dkim— selector e domínio; juntos formam o nome de pesquisa DNS.h=from:to:subject:date:mime-version— cabeçalhos assinados, em ordem.bh=— hash base64 do corpo canonicalizado.b=— a assinatura RSA real sobre os cabeçalhos canonicalizados.
Se você mudar algum valor de cabeçalho assinado ou modificar o corpo, bh e b não correspondem mais e a verificação falha.
Publicando a chave pública no DNS
Assinar localmente é apenas metade do trabalho. Para que os destinatários verifiquem, a contraparte pública da sua chave RSA deve ser publicada como um registro DNS TXT em <selector>._domainkey.<domain>:
Record: dkim._domainkey.example.com
Type: TXT
Value: v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC...
Extraia a chave pública do seu PEM com OpenSSL:
openssl rsa -in sample-private-key.pem -pubout -out sample-public-key.pem
Remova as linhas de cabeçalho/rodapé e concatene o conteúdo base64 para o p= valor. Então confirme que o registro está ativo antes de confiar nele:
dig dkim._domainkey.example.com TXT
Ferramentas online como Validador DKIM e Verificador DKIM da MXToolbox informará se uma mensagem real que você enviou é verificável de ponta a ponta. Use‑as antes de mudar o tráfego de produção.