Table of Contents
DKIM Signature and verification using DKIMproxy
This article was initially published in GNU/Linux Magazine France #125 and released under Creative Common.
DomainKeys Identified Mail is an alternative to strong authentication systems that are PGP and S/MIME. Unlike those, DKIM furnishes an authentication not of the author, but of the domain it belongs to. DKIM is the legitimate son of DomainKeys, from Yahoo!, and Identified Internet Mail, from Cisco.
1. Identity and Email
Email Authentication is quite a recurrent topic to sysadmins. Since SMTP exists, we saw all sorts of good and bads, from the excellent PGP to the almost successful SenderID (that died because of some patent story.. again). We saw funky formats, and others more reasonnable but incredibly complex to set up (S/MIME).
Yet, in the end, what the user wants is simply to send an email. Except in a few specific cases, the authenticity of its communications, he doesn't really care about and trusts the “system”. Then again, it's quite frustrating to be unable to determine who is behind the keyboard. Especially when we do not know the person, and we can't rely on the content of the message to evaluate its authenticity. And considering the importance of email communications today, and what we do with simple emails, it's understandable that most of the big email companies have, at some point, proposed or integrated an authentication solution.
2. DKIM, what is it ?
DKIM seems to be able to solve this problem in some elegant manner. Similarly to SenderID (but without the patents), it transfers the realization of the signature of the email from the MUA to the MTA: the client does not sign its email, but the server does it when it processes it. The server has a pair of RSA keys and uses the private key to sign outgoing emails. The public key is made available via DNS to anybody who requests it to verify the signature later on. The recipient of a DKIM signed email will not see, at first, the difference with any classic email. But if he takes a look to the headers, he will see the following (on an email coming from gmail):
DKIM-Signature: v=1; a=rsa-sha256;c=relaxed/relaxed;d=gmail.com;s=gamma; h=domainkey-signature:mime-version:received:date:message-id:subject:from:to: content-type;bh=+h+GzK7vCkJwfpUPIHylR/vBv4qM5Cu1dYDVBxcvmqM=;b=Bhj 1OhxJOnKBzu0/6ooJGzYJRfR69wNYQOJcQiJmnK+eMpicdxI8uWlHw+E6NdHBt0f6dRXe VCSLM2wjK41ZaFdFObKZNczOl86LyannP3/L806fuv1v99Xw1IiHhLAxUYA+7mE3vkSKAJ 3Y6aCFNGHXkze0uJVtD6MLFR3Sz90=
Then, it is his responsibility to valide the signature using the public key. The signature provides two guarantees:
- That the email has been sent by the domain that owns the key (the domain has the responsibility for authenticating its users before letting them then messages)
- That the fields listed in the h= value have been signed and therefore that their integrity is ensured
All of this is, obviously, defined in a RFC. For DKIM Signature, it's RFC 4871 from may 2007. And what we will discuss in this article is the setup of a DKIMproxy gateway for signing outgoing emails from a postfix managed domain, and verifying incoming emails on this same domain.
The architecture is composed of two parts. The first one will be in charge of sending emails from the owener domain. This sending process requires to add a signature to each email before being sent on the network.
Signature architecture: Jean-Kevin sends an email to an external contact. Postfix takes over it, then forwards it to DKIMProxy. DKIMproxy signs the relevant headers using the private key of the domain domaine.net, store the signature in the header DKIM-Signature and send back the signed email to Postfix. Postfix can finally send the email to its recipient.
The second part will do the opposite. When an email is received from the internet, it will valide eventual signature and add a header containing the validation status.
Verification architecture: Jean-kevin received an email from the outside. Postfix takes over it, forward it to DKIMProxy that queries the public key of the sender's domain that realized the signature using DNS. It then verifies the signature and store the verification result in the Authentication-Results header. The email is then sent back to Postfix that continues the delivery process. Jean-Kevin can finally consult its emails like he usually does.
The particular mechanism to understand here, and that is the very force of DKIM, is that here is no need for any sort of key management system. No PKI, no GPG key server or anything else. The public key for example.net will simply be made available in the DNS records of the domain (as we will later see, Bind can store such record using the TXT record type). And then, every time and email is received, the verified will simply query the DNS record containing the public key, and verify the email signature.
That means two things. First, the security of DKIM relies entirely on the security of DNS. If an individual can modify the public key exposed for example.net, he control all of the signatures for that domain. Second, the verified must perform a large quantity of DNS queries to obtain all of the public keys for all of the sender's domains. The IETF announces in RFC 4871 more than 70 millions of domains hosting emails services. It can quickly represent a humongous volume of queries, and blocking the DNS resolver would block the entire verification process, thus blocking email delivery for end users.
However, you MUST keep in mind that DKIM does not authenticate a user, as PGP and S/MIME do, but a domain. As such, DKIM alone cannot certify the authenticity of an email. We need to make sure that the domain imposes strong authentication mechanisms to its users. Many techniques exist, such as DIGEST-MD5, but are beyond the scope of this article.
With that in mind, we can go ahead and setup our configuration.
4. Setting up the signature
Imagine that example.net furnished email services to its clients. It is constituted of a Postfix server handling SMTP, a random IMAP server (I have a personal preference for Cyrus-Imap) and a DNS master running on Bind 9. We are going to, in the good order, setup DKIMProxy, then generated RSA keys, then configure Postfix so it forwards outgoing emails to DKIMProxy, and finally set up Bind to broadcast our public RSA key on the interwebz.
DKIMProxy is a set of Perl programs written by Jason Long. It is composed of the Mail::DKIM module, of DKIMproxy.in for signature verification, and DKIMproxy.out for email signing. For the installation, we have several options. On Debian Lenny, the official DKIMproxy version is 1.0.1, which is obviously outdated. But Thomas Goirand maintains a package in version 1.2.6 (the current stable at the writing of this article) on his repository. To install it from there, add deb ftp://ftparchive.gplhost.com/debian lenny main to your sources.list.
The solution I've chosen for this article is to install Mail::DKIM from the package, but to build the latest beta of DKIMproxy 1.3-beta1 from the sources.
Whatever you choose, the configuration steps should still be the same, even on other systems (Linux *, *BSD, …).
root@zerhuel:~# apt-get install libmail-dkim-perl
For DKIMproxy :
julien@zerhuel:~$ wget http://dkimproxy.sourceforge.net/dkimproxy-1.3beta1.tar.gz
The the classic: create a directory in ~/dkimproxy then ./configure and make install
julien@zerhuel:~/dkimproxy-1.3beta1$ mkdir ~/dkimproxy julien@zerhuel:~/dkimproxy-1.3beta1$ ./configure --prefix=~/dkimproxy [ ... ] julien@zerhuel:~/dkimproxy-1.3beta1$ make install [ ... ] julien@zerhuel:~/dkimproxy-1.3beta1$ cd ~/dkimproxy julien@zerhuel:~/dkimproxy$ ls -l total 16 drwxr-xr-x 2 julien julien 4096 jan 26 12:10 bin drwxr-xr-x 2 julien julien 4096 jan 26 12:10 etc drwxr-xr-x 3 julien julien 4096 jan 26 12:10 lib drwxr-xr-x 3 julien julien 4096 jan 26 12:10 share
The etc/ directory contains two example files for the two proxies in and out. We will start by creating a configuration file for the out proxy.
julien@zerhuel:~/dkimproxy$ cd etc julien@zerhuel:~/dkimproxy$ vim dkimproxy_out.conf # ip and port to receive emails from postfix listen 127.0.0.1:10017 # ip and port where to send the emails back to postfix relay 127.0.0.1:10018 # file containing the signature rules sender_map /home/julien/dkimproxy/etc/sender_map
Nothing really original in this file. The core of the signature configuration parameter will be in the 'sender_map' file, that we need to create.
julien@zerhuel:~/dkimproxy$ vim sender_map example.net dkim(s=mondomaine-dkim,d=example.net,c=relaxed,a=rsa-sha256,key=/home/julien/dkimproxy/etc/private.key)
This very one line declares strictly the way emails must be signed for the domain example.net. The fields are described in the RFC
- s= the 'selector', corresponds to the name of the TXT record in the bind server (we will see about that later);
- d= the name of the domain to sign;
- c= The signature mode. The RFC defines two: a 'simple' one, that is strict and doesn't autorise any header manipulation (upercase/lowercase and such), and a 'relaxed' one that is a bit more permissive;
- a= the signature mode, rsa-sha1 or rsa-sha256;
- key= the path to the private key to use for the signature.
And, by the way, speaking of key, let's generate those RSA keys using OpenSSL:
julien@zerhuel:~/dkimproxy/etc$ openssl genrsa -out private.key 1024 Generating RSA private key, 1024 bit long modulus .............++++++ .++++++ e is 65537 (0x10001)
The worldwide security guild imposes me to remind you that a private key must be protecte by, at the minimum, a 'chmod 400'.
The configuration of dkimproxy.out is now finished. Let's try to start it. The 'bin' directory contains the program to launch with, as a parameter, the path of the configuration file.
julien@zerhuel:/home/julien/dkimproxy/etc$ cd ../bin/ julien@zerhuel:/home/julien/dkimproxy/bin$ ls dkimproxy.in dkimproxy.out dkim_responder.pl dkimsign.pl dkimverify.pl julien@zerhuel:/home/julien/dkimproxy/bin$ ./dkimproxy.out --conf_file=/home/julien/dkimproxy/etc/dkimproxy_out.conf Becoming sub class of "Net::Server::PreFork" 2010/01/26-12:57:07 main (type MySmtpProxyServer) starting! pid(22144) Binding to TCP port 10017 on host 127.0.0.1 Group Not Defined. Defaulting to EGID '1000 4 20 24 25 29 44 46 1000 1004 1005 1664' User Not Defined. Defaulting to EUID '1000'
It starts. From another terminal, we can connect to port TCP/10017 and see the following message:
julien@zerhuel:~$ nc localhost 10017 421 Internal error (Next hop is down)
It works! What do you mean “It works!” ? Not at all, it's throwing “Internal error”… how can you say it works ? Yes, but it is absolutely normal at this stage. Because dkimproxy.out being a… proxy (thanks for following), it needs to tries to relay back to postfix (the relay parameter). And since we haven't configured postfix yet, it crashes.
4.2. Relay from Postfix to DKIMProxy
4.2.1. Some reminders concerning Postfix
Postfix is a SMTP server written by Wietse Venema to replace Sendmail. Its internal architecture aims to be modular, and this is where it gets interesting for us, because we are going to exploit that modularity to transfert outgoing email to dkimproxy.out. The internal mechanic of Postfix being quite complex, I won't cover it here. However, it is important to detail two concepts:
126.96.36.199. Emails handling
Most SMTP servers receive emails on port TCP/25. Here, this is not possible because if an email coming from the outside must be able to arrive on port TCP/25 (otherwise, it won't arrive), we will have to use another port for local submissions. Why ? Let's go back to the architecture described in figure 1.
If dkimproxy.out would be on the path of all of the emails, would they be submissions of receptions, it would then apply a DKIM signature to all of the emails claiming to come from example.net. This is not acceptable because, in this case, anybody from the outside could claim to be 'email@example.com' and send an email to 'firstname.lastname@example.org' with a valid DKIM signature. We will then dissociate submission and reception.
188.8.131.52. Services declaration
Postfix has a file called 'master.cf' that contains all of the services managed by Postfix. This file allows to manage the modular architecture of postfix, and this is where we are going to declare our submission and reception services, as well as the interactions with DKIMproxy.
I presume here that you have a working Postfix installation on Debian. Debian is not mandatory, but you will have to adapt the path for any other system.
In 'master.cf', we are going to add two sections: one for email submission, listening on port TCP/587 and on which local users will connect to send their emails, and another section for the 'relay' connection from dkimproxy.out, listening on port TCP/10018. The submission module will forwards email to be signed to DKIMproxy using a content-filter parameter.
# the submission service forward emails to dkimproxy.out submission inet n - - - - smtpd -o receive_override_options=no_address_mappings -o content_filter=dksign:[127.0.0.1]:10017 -o smtpd_tls_security_level=may -o smtpd_sasl_auth_enable=yes -o smtpd_client_restrictions=permit_sasl_authenticated,permit_mynetworks,reject [...] # local TCP socket for relay with dkimproxy.out 127.0.0.1:10018 inet n - n - 10 smtpd -o content_filter= -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject -o smtpd_authorized_xforward_hosts=127.0.0.0/8
Because in the submission module we call a 'dksign' service, we need to declare this service at the end of 'master.cf'
# dksign declaration dksign unix - - n - 4 smtp -o smtp_send_xforward_command=yes -o smtp_discard_ehlo_keywords=8bitmime,starttls
This is all for Postfix configuration. Reload the server, as well as dkimproxy.out.
root@zerhuel:/etc/postfix# /etc/init.d/postfix restart Stopping Postfix Mail Transport Agent: postfix. Starting Postfix Mail Transport Agent: postfix. root@zerhuel:/etc/postfix# exit julien@zerhuel:/etc/postfix$ cd /home/julien/dkimproxy/bin/ julien@zerhuel:/home/julien/dkimproxy/bin$ ./dkimproxy.out --conf_file=../etc/dkimproxy_out.conf Becoming sub class of "Net::Server::PreFork" 2010/01/26-13:21:13 main (type MySmtpProxyServer) starting! pid(22497) Binding to TCP port 10017 on host 127.0.0.1 Group Not Defined. Defaulting to EGID '1000 4 20 24 25 29 44 46 1000 1004 1005 1664' User Not Defined. Defaulting to EUID '1000'
And retry the connection to dkimproxy.out:
julien@zerhuel:~$ nc localhost 10017 220 smtp.example.net
No more bad words from dkimproxy.out. If I configure my webmail to submit email on port TCP/587, and perform an attempt to send an email from 'email@example.com' to 'firstname.lastname@example.org', then this dear jean-pierre receives the following signature:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=example.net; h= mime-version:date:from:to:subject:message-id:content-type: content-transfer-encoding; s=mondomaine-dkim; bh=g3zLYH4xKxcPrHO pQcnk/GaJedfustWU5uGs=; b=QeUaXiEgRznE/WlJF4mf6ejD7a+cT8Wf6Ag6+s 3BupDZzwXgKEwy9s6PIK6KZbPk8ana6aqLHeOS662McJQQOYLzvTb43Ws73KHfVz wtXdDQ6Vh1+kVQ4G52ruEAEUoh8ayyfLctwbJUK/IdRPdSTp+7vOrVj9s1cb5aOY LEpxo=
And in the console of dkimproxy.out, we see the following:
dkimproxy.out: DKIM signing - signed; message-id=<68d1e42906c74bb3f7ae9cd0bb93a204@localhost>, signer=<@example.net>, from= DKIM signing - signed; message-id=<68d1e42906c74bb3f7ae9cd0bb93a204@localhost>, signer=<@example.net>, from=
4.3. Export the public key
Yes because, until now, Jean-Pierre receive our emails contain a signature, but he's completely incapable of verifying it. We need to configure the Bind server to serve the public key through the DNS. One more time, I won't get too much into the details of configuration a Bind 9 zone. The following operation is however not very complicated: you have to created two entries the zone file of example.net as follow:
root@zerhuel:/etc/bind# vim example.net.db [ ... ] ;DKIM for email signature _domainkey.example.net. IN TXT "k=rsa; t=s; o=-;" mondomaine-dkim._domainkey.example.net. IN TXT "k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrbcmFbR41RmAZ3GfahHhxFz9I69JpmEcqariKMw
These two lignes contain the elements we have defined during the setup of dkimproxy.out. The second line starts with the 'selector' value, and contain the public key in the k= tag. You can now reload the zone with these values (don't forget to increment the serial number if you have slaves), and perform a quick test using dig:
julien@zerhuel:~$ dig txt mondomaine-dkim._domainkey.example.net @localhost [ ... ] ;; ANSWER SECTION: mondomaine-dkim._domainkey.example.net. 259200 IN TXT "k=rsa\; t=s\; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrbcmFbR 41RmAZ3GfahHhxFz9I69JpmEcqariKMw40iTLQ3vIAHrbtwe3/HUtJD+7o0tRE7l5vCgIBA2tw7V5ohMDMjwYTNMsf4zpZ11bgVoq4Yk+TOOuNmi0bEJJt/P RpdUO2ktAdUc0kG0BzWoDIzegD18FIO8cgATws86k12QIDAQAB;"
Dig confirms that the public key is served by the DNS. Jean-Pierre is happy (really, no kidding, it happens) because he now can verify the signature from the domain of Jean-Kevin.
<note>At this point of the article, you probably wonder who the hell are Jean-Pierre and Jean-Kevin. Well… regarding Jean-Pierre, you have to be a reader of Gnu/Linux Magazine France to know him. And as for Jean-Kevin, he's pretty much nobody.</note>
Now, what's next ? We have realized proper email signing for example.net, but what misses is signature verification for incoming emails. This last step being actually a lot more simple, I've kept it for the end.
5. Verify incoming emails signatures
The validation of signatures require very little work on the recipients side (or, to be honest, there is a lot to do, but it's all in the code of DKIMproxy). We will set up dkimproxy.in with two parameter: its listening port and address. Then we will configure postfix to transmit incoming email arriving on TCP/25 to dkimproxy.in. As seen in figure 2, all incoming email will the go throught dkimproxy.in. This program will query the relevant public key via DNS and verify the signature(s) contained in the DKIM-Signature header(s). Finally, it will add two headers to the email:
- Authentication-Results: this header is standardized by RFC 5451, it contains the result of the validation operation.
Authentication-Results: zerhuel.domaine.net; dkim=pass email@example.com; domainkeys=pass firstname.lastname@example.org
- X-DKIM-Authentication-Results: a header specific to DKIMproxy that contains “pass” or “fail” depending on the result of the validation.
Back to our ~/dkimproxy/etc directory. We create a simple 'dkimproxy_in.conf' configuration file containing the following:
julien@zerhuel:~/dkimproxy$ vim dkimproxy_in.conf listen 127.0.0.1:10015 relay 127.0.0.1:10016
And then launch the program:
julien@zerhuel:/home/julien/dkimproxy/bin$ ./dkimproxy.in --conf_file=../etc/dkimproxy_in.conf Becoming sub class of "Net::Server::PreFork" 2010/01/26-14:32:55 main (type MySmtpProxyServer) starting! pid(23179) Binding to TCP port 10005 on host 127.0.0.1 Group Not Defined. Defaulting to EGID '1000 4 20 24 25 29 44 46 1000 1004 1005 1664' User Not Defined. Defaulting to EUID '1000'
5.2. And finally, Postfix
Similarly to the previous configuration, we are going to modify 'master.cf' to add the entry and exit points to dkimproxy.in. In fact, Postfix receives incoming emails in the smtpd module, so it's in this module that we have to define a 'proxy_filter' to dkimproxy.in.
root@zerhuel:~# vim /etc/postfix/master.cf smtp inet n - - - - smtpd -o smtpd_proxy_filter=127.0.0.1:10015 [ ... ] 127.0.0.1:10016 inet n - n - 10 smtpd -o content_filter= -o smtpd_authorized_xforward_hosts=127.0.0.0/8 -o receive_override_options=no_unknown_recipient_checks -o smtpd_recipient_restrictions=permit_mynetworks,reject
We could add, after getting the email back from dkimproxy.in, a jump to spamassassin or dspam using the 'content_filter' parameter that we left blank. Once these parameter saved, reload postfix and admire the result in the headers of the emails you receive.
6. User Experience
The technical part of the architecture is now running. I find it important to look more closely at the mechanisms behind DKIM, because the technical side doesn't do everything. In fact, DKIM simply provides an information to the user. The level of trust one gives to that information varies. The first step is to decide if 'somedomain.com' is worth being trusted or not. Nothing block you from refusing to trust MyHomeServerInTheBasement.org because users from this domain are not authenticated properly, thus allowing anybody to send emails from this domain.
And a more complex side of DKIM is to evaluate the trust level to give to third parties. YahooGroup, for example, will perform a perfectly valid DKIM signature on every email sent by any member of any group it hosts. Therefore, if Jean-Kevin sends an email to email@example.com, Jean-Pierre will receive with a valid DKIM signature. However, that signature has not in any case been realized by the domain of Jean-Kevin, but by Yahoo!. What is the level of trust to give to this type of signature ? How to represent it ? The RFCs don't give definitive answers to these questions and prefer to leave it to the appreciation of the postmasters…
6.1. In a webmail: Roundcube and DKIMStatus
Roundcube has a plugin to display the result contained in the Authentication-Results header,using an appropriate icon. I maintain this plugin on my blog page : DKIMStatus I display a different icon if the signature has been realized by the domain of the author, or by a third party domain/
DKIM allow, clearly, to improve the trust of email communications. It is however not finished to be developped, and new RFCs arrive every months (like Author Domain Signing Practices, RFC 5617, that defines announcement of signing policy of a domain over DNS records). Without doubts, the adoption of DKIM by the biggest email providers will push its progression toward emails client softwares and webmails. Still today, few domains implement DKIM, but this technology is here to stay and will, over time, by part of the default email package.
note: for discussion or support with your dkim installation, reach out for the dkim mailing list: dkimproxy-users