DKIM Signing Outbound Messages in Exchange Server 2007

DomainKeys Identified Mail is a proposed standard that, in its own words, allows a domain (such as skiviez.com) to assert responsibility for the contents of an e-mail message.

The outgoing mail transport agent (“mail server”) computes a hash of the body of the mail message and adds a special, cryptographically signed DKIM-Signature header to the mail message that contains this hash as well as some other information about the message. The public part of the key, used by receiving mail transport agents to verify the signature on incoming messages, is stored as a TXT record in the DNS for the signing domain.

The result of all of this is that the recipient can now know that the body and certain headers of the e-mail message were not tampered with or changed by a third party while the message was in transit. If the signature contained within the DKIM-Signature header doesn’t verify, then it’s possible that the message is a phishing message, spam, or some other false representation of members of the signing domain. (IMHO, however, it is more likely to just indicate a broken DKIM implementation, as there are several obnoxious corner cases that we will see.)

Where is the Exchange support?

No currently released version of Microsoft Exchange supports DKIM natively, not even 2010. This is purportedly because Microsoft threw its weight behind another standard called SPF, or Sender Protection Framework. SPF is much simpler to implement because it does not make a statement about the integrity of the message contents; that is, there is no signing step that requires processing of each outbound message and stamping it with a special header. Instead, a TXT record in the DNS for the sending domain lists the IP addresses from which e-mail messages from that domain are allowed to originate.

For example, let’s say that a store has a mail transport agent at mail.example.com and a Web site at www.example.com. An SPF record for example.com might make a statement like the following:

“E-mail messages purporting to be from @example.com should only originate from either mail.example.com or www.example.com. All other originating IP addresses should be treated with suspicion.”

You can see why this is easier to implement; you just add an entry to DNS, and you don’t have to configure the outbound mail transport agent to do anything. The only reason a mail transport agent would need to change is for it to gain the ability to inspect incoming messages and validate the MAIL FROM part of the message headers against the corresponding SPF TXT records in DNS. And that is what Exchange does today.

While SPF does not validate the integrity of the message–a hacked, legitimate company mail server that spews out spam unwittingly will still pass an SPF–it is a simple, convenient way to mark phishing and spoofing attempts originating from other locations, like a random house in Wisconsin that is unwittingly part of a botnet, with suspicion.

But, then again, a hacked mail transport agent using DKIM that spews out spam unwittingly will still pass a DKIM verification. Spam can be signed just like regular mail. So the advantage of DKIM over SPF is that DKIM provides the ability to prove that the contents of the message were not modified in transit, and if an organization so chose, they could sign different messages with different keys or decide to not sign certain kinds of messages at all, allowing the recipient to interpret these different kinds of mail in different ways.

In reality, most e-mail doesn’t need to be that secure, so the utility-to-difficulty ratio may be a little out-of-whack here, especially when other standards for signing messages already exist.

So DKIM is more complicated and not supported by Microsoft. Why implement DKIM on Exchange?

Because fuck Yahoo!, that’s why.

Actually, let’s step back a minute.

Yahoo! invented something called Domain Keys, which predates DKIM and which DKIM is largely based on, in an attempt to combat spam in the ways described previously. The aim was for Yahoo! mail servers to be able to easily identify entire classes of suspect e-mail and send them to people’s spam folders (which, in Yahoo! weasel words, is referred to as the “bulk mail folder”). Then they integrated it into the Yahoo! mail system.

If your business sends mail of any kind of volume to Yahoo! mail servers, such as an e-mail newsletter that users subscribe to, you’ll quickly see a message along the lines of the following:

421 4.16.55 [TS01] Messages from x.x.x.x temporarily deferred due to excessive user complaints

And you might also notice your e-mail messages going straight to the spam folder on Yahoo! mail accounts by default, with a X-Yahoo-Filtered-Bulk: x.x.x.x header appearing in the message headers. If this has happened, it is because Yahoo! has decided that you are naughty and placed the IP address of your mail transport agent on an internal blacklist. If a Yahoo! user has previously marked e-mail from your domain as “Not Spam,” then that user will continue to receive your e-mail, but any other Yahoo! users will find your e-mail going to the spam folder by default until they either click the “Not Spam” button on one of them or add your from address to their contact list.

“But we’re a legitimate small business,” you say. “Only about 5,000 of the e-mails on our mailing list are actual Yahoo! addresses, the newsletter list is opt-in, and there are ‘Unsubscribe’ links in every e-mail. It’s not like we’re spamming users out of the blue,” you continue. “These are users who have asked for this service and can cancel at anytime. So why has Yahoo! blacklisted us and treat us like spam?”

The answer, unfortunately, is that Yahoo! is incompetent and can’t build a mail service that can handle spam like Gmail can handle spam. Instead, their e-mail system depends on the following metrics:

  • If a large burst of e-mail comes from the same IP address within a certain time period, temporarily consider it as a spam and temporarily deny the requests. The mail will still arrive, but the “fat pipe” will be squashed down so that overall delivery takes longer to complete. The idea is that if the mail is originating from a spammer, they are unlikely to try again.
  • If a “significant” number of Yahoo! users mark mail messages originating from the same IP address as spam by clicking on the “Spam” button in their account, add that IP address to the X-Yahoo-Filtered-Bulk blacklist.

In other words, if enough people mark your e-mail as spam, your IP address is fucked, and you’ll soon be fielding complaints from customers who insist that they aren’t receiving your Web sites transactional e-mails, such as a “forgot my password” or “shipping confirmation” e-mail, because Yahoo! has now decided to deliver all e-mails that originate from your IP address to the spam folder, which many users do not think to check or cannot find. So they blame you.

If you think that Yahoo! has blacklisted you in error, or if you have improved your e-mail sending practices and wish to have them consider you for removal, you can fill out this form with incorrect JavaScript required field validation to request that the Yahoo! postmaster take you off the naughty list. About 27 hours later, a Yahoo! representative with a sub-room temperature IQ will e-mail you back a long, canned reply that generally amounts to “No.”

But in that “no” message, they offer the possibility of signing up to the Complaint Feedback Loop. When you participate in this program, if a Yahoo! user presses the “Spam” button on one of your e-mails, then Yahoo! will e-mail you the e-mail address of that user along with a copy of the e-mail that you sent. You can then unsubscribe that user from your mailing list yourself (since they neglected to use the unsubscribe link in your e-mail and will continue to damage the reputation of your IP address if you continue to send mail to them), or you can see which kinds of e-mails you are sending are creating the most problems.

But participation in their feedback loop requires that you use DKIM to sign all of your outbound mail messages. The reasons for this are not clear, but I assume it has to do with ensuring that you are only notified about the problematic e-mails that legitimately came from your organization, and not ones generated by a spammer. Or, it could simply be a way to drive adoption of Yahoo!’s DKIM standard.

At any rate, Yahoo! mail is the most widely used e-mail service on the planet, so when the idiots in the higher echelons of Yahoo! say “Jump!” then we must ask “How high?” When they say “implement DKIM”, we implement DKIM.

So what are options for implementing DKIM on Exchange?

One option is to simply not do it in Exchange and set up a relaying mail server that has DKIM support, like hMailServer or Postfix with dkim-milter. But if you’re at a small business, the idea of maintaining yet another server for what is conceptually a simple task is not a pleasant thought.

Another option is to use a dedicated device like a Barracuda or an IronPort. This device would sit in front of Exchange and rewrite the mail headers in transit, adding the DKIM-Signature header as it flies out of your office. But these devices are not cheap and are out of reach of many small businesses. And the thought of acquiring a specialized device for doing something any mail transport agent should be able to do natively is not a pleasant thought.

You can buy an off-the-shelf plug-in for Exchange like this one from a company in Hong Kong. The reviews on the Internet do generally seem to indicate that it does work. But do we really want to spend $300 – $800 on a component from a company without a reputation and give that component read access to all of our organization’s outbound mail messages? Trust is certainly a driving factor here.

A fourth option, and the option I pursued since I considered giving up on DKIM if Yahoo! didn’t change its ways after our implementation, is to take advantage of Exchange 2007′s new Transport Agents functionality. This allows you to write custom, managed code that runs on that .NET Framework that integrates with the Exchange message processing pipeline. In our case, we could write a custom transport agent that appends a DKIM-Signature header to outgoing MIME messages.

Setting up the project

In Visual Studio, we just need to use a plain old “C# Class Library” project. The project must target version 2.0 of the CLR (that’s .NET Framework 2.0, .NET Framework 3.0, or .NET Framework 3.5, thanks to the dipshits in Microsoft’s marketing department) and not the .NET Framework 1.x or, maddeningly, the .NET Framework 4.x. This is because the transport agent process provided by Exchange Server 2007 that will load our transport agent is running on CLR 2.0, and a process that’s been loaded with an earlier version of the CLR can’t load a later version.

We also need to reference two assemblies provided by the Exchange Server, Microsoft.Exchange.Data.Common.dll and Microsoft.Exchange.Data.Transport.dll. You would think that you would be able to find these in, say, the Exchange Server SDK that’s available from Microsoft Downloads. But you’d be wrong. Microsoft keeps diddling with the version of these assemblies with various update rollups for Exchange, and the only way to get a copy is to pull them off an actual Exchange Server 2007 installation. Mine were located in C:\Program Files\Microsoft\Exchange Server\Public; I just copied them to my local computer and referenced them in my new class library assembly for local development.

To start, we just need to create two classes. The first is our actual agent, which derives from the RoutingAgent class defined in the DLLs we just copied:

public sealed class DkimSigningRoutingAgent : RoutingAgent
{
	public DkimSigningRoutingAgent()
	{
		// What "Categorized" means in this sense,
		// only the Exchange team knows
		this.OnCategorizedMessage += this.WhenMessageCategorized;
	}
 
	private void WhenMessageCategorized(
		CategorizedMessageEventSource source,
		QueuedMessageEventArgs e)
	{
		// This is where the magic will happen
	}
}

The second is a factory class that the Exchange server’s little plug-in agent looks for; it’ll instance new copies of our agent:

public sealed class DkimSigningRoutingAgentFactory : RoutingAgentFactory
{
	public override RoutingAgent CreateAgent(SmtpServer server)
	{
		return new DkimSigningRoutingAgent();
	}
}

You’re probably thinking, “Wow! The documentation provided by the Exchange team sure blows donkey chunks, how did you ever figure out that that was what you needed to do?” The answer is by spelunking through the Exchange Server SDK examples and by religiously following this guy’s blog.

Now we’re ready to start reading mail messages and “canonicalizing” them into the format that the DKIM spec expects.

Exchange and “Internet Mail”

If there is one thing that is annoying about Exchange, it is that 20-some years after the fact it seems skeptical that this whole “Internet Mail” thing is going to catch on. Getting Exchange to give you the raw message in MIME format is not as simple as one might think.

You see, to be able to hash the body of the message and sign certain headers in the message, we need to know exactly how Exchange is going to format it when it is sent. Even a single space or newline that is added after we compute the hash can throw the whole thing off.

In our routing agent’s OnCategorizedMessage event listener, the event arguments give us access to an instance of the MailItem class. It has a boatload of properties for accessing the body and headers of the message programmatically. Unfortunately, we can’t use these properties because they represent the semantic values, not the raw ones. Instead, we’ll need to use the GetMimeReadStream() and GetMimeWriteStream() methods to read the raw mail message and write out the modified version, respectively.

Implementing the routing agent

Let’s start by completing the Routing Agent implementation. We’ll keep it simple by moving all of the hard signing stuff into an IDkimSigner interface, which we’ll worry about implementing later:

public sealed class DkimSigningRoutingAgent : RoutingAgent
{
    private static ILog log = LogManager.GetLogger(
        MethodBase.GetCurrentMethod().DeclaringType);
 
    private IDkimSigner dkimSigner;
 
    public DkimSigningRoutingAgent(IDkimSigner dkimSigner)
    {
        if (dkimSigner == null)
        {
            throw new ArgumentNullException("dkimSigner");
        }
 
        this.dkimSigner = dkimSigner;
 
        this.OnCategorizedMessage += this.WhenMessageCategorized;
    }
 
    private void WhenMessageCategorized(
        CategorizedMessageEventSource source,
        QueuedMessageEventArgs e)
    {
        try
        {
            this.SignMailItem(e.MailItem);
        }
        catch (Exception ex)
        {
            log.Error(
                Resources.DkimSigningRoutingAgent_SignFailed,
                ex);
        }
    }
 
    private void SignMailItem(MailItem mailItem)
    {
        if (!mailItem.Message.IsSystemMessage &&
            mailItem.Message.TnefPart == null)
        {
            using (var inputStream = mailItem.GetMimeReadStream())
            {
                if (this.dkimSigner.CanSign(inputStream))
                {
                    using (var outputStream = mailItem.GetMimeWriteStream())
                    {
                        this.dkimSigner.Sign(inputStream, outputStream);
                    }
                }
            }
        }
    }
}

The only real quirk is the if statement in the SignMailItem() function, which I mostly discovered through trial and error. If the mail item is a “system message” (whatever that means) then all of the mailItem‘s methods will be read only (throwing exceptions if we try to mutate), so we shouldn’t even bother. And if the mail item has a TNEF part, then in it’s in a bizarro proprietary Microsoft format, and the DKIM spec just isn’t going to work with that. Finally, if something blows up, we catch the exception and log it–better to send a message without a signature than not send it all.

Defining an interface for DKIM signing

So the next step is to make up that IDkimSigner implementation and make it do the dirty work. You can see that I’ve made it simple in that we only need to write two methods:

public interface IDkimSigner : IDisposable
{
    bool CanSign(Stream inputStream);
 
    void Sign(Stream inputStream, Stream outputStream);
}

A method for sanity checking

The first method will scan our mail item’s content stream and do a sanity check and ensure that we can actually sign the message. For example, if our IDkimSigner implementation is configured to sign messages originating from warehouse1.example.com and we pass CanSign() a message from warehouse2.example.com, then we can return false to indicate that we just don’t know what to do with the message. Let’s implement that method.

private string domain;
 
public bool CanSign(Stream inputStream)
{
    bool canSign;
    string line;
    StreamReader reader;
 
    if (this.disposed)
    {
        throw new ObjectDisposedException("DomainKeysSigner");
    }
 
    if (inputStream == null)
    {
        throw new ArgumentNullException("inputStream");
    }
 
    canSign = false;
    reader = new StreamReader(inputStream);
 
    inputStream.Seek(0, SeekOrigin.Begin);
 
    line = reader.ReadLine();
    while (line != null)
    {
        string header;
        string[] headerParts;
 
        // We've reached the end of the headers (headers are
        // separated from the body by a blank line).
        if (line.Length == 0)
        {
            break;
        }
 
        // Read a line. Because a header can be continued onto
        // subsequent lines, we have to keep reading lines until we
        // run into the end-of-headers marker (an empty line) or another
        // line that doesn't begin with a whitespace character.
        header = line + "\r\n";
        line = reader.ReadLine();
        while (!string.IsNullOrEmpty(line) &&
            (line.StartsWith("\t", StringComparison.Ordinal) ||
            line.StartsWith(" ", StringComparison.Ordinal)))
        {
            header += line + "\r\n";
            line = reader.ReadLine();
        }
 
        // Extract the name of the header. Then store the full header
        // in the dictionary. We do this because DKIM mandates that we
        // only sign the LAST instance of any header that occurs.
        headerParts = header.Split(new char[] { ':' }, 2);
        if (headerParts.Length == 2)
        {
            string headerName;
 
            headerName = headerParts[0];
 
            if (headerName.Equals("From", StringComparison.OrdinalIgnoreCase))
            {
                // We don't break here because we want to read the bottom-most
                // instance of the From: header (there should be only one, but
                // if there are multiple, it's the last one that matters).
                canSign = header
                    .ToUpperInvariant()
                    .Contains("@" + this.domain.ToUpperInvariant());
            }
        }
    }
 
    inputStream.Seek(0, SeekOrigin.Begin);
 
    return canSign;
}

Barf. But we have to do this style of ghetto parsing because, after all, we’re dealing with the raw e-mail message format. All we’re doing is scanning through the headers until we reach the last From: header, and then we make sure that the From: e-mail address belongs to the domain that our instance knows how to sign. Then we seek back to the beginning of the stream to be polite.

A method for signing

The second method that we have to implement is the one that actually does all of the dirty work. And in DKIM signing, we can break it down into five steps:

  1. Compute a hash of the body of the message.
  2. Create an unsigned version of the DKIM-Signature header that contains that body hash value and some other information, but has the signature component set to an empty string.
  3. “Canonicalize” the headers that we are going to sign. By “canonicalize”, we mean “standardize capitalization, whitespace, and newlines into a format required by the spec, since other mail transport agents who get their grubby paws on this message might reformat the headers”.
  4. Slap our unsigned version of the DKIM-Signature header to the end of our “canonicalized” headers, sign that data, and slap the resulting signature to the end of the DKIM-Signature header.
  5. Write this signed DKIM-Signature into the headers of the mail message, and send it on its merry way.

Divide and conquer!

Implementing the Sign() method

Our implementation for the Sign() method will tackle each step in turn:

public void Sign(Stream inputStream, Stream outputStream)
{
    if (this.disposed)
    {
        throw new ObjectDisposedException("DomainKeysSigner");
    }
 
    if (inputStream == null)
    {
        throw new ArgumentNullException("inputStream");
    }
 
    if (outputStream == null)
    {
        throw new ArgumentNullException("outputStream");
    }
 
    var bodyHash = this.GetBodyHash(inputStream);
    var unsignedDkimHeader = this.GetUnsignedDkimHeader(bodyHash);
    var canonicalizedHeaders = this.GetCanonicalizedHeaders(inputStream);
    var signedDkimHeader = this.GetSignedDkimHeader(unsignedDkimHeader, canonicalizedHeaders);
 
    WriteSignedMimeMessage(inputStream, outputStream, signedDkimHeader);
}

Computing the body hash

The first step, computing the hash of the body, is actually pretty easy. There is only one quirk in that DKIM spec says that if the body ends with multiple empty lines, then the body should be normalized to just one terminating newline for the purposes of computing the hash. The code is not exciting, and you can download it at the end of this article.

Creating the “unsigned” header

The next step is to create the “unsigned” DKIM-Signature header. This is where the DKIM spec is just weird. The DKIM-Signature header contains a lot of information in it, such as the selector, domain, and the hashing algorithm (SHA1 or SHA256) being used. Since that information is vital to ensuring the integrity of the signature, it’s important that that information be a part of the DKIM signature.

If I were designing this, I would append two headers to e-mail messages: a DKIM-Information header that contained all of the above information and is part of the data that is signed and a DKIM-Signature header that contains just the signature data. But the DKIM spec makes use only the one DKIM-Signature header, and for the purposes of signing, we treat the “signature part” of the header (b=) as an empty string:

private string GetUnsignedDkimHeader(string bodyHash)
{
    return string.Format(
        CultureInfo.InvariantCulture,
        "DKIM-Signature: v=1; a={0}; s={1}; d={2}; c=simple/simple; q=dns/txt; h={3}; bh={4}; b=;",
        this.hashAlgorithmDkimCode,
        this.selector,
        this.domain,
        string.Join(" : ", this.eligibleHeaders.OrderBy(x => x, StringComparer.Ordinal).ToArray()),
        bodyHash);
}

You can see here that I’ve got some instance variables that were set in our IDkimSigner implementation’s constructor, such as the hash algorithm to use, the selector, domain, headers to include in the signature, and so on. We also insert our recently-computed hash of the body here.

You can also see that I’m using “simple” body canonicalization and “simple” header canonicalization. The DKIM spec gives us a few options in determining how the message is represented for signing and verification purposes. For the “simple” body canonicalization, it means “exactly as written, except for the weird rule about multiple newlines at the end of the body”. For the “simple” header canonicalization, it means “exactly as written, whitespace, newlines, and everything”.

There is a “relaxed” canonicalization method, but it’s more work, since you have to munge the headers and body into a very particular format, and I didn’t feel like writing a MIME parser.

Extracting “canonicalized” headers

The third step is to get a list of the canonicalized headers. In the constructor, I accept a list of headers to sign: From, To, Message-ID, and so on. (From is always required to be signed.) Then I use parsing code similar to that used in the CanSign() method and build a list of of the raw headers. The only real gotcha to watch out for is that headers can be wrapped onto more than one line, and since we’re using the “simple” canonicalization algorithm, we’ll need to preserve those whitespaces and newlines exactly as we extract them from the stream. Then I sort the headers alphabetically, since that’s how I specified them in the GetUnsignedDkimHeader() method specified above.

Signing the message

The logic behind signing the message is not that difficult. We smash all of the canonicalized headers together, add our unsigned DKIM-Signature header to the end, and compute our signature on this. Then we append the signature to the b= element, previously empty, of our DKIM-Signature header:

private string GetSignedDkimHeader(
    string unsignedDkimHeader,
    IEnumerable<string> canonicalizedHeaders)
{
    byte[] signatureBytes;
    string signatureText;
    StringBuilder signedDkimHeader;
 
    using (var stream = new MemoryStream())
    {
        using (var writer = new StreamWriter(stream))
        {
            foreach (var canonicalizedHeader in canonicalizedHeaders)
            {
                writer.Write(canonicalizedHeader);
            }
 
            writer.Write(unsignedDkimHeader);
            writer.Flush();
 
            stream.Seek(0, SeekOrigin.Begin);
 
            signatureBytes = this.cryptoProvider.SignData(stream, this.hashAlgorithmCryptoCode);
        }
    }
 
    signatureText = Convert.ToBase64String(signatureBytes);
    signedDkimHeader = new StringBuilder(unsignedDkimHeader.Substring(0, unsignedDkimHeader.Length - 1));
 
    signedDkimHeader.Append(signatureText);
    signedDkimHeader.Append(";\r\n");
 
    return signedDkimHeader.ToString();
}

The only gotcha here, which I lost a few hours to, is a weird quirk of the .NET Framework 3.5 implementation of the SignData() function of the RSACryptoServiceProvider class. One of the overloads of the SignData() function accepts an instance of a HashAlgorithm to specify the kind of hash to use. The SHA-256 implementation was added in .NET 3.5 SP1, but it was done in such a way that an internal switch statement used internally by the .NET crypto classes wasn’t updated until .NET 4.0 to recognize the new SHA256CryptoServiceProvider type. Some guy blogs about why this is, but what it essentially means is that if you pass a SHA256CryptoServiceProvider instance to the SignData() method on .NET 2.0/3.0/3.5/3.5SP1, you get an exception, and on .NET 4.0 you don’t. Since Exchange 2007 uses .NET 3.5 SP1, we have to use the recommended workaround of using the overload that accepts a string representation of the hash algorithm.

Writing out the message

The last step is to write out the message with our newly created DKIM-Signature header. This really is a simple as taking the output stream, writing the DKIM-Signature header, and then dumping in the entire contents of the input stream.

Getting a key to sign messages with

Let us take a brief interlude from our DKIM circles of hell and obtain a key with which we will actually sign the DKIM-Signature header we’ve worked so hard to create.

We need to generate an RSA public/private key pair: a public key to store in DNS in the format required by the DKIM spec, and a private key to actually sign the messages with. The nice folks over at Port25 have a DKIM wizard that does exactly that.

It’s smashingly simple–just enter your domain name (say, “example.com“), a “selector” (say, “key1“), and select a key size (bigger is better, right?). The “selector” is a part of the DKIM spec that allows a single signing domain to use multiple keys. For example, you could use a key with selector name “newsletters” to sign all of the crap newsletter e-mails that you send out, and another key with selector name “tx” to sign all of the transactional e-mails that you send out.

It then spits out the syntax of the TXT records that you need to add to DNS for that selector:

key1._domainkey.example.com IN  TXT     "k=rsa\; p={BIG HONKING PUBLIC KEY HERE}"
_domainkey.example.com      IN  TXT     "t=y; o=~;"

The first record is where the public part of the key is stored. Whenever a mail transport agent sees one of our DKIM-Signature headers with a selector of key1, it’ll know to go hunting in DNS for a TXT record named key1._domainkey.example.com and pull the public key for verification from there.

The second record is part of the older DomainKeys specification and it is not strictly necessary. As written here, it means that we’re in testing mode (“t=y”)–that is, don’t freak out if you see a bad signature because we’re still dicking around with the setup of our implementation–and that not all messages originating from this domain will be signed (“o=~”)–maybe we won’t bother signing our newsletter e-mails, for example.

We’ll also have the private key specified in a format similar to the one below:

-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBANXBbZybdmjKDTONFVqAWXmGzR6GSZX5LV3OF//1jRz7dzGWTCKK
jembqBxqhr0Y2ua2l4D4EZi6FwDmdqgLS6MCAwEAAQJAD4qhypovEM1oClB+tfbR
Cpn3ffmrjgDxAHoEmrKi0PGBn8fumW22bad2tmrAjWWTVmeXJvQyEy1awq0M2PMR
0QIhAPEnqivb5dKZbTeKhiF4c6IUHfwEq8wNf2LWZvdH3ROrAiEA4un604mDss4Q
qAVEx686pUttfWyJrYkcZ/tx7kOoL+kCICEysqyDAypw0KY6vahR6qk/V7lf8z6O
BSFYHqigDgEtAiEAsK9r5UcQSyv1AD+J/MpOqeJ/kMfwtDUs7zJ01gfMb/ECIQDg
8d/XVJDi4Cqbt4wfcHZxADAgqyK8Z5M69fBecnExVg==
-----END RSA PRIVATE KEY-----

One thing I have glossed over in the code discussion until now was how that this.cryptoProvider instance that actually computes the signature got created.

We’ll need to read this key and load it into the cryptography classes used by the .NET Framework and by Windows to actually sign mail messages and get that this.cryptoProvider instance. Surely there is a simple API for this, yes?

Instancing a CryptoProvider

One problem is that the documentation in MSDN for the CryptoAPI is bad. I say “bad” because it certainly seems like .NET and Windows don’t expose native support for processing a PEM-encoded key, and if it does, well, I couldn’t find the documentation for it. Instead, the RSACryptoServiceProvider prefers to store its keys in an XML format that nothing else in the world seems to use.

This means that our implementation is so close to being finished that we can almost taste it, but now we have to complete a side quest to actually read our damn key and get an instance of the RSACryptoServiceProvider. Or, we could generate a certificate ourselves and store it in the Certificates MMC snap-in, but why should we have to do that? I’d rather just plop the damn key in the application configuration file like the rest of the goddamned world does it, “secure container” my ass.

We can thank the moon and the stars that some guy has written a PEM reader for us. How does it work? I have no idea, but I tested it on several keys and it seemed to work fine, which is good enough for me. I tossed this code into a static CryptHelper class, and now getting an instance of the RSACryptoServiceProvider is as simple as

this.cryptoProvider = CryptHelper
     .GetProviderFromPemEncodedRsaPrivateKey(encodedKey);

Loading the routing agent into Exchange Server 2007

I took all of this code and then added boring administrative stuff like logging and moving some hardcoded values (such as the PEM-encoded key, the selector, and the domain) into the usual .NET App.config file mechanism.

Installing the agent on the Exchange Server is surprisingly simple. After compiling the project and futzing with the configuration file, we just copy the DLLs and configuration file to a folder on the Exchange Server, say, C:\Program Files\Skiviez\Wolverine\DKIM Signer for Exchange\.

Then we launch the Exchange Management Shell (remember to right-click it and “Run as Administrator”) and execute a command to tell Exchange to actually register our agent:

Install-TransportAgent
     -Name "DKIM Signer for Exchange"
     -TransportAgentFactory "Skiviez.Wolverine.Exchange.DkimSigner.DkimSigningRoutingAgentFactory"
     -AssemblyPath "C:\Program Files\Skiviez\Wolverine\DKIM Signer for Exchange\Skiviez.Wolverine.Exchange.DkimSigner.dll"

followed by

Enable-TransportAgent -Name "DKIM Signer for Exchange"

Interestingly, there will be a note telling you to close the Powershell window. It is not kidding. For some reason, the Install-TransportAgent cmdlet will keep a file handle open on our DLL, preventing Exchange from actually loading it until we close the Powershell window.

To make it actually work, we need to restart the Microsoft Exchange Transport service. I’ve found that restarting the Microsoft Exchange Mail Submission right after that is a good idea; otherwise, there can be a short delay of about 15 minutes before people’s Outlooks attempt to send outbound mail again.

Testing the implementation

To make sure things are actually working, Port25 comes to the rescue with their verification tool. You just send an e-mail to check-auth@verifier.port25.com and within a few minutes, they’ll send you an e-mail back with a boatload of debugging information. If it’s all good, you’ll see a result like the following:

Summary of Results
SPF check:          pass
DomainKeys check:   neutral
DKIM check:         pass
Sender-ID check:    pass
SpamAssassin check: ham

(What you’re looking for is the “pass” next to the DKIM check. The DomainKeys part being neutral is OK, since DomainKeys is the older standard and we’re choosing not to implement it.)

Conclusions and Delusions

I’ve been using this code for a few weeks now and it seems to work fine–the messages that I’ve sent through the server to Port25 and my Yahoo! test account all end up showing the DKIM passing. The usual “it works on my machine!” disclaimers apply, however, as I’m sure there are myriad configuration differences in Exchange that could this not to work. Bug fixes are welcome, but don’t come crying if it sends all of your e-mail to that big junk folder in the sky.

And thanks to some blowhards at Yahoo!, the world now has a public domain implementation of DKIM signing for Exchange to play with.

And in case you’re curious–after doing all this work to set up DKIM and participate in the Complaint Feedback Loop at their suggestion–their answer is still “no,” without elaboration. When Yahoo! finally goes under, I won’t be one shedding a nostalgic tear.

There are some unit tests, but they do have our private key in them, and I couldn’t be bothered to siphon those out. The code below is just the bits that do the actual signing.

Download the code used in this article.