Sunday, September 20, 2009 

Sending a DKIM Signed Email from C#

First off this code is intended to be instructional only! Yes it is possible to send a DKIM email from .net's SmtpClient. But there are a few hacks/warnings to go with the following code. I'm pretty sure DKIM is designed for signatures to be added by the STMP server. All the emails I've seen with DKIM signatures sign headers that would only be available to the server, e.g: Message-ID and Received. What does the RFC 4871 have to say about this:

Note that verifiers may treat unsigned header fields with extreme skepticism, including refusing to display them to the end user or even ignoring the signature if it does not cover certain header fields. For this reason, signing fields present in the message such as Date, Subject, Reply-To, Sender, and all MIME header fields are highly advised.
Second: We need to sign the message body exactly as SmtpClient is going to deliver it to the SMTP server. Unfortunately MailMessage doesn't give us access to this. I've found some code to do the Quoted-printable encoding here: Nice Clean C# Generate Quoted-Printable. Still we couldn't guarantee we are going to hash the body exactly as SmtpClient is going to send. In this example I'm just replacing the carriage returns for an extremely simple sample email.

Let's get started. First use OpenSSL to generate your public and private keys: DomainKeys Public/Private Key-pair Generation. Now add the public key to the relevant TXT record on the domain you intend to send email from. I bought a cheap .info domain for this demo, and pointed the registrar to ZoneEdit. I had no trouble adding to TXT record to my domain via ZoneEdit.

...and now the code: (again I'm using Bouncy Castle Crypto APIs to create the signature)

string smtp = "~~ YOUR STMP SERVER HERE ~~";
string from = "russ@dkimtester.info";
string subject = "dkim test email";
string to = "check-auth@verifier.port25.com";
string body = "This is the body of the message." + Environment.NewLine + "This is the second line";
string base64privatekey = @"-----BEGIN RSA PRIVATE KEY-----
~~ YOUR PRIVATE KEY HERE ~~
-----END RSA PRIVATE KEY-----";

HashAlgorithm hash = new SHA256Managed();
// HACK!! simulate the quoted-printable encoding SmtpClient will use
string hashBody = body.Replace(Environment.NewLine, "=0D=0A") + Environment.NewLine;
byte[] bodyBytes = Encoding.ASCII.GetBytes(hashBody);
string hashout = Convert.ToBase64String(hash.ComputeHash(bodyBytes));
// timestamp  - seconds since 00:00:00 on January 1, 1970 UTC
TimeSpan t = DateTime.Now.ToUniversalTime() - DateTime.SpecifyKind(DateTime.Parse("00:00:00 January 1, 1970"), DateTimeKind.Utc);

string signatureHeader = "v=1; " +
 "a=rsa-sha256; " +
 "c=relaxed/relaxed; " +
 "d=dkimtester.info; " +
 "s=p; " +
 "t=" + Convert.ToInt64(t.TotalSeconds) + "; " +
 "bh=" + hashout + "; " +     
 "h=From:To:Subject:Content-Type:Content-Transfer-Encoding; " +
 "b="; 

string canonicalizedHeaders =
"from:" + from + Environment.NewLine +
"to:" + to + Environment.NewLine +
"subject:" + subject + Environment.NewLine +
@"content-type:text/plain; charset=us-ascii
content-transfer-encoding:quoted-printable
dkim-signature:" + signatureHeader;

TextReader reader = new StringReader(base64privatekey);
Org.BouncyCastle.OpenSsl.PemReader r = new Org.BouncyCastle.OpenSsl.PemReader(reader);
AsymmetricCipherKeyPair o = r.ReadObject() as AsymmetricCipherKeyPair;
byte[] plaintext = Encoding.ASCII.GetBytes(canonicalizedHeaders);
ISigner sig = SignerUtilities.GetSigner("SHA256WithRSAEncryption");
sig.Init(true, o.Private);
sig.BlockUpdate(plaintext, 0, plaintext.Length);
byte[] signature = sig.GenerateSignature();
signatureHeader += Convert.ToBase64String(signature);

MailMessage message = new MailMessage();
message.From = new MailAddress(from);
message.To.Add(new MailAddress(to));
message.Subject = subject;
message.Body = body;

message.Headers.Add("DKIM-Signature", signatureHeader);
SmtpClient client = new SmtpClient(smtp);
client.Send(message);
Console.Write("sent to: " + to);

An email sent to gmail gets the cool "signed-by" info:

Labels: ,