dkim-rotate(7) | dkim-rotate(7) |
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.
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.
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.
dkim-rotate will maintain and update the following output files and directories:
(Each dkim-rotate instance is completely separate; they do not share state, or configuration.)
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.
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.
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 oiutput 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; ..."
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.
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).
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;
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.
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:
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} }} }
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;
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).
dkim-rotate will make a README.txt file in the pub/ directory.
(Currently there is no way to configure the contents of this file.)
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 |
-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.
#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
<https://blog.cryptographyengineering.com/2020/11/16/ok-google-please-publish-your-dkim-secret-keys/>