Wednesday, April 1, 2009

How to set DKIM signature in IndiMail

What is DKIM
DomainKeys Identified Mail (DKIM) lets an organization take responsibility for a message while it is in transit. DKIM has been approved as a Proposed Standard by IETF and published it as RFC 4871. There are number of vendors/software available which provide DKIM signing. IndiMail is one of them. You can see the full list here.
DKIM uses public-key cryptography to allow the sender to electronically sign legitimate emails in a way that can be verified by recipients. Prominent email service providers implementing DKIM (or its slightly different predecessor, DomainKeys) include Yahoo and Gmail. Any mail from these domains should carry a DKIM signature, and if the recipient knows this, they can discard mail that hasn't been signed, or has an invalid signature.


IndiMail from version 1.5 onwards, comes with a drop-in replacement for qmail-queue for DKIM signature signing and verification (see qmail-dkim(8) for more details). You need the following steps to enable DKIM. IndiMail from version 1.5.1 onwards comes with a filter dk-filter, which can be enabled before mail is handed over to qmail-local or qmail-remote (see spawn-filter(8) for more details).
You may want to look at an excellent setup instructions by Roberto Puzzanghera for configuring dkim for qmail at http://notes.sagredo.eu/node/92


Create your DKIM signature
% mkdir -p /etcindimail/control/domainkeys
% cd /etc/indimail/control/domainkeys
# openssl genrsa -out rsa.private 1024
# openssl rsa -in rsa.private -out rsa.public -pubout -outform PEM
# mv rsa.private default
# chown indimail:qmail default (name of our selector)
# chmod 440 default
Create your DNS records
$ grep -v ^- rsa.public | perl -e 'while(<>){chop;$l.=$_;}print "t=y; p=$l;\n";'
_domainkey.indimail.org.  IN TXT  "t=y; o=-;"
default._domainkey.indimail.org.  IN TXT  "DNS-public-key"

choose the selector (some_name) and publish this into DNS TXT record for:

selector._domainkey.indimail.org (e.g. selector can be named 'default')

Wait until it's on all DNS servers and that's it.

Set SMTP to sign with DKIM signatures
qmail-dkim uses openssl libraries and there is some amount of memory allocation that happens. You may want to increase your softlimit (if any) in your qmail-smtpd run script.
# cd /service/qmail-smtpd.25/variables
# echo "/usr/bin/qmail-dkim" > QMAILQUEUE
# echo "/etc/indimail/control/domainkeys/default" > DKIMSIGN
# svc -d /service/qmail-smtpd.25; svc -u /service/qmail-smtpd.25


Set SMTP to verify DKIM signatures

You can setup qmail-stmpd for verification by setting
DKIMIVERIFY environment variable instead of DKIMSIGN environment variable.

# cd /service/qmail-smtpd.25/variables
# echo "/usr/bin/qmail-dkim" > QMAILQUEUE
# echo "" > DKIMVERIFY
# svc -d /service/qmail.smtpd.25; svc -u /service/qmail-smtpd.25

DKIM Author Domain Signing Practices
IndiMail supports ADSP. A DKIM Author Signing Practice lookup is done by the verifier to determine whether it should expect email with the From: address to be signed.
The Sender Signing Practice is published with a DNS TXT record as follows:
_adsp._domainkey.indimail.org. IN TXT "dkim=unknown"
The dkim tag denotes the outbound signing Practice. unknown means that the indimail.org domain may sign some emails. You can have the values "discardable" or "all" as other values for dkim tag. discardable means that any unsigned email from indimail.org is recommended for rejection. all means that indimail.org signs all emails with dkim.
You may decide to consider ADSP as optional until the specifications are formalised. To set ADSP you need to set the environment variable SIGN_PRACTICE=adsp. i.e
# echo adsp > /service/smtpd.25/variables/SIGN_PRACTICE
You may not want to do DKIM signing/verificaton by SMTP. In that case, you have the choice of using the QMAILREMOTE, QMAILLOCAL environment variables which allows IndiMail to run any script before it gets passed to qmail-remote, qmail-local respectively.


Setting qmail-remote to sign with DKIM signatures
On your host which sends out outgoing mails,
it only make sense to do DKIM signing and not verification.

# cd /service/qmail-send.25/variables
# echo "/usr/bin/spawn-filter" > QMAILREMOTE
# echo "/usr/bin/dk-filter" > FILTERARGS
# echo "/etc/indimail/control/domainkeys/default" > DKIMSIGN
# echo "-h" > DKSIGNOPTIONS
# svc -d /service/qmail-send.25; svc -u /service/qmail-send.25

Setting qmail-local to verify DKIM signatures

On your host which serves as your incoming gateway
for your local domains, it only makes sense to do
DKIM verification with qmail-local

# cd /service/qmail-send.25/variables
# echo "/usr/bin/spawn-filter" > QMAILLOCAL
# echo "/usr/bin/dk-filter" > FILTERARGS
# echo "/etc/indimail/control/domainkeys/default" > DKIMVERIFY
# svc -d /service/qmail-send.25; svc -u /service/qmail-send.25


Testing outbound signatures
Once you have installed your private key file and added your public key to your DNS data, you should test the server and make sure that your outbound message are having the proper signatures added to them. You can test it by sending an email to sa-test (at) sendmail dot net. This reflector will reply (within seconds) to the envelope sender with a status of the DomainKeys and DKIM signatures.
If you experience problems, consult the qmail-dkim man page or post a comment below and I’ll try to help.
You can also use the following for testing.
  • dktest@temporary.com, is Yahoo!'s testing server. When you send a message to this address, it will send you back a message telling you whether or not the domainkeys signature was valid.
  • sa-test@sendmail.net is a free service from the sendmail people. It's very similar to the Yahoo! address, but it also shows you the results of an SPF check as well.
All the above was quite easy. If you don't think so, you can always use the magic options --dkverify (for verification) or --dksign --private_key=domain_key_private_key_file to svctool (svctool --help for all options) to create supervice run script for qmail-smtpd, qmail-send.
References
  1. http://notes.sagredo.eu/node/82



7 comments:

Fred said...

Hi,
Thank you very much for your work in implementing DKIM signing into qmail. It is the best implementation I have found. I have tried both your dkim-netqmail-1.06.patch-1.1 and dkim-netqmail-1.06.patch-1.4 patches.

First, I apologize for this lengthy comment to your blog, especially since it doesn't apply specifically to IndiMail. However, after much searching, this is the only place I could find to communicate with you. Second, I apologize if RFC 4871 is not the most current reference for how DKIM is supposed to work. It was the best I could find. Feel free to communicate with me directly (I assume, since I am using my gmail.com address for my identity, you will see it.)

However, after reading RFC 4871, I have the following three comments.

FIRST comment, which is the most important to me.
According to the definitions in RFC 5016 we are a third party signer, along the lines of B.1.4 in RFC 4871. We have no control over the domain in the From header field. In that section of RFC 4871 it suggests that we put in a Sender header field, and use the domain in that field for the signing. (This is implemented correctly in dkim-netqmail-1.06.patch-1.4 by qmail-dk but not by qmail-dkim.) However, when this is done, in Outlook 2003 at least, the email (not the header) says "From: [Sender address] on behalf of [From address] <[Sender address]>" which we find undesireable. We want the From as displayed to the reader to be the actual author's address only. gmail does better, but we have a lot of Outlook users.

RFC 4871 specifically states in 1.1 "INFORMATIVE RATIONALE: The signing identity specified by a DKIM signature is not required to match an address in any particular header field because of the broad methods of interpretation by recipient mail systems, including MUAs." We would like to sign with the domain in our Return-Path header field.

I believe it would be desirable to have an environment variable which indicates which header field contains the signing domain ("d=" tag). Something like DKIMSIGNHEADER=Return-Path or DKIMSIGNHEADER=Sender or DKIMSIGNHEADER=From. You could default to Sender or From if it doesn't exist.


SECOND comment
In dkim-netqmail-1.06.patch-1.4 (unlike 1.1) the selector ("s=" tag) is correctly acquired from the DKIMSIGN variable's base file name. However, for qmail-dkim to also use that for the signing identity ("i=" tag), in the absence of DKIMIDENTITY, is not really workable. RFC 4871 describes numerous situations in which it may be desirable to change the selector for a given domain. The selector and the identity are not really related, and should not be forced to be the same. What are more closely related are the "d=" and "i=" tags.

From RFC 4871
QUOTE:
(end of page 18) d= The domain of the signing entity (plain-text; REQUIRED). This is the domain that will be queried for the public key. This domain MUST be the same as or a parent domain of the "i=" tag (the signing identity, as described below)

(top of page 20) i= Identity of the user or agent (e.g., a mailing list manager) on behalf of which this message is signed (dkim-quoted-printable; OPTIONAL, default is an empty Local-part followed by an "@" followed by the domain from the "d=" tag). The syntax is a standard email address where the Local-part MAY be omitted. The domain part of the address MUST be the same as or a subdomain of the value of the "d=" tag.

(3.6.2.1) Given a DKIM-Signature field with a "d=" tag of "example.com" and an "s=" tag of "foo.bar", the DNS query will be for "foo.bar._domainkey.example.com".
END QUOTE

According to these quotes, if "i=" and "s=" are the same, it would be an almost impossible situation, since "i=" must be a subdomain of "d=". For example, if d=example.com and you wanted i=foo.bar.example.com, you would then need to use s=foo.bar.example.com and the DNS query will be for foo.bar.example.com._domainkey.foo.bar.example.com. If you wanted to change the selector for the subdomain foo.bar.example.com you could not.

Fortunately, if I set DKIMIDENTITY='' the "i=" tag is ommitted. But, if I do not set the variable DKIMIDENTITY at all, qmail-dkim makes it the same as the "s=" tag. However, as noted above, RFC 4871 says the default for the "i=" tag is an empty Local-part followed by an "@" followed by the domain from the "d=" tag. Therefore, in order to be compliant, I believe qmail-dkim should totally omit the "i=" tag if DKIMIDENTITY is absent, since it is an optional tag. Certainly having the default be the value of the "s=" tag is not compliant, since it's format should be that of a standard email address.

For example, when sending an email to a gmail.com address, with DKIMIDENTITY='' so qmail-dkim does not include the "i=" tag, the header as modified by gmail.com includes "dkim=pass (test mode) header.i=@[d= tag domain]" which shows that they use the default as defined in RFC 4871.


THIRD comment
I think it would be desirable for there to be some mechanism to pass the values for some of the tags to qmail-dkim. Looking through dkim-netqmail-1.06.patch-1.4 it appears that you use "DKIMSignOptions opts" to set the values for tags. (I am a Perl programmer, and not a C programmer, so the following may be incorrect.) In particular, the following would be reasonable since they would likely not change on a "per email" basis:
c= Message canonicalization (opts.nCanon)
h= Signed header fields (opts.szRequiredHeaders)
l= Body length count (opts.nIncludeBodyLengthTag, pass Boolean - whether to include or not)
t= Signature Timestamp (opts.nIncludeTimeStamp, pass Boolean - whether to include or not)
x= Signature Expiration (opts.expireTime, pass number of seconds from starttime)
z= Copied header fields (opts.nIncludeCopiedHeaders, pass Boolean - whether to include or not)
a= The algorithm used to generate the signature (opts.nHash, I assume, pass sha1 or sha256)

This could be accomplished with environment variables. Certainly your defaults should be used in the absence of the environment variable for required tags. For optional tags, setting the variable to "" or the absence of the variable would mean omit it - for example if I did not want there to be any Signature Expiration.

Keep up the good work.
Fred

cprogrammer said...

Fred,

Thanks for taking the time to post the comments. I have understood the first comment and the third comment. I will implement those (maybe after few more dialogues with you, to understand it better). Meanwhile let me understand the 2nd comment better (it will take me some time I think).

Fred said...

CProgrammer,
Wow! What a quick response!

Regarding qmail-dkim not using the Sender header field even if it is present, I believe I have found out why. In dkimsign.cpp (after running patch) beginning at line 402, I believe these two are out of order:
if (!sFrom.empty())
sAddress.assign(sFrom);
else
if (!sSender.empty())
sAddress.assign(sSender);
This sets sAddress equal to From if it is present. It probably should be
if (!sSender.empty())
sAddress.assign(sSender);
else
if (!sFrom.empty())
sAddress.assign(sFrom);
------------
Though I've never programmed anything in C, much of the logic of programming is the same in any language. So, I am trying to change your code to use the Return-Path header field and an environment variable for the expiration seconds after starttime. I have done the following (line numbers are from after my additions):

dkimsign.h add the following after line 70:
string sReturnPath;dkimsign.cpp add the following after line 208:
if (_strnicmp(sHdr.c_str(), "Return-Path:", 12) == 0)
sReturnPath.assign(sHdr.c_str() + 12);
dkimsign.cpp remove lines lines 404-408 (after above addition), and replace with the following:
if (!sReturnPath.empty())
sAddress.assign(sReturnPath);
else
if (!sSender.empty())
sAddress.assign(sSender);
else
if (!sFrom.empty())
sAddress.assign(sFrom);
else
qmail-dkim.c add after line 241:
char *dkimexpire = 0;qmail-dkim.c add after line 832:
dkimexpire = env_get("DKIMEXPIRE");qmail-dkim.c replace line 846:
opts.expireTime = starttime + 604800;

with
if (dkimexpire && *dkimexpire) {
opts.expireTime = starttime + dkimexpire;
}
I have not yet tried to compile it! That will wait until tomorrow (it is 11 p.m. in San Diego, CA).

If I knew C, regarding the Return-Path, I would likely do something like this (which probably mixes Perl syntax with C syntax!):

if (!sReturnPath.empty() && env_get("DKIMDOMAINHEADER") == 'Return-Path')
sAddress.assign(sReturnPath);
Probably it would be better to assign the value of env_get("DKIMDOMAINHEADER") to a variable then use that variable in the "if" statement.

------------
Since I can set DKIMIDENTITY to a blank string causing the "i=" tag to be omitted, my second comment is taken care of for my purposes.

------------
Regarding my second comment. It appears to me that setting the "i=" tag to be the same as the "s=" tag is not compliant with RFC 4871. The "i=" tag must be a subdomain of the "d=" tag, and it should be an email address, with the Local-part, or user name before the @ - optional. That is, if the "d=" tag is "example.com" then the "i=" tag could be something like "fred@foo.example.com" or just "@foo.example.com"

The "s=" tag really has nothing to do with the "i=" tag. As stated in section 3.6.2.1 of RFC 4871, it is used only to request the DNS TXT record, appending it to _domainkey.example.com (assuming d=example.com). So, if s=default and d=example.com, then the verifier should request the TXT record for default._domainkey.example.com.

Of course, if I set a value for DKIMIDENTITY, there is no problem. The problem arises when DKIMIDENTITY is never set, and you use the value of the "s=" tag for the "i=" tag.

In my opinion, if the "i=" tag would end up as the default value, that is, identical to the "d=" tag with the addition of the @ at the beginning, then it should probably just be left out, leaving it up to the verifier to use the default value (as gmail.com does). Put another way, if d=example.com and i=@example.com, just do not add the "i=" tag at all.

I hope that helps you understand my comment.

------------
A final comment. Per qmail-dkim.8 "If there is a % character in the environment variable, it is removed and replaced by the domain name in the From: header." Since I do not have the % character in the environment variable, I do not know exactly how you implement that. However, I think it would be best to use the domain name in whatever ends up in the "d=" tag. As your code stands (with the correction for the order of assigning sAddress) it could be the domain from the Sender: header. With my additions for Return-Path, if they work, it could be that domain.

Fred

cprogrammer said...

Fred,

I have made all the changes. Thanks to you I realized I had made a stupid mistake in assigning the identity (i=) to be same as the selector. The changes are following
1. The library will fetch the domain from Return-Path, Sender, From (in the given order)
2. if DKIMIDENTITY is not set, qmail-dkim will not set i= tag.
3. Now qmail-dkim will use DKIMSIGNOPTIONS environment variable to set any DKIM options. The environment can be in getopt style.
5. The man page qmail-dkim has been updated

I could not get your email address. You can email me at mbhangui at gmail.com and I will send you the latest patch. Once I test it, I will also upload the same to sourceforge.net

Roberto said...

Hi,
thank you for your dkim patch. I'm playing with 1.6 version.

Trying to sign at smtp level via telnet, it's seems like the "%" character (to be replaced by the sender's domain) throws into error qmail-queue, as I get a "451 qq internal bug (#4.3.0)".
qmail-smtpd log says also "message delayed".

This is how I am setting qmail-smtpd/run:

QMAILQUEUE=/var/qmail/bin/qmail-dkim
export QMAILQUEUE
DKIMSIGN=/var/qmail/control/domainkeys/%/private
export DKIMSIGN

Anyway, I manage to sign my outgoing messages without the "%", and also when I call qmail-inject in such a way:

env QMAILQUEUE=/var/qmail/bin/qmail-dkim DKIMSIGN="/var/qmail/control/domainkeys/%/private"
/var/qmail/bin/qmail-inject -fsender@domain.xyz recipient@domain.abc < mailtest.txt

I've read all man pages more times, and after many tests I can't figure out yet what I am missing.

Thanks for any help

cprogrammer said...

Roberto I am unable to simulate your problem. But I am sure there is a bug.

If you could share with me your smtp run script and the exact telnet commands, it would help me to locate the bug.

my email is mbhangui at gmail.com

cprogrammer said...

Roberto looks like you are running out of softlimit.

The only thing different that happens when you give a percentage sign is memory gets allocated to append the domain name to the key. This memory allocation must be failing.

Your smtp run script uses softlimit. Increase your limit by 5 times and see if you still get the error.

IndiMail Queue Mechanism

Indimail has the ability of configuring multiple local and remote queues. A queue is a location on your hard disk where email are deposited ...