I'm finishing up writing what I have learned in the last
couple of weeks on setting up a DKIM/DK signing/verifying
mail system using Postfix, milters, amavisd-new and
SpamAssassin. The following text will be part of the
documentation for amavisd-new (text is also available
at http://www.ijs.si/software/amavisd/amavisd-new-docs.html ),
but may be general enough so I hope it can be of interest
to the SpamAssassin community.

Comments, experience, suggestions and further discussion
on the topic is most welcome. If considered off-topic,
off-list mail would be welcome too.

  Mark


Setting up DKIM and DomainKeys mail signing and verification
============================================================

The goals of DKIM and DomainKeys are:
  * assurance of sender identities
  * protection against message tampering.

A DKIM draft states the following, which applies to its predecessor
DomainKeys as well:

  DomainKeys Identified Mail (DKIM) defines a mechanism by which email
  messages can be cryptographically signed, permitting a signing domain
  to claim responsibility for the introduction of a message into the mail
  stream. Message recipients can verify the signature by querying the
  signer's domain directly to retrieve the appropriate public key, and
  thereby confirm that the message was attested to by a party in
  possession of the private key for the signing domain.

A gentle introduction and deployment guide is available at:
http://antispam.yahoo.com/domainkeys. Except for some minor details, it
applies to DKIM system as well.

With added support in Postfix 2.3 for a milter protocol, it became
possible to use with Postfix many of existing milters (mail filters) that
were originally developed with sendmail in mind. It was hoped that a
widespread use of milters with sendmail offered a fertile ground for
software development, producing software of sufficient quality to be able
to use it with Postfix. It remains to be seen whether quality of freely
available milters comes anywhere close to high standards we are accustomed
to with Postfix, but with a bit of luck and reasonable expectations, some
of it can be put to good use.

Two of such milters are dkim-milter offering support for DomainKeys
Identified Mail (DKIM) Signatures, and dk-milter, offering support for
Domain-based Email Authentication (DomainKeys). The DomainKeys (DK) is a
predecessor of DKIM, as recognized by draft-delany-domainkeys-base-06:

  The DomainKeys specification was a primary source from which the
  DomainKeys Identified Mail [DKIM] specification has been derived. The
  purpose in submitting this document is as an historical reference for
  deployed implementations written prior to the DKIM specification.

At the time of this writing it appears the dkim-milter is more reliable
and better maintained than dk-milter, which is slowly fading into
oblivion. Similar holds true in the world of Perl modules: there are
modules Mail::DomainKeys and Mail::DKIM, both of which can be used by
SpamAssassin. Again the Mail::DKIM seems to be of higher quality than an
older Mail::DomainKeys. SpamAssassin makes it very easy to use each or
both of them (for verification only), just by enabling the already
provided plugins.

Despite DomainsKeys slowly giving grounds to DKIM, the DomainsKeys is
currently still in use by several large players in the Internet world, so
this section will describe how to integrate both of them with Postfix and
amavisd-new (an after-queue content filter) into a mail system.

Mail signing and verification is a two-part job: signing of originating
mail (or mail being redistributed) from our domain, and verifying
signatures of incoming mail. Both tasks can be done by the same program,
or they can be performed by separate entities. Traditionally with
sendmail, both tasks are performed by the same milter, which may be easier
to maintain, but has certain disadvantages.

Verifying signatures should be performed early, before any local mail
transformations get a chance of invalidating signature, e.g. by performing
MIME conversions to quote-printable, by fixing syntactically invalid mail
header, by editing/inserting/removing certain header fields, or by a local
mailing list modifying mail text, e.g. by appending footnotes.

Signing outgoing mail should be performed late, after mail sanitation,
after conversion to 7-bit characters (to avoid later uncontrollable
changes by a relaying or receiving MTA), and after adding header fields by
a content filter. Similar applies to local mailing lists, which may be
rewriting messages, requiring them to be re-signed by the domain hosting a
mailing list, just before being sent out.

Since SpamAssassin only provides signature verification but not signing,
one obvious choice for signing is to use dkim-milter and dk-milter in
signing-only mode, invoked by a Postfix smtpd service which is receiving
content-checked mail from a content filter such as amavisd-new. As this
second-stage smtpd service does not reliably know how a given message came
into a mail system and whether it is supposed to be signed or not, a clean
solution is to provide two parallel paths through a content filter, one
used for mail to be signed (originating mail), the other for all the rest:

              +------+
              |verify|            (verify)
              +--+---+          SpamAssassin
                 ^                   ^v
incoming:        |              +----++-----+
  MX ---->  25 smtpd ---> 10024 >           >--> 10025 smtpd -->
submission:                     |           |
  SASL -->  25 smtpd \          |  amavisd  |
                      +->       |           |
  mynets->  25 smtpd ---> 10026 >ORIGINATING>--> 10027 smtpd -->
       --> 587 smtpd --->       +-----------+            |
               (convert to 7-bit)                        v
                                                       +----+
                                                       |sign|
                                                       +----+

There are other benefits to providing two parallal paths: a content filter
may be configured to apply different rules and settings to mail that is
known to be originating from our users, compared to the rest. Some
suggestions: apply less strict banning rules, enable spam administrator
notifications for internally originating spam and viruses, letting
SpamAssassin rules be conditionalized based on amavisd-new policy banks
loaded, etc.

For verification there are two choices: either a SpamAssassin plugin can
do it by calling Perl modules, or a milter in verification-only mode can
be invoked by the incoming Postfix smtpd service. In the setup described,
SpamAssassin sees almost the same message as a milter on the incoming
smtpd would (just few header fields including a Received are prepended),
so there is no concern that a signature would become invalid.

Invoking signature verification by SpamAssassin has an advantage that
DKIM-based or DomainKeys-based whitelisting or scoring can be used, but
has a disadvantage that possibly not all mail is checked (e.g. large mail
may be exempt from spam checks).

On the other hand, invoking signature verification by calling a milter
from incoming smtpd service can insert a header field reporting a result
of verification, which may be used by MUA or by SpamAssassin rules,
although currently no such rules are provided ready-made.

For the purpose of gathering experience, both methods for signature
verification are described. It turns out that one or the other
implementation (in C or in Perl) may be buggy, and being able to compare
both results of signature verification can help with troubleshooting.

Let's begin by starting both milters, each in two instances, one dedicated
for signing, the other for verification. For security purposes all milters
should be run under a dedicated username, certainly not as root, not as
user amavis and not as user postfix:

dk, verifying:
  dk-filter   -u dkfilter -b v -m MTA -H \
    -l -p inet:[EMAIL PROTECTED] -P /var/run/dk-filter-v.pid

dkim, verifying:
  dkim-filter -u dkfilter -b v -m MTA \
    -l -p inet:[EMAIL PROTECTED] -P /var/run/dkim-filter-v.pid

dk, signing:
  dk-filter   -u dkfilter -b s -m ORIGINATING \
    -H -c nofws \
    -d example.com -S myselector -s /var/db/dkim/mykey.pem \
    -l -p inet:[EMAIL PROTECTED] -P /var/run/dk-filter-s.pid

dkim, signing:
  dkim-filter -u dkfilter -b s -m ORIGINATING \
    -c relaxed/simple -S rsa-sha1 \
    -d example.com -s myselector -k /var/db/dkim/mykey.pem \
    -l -p inet:[EMAIL PROTECTED] -P /var/run/dkim-filter-s.pid

Note that both signing milters may (but need not) use the same signing key
and the same selector. Generating a pair of public and private key and
publishing a public key and a policy in DNS is described in the
documentation of each milter.

Now we can tie both verifying milters to a Postfix smtpd service listening
for incoming mail:

master.cf:
  smtp inet n - n - 300 smtpd
    -o milter_default_action=accept
    -o milter_macro_daemon_name=MTA
    -o smtpd_milters=inet:127.0.0.1:4442,inet:127.0.0.1:4443

and tie both signing milters to a Postfix smtpd service that is receiving
checked mail from amavisd, intended to be signed:

master.cf:
  # mail return from a content filter (non-signing)
  10025 inet n - n - - smtpd
    -o content_filter=
    ... (other options, mail not to be signed) ...

  # mail from our users returning from a content filter (DKIM or DK signing)
  10027 inet n - n - - smtpd
    -o content_filter=
    ... (other options, mail to be signed) ...
    -o milter_default_action=accept
    -o milter_macro_daemon_name=ORIGINATING
    -o smtpd_milters=inet:127.0.0.1:4444,inet:127.0.0.1:4445

Note the order for signing milters: dk-milter is invoked first, and
dkim-milter second, so that DK signature will be protected by a later DKIM
signature. The reason is that DomainKeys specification does not protect
its own signature in its signed data, but the newer DKIM fixes this
omission and does protect its own signature.

Default content filter is to be amavisd, listening on port 10026 (intended
for signing). Locally submited or SASL-authenticated mail will go to a
content filter on this default port 10026 and will be signed on its way
out. All other mail (incoming) will be diverted to port 10024 for content
filtering by a final catchall FILTER, and will never hit the signing
milter:

main.cf:
  content_filter = smtp-amavis:[127.0.0.1]:10026

  smtpd_recipient_restrictions =
    ...
    permit_mynetworks
    permit_sasl_authenticated
    reject_unauth_destination
    ...
    check_sender_access regexp:/etc/postfix/filter_10024_catchall
    permit

/etc/postfix/filter_10024_catchall:
  /^/  FILTER smtp-amavis:[127.0.0.1]:10024

In SpamAssassin all that is necessary is to add (or uncomment) lines in
any of the .pre files (e.g. in local.pre, or in init.pre and v312.pre):
  loadplugin Mail::SpamAssassin::Plugin::DomainKeys
  loadplugin Mail::SpamAssassin::Plugin::DKIM

Perl modules Mail::DomainKeys (version 0.86 or better) and Mail::DKIM
(0.19 or better) need to be installed.

The following SpamAssassin rules (in local.cf) work fairly well, giving
verified mail a little bit of advantage and slightly favourize mail from
some popular domains, and encourage people to start signing their mail.
Possible signed spam can be counterbalanced by other measures (see below).
  score DK_VERIFIED -1.5
  score DK_POLICY_SIGNSOME 0
  score DK_POLICY_TESTING  0

  score DKIM_VERIFIED -1.5

  # DKIM and DK-based whitelisting may be used reliably:
  score USER_IN_DKIM_WHITELIST -3.0
  whitelist_from_dkim [EMAIL PROTECTED]
  whitelist_from_dk   [EMAIL PROTECTED]

In amavisd.conf two parallel paths need to be provided, one receiving on
port 10024 and forwarding to 10025, the other receiving on port 10026 and
forwarding to 10027.
  $inet_socket_port = [10024,10026];  # listen on two ports

The 10024/10025 will use a default policy bank, the other, dedicated to
mail needing to be signed, will use a policy bank ORIGINATING:
  $forward_method = 'smtp:[127.0.0.1]:10025';  # MTA with non-signing service
  $notify_method  = 'smtp:[127.0.0.1]:10027';  # MTA with DKIM signing service

  # switch policy bank to 'ORIGINATING' for mail received on port 10026:
  $interface_policy{'10026'} = 'ORIGINATING';

  $policy_bank{'ORIGINATING'} = {  # mail originating from our users
    # force MTA to convert mail to 7-bit before DKIM signing
    # to avoid later conversions which could destroy signature:
    smtpd_discard_ehlo_keywords => ['8BITMIME'],
    #
    # forward to a smtpd service providing DKIM/DomainKeys signing service:
    forward_method => 'smtp:[127.0.0.1]:10027',
    #
    # other special treatment of locally originating mail, e.g.:
    spam_admin_maps => ["[EMAIL PROTECTED]"],  # warn of spam from us
    banned_filename_maps => ['ALT-RULES'],  # more relaxed rules...
  };

The smtpd_discard_ehlo_keywords=>['8BITMIME'] serves to persuade Postfix
to convert mail to 7-bit quoted-printable before submitting it to content
filtering and signing (this configuration variable was introduced with
version 2.4.3 of amavisd-new). Avoiding 8-bit characters in mail body
makes signatures less susceptible to breaking by some relaying or
receiving MTA over which we have no control. Note that the same effect
(making Postfix convert outgoing mail to 7-bits before DKIM signing) could
be achieved by a Postfix setting smtp_discard_ehlo_keywords=8bitmime on a
smtp service feeding mail to be signed to amavisd, but this would require
setting up two such services, one with the option and one without.

While testing how the configured system plays with some mailing lists
(such as postfix-users or SpamAssassin users list), one has to keep in
mind that amavisd-new caches spam checking results of recently seen
message bodies, so a mail going out to a mailing list is not yet signed as
it reaches a content filter, but the SpamAssassin verdict is remembered as
that point (claiming the message is not signed). When this message with
unchanged body comes back from a mailing list, this time signed in the
header by our domain, the signature should prove correct, yet the cached
result from a minute ago still claims the message is not signed. If this
is a problem, one can turn off caching of spam checking results for ham by
setting: $spam_check_negative_ttl = 0;

ome experience with DKIM and DomainKeys

Recent versions of software components must be used to avoid bugs and
interoperability problems with earlier versions:
  * use Postfix 2.3.3 or later (fixing minor 2.3 problems with milter);
  * amavisd-new 2.4.3 polished some corner issues on modifying mail header
    on releasing from a quarantine and defanging, and added some goodies
    affecting DKIM and DomainKeys to facilitate integration;
  * Mail::DomainKeys 0.86 or later must be used, the last couple of
    versions squashed several bugs, one at a time. Version 0.84 dropped
    the use of unreliable Email::Address (which could cause deep recursion
    in evaluating regular expressions, bringing processing to a halt).
    Nevertheless, one patch is still needed (
    http://www.ijs.si/software/amavisd/Mail-DomainKeys-mark.patch ),
    hopefully to be included with the next version;
  * Mail::DKIM 0.19 is very solid, it is the only component where no bugs
    or design flaws were found during experimenting; in fact, this module
    helped to discover bugs in other components;
  * SpamAssassin 3.1.5 added whitelisting based on verified DomainKeys
    signatures, similar to what was already available for DKIM
    whitelisting in earlier versions of SA;
  * both the dkim-milter 0.5.1 and the dk-milter 0.4.1 need a patch as
    described in the Postfix documentation file MILTER_README. The
    dkim-milter already supplies a required patch in its bug tracking
    system under "[1537905] delayed queue ID"; which will be included in
    the next release;
  * the dk-milter 0.4.1 seems to be neglected lately and is rather buggy
    compared to dkim-milter. Nevertheless, with command options as given
    in the above example, it does its job sufficiently well in a described
    Postfix + amavisd-new setup, so that it may be deployed for a
    production use; see its bug tracking system for details;
  * the dkim-milter 0.4.1 works well with simple/simple or relaxed/simple
    canononicalization algorithms, while the relaxed/relaxed is to be
    avoided (and may even be dropped when the specification reaches a RFC
    stage);

Instead of signing mail with dkim-milter, the same can be achieved by
using Jason Long's DKIM Proxy, which is a Perl program calling a Perl
module Mail::DKIM, i.e. the same modules as used by a SpamAssassin DKIM
plugin. As the Mail::DKIM turned out to be a reliable and quite efficient
module, this may be a good alternative to dkim-milter (which is also quite
good).

On the other hand there exist a dkfilter SMTP-proxy by the same author,
which calls a Perl module Mail::DomainKays, which in turn is not
recommended for processing of entire messages because of its design
limitation which requires loading the whole message into memory. The use
of Mail::DomainKays from within SpamAssassin does not represent such a
problem, as messages checked by SpamAssassin are already limited in size.

Mail transformations as performed by some mailing lists are the most
challenging problem facing DKIM and DomainKeys deployment (and to other
schemes as well). Nevertheless, mailing lists can be configured to either
avoid transformations which invalidate mail signatures, or can re-sign
fan-out mail. Examples of mailing lists which work very well with DKIM and
DomainKeys, preserving existing signatures provided by posters, are the
postfix-users ( [EMAIL PROTECTED] ) and the SpamAssassin users
list ( users@spamassassin.apache.org ). Example of re-signing mailing
lists are Yahoo groups. A representative of another type of mailing lists
is currently Mailman, which modifies mail body, but at least it is
stripping original signatures and adding a Sender header, so that original
signatures do not appear to be broken, they are just missing, and mail may
be re-signed.

Several big players are already signing mail from their users or
employees: Yahoo! (worldwide), gmail.com and google.com, eBay, Earthlink,
Cisco, (not to mention porcupine.org :)

When signatures are missing on mail from domains which are known to be
signing all their mail (yahoo.com, gmail.com), the most common reason is
that a sender submitted his mail through some other provider, but supplied
his Yahoo or gmail e-mail address in the From header field. Similar to
other schemes designed to prevent faking of sending address, the DKIM (and
the DomainKeys) encourage mail submission only through a domain which is
used in the From address - although there are other possibilities for
roving users, especially in the DKIM system. People will need to become
aware that their best choice is to submit mail through their home domain
to prevent their messages being treated as second-class or appear
suspicious.

Note that some spam is also being signed by DomainKeys or DKIM lately,
which is a good thing -- it indicates the sender owns (or ownz) a domain
they are sending mail from. This either shows sender's sincere interest of
not hiding behind a faked sender mail address (in which case such mail can
be easily filtered if desired), or they are using short-lived temporary
domain (perhaps through domain kiting), which can be counteracted by black
lists of few-days old freshly registered domains. Adding a small negative
spam score to successfully verified mail will encourage people to start
signing their mail, benefiting legitimate senders and recipients, while
signed spam can be counterbalanced by other measures.

Signing and verifying mail is a good mechanism for companies to reliably
whitelist mail from their partner companies or frequent clients.


  Mark

Reply via email to