% dkim-rotate(7)

NAME
====

`dkim-rotate` - Principles of Operation

INTRODUCTION
============

`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:

`/var/lib/dkim-rotate/`_instance_`/zone`

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

`/var/lib/dkim-rotate/`_instance_`/priv/`_keyname_`.pem`

:   Private key for use by the MTA.
    See [MTA CONFIGURATION].

`/var/lib/dkim-rotate/`_instance_`/exim`

:   File in format suitable for Exim `${lsearch }`.
    See [MTA CONFIGURATION].

`/var/lib/dkim-rotate/`_instance_`/pub/`

:   Directory where private keys are published (deliberately leaked).
    See [KEY PUBLICATION].

`/var/lib/dkim-rotate/`_instance_`/state`

:   Principal state file.

`/var/lib/dkim-rotate/`_instance_`/`...

:   Other state and temporary files are stored here.

(Each dkim-rotate _instance_ is completely separate;
they do not share state, or configuration.)

SELECTORS
=========

`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-rotate/`_instance_`/zone`.

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 output zone is `dkim-rotate.example.net`.
Within that zone, dkim-rotate will create DKIM TXT records,
which look like this in the output zone file:
```
k IN TXT "v=DKIM1; h=sha256; s=email; n=...; p=..."
```

This implies the following RRset:
```
k.dkim-rotate.example.net. IN TXT "v=DKIM1; ..."
```

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

```
$ORIGIN example.com.
a.example-net._domainkey     CNAME   a.dkim-rotate.example.net.
b.example-net._domainkey     CNAME   b.dkim-rotate.example.net.
c.example-net._domainkey     CNAME   c.dkim-rotate.example.net.
d.example-net._domainkey     CNAME   d.dkim-rotate.example.net.
e.example-net._domainkey     CNAME   e.dkim-rotate.example.net.
f.example-net._domainkey     CNAME   f.dkim-rotate.example.net.
g.example-net._domainkey     CNAME   g.dkim-rotate.example.net.
h.example-net._domainkey     CNAME   h.dkim-rotate.example.net.
i.example-net._domainkey     CNAME   i.dkim-rotate.example.net.
j.example-net._domainkey     CNAME   j.dkim-rotate.example.net.
k.example-net._domainkey     CNAME   k.dkim-rotate.example.net.
l.example-net._domainkey     CNAME   l.dkim-rotate.example.net.
```

So, overall, we have something like this:
```
example.com.                           MX      mx0.example.com.

k.example-net._domainkey.example.com.  CNAME   k.dkim-rotate.example.net.
k.dkim-rotate.example.net.             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` command).

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.


MTA CONFIGURATION
=================

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;

`privkey`

:   The filename of the private key to use.
    This will be in the form
     `/var/lib/dkim-rotate/`_instance_`/priv/`_hex_`.pem`.

`selector`

:   The selector under which the corresponding public key is advertised.

`header_note`

:   Some text which it would be useful to put into the email headers.
    It starts `NOTE REGARDING DKIM KEY COMPROMISE`.

    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 out.

`url`, `readme_url`

:   URLs for the instance's public WWW directory,
    the `README.txt` file,
    corresponding to this instance.

`key_reveal_url`

:   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 arbitrary 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:

```
smtp:
  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} }   \\
 }}                                                            \\
END

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')}} }
END
$dkim_smtp_options =~ s{^(.*\S)\s*\\$}{ sprintf "%-70s\t\\", $1 }mge;
```

KEY PUBLICATION
===============

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 it.

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).

README
------

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 LIFECYCLE
=============

Key statuses and lifecycle
--------------------------

 -------- ----------------  ---------------- -------------- -------------
  abbrev  meaning           time_t           selector       how
                            (in                             many
                            statefile)                                
 -------- ----------------  ---------------- -------------- -------------
  `-1`    advertised;       first            advertised     0/1
          not yet used      advertised       (DNS)         
                                                                      
  `+0`    signing           (first used;     advertised     0/1;
                            not relevant)    (DNS)          usually 1
                                                                      
  `+N`..  emails            last used        advertised     [0 .. `sel_limit`]
          percolating       for signing      (DNS)
                                                                      
  `+X`..  deadvertisment    last advertised  archival only  [0 ..];
          propagating                                       usually 0/1
                                                                      
  `R`     revealed          (not longer in   archival only  many
                            statefile)                          
 -------- ----------------  ---------------- -------------- --------------


Example key lifecycle
---------------------

 -- -------------  -------------- ---------------------------------------
    `-0200`        generate and
    [0][1]         advertise

                                   `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.


AUTHOR
======

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


SEE ALSO
========

dkim-rotate(5)

:   Configuration file

dkim-rotate(1)

:   Command line reference

RFC6376

:   DKIM Signatures

_Ok Google: please publish your DKIM secret keys_

:   article by Matthew Green

    <https://blog.cryptographyengineering.com/2020/11/16/ok-google-please-publish-your-dkim-secret-keys/>
