For a long time I've had an item on my todo list to implement a DKIM module for my mail server. This weekend, I decided to tackle that project. I dug up my old DomainKeys implementation, read (and reread) the DKIM specification, and started modifying my old code to do DKIM.
One of the differences between DomainKeys and DKIM is that there are two signature algorithms supported: "rsa-sha1" and "rsa-sha256". I had been using POW (Python OpenSSL Wrappers) to do the hashing and RSA signatures for SHA1. However, POW seems to be really old (latest version released in 2001) and does not include SHA256 support. So, I turned to Python 2.5 which has a new hashlib module that supports several different modern hash algorithms. However, my mail server only had Python 2.4 installed.
Rather than blindly upgrading Python on my production server and possibly breaking other things that depend on installed modules, I installed a similar FreeBSD 6.2 system under Parallels on my MacBook. I installed Python 2.4 from the ports system there, and then installed Python 2.5 at the same time. As it turns out, FreeBSD ports handles having two different versions of Python installed at the same time fairly seamlessly. It installes both /usr/local/bin/python2.4 and /usr/local/bin/python2.5 binaries with a hard link to the "default" version at /usr/local/bin/python. Installing 2.5 with an already installed 2.4 does not change the default version, so one must explicitly use the python2.5 when required. Installing Python modules from the ports system installs to the default Python installation unless the PYTHON_VERSION=python2.5 variable is set in the port make command line. (I'm mostly relating this part of the tale for my own benefit, for when I forget how to build Python ports for the correct version in the future.)
After testing this on a virtual machine, I installed Python 2.5 on my production server and nothing broke. I could now use the hashlib module to calculate SHA256 hashes. However, I was still using the POW module for RSA signature calculation, and that operation does not support SHA256 either. (Part of the RSA signing operation uses information about which hash algorithm was used to create the hash being signed, to prevent a situation where somebody could use a weaker hash function to create a collision with a value produced by a stronger hash function.) Not only does POW not support SHA256, but the OpenSSL library installed in my FreeBSD system does not either. OpenSSL is part of the base system, and I did not want to try to go down that upgrade path today.
After thinking about the problem for a bit and concluding "how hard could it be?", I decided to reimplement the portions of RSA signing and verification that I needed for DKIM. Python has good built-in large integer capability, so the fundamental "powmod" (modular exponentiation) operation was the easy part. I had to learn the details about technologies with obscure acronyms like ASN.1, PKCS#1, X.509, and so on. After poring over many documents which always have a habit of referring to the fundamental information contained in yet another document, I finally sorted out everything I need for both signing and verification to work. I also gained a much better understanding of how key management works in practice, which could become useful sometime.
My DKIM code appears to work and is acceptable to both gmail.com and the (older) DKIM testing reflector. It's written in pure Python and its only nonstandard dependency is the dnspython library (for retrieving TXT records from DNS). I plan to release this code as a Python library once I clean it up a bit and write some documentation.
Update: My pydkim code is available for download at: http://hewgill.com/pydkim/