Provided by: dkim-rotate_0.4_all bug


       dkim-rotate - Principles of Operation


       dkim-rotate  is  a  tool  for  managing DKIM (email antispam) keys in a manner that avoids
       unnecessarily making emails nonrepudiable.

   Problem statement
       Using a static or nearly-static DKIM signing key enables anyone who obtains a copy  of  an
       email to verify its authenticity.

       This can be used to verify the authenticity of data from a data breach, for example.  This
       is not a desirable property, from the point of view of an email system’s users, and wasn’t
       an intended consequence of DKIM’s antispam function.

       For  fuller  discussion  of  the  nonrepudiability problem with DKIM, see the blog post Ok
       Google: please publish your DKIM secret keys, referenced in the SEE ALSO section.

   Solution - function of dkim-rotate
       We periodically generate a new key.  We deadvertise old keys (removing them from  the  set
       advertised in the DNS), We publish the private halves of old keys.

       The  overall result is that because old emails are forgeable (by anyone, since the private
       key has been published), emails become no longer nonrepudiable.

       We add appropriate warnings, and alter the DNS, to alert naive verifiers to the situation.

   Output and state files
       dkim-rotate will maintain and update the following output files and directories:

              Zonefile in standard master file  syntax.   Created  by  taking  the  config  file,
              editing  the  serial number before ;!SERIAL, and appending TXT RR definitions.  The
              appeneded RRs have single-character alphabetic labels, the  selectors.   See  dkim-

              Private key for use by the MTA.  See MTA CONFIGURATION.

              File in format suitable for Exim ${lsearch }.  See MTA CONFIGURATION.

              Directory  where  private  keys  are  published  (deliberately  leaked).   See  KEY

              Principal state file.

              Other state and temporary files are stored here.

       (Each  dkim-rotate  instance  is  completely  separate;  they  do  not  share  state,   or


       dkim-rotate  maintains  a  collection  of DKIM keys.  The (currently advertised) keys each
       have a “selector”, dkim-rotate uses a small fixed set of  selectors,  in  rotation.   Each
       selector  is  an  (ASCII  lowercase)  letter,  so  dkim-rotate  supports  use  of up to 26
       selectors.  The default is 12.

   Current keys - selectors
       A DKIM signature found in an email indicates  where  to  find  the  key.   It  includes  a
       “selector”,  which  is a set of DNS labels to be prepended to the base DKIM domain for the
       mail domain which originated the email and by whose authority the message is being signed.

       A selector can be reused as soon as the key  which  was  previously  using  that  selector
       should  longer be advertised.  When creating keys, dkim-rotate will automatically choose a
       suitable available selector.

       The selector in DKIM terms is (usually)  the  dkim-rotate  selector  plus  a  fixed  label
       indicating  the signing authority (ie, the dkim-rotate instance).  dkim-rotate itself does
       not know the actual DKIM selectors; the suffix is added in the MTA and DNS configurations.

   DNS selector advertisement
       dkim-rotate outputs a  DNS  zonefile,  complete  with  serial  number,  as  /var/lib/dkim-

       Usually,  this  will  be  published directly by a nameserver, as a dedicated DNS zone, not
       used for other purposes.  This allows the management of the  mail  domains’  zones  to  be
       separated from the DKIM system.

       Let  us imagine that the dkim-rotate oiutput zone is  Within that
       zone, dkim-rotate will create DKIM TXT records, which look like this in  the  output  zone

              k IN TXT "v=DKIM1; h=sha256; s=email; n=...; p=..."

       This implies the following RRset:

     IN TXT "v=DKIM1; ..."

       A  mail domain (let us imagine,, which wishes to indicate that this system is
       authorised to make DKIM signatures, will use a set of CNAMEs to delegate that authority:

              a.example-net._domainkey     CNAME
              b.example-net._domainkey     CNAME
              c.example-net._domainkey     CNAME
              d.example-net._domainkey     CNAME
              e.example-net._domainkey     CNAME
              f.example-net._domainkey     CNAME
              g.example-net._domainkey     CNAME
              h.example-net._domainkey     CNAME
              i.example-net._domainkey     CNAME
              j.example-net._domainkey     CNAME
              k.example-net._domainkey     CNAME
              l.example-net._domainkey     CNAME

       So, overall, we have something like this:


                 TXT     "v=DKIM1; ..."

   DNS output file and nameserver configuration
       The zonefile is written to /var/lib/dkim-rotate/instance/zone.

       After it has been updated, dkim-rotate runs rndc  reload  (or  the  configured  dns_reload

       If  this  all  occurs  successfully,  dkim-rotate  assumes that dns_lag later, the new DNS
       records (and any deletions) are available everywhere.

       dkim-rotate does not use DNS Dynamic Update.


       dkim-rotate provides the selector, and the private key, to the MTA.

       This is done by writing /var/lib/dkim-rotate/instance/exim.  This is in a  key-colon-value
       format, which is convenient for use by Exim’s lsearch lookup facility.

       After  this  file is updated, dkim-rotate runs the configured mta_reload command.  This is
       just true (a no-op) by default (and Exim doesn’t need it).

   MTA configuration output format
       The output file is lines of the form:

              key: value

       It may also contain #-comment lines.  The values are literal text, without any quotes (and
       therefore cannot contain newlines).

       The keys are;

              The  filename  of  the private key to use.  This will be in the form /var/lib/dkim-

              The selector under which the corresponding public key is advertised.

              Some text which it would be useful to put into the email headers.  It  starts  NOTE

              This could be put into a note= or warning= tag in the actual DKIM-Signature header.

              If  that  is not possible (e.g. Exim doesn’t support it) it could be put into DKIM-
              Signature-Warning, say.  It is probably a good idea to arrange that  it  is  itself
              covered  by the signature, to make it more complicated for an adversary to strip it

       url, readme_url
              URLs for the instance’s public WWW directory, the README.txt file, corresponding to
              this instance.

              The  URL  at which the private key will be revealed to the world, after the key has
              been retired.  (Obviously, when this URL appears in the /exim file, the URL is  not
              yet valid.)

   Example Exim configuration
       DKIM  signing is done with additional options on the smtp transport.  The mailserver ought
       not to be a signing oracle for arbtrary incoming emails which are being  relayed  (eg  via
       forward  files)  —  only  for  emails  generated locally, or from appropriately authorised
       places.  And we should choose, for the signing domain, the domain  which  appears  in  the
       From: header, and sign only if DKIM is enabled for that domain.

       The required config looks something like this:

                driver = smtp
                # ... other options ...
                # lookup fd caching ensures coherence of all of these, see exim 4.94 spec 9.8
                dkim_domain = ${if  and{                                  \
                  { match_domain {${domain:$h_from:}} {+dkim_domains} }   \
                  { !def:h_dkim-signature: }                              \
                  { !def:h_list-id: }                                     \
                  { or{                                                   \
                       { def:authenticated_id }                           \
                       { match_ip {$sender_host_address} {+relay_hosts} } \
               }}                                                         \
               } {${domain:$h_from:}} {} }
                dkim_selector = ${lookup {selector} lsearch {/var/lib/dkim-rotate/example-net/exim} }.example-net
                dkim_private_key = ${lookup {privkey} lsearch {/var/lib/dkim-rotate/example-net/exim} }
                dkim_sign_headers = _DKIM_SIGN_HEADERS : DKIM-Signature-Warning
                headers_add = ${if  and{                                  \
                  { match_domain {${domain:$h_from:}} {+dkim_domains} }   \
                  { !def:h_dkim-signature: }                              \
                  { !def:h_list-id: }                                     \
                  { or{                                                   \
                       { def:authenticated_id }                           \
                       { match_ip {$sender_host_address} {+relay_hosts} } \
               }}                                                         \
               } {DKIM-Signature-Warning: ${lookup {header_note} lsearch {/var/lib/dkim-rotate/example-net/exim} }} }

   Example Exim configuration (perl version)
       It  is  a  shame  that  Exim  doesn’t  seem  to have better and more cooked facilities for
       controlling dkim signing.  The required configuration is quite annoying repetitive.

       The following Perl can generate something like the config above:

              sub dkim_lookup { "\${lookup {$_[0]} lsearch {/var/lib/dkim-rotate/example-net/exim} }" }
              my $dkim_domain_expr = "\${domain:\$h_from:}";
              my $dkim_condition = <<END;
               and{                                                          \\
                  { match_domain {$dkim_domain_expr} {+dkim_domains} }       \\
                  { !def:h_dkim-signature: }                                 \\
                  { !def:h_list-id: }                                        \\
                  { or{                                                      \\
                       { def:authenticated_id }                              \\
                       { match_ip {\$sender_host_address} {+relay_hosts} }   \\
               }}                                                            \\

              my $dkim_smtp_options = <<END;
                # lookup fd caching ensures coherence of all of these, see exim 4.94 spec 9.8
                dkim_domain = \${if $dkim_condition } {$dkim_domain_expr} {} }
                dkim_selector = ${\ dkim_lookup('selector')}.example-net
                dkim_private_key = ${\ dkim_lookup('privkey')}
                dkim_sign_headers = _DKIM_SIGN_HEADERS : DKIM-Signature-Warning
                headers_add = \${if $dkim_condition } {DKIM-Signature-Warning: ${\ dkim_lookup('header_note')}} }
              $dkim_smtp_options =~ s{^(.*\S)\s*\\$}{ sprintf "%-70s\t\\", $1 }mge;


       dkim-rotate  publishes  secret  keys  by  writing  them  to  a  directory   /var/lib/dkim-
       rotate/instance/pub/.   This  is  an ever-growing archive.  Nothing ahould be deleted from

       This directory  should  be  made  available  via  webserver,  and  the  corresponding  URL
       configured via the pub_url config directive.

       dkim-rotate  will  make  subdirectories 00 to ff here.  These are radix prefix directories
       which exist both to avoid the creation of a very large single directory of key files,  and
       to make it harder to enumerate the private keys.

       In particular, these subdirectories are not globally-readable, although they are globally-
       executable.  The webserver should run without privilege, so that the individual  keys  can
       be read, but the directories cannot be listed (and won’t be archived by any crawlers).

       dkim-rotate will make a README.txt file in the pub/ directory.

       (Currently there is no way to configure the contents of this file.)


   Key statuses and lifecycle
        abbrev    meaning             time_t       (in   selector         how many
          -1      advertised;   not   first advertised   advertised       0/1
                  yet used                               (DNS)
          +0      signing             (first used; not   advertised       0/1; usually 1
                                      relevant)          (DNS)

         +N..     emails              last   used  for   advertised       [0          ..
                  percolating         signing            (DNS)            sel_limit]
         +X..     deadvertisment      last advertised    archival only    [0        ..];
                  propagating                                             usually 0/1
           R      revealed            (not  longer  in   archival only    many

   Example key lifecycle
             -0200 [0][1]     generate   and
                                               dns_lag (4h) + 2h slop
             +0400            start signing
                                               1d key rollover interval
             +0400 +1d        stop signing
                                               email_lag (3.5d timeout + 4h retry)  +
                                               2h slop
             -0200 +5d        deadvertise
                                               dns_lag (4h) + 2h slop
             +0400 +5d        reveal

       [0] -0200 means “2200 the previous day”

       [1]  If  a  free  selector is already available, this might be generated and advertised at
       +0400 -1d.

   Example cron configuration
                #mins hrs dom mon dow command
                26 22 * * *       dkim-rotate --minor
                26 4  * * *       dkim-rotate --major

       These jobs should be scheduled in a suitable local time  (in  the  timezone  of  the  mail
       server’s  users),  because  it  is good for all mails sent on a particular calendar day to
       become un-nonrepudiable (and un-deliverable) at once.

       To cope nicely with timezone changes the interval between --minor and the main run  should
       be  at  least  dns_lag  +  1h  +  an  allowance  for  processing  time etc.  The suggested
       configuration has a 6h interval, which suits the default dns_lag of 4h.


       Copyright 2022 Ian Jackson and contributors to dkim-rotate.
       There is NO WARRANTY.
       SPDX-License-Identifier: GPL-3.0-or-later


              Configuration file

              Command line reference

              DKIM Signatures

       Ok Google: please publish your DKIM secret keys
              article by Matthew Green