Scroll to navigation

dkim-rotate(7) 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 (in statefile) selector how many
-1 advertised; not yet used first advertised advertised (DNS) 0/1
+0 signing (first used; not relevant) advertised (DNS) 0/1; usually 1
+N.. emails percolating last used for signing advertised (DNS) [0 .. sel_limit]
+X.. deadvertisment propagating last advertised archival only [0 ..]; usually 0/1
R revealed (not longer in statefile) archival only many

Example key lifecycle

-0200 [0][1] generate and 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
DKIM Signatures
article by Matthew Green